/** * @file * js for taxonomy ui tweaks */ (function ($, Drupal, window, document, undefined) { function TaxonomyWranglerUID() { var prefix = 'taxonomy-wrangler-'; var randomId = prefix + this.count; while($('#' + randomId).length) { this.count++; randomId = prefix + this.count; } return randomId; } TaxonomyWranglerUID.count = 0; function TaxonomyWranglerAccordion(table) { this.table = table; this.rows = $('tbody tr', table); this.init(); } TaxonomyWranglerAccordion.instances = {}; var ctrlDown = false; $(window).keydown(function(e) { if (e.keyCode == 17) { ctrlDown = true; } }).keyup(function(e) { if (e.keyCode == 17) { ctrlDown = false; } }); TaxonomyWranglerAccordion.prototype.init = function() { var accordion = this; $('tbody tr').not(':eq(0)').each(function(){ var $row = $(this); accordion.collapseTree($row); accordion.attachAccordionEvents($row); }); }; TaxonomyWranglerAccordion.prototype.attachAccordionEvents = function($row) { var accordion = this; $row.find('.taxonomy-wrangler-accordion-item').bind('click.tweaksAccordion', function(e){ e.preventDefault(); if (ctrlDown) { accordion.toggleTree($row); } else { accordion.toggleRow($row); } return false; }); } TaxonomyWranglerAccordion.prototype.refreshRows = function(){ var accordion = this; this.rows.not(':eq(0)').each(function(){ var $this = $(this); var hasChildren = accordion.rows.filter('[data-parent="'+ $this.data('tid') +'"]').length > 0 ? true : false; var $collapseLink = $this.find('.taxonomy-wrangler-accordion-item--parent'); //create a new parent if (hasChildren && !$collapseLink.length) { $this.find('.tabledrag-handle').after(''); accordion.attachAccordionEvents($this); } else if (!hasChildren && $collapseLink.length) { $collapseLink.remove(); } }); }; /** * This is a bit different than a typical accordion in that the * data structure is hierarchical, but its representation in the DOM is flat */ TaxonomyWranglerAccordion.prototype.showRow = function($row) { $row.css('display', 'table-row').removeClass('tabledrag-hidden'); } TaxonomyWranglerAccordion.prototype.hideRow = function($row) { $row.css('display', 'none').addClass('tabledrag-hidden'); } TaxonomyWranglerAccordion.prototype.setCollapsed = function($row, collapsed, recurse) { var accordion = this; var $children = this.rows.filter('[data-parent=' + $row.data('tid') + ']'); if (collapsed && $children.length) { $row.data('is-collapsed', true); $row.find('.taxonomy-wrangler-accordion-item').addClass('is-collapsed'); if ($children.length) { $row.addClass('tabledrag-leaf'); } } else { $row.data('is-collapsed', false).removeClass('tabledrag-leaf'); $row.find('.taxonomy-wrangler-accordion-item').removeClass('is-collapsed'); } if (recurse) { accordion.rows.filter('[data-parent=' + $row.data('tid') + ']').each(function(){ accordion.setCollapsed($(this), collapsed, true); }); } } /* * Hide all children */ TaxonomyWranglerAccordion.prototype.hideChildren = function($row) { var $item = $row.find('.taxonomy-wrangler-accordion-item'); var accordion = this; this.rows.filter('[data-parent=' + $row.data('tid') + ']').each(function(){ accordion.hideRow($(this)); accordion.hideChildren($(this)); }); }; /** * Show children if they are not collapsed */ TaxonomyWranglerAccordion.prototype.showChildren = function($row) { var accordion = this; accordion.rows.filter('[data-parent=' + $row.data('tid') + ']').each(function(){ accordion.showRow($(this)); if (!$(this).data('is-collapsed')) { accordion.showChildren($(this)); } }); }; TaxonomyWranglerAccordion.prototype.toggleRow = function($row) { if ($row.data('is-collapsed')) { this.setCollapsed($row, false); this.showChildren($row); } else { this.setCollapsed($row, true); this.hideChildren($row); } }; TaxonomyWranglerAccordion.prototype.expandTree = function($row) { this.setCollapsed($row, false, true); this.showChildren($row); }; TaxonomyWranglerAccordion.prototype.collapseTree = function($row) { this.setCollapsed($row, true, true); this.hideChildren($row); }; TaxonomyWranglerAccordion.prototype.toggleTree = function($row) { if ($row.data('is-collapsed')) { this.expandTree($row); } else { this.collapseTree($row); } } Drupal.behaviors.taxonomyWranglerAccordion = { attach: function(context, settings) { idCount = 0; $('.taxonomy-wrangler-accordion', context) .not('.taxonomy-wrangler-accordion-processed') .addClass('taxonomy-wrangler-accordion-processed') .each(function(){ var id = $(this).attr('id') if (!!!id || id == '') { $(this).attr('id', TaxonomyWranglerUID()); } TaxonomyWranglerAccordion.instances[id] = new TaxonomyWranglerAccordion(this); }); } }; function TaxonomyWranglerCollection() { this.updatedTerms = {}; }; TaxonomyWranglerCollection.prototype.updateTerm = function(row) { var $row = $(row); var tid = $row.data('tid'); var now = Date.now(); this.updatedTerms[tid] = { "tid": $row.data('tid'), "parent": $row.data('parent'), "depth": $row.data('depth'), "weight": $row.data('weight'), "updated": now } //el.attr('data-{key}') & el.data('key') are not the same! $row.attr('data-parent', $row.data('parent')) .attr('data-depth', $row.data('depth')) .attr('data-weight', $row.data('weight')) .data('updated', now); }; /** * New TableDrag "class" that inherits Drupal.tableDrag */ function TaxonomyWranglerDrag(table, tableSettings) { Drupal.tableDrag.call(this, table, tableSettings); } TaxonomyWranglerDrag.prototype = new Drupal.tableDrag(); TaxonomyWranglerDrag.instances = {}; /** * Find the target used within a particular row and group. * This isn't strictly necessary in this case because the data * is stored on the row element itself. * Copied and modified from misc/tabledrag.js */ TaxonomyWranglerDrag.prototype.rowSettings = function (group, row) { for (var delta in this.tableSettings[group]) { // Return a copy of the row settings. var rowSettings = {}; for (var n in this.tableSettings[group][delta]) { rowSettings[n] = this.tableSettings[group][delta][n]; } return rowSettings; } }; /** * After the row is dropped, update the table fields according to the settings * set for this table. * * @param changedRow * DOM object for the row that was just dropped. */ TaxonomyWranglerDrag.prototype.updateFields = function (changedRow) { var $c = $(changedRow); for (var group in this.tableSettings) { // Each group may have a different setting for relationship, so we find // the source rows for each separately. this.updateField(changedRow, group); } }; /** * After the row is dropped, update a single table field according to specific * settings. This code is cribbed from tabledrag.js and modified to * use the data properties on the row object rather than input fields in the row. * * @param changedRow * DOM object for the row that was just dropped. * @param group * The settings group on which field updates will occur. */ TaxonomyWranglerDrag.prototype.updateField = function (changedRow, group) { var rowSettings = this.rowSettings(group, changedRow); // Set the row as its own target. if (rowSettings.relationship == 'self' || rowSettings.relationship == 'group') { var sourceRow = changedRow; } // Siblings are easy, check previous and next rows. else if (rowSettings.relationship == 'sibling') { var previousRow = $(changedRow).prev('tr').get(0); var nextRow = $(changedRow).next('tr').get(0); var sourceRow = changedRow; if ($(previousRow).is('.draggable') && $('.' + group, previousRow).length) { if (this.indentEnabled) { if ($('.indentations', previousRow).length == $('.indentations', changedRow)) { sourceRow = previousRow; } } else { sourceRow = previousRow; } } else if ($(nextRow).is('.draggable') && $('.' + group, nextRow).length) { if (this.indentEnabled) { if ($('.indentations', nextRow).length == $('.indentations', changedRow)) { sourceRow = nextRow; } } else { sourceRow = nextRow; } } } // Parents, look up the tree until we find a field not in this group. // Go up as many parents as indentations in the changed row. else if (rowSettings.relationship == 'parent') { var previousRow = $(changedRow).prev('tr'); while (previousRow.length && $('.indentation', previousRow).length >= this.rowObject.indents) { previousRow = previousRow.prev('tr'); } // If we found a row. if (previousRow.length) { sourceRow = previousRow[0]; } // Otherwise we went all the way to the left of the table without finding // a parent, meaning this item has been placed at the root level. else { // Use the first row in the table as source, because it's guaranteed to // be at the root level. Find the first item, then compare this row // against it as a sibling. sourceRow = $(this.table).find('tr.draggable:first').get(0); if (sourceRow == this.rowObject.element) { sourceRow = $(this.rowObject.group[this.rowObject.group.length - 1]).next('tr.draggable').get(0); } var useSibling = true; } } // Because we may have moved the row from one category to another, // take a look at our sibling and borrow its sources and targets. this.copyDragClasses(sourceRow, changedRow, group); rowSettings = this.rowSettings(group, changedRow); // In the case that we're looking for a parent, but the row is at the top // of the tree, copy our sibling's values. if (useSibling) { rowSettings.relationship = 'sibling'; rowSettings.source = rowSettings.target; } var $changedRow = $(changedRow); // Check if data exists in this row. if (typeof $(sourceRow).data(rowSettings.target) !== 'undefined') { switch (rowSettings.action) { case 'depth': // Get the depth of the target row. $changedRow.data(rowSettings.target, $('.indentation', $(sourceRow)).length); break; case 'match': // Update the value. $changedRow.data(rowSettings.target, $(sourceRow).data(rowSettings.source)); break; case 'order': var siblings = this.rowObject.findSiblings(rowSettings); // We're not using fields for this so we just assume // weight starting from 0 var weight = 0; $(siblings).each(function () { $(this).data('weight', weight); weight++; $(this).trigger('taxonomyWranglerTermDrag.rowDataChange', [this]); }); break; } } $changedRow.trigger('taxonomyWranglerTermDrag.rowDataChange', [changedRow]); }; TaxonomyWranglerDrag.prototype.dropRow = function (event, self) { // Drop row functionality shared between mouseup and blur events. if (self.rowObject != null) { var droppedRow = self.rowObject.element; // The row is already in the right place so we just release it. if (self.rowObject.changed == true) { // Update the fields in the dropped row. self.updateFields(droppedRow); // If a setting exists for affecting the entire group, update all the // fields in the entire dragged group. for (var group in self.tableSettings) { var rowSettings = self.rowSettings(group, droppedRow); if (rowSettings.relationship == 'group') { for (var n in self.rowObject.children) { self.updateField(self.rowObject.children[n], group); } } } self.rowObject.markChanged(); if (self.changed == false) { self.changed = true; } } if (self.indentEnabled) { self.rowObject.removeIndentClasses(); } if (self.oldRowElement) { $(self.oldRowElement).removeClass('drag-previous'); } $(droppedRow).removeClass('drag').addClass('drag-previous'); self.oldRowElement = droppedRow; self.onDrop(); self.rowObject = null; } // Functionality specific only to mouseup event. if (self.dragObject != null) { $('.tabledrag-handle', droppedRow).removeClass('tabledrag-handle-hover'); self.dragObject = null; $('body').removeClass('drag'); clearInterval(self.scrollInterval); // Hack for IE6 that flickers uncontrollably if select lists are moved. if (navigator.userAgent.indexOf('MSIE 6.') != -1) { $('select', this.table).css('display', 'block'); } } }; /** * Indent a row within the legal bounds of the table. * * @param indentDiff * The number of additional indentations proposed for the row (can be * positive or negative). This number will be adjusted to nearest valid * indentation level for the row. */ TaxonomyWranglerDrag.prototype.row.prototype.indent = function (indentDiff) { // Determine the valid indentations interval if not available yet. if (!this.interval) { var prevRow = $(this.element).prevAll('tr:not(.tabledrag-hidden)').get(0); var nextRow = $(this.group).filter(':last').nextAll('tr:not(.tabledrag-hidden)').get(0); this.interval = this.validIndentInterval(prevRow, nextRow); } // Adjust to the nearest valid indentation. var indent = this.indents + indentDiff; indent = Math.max(indent, this.interval.min); indent = Math.min(indent, this.interval.max); indentDiff = indent - this.indents; for (var n = 1; n <= Math.abs(indentDiff); n++) { // Add or remove indentations. if (indentDiff < 0) { $('.indentation:first', this.group).remove(); this.indents--; } else { $('td:first', this.group).prepend(Drupal.theme('tableDragIndentation')); this.indents++; } } if (indentDiff) { // Update indentation for this row. this.changed = true; this.groupDepth += indentDiff; this.onIndent(); } return indentDiff; }; /** * Perform the swap between two rows. Take into account the collapsed * accordion rows when swapping after. * * @param position * Whether the swap will occur 'before' or 'after' the given row. * @param row * DOM element what will be swapped with the row group. */ TaxonomyWranglerDrag.prototype.row.prototype.swap = function (position, row) { var swapRow = row; if (position == 'after') { var $row = $(row); if ($row.data('is-collapsed')) { var $target = $row.nextUntil(":visible"); if ($target.length) { swapRow = $target.get($target.length - 1); } else { // If there are no other rows visible, we must be at the bottom // of the table, get the last row. swapRow = $row.nextAll(":last").get(0); } } } Drupal.detachBehaviors(this.group, Drupal.settings, 'move'); $(swapRow)[position](this.group); Drupal.attachBehaviors(this.group, Drupal.settings); this.changed = true; this.onSwap(row); }; /** * Ensure that two rows are allowed to be swapped. * * @param row * DOM object for the row being considered for swapping. */ TaxonomyWranglerDrag.prototype.row.prototype.isValidSwap = function (row) { if (this.indentEnabled) { var prevRow, nextRow; if (this.direction == 'down') { prevRow = row; nextRow = $(row).nextAll('tr:not(:hidden)').get(0); } else { prevRow = $(row).prevAll('tr:not(:hidden)').get(0); nextRow = row; } this.interval = this.validIndentInterval(prevRow, nextRow); // We have an invalid swap if the valid indentations interval is empty. if (this.interval.min > this.interval.max) { return false; } } // Do not let an un-draggable first row have anything put before it. if (this.table.tBodies[0].rows[0] == row && $(row).is(':not(.draggable)')) { return false; } return true; }; /** * Move a block in the blocks table from one region to another via select list. * * This behavior is dependent on the tableDrag behavior, since it uses the * objects initialized in that behavior to update the row. */ Drupal.behaviors.taxonomyWranglerTermDrag = { attach: function (context, settings) { for (var base in settings.taxonomyWrangler.tables) { if (typeof TaxonomyWranglerDrag.instances[base] == 'undefined') { $('#' + base, context).once('tabledrag', function () { // Create the new tableDrag instance. Save in the Drupal variable // to allow other scripts access to the object. TaxonomyWranglerDrag.instances[base] = new TaxonomyWranglerDrag(this, settings.taxonomyWrangler.tables[base]); }); } } var table = $('#taxonomy-wrangler', context); var tableDrag = TaxonomyWranglerDrag.instances['taxonomy-wrangler']; // Get the blocks tableDrag object. var $messages = $('#taxonomy-wrangler-messages-js', context); // Compare server timestamp with javascript changed stamp and remove changed classes. if (settings.taxonomyWrangler && settings.taxonomyWrangler.updatedTimestamps) { $.each(settings.taxonomyWrangler.updatedTimestamps, function (tid, stamp) { var $row = table.find('tr[data-tid="'+ tid +'"]'); if ($row.data('updated') == stamp) { $row.removeClass('drag-previous').find('span.tabledrag-changed').remove(); } }); } if (tableDrag.tm === null) { $messages.removeClass('is-busy').empty(); } if (!table.hasClass('taxonomy-wrangler-processed')) { table.addClass('taxonomy-wrangler-processed'); var rows = $('tr', table).length; var $termData = $('input[name="term_data"]', context); var $submit = $('input[name="op"][value="Save"]', context) var $rows = $('tr', table); tableDrag.collection = new TaxonomyWranglerCollection(); var updatedTerms = {}; $submit.attr('disabled', 'disabled'); var accordion = !!TaxonomyWranglerAccordion.instances[table.attr('id')] ? TaxonomyWranglerAccordion.instances[table.attr('id')] : null; $rows.bind('taxonomyWranglerTermDrag.rowDataChange', function(e, row){ tableDrag.collection.updateTerm(row); var termsArray = []; var tids = []; accordion.refreshRows(); $.each(tableDrag.collection.updatedTerms, function(i, val) { termsArray.push(val); tids.push(val.tid + ':' + val.weight); }); $termData.val(JSON.stringify({ "termData": termsArray })); if (tableDrag.tm) { clearTimeout(tableDrag.tm); tableDrag.tm = null; } tableDrag.tm = setTimeout(function(){ $submit.removeAttr('disabled').trigger('mousedown').attr('disabled', 'disabled'); tableDrag.tm = null; }, 1000); if (!$messages.hasClass('is-busy')) { $messages.addClass('is-busy').append('
 
Updating terms
'); } }); } } }; })(jQuery, Drupal, this, this.document);