(function($) { // Global plugin settings. var settings = null; /* Compares the first number found in a pair of strings in order to determine sort order. * @a - The first number. * @b - The second number. */ function compareFirstNumber(a, b) { a = new Number(a.match(/\d+/)); b = new Number(b.match(/\d+/)); if (a < b) { return -1; } if (a > b) { return 1; } return 0; } /* Chops off the last string segment designated by delimiter. * If no delimiter is found then the original string is returned instead. * @string - Required. The string to chop. * @delimiter - Optional. The delimiter used to chop up the string. Defaults to '_'. */ function stringChop(string, delimiter) { var chopped = string; if (delimiter == undefined) { delimiter = '_'; } var endIndex = string.lastIndexOf(delimiter); if (endIndex > 1) {chopped = string.slice(0, endIndex);} return chopped; } /* Answers the ID found in the string based off of a certain delimiter. * @string - Required. The string to obtain the ID from. * @delimiter - Optional. The delimiter used to distinquish the ID from the string. Defaults to: '_'. */ function getId(string, delimiter) { var id = string; if (delimiter == undefined) { delimiter = '_'; } var endIndex = string.lastIndexOf(delimiter) + 1; if (endIndex < string.length) {id = string.slice(endIndex);} return id; } /* Replaces an existing number within a string with a new number based on position in the string (either first or last postion). * @string - The string for which to replace an old number with a new number. * @newNumber - The new number to be replaced in the string. * @position - The position of the old number in the string to be replaced. */ function replaceNumber(string, newNumber, position) { if (string != undefined) { if (position == undefined) { position = 0; } var numbers = string.match(/\d+/g); if (numbers != null && numbers.length > 1) { var oldNumber; var index; switch(position) { case 0: oldNumber = numbers[0]; index = string.indexOf(oldNumber); break; case 1: oldNumber = numbers.reverse()[0]; index = string.lastIndexOf(oldNumber); break; } var prefix = string.substring(0, index); var suffix = string.substring(index + oldNumber.length, string.length); string = prefix + newNumber + suffix; } } return string; } /* Increments the first or last number in a string (if any, defaults to "first"). * If no number is found then the original string is returned. * @string - The string to manipulate. * @position - The position of the number (first or last) to increment. */ function incrementNumber(string, position) { if (string != undefined) { if (position == undefined) { position = "first"; } var numbers = string.match(/\d+/g); if (numbers != null) { var oldNumber; var newNumber; var index; switch(position) { case "first": oldNumber = numbers[0]; newNumber = new Number(oldNumber) + 1; index = string.indexOf(oldNumber); break; case "last": oldNumber = numbers.reverse()[0]; newNumber = new Number(oldNumber) + 1; index = string.lastIndexOf(oldNumber); break; } var prefix = string.substring(0, index); var suffix = string.substring(index + oldNumber.length, string.length); string = prefix + newNumber + suffix; } } return string; } /* Builds an array of all body element attributes (if not already set). * @attribute - The attribute to find. */ function buildBodyAttributes(attribute) { bodyAttributes = $.map($("body").find('[' + attribute + ']'), function(element) { return $(element).attr(attribute); }); return bodyAttributes; } /* Makes an array of attributes unique by comparing each attribute in the array against all body element attributes. * @attributes - The attributes to make unique. * @attribute - The attribute to search for. * @index - The starting index in the arravy of attributes. */ function uniquifyAttributes(attributes, attribute, index) { if (index < attributes.length) { var numbers = attributes[index].match(/\d+/g); var strings = attributes[index].split(/\d+/); if (numbers != null && numbers.length == 1) { var constant = attributes[index].split(/\d+/).join('-'); var familiars = []; var bodyAttributes = buildBodyAttributes(attribute); for (x in bodyAttributes) { var current = bodyAttributes[x].split(/\d+/).join('-'); if (current == constant) { familiars.push(bodyAttributes[x]); } } var last = incrementNumber(familiars.sort(compareFirstNumber).pop()); attributes.splice(index, 1, last); bodyAttributes.push(last); } index++; uniquifyAttributes(attributes, attribute, index); } bodyAttributes = null; return attributes; } /* Ensures all child element attibutes are unique for a given root element. * @element - TODO... * @attribute - TODO... * @selector - TODO... */ function uniquifyChildren(element, attribute, selector) { // Acquire the child elements. var children = $(element).find(selector); var attributes = []; // Extract the attributes from the child elements. children.each(function() { attributes.push($(this).attr(attribute)); }); // Make the attributes unique. attributes = uniquifyAttributes(attributes, attribute, 0); // Update the child elements with the unique attributes. for (x in attributes) { $(children[x]).attr(attribute, attributes[x]); } } /* Generates a hidden input field based off original input field data that instructs ActiveRecord to delete * a record based on the ID of the input field. * @input - The input element from which to build the deletion input element. */ function generateDestroyInput(input) { if (input == undefined || $(input).length) { $(input).attr("id", stringChop($(input).attr("id")) + "__delete"); $(input).attr("name", stringChop($(input).attr("name"), '[') + "[_delete]"); $(input).val(1); return input; } else { return null; } } /* Animates the deletion of a DOM element. Defaults to simply fading and hiding the element from view but * can be forced to remove the element from the DOM completely. * @element - The element to destroy. * @remove - Boolean for whether to remove or just hide the element. */ function animateDestroy(element, remove) { if (remove == undefined) { remove = false; } $(element).fadeOut(500, function() { if (remove) { $(this).remove(); } }); } /* Executes the new action. * @element - The element that spawned the new action. */ function newInputAction(element) { // Outer group ID var ogid = '#' + $(element).attr("data-ogid"); // Inner group ID var igid = '#' + $(element).attr("data-igid"); // Position var position = $(element).attr("data-position"); if ($(ogid).hasClass("hidden")) { $(ogid).removeAttr("style").fadeIn(500, function() { $(this).removeClass("hidden"); }); // Delete hidden metadata (if any). $(settings.recordSelector + ":visible input:hidden[id$=__delete]").remove(); } else { var count = $(igid).children(settings.recordSelector + ":visible").size(); var record = $(igid).children(settings.recordSelector + ":visible:last").clone(true); record.attr("id", uniquifyAttributes([record.attr("id")], "id", 0).toString()); $(igid).append(record); // Remove excess cloned records. var records = $.makeArray($(record).find(settings.recordSelector)); if (records.length > 1) { records.shift(); for (x in records) { $(records[x]).remove(); } } // Ensure the cloned children are unique. uniquifyChildren(record, "id", "[id]:not([id*=attributes])"); uniquifyChildren(record, "for", "[for]"); uniquifyChildren(record, "data-ogid", "[data-ogid]"); uniquifyChildren(record, "data-igid", "[data-igid]"); // Increment the cloned, nested children. $(record).find("[id*=attributes]").each(function() { if (position == 0) { $(this).attr("id", replaceNumber($(this).attr("id"), count)); } $(this).attr("id", incrementNumber($(this).attr("id"), "last")); return this; }); $(record).find("[name*=attributes]").each(function() { if (position == 0) { $(this).attr("name", replaceNumber($(this).attr("name"), count)); } $(this).attr("name", incrementNumber($(this).attr("name"), "last")); return this; }); // Clear inputs. $(record).find("input:visible").val(''); $(record).find("select:visible").val(''); // Delete hidden metadata. $(record).find("input:hidden[id$=_id]").remove(); } return false; } // Executes the destroy action. function destroyAction(element, message) { var result = confirm(message); if (result) { $.post($(element).attr("href"), "_method=delete"); animateDestroy($(element).closest(settings.recordSelector + ":visible")); } return false; } // Executes the destroy input action. function destroyInputAction(element) { var group = $(element).closest(settings.groupSelector); var record = $(element).closest(settings.recordSelector); // Create hidden deletion input from original identifier input so Rails knows to delete the record. // NOTE: The following three lines are a workaround to a Safari and IE bug where the hidden input // is not found using: $(record).prev("input"); var idName = stringChop($(record).attr("id")); var idNumber = getId($(record).attr("id")); var hiddenInput = $(group).find("input:hidden").filter("[name$=[id]][value=" + idNumber + ']'); // $("#rule_1_table").find("input:hidden").filter("[name$=[id]][value=6]"); if (hiddenInput.length > 0) { $(record).prepend(generateDestroyInput($(hiddenInput).clone())); } // Remove the record from view or hide entire group if only one record left. if ($(group).find(".record:visible").size() > 1) { if (hiddenInput.length > 0) { animateDestroy(record); } else { animateDestroy(record, true); } } else { $(record).find("input:visible").val(''); $(record).find("select:visible").val(''); $(group).fadeOut(500, function() { $(group).addClass("hidden"); }); } return false; } // Plugin $.rest = function(options) { settings = $.extend(true, {}, $.rest.defaults, options); // New $(settings.newSelector).live("click", function() { return $(this).attr("data-type") == "input" ? newInputAction(this) : true; }); // Edit $(settings.editSelector).live("click", function() { // TODO - Need to supply confirm/worning dialog here. return true; }); // Destroy $(settings.destroySelector).live("click", function(event) { return $(event.target).attr("data-type") == "input" ? destroyInputAction(this) : destroyAction(this, settings.destroyConfirm); }); return false; }; // Plugin Public Defaults $.rest.defaults = { newSelector: "a.new", editSelector: "a.edit", destroySelector: "a.destroy", destroyConfirm: "Are you sure you want to delete this?", groupSelector: ".group", recordSelector: ".record" }; }) (jQuery);