mirror of
https://github.com/internetee/registry.git
synced 2025-05-17 17:59:47 +02:00
293 lines
9.1 KiB
CoffeeScript
293 lines
9.1 KiB
CoffeeScript
###
|
|
Authors: Nick Giancola (@patbenatar), Brendan Loudermilk (@bloudermilk)
|
|
Homepage: https://github.com/patbenatar/jquery-nested_attributes
|
|
###
|
|
$ = jQuery
|
|
|
|
methods =
|
|
init: (options) ->
|
|
$el = $(@)
|
|
throw "Can't initialize more than one item at a time" if $el.length > 1
|
|
if $el.data("nestedAttributes")
|
|
throw "Can't initialize on this element more than once"
|
|
instance = new NestedAttributes($el, options)
|
|
$el.data("nestedAttributes", instance)
|
|
return $el
|
|
add: ->
|
|
$el = $(@)
|
|
unless $el.data("nestedAttributes")?
|
|
throw "You are trying to call instance methods without initializing first"
|
|
$el.data("nestedAttributes").addItem()
|
|
return $el
|
|
|
|
$.fn.nestedAttributes = (method) ->
|
|
if methods[method]?
|
|
return methods[method].apply @, Array.prototype.slice.call(arguments, 1)
|
|
else if typeof method == 'object' || !method
|
|
return methods.init.apply(@, arguments)
|
|
else
|
|
$.error("Method #{method} does not exist on jQuery.nestedAttributes")
|
|
|
|
class NestedAttributes
|
|
|
|
RELEVANT_INPUTS_SELECTOR: ":input[name][name!=\"\"]"
|
|
|
|
settings:
|
|
collectionName: false # If not provided, we will autodetect
|
|
bindAddTo: false # Required
|
|
removeOnLoadIf: false
|
|
collectIdAttributes: true
|
|
beforeAdd: false
|
|
afterAdd: false
|
|
beforeMove: false
|
|
afterMove: false
|
|
beforeDestroy: false
|
|
afterDestroy: false
|
|
destroySelector: '.destroy'
|
|
deepClone: true
|
|
$clone: null
|
|
|
|
######################
|
|
## ##
|
|
## Initialization ##
|
|
## ##
|
|
######################
|
|
|
|
constructor: ($el, options) ->
|
|
|
|
# This plugin gets called on the container
|
|
@$container = $el
|
|
|
|
# Merge default options
|
|
@options = $.extend({}, @settings, options)
|
|
|
|
# If the user provided a jQuery object to bind the "Add"
|
|
# bind it now or forever hold your peace.
|
|
@options.bindAddTo.click(@addClick) if @options.bindAddTo
|
|
|
|
# Cache all the items
|
|
@$items = @$container.children()
|
|
|
|
# If the user didn't provide a collectionName, autodetect it
|
|
unless @options.collectionName
|
|
@autodetectCollectionName()
|
|
|
|
# Initialize existing items
|
|
@$items.each (i, el) =>
|
|
$item = $(el)
|
|
|
|
# If the user wants us to attempt to collect Rail's ID attributes, do it now
|
|
# Using the default rails helpers, ID attributes will wind up right after their
|
|
# propper containers in the form.
|
|
if @options.collectIdAttributes and $item.is('input')
|
|
# Move the _id field into its proper container
|
|
$item.appendTo($item.prev())
|
|
# Remove it from the $items collection
|
|
@$items = @$items.not($item)
|
|
else
|
|
# Try to find and bind the destroy link if the user wanted one
|
|
@bindDestroy($item)
|
|
|
|
# Now that we've collected ID attributes
|
|
@hideIfAlreadyDestroyed $(item) for item in @$items
|
|
|
|
# Remove any items on load if the client implements a check and the check passes
|
|
if @options.removeOnLoadIf
|
|
@$items.each (i, el) =>
|
|
$el = $(el)
|
|
if $el.call(true, @options.removeOnLoadIf, i)
|
|
$el.remove()
|
|
|
|
|
|
########################
|
|
## ##
|
|
## Instance Methods ##
|
|
## ##
|
|
########################
|
|
|
|
autodetectCollectionName: ->
|
|
pattern = /\[(.[^\]]*)_attributes\]/
|
|
try
|
|
match = pattern.exec(@$items.first().find("#{@RELEVANT_INPUTS_SELECTOR}:first").attr('name'))[1]
|
|
if match != null
|
|
@options.collectionName = match
|
|
else
|
|
throw "Regex error"
|
|
catch error
|
|
console.log "Error detecting collection name", error
|
|
|
|
addClick: (event) =>
|
|
|
|
@addItem()
|
|
|
|
# Don't let the link do anything
|
|
event.preventDefault()
|
|
|
|
addItem: ->
|
|
# Piece together an item
|
|
newIndex = @$items.length
|
|
$newClone = @applyIndexToItem(@extractClone(), newIndex)
|
|
|
|
# Give the user a chance to make their own changes before we insert
|
|
if (@options.beforeAdd)
|
|
|
|
# Stop the add process if the callback returns false
|
|
return false if !@options.beforeAdd.call(undefined, $newClone, newIndex)
|
|
|
|
# Insert the new item after the last item
|
|
@$container.append($newClone)
|
|
|
|
# Give the user a chance to make their own changes after insertion
|
|
@options.afterAdd.call(undefined, $newClone, newIndex) if (@options.afterAdd)
|
|
|
|
# Add this item to the items list
|
|
@refreshItems()
|
|
|
|
extractClone: ->
|
|
|
|
# Are we restoring from an already created clone?
|
|
if @$restorableClone
|
|
|
|
$record = @$restorableClone
|
|
|
|
@$restorableClone = null
|
|
|
|
else
|
|
$record = @options.$clone || @$items.first()
|
|
|
|
# Make a deep clone (bound events and data)
|
|
$record = $record.clone(@options.deepClone)
|
|
|
|
@bindDestroy($record) if @options.$clone or !@options.deepClone
|
|
|
|
# Empty out the values of text inputs and selects
|
|
$record.find(':text, textarea, select').val('')
|
|
|
|
# Reset checkboxes and radios
|
|
$record.find(':checkbox, :radio').attr("checked", false)
|
|
|
|
# Empty out any hidden [id] or [_destroy] fields
|
|
$record.find('input[name$="\\[id\\]"]').remove()
|
|
$record.find('input[name$="\\[_destroy\\]"]').remove()
|
|
|
|
# Make sure it's not hidden as we return.
|
|
# It would be hidden in the case where we're duplicating an
|
|
# already removed item for its template.
|
|
return $record.show()
|
|
|
|
applyIndexToItem: ($item, index) ->
|
|
collectionName = @options.collectionName
|
|
|
|
$item.find(@RELEVANT_INPUTS_SELECTOR).each (i, el) =>
|
|
|
|
$el = $(el)
|
|
|
|
idRegExp = new RegExp("_#{collectionName}_attributes_\\d+_")
|
|
idReplacement = "_#{collectionName}_attributes_#{index}_"
|
|
nameRegExp = new RegExp("\\[#{collectionName}_attributes\\]\\[\\d+\\]")
|
|
nameReplacement = "[#{collectionName}_attributes][#{index}]"
|
|
|
|
newID = $el.attr('id').replace(idRegExp, idReplacement) if $el.attr('id')
|
|
newName = $el.attr('name').replace(nameRegExp, nameReplacement)
|
|
|
|
$el.attr
|
|
id: newID
|
|
name: newName
|
|
|
|
$item.find('label[for]').each (i, el) =>
|
|
$el = $(el)
|
|
try
|
|
forRegExp = new RegExp("_#{collectionName}_attributes_\\d+_")
|
|
forReplacement = "_#{collectionName}_attributes_#{index}_"
|
|
newFor = $el.attr('for').replace(forRegExp, forReplacement)
|
|
$el.attr('for', newFor)
|
|
catch error
|
|
console.log "Error updating label", error
|
|
|
|
return $item
|
|
|
|
hideIfAlreadyDestroyed: ($item) ->
|
|
$destroyField = $item.find("[name$='[_destroy]']")
|
|
if $destroyField.length && $destroyField.val() == "true"
|
|
@destroy $item
|
|
|
|
# Hides a item from the user and marks it for deletion in the
|
|
# DOM by setting _destroy to true if the record already exists. If it
|
|
# is a new escalation, we simple delete the item
|
|
destroyClick: (event) =>
|
|
event.preventDefault()
|
|
@destroy $(event.target).parentsUntil(@$container).last()
|
|
|
|
destroy: ($item) ->
|
|
# If you're about to delete the last one,
|
|
# cache a clone of it first so we have something to show
|
|
# the next time user hits add
|
|
@$restorableClone = @extractClone() unless @$items.length-1
|
|
|
|
index = @indexForItem($item)
|
|
itemIsNew = $item.find('input[name$="\\[id\\]"]').length == 0
|
|
|
|
if (@options.beforeDestroy)
|
|
|
|
# Stop the destroy process if the callback returns false
|
|
return false if !@options.beforeDestroy.call(undefined, $item, index, itemIsNew)
|
|
|
|
# Add a blank item row if none are visible after this deletion
|
|
@addItem() unless @$items.filter(':visible').length-1
|
|
|
|
if itemIsNew
|
|
|
|
$item.remove()
|
|
|
|
else
|
|
|
|
# Hide the item
|
|
$item.hide()
|
|
|
|
# Add the _destroy field
|
|
otherFieldName = $item.find(':input[name]:first').attr('name')
|
|
attributePosition = otherFieldName.lastIndexOf('[')
|
|
destroyFieldName = "#{otherFieldName.substring(0, attributePosition)}[_destroy]"
|
|
# First look for an existing _destroy field
|
|
$destroyField = $item.find("input[name='#{destroyFieldName}']")
|
|
# If it doesn't exist, create it
|
|
if $destroyField.length == 0
|
|
$destroyField = $("<input type=\"hidden\" name=\"#{destroyFieldName}\" />")
|
|
$item.append($destroyField)
|
|
$destroyField.val(true).change()
|
|
|
|
@options.afterDestroy.call($item, index, itemIsNew) if (@options.afterDestroy)
|
|
|
|
# Remove this item from the items list
|
|
@refreshItems()
|
|
|
|
# Rename the remaining items
|
|
@resetIndexes()
|
|
|
|
indexForItem: ($item) ->
|
|
regExp = new RegExp("\\[#{@options.collectionName}_attributes\\]\\[\\d+\\]")
|
|
name = $item.find("#{@RELEVANT_INPUTS_SELECTOR}:first").attr('name')
|
|
return parseInt(name.match(regExp)[0].split('][')[1].slice(0, -1), 10)
|
|
|
|
refreshItems: ->
|
|
@$items = @$container.children()
|
|
|
|
# Sets the proper association indices and labels to all items
|
|
# Used when removing items
|
|
resetIndexes: ->
|
|
@$items.each (i, el) =>
|
|
$el = $(el)
|
|
|
|
# Make sure this is actually a new position
|
|
oldIndex = @indexForItem($el)
|
|
return true if (i == oldIndex)
|
|
|
|
@options.beforeMove.call($el, i, oldIndex) if (@options.beforeMove)
|
|
|
|
# Change the number to the new index
|
|
@applyIndexToItem($el, i)
|
|
|
|
@options.afterMove.call($el, i, oldIndex) if (@options.afterMove)
|
|
|
|
bindDestroy: ($item) ->
|
|
$item.find(@options.destroySelector).click(@destroyClick) if (@options.destroySelector)
|