taxonomy_wrangler.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  1. /**
  2. * @file
  3. * js for taxonomy ui tweaks
  4. */
  5. (function ($, Drupal, window, document, undefined) {
  6. function TaxonomyWranglerUID() {
  7. var prefix = 'taxonomy-wrangler-';
  8. var randomId = prefix + this.count;
  9. while($('#' + randomId).length) {
  10. this.count++;
  11. randomId = prefix + this.count;
  12. }
  13. return randomId;
  14. }
  15. TaxonomyWranglerUID.count = 0;
  16. function TaxonomyWranglerAccordion(table) {
  17. this.table = table;
  18. this.rows = $('tbody tr', table);
  19. this.init();
  20. }
  21. TaxonomyWranglerAccordion.instances = {};
  22. var ctrlDown = false;
  23. $(window).keydown(function(e) {
  24. if (e.keyCode == 17) {
  25. ctrlDown = true;
  26. }
  27. }).keyup(function(e) {
  28. if (e.keyCode == 17) {
  29. ctrlDown = false;
  30. }
  31. });
  32. TaxonomyWranglerAccordion.prototype.init = function() {
  33. var accordion = this;
  34. $('tbody tr').not(':eq(0)').each(function(){
  35. var $row = $(this);
  36. accordion.collapseTree($row);
  37. accordion.attachAccordionEvents($row);
  38. });
  39. };
  40. TaxonomyWranglerAccordion.prototype.attachAccordionEvents = function($row) {
  41. var accordion = this;
  42. $row.find('.taxonomy-wrangler-accordion-item').bind('click.tweaksAccordion', function(e){
  43. e.preventDefault();
  44. if (ctrlDown) {
  45. accordion.toggleTree($row);
  46. }
  47. else {
  48. accordion.toggleRow($row);
  49. }
  50. return false;
  51. });
  52. }
  53. TaxonomyWranglerAccordion.prototype.refreshRows = function(){
  54. var accordion = this;
  55. this.rows.not(':eq(0)').each(function(){
  56. var $this = $(this);
  57. var hasChildren = accordion.rows.filter('[data-parent="'+ $this.data('tid') +'"]').length > 0 ? true : false;
  58. var $collapseLink = $this.find('.taxonomy-wrangler-accordion-item--parent');
  59. //create a new parent
  60. if (hasChildren && !$collapseLink.length) {
  61. $this.find('.tabledrag-handle').after('<a href="#" class="taxonomy-wrangler-accordion-item taxonomy-wrangler-accordion-item--parent"><span></span></a>');
  62. accordion.attachAccordionEvents($this);
  63. }
  64. else if (!hasChildren && $collapseLink.length) {
  65. $collapseLink.remove();
  66. }
  67. });
  68. };
  69. /**
  70. * This is a bit different than a typical accordion in that the
  71. * data structure is hierarchical, but its representation in the DOM is flat
  72. */
  73. TaxonomyWranglerAccordion.prototype.showRow = function($row) {
  74. $row.css('display', 'table-row').removeClass('tabledrag-hidden');
  75. }
  76. TaxonomyWranglerAccordion.prototype.hideRow = function($row) {
  77. $row.css('display', 'none').addClass('tabledrag-hidden');
  78. }
  79. TaxonomyWranglerAccordion.prototype.setCollapsed = function($row, collapsed, recurse) {
  80. var accordion = this;
  81. var $children = this.rows.filter('[data-parent=' + $row.data('tid') + ']');
  82. if (collapsed && $children.length) {
  83. $row.data('is-collapsed', true);
  84. $row.find('.taxonomy-wrangler-accordion-item').addClass('is-collapsed');
  85. if ($children.length) {
  86. $row.addClass('tabledrag-leaf');
  87. }
  88. }
  89. else {
  90. $row.data('is-collapsed', false).removeClass('tabledrag-leaf');
  91. $row.find('.taxonomy-wrangler-accordion-item').removeClass('is-collapsed');
  92. }
  93. if (recurse) {
  94. accordion.rows.filter('[data-parent=' + $row.data('tid') + ']').each(function(){
  95. accordion.setCollapsed($(this), collapsed, true);
  96. });
  97. }
  98. }
  99. /*
  100. * Hide all children
  101. */
  102. TaxonomyWranglerAccordion.prototype.hideChildren = function($row) {
  103. var $item = $row.find('.taxonomy-wrangler-accordion-item');
  104. var accordion = this;
  105. this.rows.filter('[data-parent=' + $row.data('tid') + ']').each(function(){
  106. accordion.hideRow($(this));
  107. accordion.hideChildren($(this));
  108. });
  109. };
  110. /**
  111. * Show children if they are not collapsed
  112. */
  113. TaxonomyWranglerAccordion.prototype.showChildren = function($row) {
  114. var accordion = this;
  115. accordion.rows.filter('[data-parent=' + $row.data('tid') + ']').each(function(){
  116. accordion.showRow($(this));
  117. if (!$(this).data('is-collapsed')) {
  118. accordion.showChildren($(this));
  119. }
  120. });
  121. };
  122. TaxonomyWranglerAccordion.prototype.toggleRow = function($row) {
  123. if ($row.data('is-collapsed')) {
  124. this.setCollapsed($row, false);
  125. this.showChildren($row);
  126. }
  127. else {
  128. this.setCollapsed($row, true);
  129. this.hideChildren($row);
  130. }
  131. };
  132. TaxonomyWranglerAccordion.prototype.expandTree = function($row) {
  133. this.setCollapsed($row, false, true);
  134. this.showChildren($row);
  135. };
  136. TaxonomyWranglerAccordion.prototype.collapseTree = function($row) {
  137. this.setCollapsed($row, true, true);
  138. this.hideChildren($row);
  139. };
  140. TaxonomyWranglerAccordion.prototype.toggleTree = function($row) {
  141. if ($row.data('is-collapsed')) {
  142. this.expandTree($row);
  143. }
  144. else {
  145. this.collapseTree($row);
  146. }
  147. }
  148. Drupal.behaviors.taxonomyWranglerAccordion = {
  149. attach: function(context, settings) {
  150. idCount = 0;
  151. $('.taxonomy-wrangler-accordion', context)
  152. .not('.taxonomy-wrangler-accordion-processed')
  153. .addClass('taxonomy-wrangler-accordion-processed')
  154. .each(function(){
  155. var id = $(this).attr('id')
  156. if (!!!id || id == '') {
  157. $(this).attr('id', TaxonomyWranglerUID());
  158. }
  159. TaxonomyWranglerAccordion.instances[id] = new TaxonomyWranglerAccordion(this);
  160. });
  161. }
  162. };
  163. function TaxonomyWranglerCollection() {
  164. this.updatedTerms = {};
  165. };
  166. TaxonomyWranglerCollection.prototype.updateTerm = function(row) {
  167. var $row = $(row);
  168. var tid = $row.data('tid');
  169. var now = Date.now();
  170. this.updatedTerms[tid] = {
  171. "tid": $row.data('tid'),
  172. "parent": $row.data('parent'),
  173. "depth": $row.data('depth'),
  174. "weight": $row.data('weight'),
  175. "updated": now
  176. }
  177. //el.attr('data-{key}') & el.data('key') are not the same!
  178. $row.attr('data-parent', $row.data('parent'))
  179. .attr('data-depth', $row.data('depth'))
  180. .attr('data-weight', $row.data('weight'))
  181. .data('updated', now);
  182. };
  183. /**
  184. * New TableDrag "class" that inherits Drupal.tableDrag
  185. */
  186. function TaxonomyWranglerDrag(table, tableSettings) {
  187. Drupal.tableDrag.call(this, table, tableSettings);
  188. }
  189. TaxonomyWranglerDrag.prototype = new Drupal.tableDrag();
  190. TaxonomyWranglerDrag.instances = {};
  191. /**
  192. * Find the target used within a particular row and group.
  193. * This isn't strictly necessary in this case because the data
  194. * is stored on the row element itself.
  195. * Copied and modified from misc/tabledrag.js
  196. */
  197. TaxonomyWranglerDrag.prototype.rowSettings = function (group, row) {
  198. for (var delta in this.tableSettings[group]) {
  199. // Return a copy of the row settings.
  200. var rowSettings = {};
  201. for (var n in this.tableSettings[group][delta]) {
  202. rowSettings[n] = this.tableSettings[group][delta][n];
  203. }
  204. return rowSettings;
  205. }
  206. };
  207. /**
  208. * After the row is dropped, update the table fields according to the settings
  209. * set for this table.
  210. *
  211. * @param changedRow
  212. * DOM object for the row that was just dropped.
  213. */
  214. TaxonomyWranglerDrag.prototype.updateFields = function (changedRow) {
  215. var $c = $(changedRow);
  216. for (var group in this.tableSettings) {
  217. // Each group may have a different setting for relationship, so we find
  218. // the source rows for each separately.
  219. this.updateField(changedRow, group);
  220. }
  221. };
  222. /**
  223. * After the row is dropped, update a single table field according to specific
  224. * settings. This code is cribbed from tabledrag.js and modified to
  225. * use the data properties on the row object rather than input fields in the row.
  226. *
  227. * @param changedRow
  228. * DOM object for the row that was just dropped.
  229. * @param group
  230. * The settings group on which field updates will occur.
  231. */
  232. TaxonomyWranglerDrag.prototype.updateField = function (changedRow, group) {
  233. var rowSettings = this.rowSettings(group, changedRow);
  234. // Set the row as its own target.
  235. if (rowSettings.relationship == 'self' || rowSettings.relationship == 'group') {
  236. var sourceRow = changedRow;
  237. }
  238. // Siblings are easy, check previous and next rows.
  239. else if (rowSettings.relationship == 'sibling') {
  240. var previousRow = $(changedRow).prev('tr').get(0);
  241. var nextRow = $(changedRow).next('tr').get(0);
  242. var sourceRow = changedRow;
  243. if ($(previousRow).is('.draggable') && $('.' + group, previousRow).length) {
  244. if (this.indentEnabled) {
  245. if ($('.indentations', previousRow).length == $('.indentations', changedRow)) {
  246. sourceRow = previousRow;
  247. }
  248. }
  249. else {
  250. sourceRow = previousRow;
  251. }
  252. }
  253. else if ($(nextRow).is('.draggable') && $('.' + group, nextRow).length) {
  254. if (this.indentEnabled) {
  255. if ($('.indentations', nextRow).length == $('.indentations', changedRow)) {
  256. sourceRow = nextRow;
  257. }
  258. }
  259. else {
  260. sourceRow = nextRow;
  261. }
  262. }
  263. }
  264. // Parents, look up the tree until we find a field not in this group.
  265. // Go up as many parents as indentations in the changed row.
  266. else if (rowSettings.relationship == 'parent') {
  267. var previousRow = $(changedRow).prev('tr');
  268. while (previousRow.length && $('.indentation', previousRow).length >= this.rowObject.indents) {
  269. previousRow = previousRow.prev('tr');
  270. }
  271. // If we found a row.
  272. if (previousRow.length) {
  273. sourceRow = previousRow[0];
  274. }
  275. // Otherwise we went all the way to the left of the table without finding
  276. // a parent, meaning this item has been placed at the root level.
  277. else {
  278. // Use the first row in the table as source, because it's guaranteed to
  279. // be at the root level. Find the first item, then compare this row
  280. // against it as a sibling.
  281. sourceRow = $(this.table).find('tr.draggable:first').get(0);
  282. if (sourceRow == this.rowObject.element) {
  283. sourceRow = $(this.rowObject.group[this.rowObject.group.length - 1]).next('tr.draggable').get(0);
  284. }
  285. var useSibling = true;
  286. }
  287. }
  288. // Because we may have moved the row from one category to another,
  289. // take a look at our sibling and borrow its sources and targets.
  290. this.copyDragClasses(sourceRow, changedRow, group);
  291. rowSettings = this.rowSettings(group, changedRow);
  292. // In the case that we're looking for a parent, but the row is at the top
  293. // of the tree, copy our sibling's values.
  294. if (useSibling) {
  295. rowSettings.relationship = 'sibling';
  296. rowSettings.source = rowSettings.target;
  297. }
  298. var $changedRow = $(changedRow);
  299. // Check if data exists in this row.
  300. if (typeof $(sourceRow).data(rowSettings.target) !== 'undefined') {
  301. switch (rowSettings.action) {
  302. case 'depth':
  303. // Get the depth of the target row.
  304. $changedRow.data(rowSettings.target, $('.indentation', $(sourceRow)).length);
  305. break;
  306. case 'match':
  307. // Update the value.
  308. $changedRow.data(rowSettings.target, $(sourceRow).data(rowSettings.source));
  309. break;
  310. case 'order':
  311. var siblings = this.rowObject.findSiblings(rowSettings);
  312. // We're not using fields for this so we just assume
  313. // weight starting from 0
  314. var weight = 0;
  315. $(siblings).each(function () {
  316. $(this).data('weight', weight);
  317. weight++;
  318. $(this).trigger('taxonomyWranglerTermDrag.rowDataChange', [this]);
  319. });
  320. break;
  321. }
  322. }
  323. $changedRow.trigger('taxonomyWranglerTermDrag.rowDataChange', [changedRow]);
  324. };
  325. TaxonomyWranglerDrag.prototype.dropRow = function (event, self) {
  326. // Drop row functionality shared between mouseup and blur events.
  327. if (self.rowObject != null) {
  328. var droppedRow = self.rowObject.element;
  329. // The row is already in the right place so we just release it.
  330. if (self.rowObject.changed == true) {
  331. // Update the fields in the dropped row.
  332. self.updateFields(droppedRow);
  333. // If a setting exists for affecting the entire group, update all the
  334. // fields in the entire dragged group.
  335. for (var group in self.tableSettings) {
  336. var rowSettings = self.rowSettings(group, droppedRow);
  337. if (rowSettings.relationship == 'group') {
  338. for (var n in self.rowObject.children) {
  339. self.updateField(self.rowObject.children[n], group);
  340. }
  341. }
  342. }
  343. self.rowObject.markChanged();
  344. if (self.changed == false) {
  345. self.changed = true;
  346. }
  347. }
  348. if (self.indentEnabled) {
  349. self.rowObject.removeIndentClasses();
  350. }
  351. if (self.oldRowElement) {
  352. $(self.oldRowElement).removeClass('drag-previous');
  353. }
  354. $(droppedRow).removeClass('drag').addClass('drag-previous');
  355. self.oldRowElement = droppedRow;
  356. self.onDrop();
  357. self.rowObject = null;
  358. }
  359. // Functionality specific only to mouseup event.
  360. if (self.dragObject != null) {
  361. $('.tabledrag-handle', droppedRow).removeClass('tabledrag-handle-hover');
  362. self.dragObject = null;
  363. $('body').removeClass('drag');
  364. clearInterval(self.scrollInterval);
  365. // Hack for IE6 that flickers uncontrollably if select lists are moved.
  366. if (navigator.userAgent.indexOf('MSIE 6.') != -1) {
  367. $('select', this.table).css('display', 'block');
  368. }
  369. }
  370. };
  371. /**
  372. * Indent a row within the legal bounds of the table.
  373. *
  374. * @param indentDiff
  375. * The number of additional indentations proposed for the row (can be
  376. * positive or negative). This number will be adjusted to nearest valid
  377. * indentation level for the row.
  378. */
  379. TaxonomyWranglerDrag.prototype.row.prototype.indent = function (indentDiff) {
  380. // Determine the valid indentations interval if not available yet.
  381. if (!this.interval) {
  382. var prevRow = $(this.element).prevAll('tr:not(.tabledrag-hidden)').get(0);
  383. var nextRow = $(this.group).filter(':last').nextAll('tr:not(.tabledrag-hidden)').get(0);
  384. this.interval = this.validIndentInterval(prevRow, nextRow);
  385. }
  386. // Adjust to the nearest valid indentation.
  387. var indent = this.indents + indentDiff;
  388. indent = Math.max(indent, this.interval.min);
  389. indent = Math.min(indent, this.interval.max);
  390. indentDiff = indent - this.indents;
  391. for (var n = 1; n <= Math.abs(indentDiff); n++) {
  392. // Add or remove indentations.
  393. if (indentDiff < 0) {
  394. $('.indentation:first', this.group).remove();
  395. this.indents--;
  396. }
  397. else {
  398. $('td:first', this.group).prepend(Drupal.theme('tableDragIndentation'));
  399. this.indents++;
  400. }
  401. }
  402. if (indentDiff) {
  403. // Update indentation for this row.
  404. this.changed = true;
  405. this.groupDepth += indentDiff;
  406. this.onIndent();
  407. }
  408. return indentDiff;
  409. };
  410. /**
  411. * Perform the swap between two rows. Take into account the collapsed
  412. * accordion rows when swapping after.
  413. *
  414. * @param position
  415. * Whether the swap will occur 'before' or 'after' the given row.
  416. * @param row
  417. * DOM element what will be swapped with the row group.
  418. */
  419. TaxonomyWranglerDrag.prototype.row.prototype.swap = function (position, row) {
  420. var swapRow = row;
  421. if (position == 'after') {
  422. var $row = $(row);
  423. if ($row.data('is-collapsed')) {
  424. var $target = $row.nextUntil(":visible");
  425. if ($target.length) {
  426. swapRow = $target.get($target.length - 1);
  427. }
  428. else {
  429. // If there are no other rows visible, we must be at the bottom
  430. // of the table, get the last row.
  431. swapRow = $row.nextAll(":last").get(0);
  432. }
  433. }
  434. }
  435. Drupal.detachBehaviors(this.group, Drupal.settings, 'move');
  436. $(swapRow)[position](this.group);
  437. Drupal.attachBehaviors(this.group, Drupal.settings);
  438. this.changed = true;
  439. this.onSwap(row);
  440. };
  441. /**
  442. * Ensure that two rows are allowed to be swapped.
  443. *
  444. * @param row
  445. * DOM object for the row being considered for swapping.
  446. */
  447. TaxonomyWranglerDrag.prototype.row.prototype.isValidSwap = function (row) {
  448. if (this.indentEnabled) {
  449. var prevRow, nextRow;
  450. if (this.direction == 'down') {
  451. prevRow = row;
  452. nextRow = $(row).nextAll('tr:not(:hidden)').get(0);
  453. }
  454. else {
  455. prevRow = $(row).prevAll('tr:not(:hidden)').get(0);
  456. nextRow = row;
  457. }
  458. this.interval = this.validIndentInterval(prevRow, nextRow);
  459. // We have an invalid swap if the valid indentations interval is empty.
  460. if (this.interval.min > this.interval.max) {
  461. return false;
  462. }
  463. }
  464. // Do not let an un-draggable first row have anything put before it.
  465. if (this.table.tBodies[0].rows[0] == row && $(row).is(':not(.draggable)')) {
  466. return false;
  467. }
  468. return true;
  469. };
  470. /**
  471. * Move a block in the blocks table from one region to another via select list.
  472. *
  473. * This behavior is dependent on the tableDrag behavior, since it uses the
  474. * objects initialized in that behavior to update the row.
  475. */
  476. Drupal.behaviors.taxonomyWranglerTermDrag = {
  477. attach: function (context, settings) {
  478. for (var base in settings.taxonomyWrangler.tables) {
  479. if (typeof TaxonomyWranglerDrag.instances[base] == 'undefined') {
  480. $('#' + base, context).once('tabledrag', function () {
  481. // Create the new tableDrag instance. Save in the Drupal variable
  482. // to allow other scripts access to the object.
  483. TaxonomyWranglerDrag.instances[base] = new TaxonomyWranglerDrag(this, settings.taxonomyWrangler.tables[base]);
  484. });
  485. }
  486. }
  487. var table = $('#taxonomy-wrangler', context);
  488. var tableDrag = TaxonomyWranglerDrag.instances['taxonomy-wrangler']; // Get the blocks tableDrag object.
  489. var $messages = $('#taxonomy-wrangler-messages-js', context);
  490. // Compare server timestamp with javascript changed stamp and remove changed classes.
  491. if (settings.taxonomyWrangler && settings.taxonomyWrangler.updatedTimestamps) {
  492. $.each(settings.taxonomyWrangler.updatedTimestamps, function (tid, stamp) {
  493. var $row = table.find('tr[data-tid="'+ tid +'"]');
  494. if ($row.data('updated') == stamp) {
  495. $row.removeClass('drag-previous').find('span.tabledrag-changed').remove();
  496. }
  497. });
  498. }
  499. if (tableDrag.tm === null) {
  500. $messages.removeClass('is-busy').empty();
  501. }
  502. if (!table.hasClass('taxonomy-wrangler-processed')) {
  503. table.addClass('taxonomy-wrangler-processed');
  504. var rows = $('tr', table).length;
  505. var $termData = $('input[name="term_data"]', context);
  506. var $submit = $('input[name="op"][value="Save"]', context)
  507. var $rows = $('tr', table);
  508. tableDrag.collection = new TaxonomyWranglerCollection();
  509. var updatedTerms = {};
  510. $submit.attr('disabled', 'disabled');
  511. var accordion = !!TaxonomyWranglerAccordion.instances[table.attr('id')] ? TaxonomyWranglerAccordion.instances[table.attr('id')] : null;
  512. $rows.bind('taxonomyWranglerTermDrag.rowDataChange', function(e, row){
  513. tableDrag.collection.updateTerm(row);
  514. var termsArray = [];
  515. var tids = [];
  516. accordion.refreshRows();
  517. $.each(tableDrag.collection.updatedTerms, function(i, val) {
  518. termsArray.push(val);
  519. tids.push(val.tid + ':' + val.weight);
  520. });
  521. $termData.val(JSON.stringify({ "termData": termsArray }));
  522. if (tableDrag.tm) {
  523. clearTimeout(tableDrag.tm);
  524. tableDrag.tm = null;
  525. }
  526. tableDrag.tm = setTimeout(function(){
  527. $submit.removeAttr('disabled').trigger('mousedown').attr('disabled', 'disabled');
  528. tableDrag.tm = null;
  529. }, 1000);
  530. if (!$messages.hasClass('is-busy')) {
  531. $messages.addClass('is-busy').append('<div class="ajax-progress ajax-progress-throbber ajax-progress-throbber--taxonomy-wrangler"><div class="throbber">&nbsp;</div> Updating terms</div>');
  532. }
  533. });
  534. }
  535. }
  536. };
  537. })(jQuery, Drupal, this, this.document);