tabledrag.es6.js 52 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712
  1. /**
  2. * @file
  3. * Provide dragging capabilities to admin uis.
  4. */
  5. /**
  6. * Triggers when weights columns are toggled.
  7. *
  8. * @event columnschange
  9. */
  10. (function($, Drupal, drupalSettings) {
  11. /**
  12. * Store the state of weight columns display for all tables.
  13. *
  14. * Default value is to hide weight columns.
  15. */
  16. let showWeight = JSON.parse(
  17. localStorage.getItem('Drupal.tableDrag.showWeight'),
  18. );
  19. /**
  20. * Drag and drop table rows with field manipulation.
  21. *
  22. * Using the drupal_attach_tabledrag() function, any table with weights or
  23. * parent relationships may be made into draggable tables. Columns containing
  24. * a field may optionally be hidden, providing a better user experience.
  25. *
  26. * Created tableDrag instances may be modified with custom behaviors by
  27. * overriding the .onDrag, .onDrop, .row.onSwap, and .row.onIndent methods.
  28. * See blocks.js for an example of adding additional functionality to
  29. * tableDrag.
  30. *
  31. * @type {Drupal~behavior}
  32. */
  33. Drupal.behaviors.tableDrag = {
  34. attach(context, settings) {
  35. function initTableDrag(table, base) {
  36. if (table.length) {
  37. // Create the new tableDrag instance. Save in the Drupal variable
  38. // to allow other scripts access to the object.
  39. Drupal.tableDrag[base] = new Drupal.tableDrag(
  40. table[0],
  41. settings.tableDrag[base],
  42. );
  43. }
  44. }
  45. Object.keys(settings.tableDrag || {}).forEach(base => {
  46. initTableDrag(
  47. $(context)
  48. .find(`#${base}`)
  49. .once('tabledrag'),
  50. base,
  51. );
  52. });
  53. },
  54. };
  55. /**
  56. * Provides table and field manipulation.
  57. *
  58. * @constructor
  59. *
  60. * @param {HTMLElement} table
  61. * DOM object for the table to be made draggable.
  62. * @param {object} tableSettings
  63. * Settings for the table added via drupal_add_dragtable().
  64. */
  65. Drupal.tableDrag = function(table, tableSettings) {
  66. const self = this;
  67. const $table = $(table);
  68. /**
  69. * @type {jQuery}
  70. */
  71. this.$table = $(table);
  72. /**
  73. *
  74. * @type {HTMLElement}
  75. */
  76. this.table = table;
  77. /**
  78. * @type {object}
  79. */
  80. this.tableSettings = tableSettings;
  81. /**
  82. * Used to hold information about a current drag operation.
  83. *
  84. * @type {?HTMLElement}
  85. */
  86. this.dragObject = null;
  87. /**
  88. * Provides operations for row manipulation.
  89. *
  90. * @type {?HTMLElement}
  91. */
  92. this.rowObject = null;
  93. /**
  94. * Remember the previous element.
  95. *
  96. * @type {?HTMLElement}
  97. */
  98. this.oldRowElement = null;
  99. /**
  100. * Used to determine up or down direction from last mouse move.
  101. *
  102. * @type {?number}
  103. */
  104. this.oldY = null;
  105. /**
  106. * Whether anything in the entire table has changed.
  107. *
  108. * @type {bool}
  109. */
  110. this.changed = false;
  111. /**
  112. * Maximum amount of allowed parenting.
  113. *
  114. * @type {number}
  115. */
  116. this.maxDepth = 0;
  117. /**
  118. * Direction of the table.
  119. *
  120. * @type {number}
  121. */
  122. this.rtl = $(this.table).css('direction') === 'rtl' ? -1 : 1;
  123. /**
  124. *
  125. * @type {bool}
  126. */
  127. this.striping = $(this.table).data('striping') === 1;
  128. /**
  129. * Configure the scroll settings.
  130. *
  131. * @type {object}
  132. *
  133. * @prop {number} amount
  134. * @prop {number} interval
  135. * @prop {number} trigger
  136. */
  137. this.scrollSettings = { amount: 4, interval: 50, trigger: 70 };
  138. /**
  139. *
  140. * @type {?number}
  141. */
  142. this.scrollInterval = null;
  143. /**
  144. *
  145. * @type {number}
  146. */
  147. this.scrollY = 0;
  148. /**
  149. *
  150. * @type {number}
  151. */
  152. this.windowHeight = 0;
  153. /**
  154. * Check this table's settings for parent relationships.
  155. *
  156. * For efficiency, large sections of code can be skipped if we don't need to
  157. * track horizontal movement and indentations.
  158. *
  159. * @type {bool}
  160. */
  161. this.indentEnabled = false;
  162. Object.keys(tableSettings || {}).forEach(group => {
  163. Object.keys(tableSettings[group] || {}).forEach(n => {
  164. if (tableSettings[group][n].relationship === 'parent') {
  165. this.indentEnabled = true;
  166. }
  167. if (tableSettings[group][n].limit > 0) {
  168. this.maxDepth = tableSettings[group][n].limit;
  169. }
  170. });
  171. });
  172. if (this.indentEnabled) {
  173. /**
  174. * Total width of indents, set in makeDraggable.
  175. *
  176. * @type {number}
  177. */
  178. this.indentCount = 1;
  179. // Find the width of indentations to measure mouse movements against.
  180. // Because the table doesn't need to start with any indentations, we
  181. // manually append 2 indentations in the first draggable row, measure
  182. // the offset, then remove.
  183. const indent = Drupal.theme('tableDragIndentation');
  184. const testRow = $('<tr></tr>')
  185. .addClass('draggable')
  186. .appendTo(table);
  187. const testCell = $('<td></td>')
  188. .appendTo(testRow)
  189. .prepend(indent)
  190. .prepend(indent);
  191. const $indentation = testCell.find('.js-indentation');
  192. /**
  193. *
  194. * @type {number}
  195. */
  196. this.indentAmount =
  197. $indentation.get(1).offsetLeft - $indentation.get(0).offsetLeft;
  198. testRow.remove();
  199. }
  200. // Make each applicable row draggable.
  201. // Match immediate children of the parent element to allow nesting.
  202. $table.find('> tr.draggable, > tbody > tr.draggable').each(function() {
  203. self.makeDraggable(this);
  204. });
  205. // Add a link before the table for users to show or hide weight columns.
  206. $table.before(
  207. $('<button type="button" class="link tabledrag-toggle-weight"></button>')
  208. .on(
  209. 'click',
  210. $.proxy(function(e) {
  211. e.preventDefault();
  212. this.toggleColumns();
  213. }, this),
  214. )
  215. .wrap('<div class="tabledrag-toggle-weight-wrapper"></div>')
  216. .parent(),
  217. );
  218. // Initialize the specified columns (for example, weight or parent columns)
  219. // to show or hide according to user preference. This aids accessibility
  220. // so that, e.g., screen reader users can choose to enter weight values and
  221. // manipulate form elements directly, rather than using drag-and-drop..
  222. self.initColumns();
  223. // Add event bindings to the document. The self variable is passed along
  224. // as event handlers do not have direct access to the tableDrag object.
  225. $(document).on('touchmove', event =>
  226. self.dragRow(event.originalEvent.touches[0], self),
  227. );
  228. $(document).on('touchend', event =>
  229. self.dropRow(event.originalEvent.touches[0], self),
  230. );
  231. $(document).on('mousemove pointermove', event => self.dragRow(event, self));
  232. $(document).on('mouseup pointerup', event => self.dropRow(event, self));
  233. // React to localStorage event showing or hiding weight columns.
  234. $(window).on(
  235. 'storage',
  236. $.proxy(function(e) {
  237. // Only react to 'Drupal.tableDrag.showWeight' value change.
  238. if (e.originalEvent.key === 'Drupal.tableDrag.showWeight') {
  239. // This was changed in another window, get the new value for this
  240. // window.
  241. showWeight = JSON.parse(e.originalEvent.newValue);
  242. this.displayColumns(showWeight);
  243. }
  244. }, this),
  245. );
  246. };
  247. /**
  248. * Initialize columns containing form elements to be hidden by default.
  249. *
  250. * Identify and mark each cell with a CSS class so we can easily toggle
  251. * show/hide it. Finally, hide columns if user does not have a
  252. * 'Drupal.tableDrag.showWeight' localStorage value.
  253. */
  254. Drupal.tableDrag.prototype.initColumns = function() {
  255. const $table = this.$table;
  256. let hidden;
  257. let cell;
  258. let columnIndex;
  259. Object.keys(this.tableSettings || {}).forEach(group => {
  260. // Find the first field in this group.
  261. Object.keys(this.tableSettings[group]).some(tableSetting => {
  262. const field = $table
  263. .find(`.${this.tableSettings[group][tableSetting].target}`)
  264. .eq(0);
  265. if (field.length && this.tableSettings[group][tableSetting].hidden) {
  266. hidden = this.tableSettings[group][tableSetting].hidden;
  267. cell = field.closest('td');
  268. return true;
  269. }
  270. return false;
  271. });
  272. // Mark the column containing this field so it can be hidden.
  273. if (hidden && cell[0]) {
  274. // Add 1 to our indexes. The nth-child selector is 1 based, not 0
  275. // based. Match immediate children of the parent element to allow
  276. // nesting.
  277. columnIndex =
  278. cell
  279. .parent()
  280. .find('> td')
  281. .index(cell.get(0)) + 1;
  282. $table
  283. .find('> thead > tr, > tbody > tr, > tr')
  284. .each(this.addColspanClass(columnIndex));
  285. }
  286. });
  287. this.displayColumns(showWeight);
  288. };
  289. /**
  290. * Mark cells that have colspan.
  291. *
  292. * In order to adjust the colspan instead of hiding them altogether.
  293. *
  294. * @param {number} columnIndex
  295. * The column index to add colspan class to.
  296. *
  297. * @return {function}
  298. * Function to add colspan class.
  299. */
  300. Drupal.tableDrag.prototype.addColspanClass = function(columnIndex) {
  301. return function() {
  302. // Get the columnIndex and adjust for any colspans in this row.
  303. const $row = $(this);
  304. let index = columnIndex;
  305. const cells = $row.children();
  306. let cell;
  307. cells.each(function(n) {
  308. if (n < index && this.colSpan && this.colSpan > 1) {
  309. index -= this.colSpan - 1;
  310. }
  311. });
  312. if (index > 0) {
  313. cell = cells.filter(`:nth-child(${index})`);
  314. if (cell[0].colSpan && cell[0].colSpan > 1) {
  315. // If this cell has a colspan, mark it so we can reduce the colspan.
  316. cell.addClass('tabledrag-has-colspan');
  317. } else {
  318. // Mark this cell so we can hide it.
  319. cell.addClass('tabledrag-hide');
  320. }
  321. }
  322. };
  323. };
  324. /**
  325. * Hide or display weight columns. Triggers an event on change.
  326. *
  327. * @fires event:columnschange
  328. *
  329. * @param {bool} displayWeight
  330. * 'true' will show weight columns.
  331. */
  332. Drupal.tableDrag.prototype.displayColumns = function(displayWeight) {
  333. if (displayWeight) {
  334. this.showColumns();
  335. }
  336. // Default action is to hide columns.
  337. else {
  338. this.hideColumns();
  339. }
  340. // Trigger an event to allow other scripts to react to this display change.
  341. // Force the extra parameter as a bool.
  342. $('table')
  343. .findOnce('tabledrag')
  344. .trigger('columnschange', !!displayWeight);
  345. };
  346. /**
  347. * Toggle the weight column depending on 'showWeight' value.
  348. *
  349. * Store only default override.
  350. */
  351. Drupal.tableDrag.prototype.toggleColumns = function() {
  352. showWeight = !showWeight;
  353. this.displayColumns(showWeight);
  354. if (showWeight) {
  355. // Save default override.
  356. localStorage.setItem('Drupal.tableDrag.showWeight', showWeight);
  357. } else {
  358. // Reset the value to its default.
  359. localStorage.removeItem('Drupal.tableDrag.showWeight');
  360. }
  361. };
  362. /**
  363. * Hide the columns containing weight/parent form elements.
  364. *
  365. * Undo showColumns().
  366. */
  367. Drupal.tableDrag.prototype.hideColumns = function() {
  368. const $tables = $('table').findOnce('tabledrag');
  369. // Hide weight/parent cells and headers.
  370. $tables.find('.tabledrag-hide').css('display', 'none');
  371. // Show TableDrag handles.
  372. $tables.find('.tabledrag-handle').css('display', '');
  373. // Reduce the colspan of any effected multi-span columns.
  374. $tables.find('.tabledrag-has-colspan').each(function() {
  375. this.colSpan = this.colSpan - 1;
  376. });
  377. // Change link text.
  378. $('.tabledrag-toggle-weight').text(Drupal.t('Show row weights'));
  379. };
  380. /**
  381. * Show the columns containing weight/parent form elements.
  382. *
  383. * Undo hideColumns().
  384. */
  385. Drupal.tableDrag.prototype.showColumns = function() {
  386. const $tables = $('table').findOnce('tabledrag');
  387. // Show weight/parent cells and headers.
  388. $tables.find('.tabledrag-hide').css('display', '');
  389. // Hide TableDrag handles.
  390. $tables.find('.tabledrag-handle').css('display', 'none');
  391. // Increase the colspan for any columns where it was previously reduced.
  392. $tables.find('.tabledrag-has-colspan').each(function() {
  393. this.colSpan = this.colSpan + 1;
  394. });
  395. // Change link text.
  396. $('.tabledrag-toggle-weight').text(Drupal.t('Hide row weights'));
  397. };
  398. /**
  399. * Find the target used within a particular row and group.
  400. *
  401. * @param {string} group
  402. * Group selector.
  403. * @param {HTMLElement} row
  404. * The row HTML element.
  405. *
  406. * @return {object}
  407. * The table row settings.
  408. */
  409. Drupal.tableDrag.prototype.rowSettings = function(group, row) {
  410. const field = $(row).find(`.${group}`);
  411. const tableSettingsGroup = this.tableSettings[group];
  412. return Object.keys(tableSettingsGroup)
  413. .map(delta => {
  414. const targetClass = tableSettingsGroup[delta].target;
  415. let rowSettings;
  416. if (field.is(`.${targetClass}`)) {
  417. // Return a copy of the row settings.
  418. rowSettings = {};
  419. Object.keys(tableSettingsGroup[delta]).forEach(n => {
  420. rowSettings[n] = tableSettingsGroup[delta][n];
  421. });
  422. }
  423. return rowSettings;
  424. })
  425. .filter(rowSetting => rowSetting)[0];
  426. };
  427. /**
  428. * Take an item and add event handlers to make it become draggable.
  429. *
  430. * @param {HTMLElement} item
  431. * The item to add event handlers to.
  432. */
  433. Drupal.tableDrag.prototype.makeDraggable = function(item) {
  434. const self = this;
  435. const $item = $(item);
  436. // Add a class to the title link.
  437. $item
  438. .find('td:first-of-type')
  439. .find('a')
  440. .addClass('menu-item__link');
  441. // Create the handle.
  442. const handle = $(
  443. '<a href="#" class="tabledrag-handle"><div class="handle">&nbsp;</div></a>',
  444. ).attr('title', Drupal.t('Drag to re-order'));
  445. // Insert the handle after indentations (if any).
  446. const $indentationLast = $item
  447. .find('td:first-of-type')
  448. .find('.js-indentation')
  449. .eq(-1);
  450. if ($indentationLast.length) {
  451. $indentationLast.after(handle);
  452. // Update the total width of indentation in this entire table.
  453. self.indentCount = Math.max(
  454. $item.find('.js-indentation').length,
  455. self.indentCount,
  456. );
  457. } else {
  458. $item
  459. .find('td')
  460. .eq(0)
  461. .prepend(handle);
  462. }
  463. handle.on('mousedown touchstart pointerdown', event => {
  464. event.preventDefault();
  465. if (event.originalEvent.type === 'touchstart') {
  466. event = event.originalEvent.touches[0];
  467. }
  468. self.dragStart(event, self, item);
  469. });
  470. // Prevent the anchor tag from jumping us to the top of the page.
  471. handle.on('click', e => {
  472. e.preventDefault();
  473. });
  474. // Set blur cleanup when a handle is focused.
  475. handle.on('focus', () => {
  476. self.safeBlur = true;
  477. });
  478. // On blur, fire the same function as a touchend/mouseup. This is used to
  479. // update values after a row has been moved through the keyboard support.
  480. handle.on('blur', event => {
  481. if (self.rowObject && self.safeBlur) {
  482. self.dropRow(event, self);
  483. }
  484. });
  485. // Add arrow-key support to the handle.
  486. handle.on('keydown', event => {
  487. // If a rowObject doesn't yet exist and this isn't the tab key.
  488. if (event.keyCode !== 9 && !self.rowObject) {
  489. self.rowObject = new self.row(
  490. item,
  491. 'keyboard',
  492. self.indentEnabled,
  493. self.maxDepth,
  494. true,
  495. );
  496. }
  497. let keyChange = false;
  498. let groupHeight;
  499. /* eslint-disable no-fallthrough */
  500. switch (event.keyCode) {
  501. // Left arrow.
  502. case 37:
  503. // Safari left arrow.
  504. case 63234:
  505. keyChange = true;
  506. self.rowObject.indent(-1 * self.rtl);
  507. break;
  508. // Up arrow.
  509. case 38:
  510. // Safari up arrow.
  511. case 63232: {
  512. let $previousRow = $(self.rowObject.element)
  513. .prev('tr')
  514. .eq(0);
  515. let previousRow = $previousRow.get(0);
  516. while (previousRow && $previousRow.is(':hidden')) {
  517. $previousRow = $(previousRow)
  518. .prev('tr')
  519. .eq(0);
  520. previousRow = $previousRow.get(0);
  521. }
  522. if (previousRow) {
  523. // Do not allow the onBlur cleanup.
  524. self.safeBlur = false;
  525. self.rowObject.direction = 'up';
  526. keyChange = true;
  527. if ($(item).is('.tabledrag-root')) {
  528. // Swap with the previous top-level row.
  529. groupHeight = 0;
  530. while (
  531. previousRow &&
  532. $previousRow.find('.js-indentation').length
  533. ) {
  534. $previousRow = $(previousRow)
  535. .prev('tr')
  536. .eq(0);
  537. previousRow = $previousRow.get(0);
  538. groupHeight += $previousRow.is(':hidden')
  539. ? 0
  540. : previousRow.offsetHeight;
  541. }
  542. if (previousRow) {
  543. self.rowObject.swap('before', previousRow);
  544. // No need to check for indentation, 0 is the only valid one.
  545. window.scrollBy(0, -groupHeight);
  546. }
  547. } else if (
  548. self.table.tBodies[0].rows[0] !== previousRow ||
  549. $previousRow.is('.draggable')
  550. ) {
  551. // Swap with the previous row (unless previous row is the first
  552. // one and undraggable).
  553. self.rowObject.swap('before', previousRow);
  554. self.rowObject.interval = null;
  555. self.rowObject.indent(0);
  556. window.scrollBy(0, -parseInt(item.offsetHeight, 10));
  557. }
  558. // Regain focus after the DOM manipulation.
  559. handle.trigger('focus');
  560. }
  561. break;
  562. }
  563. // Right arrow.
  564. case 39:
  565. // Safari right arrow.
  566. case 63235:
  567. keyChange = true;
  568. self.rowObject.indent(self.rtl);
  569. break;
  570. // Down arrow.
  571. case 40:
  572. // Safari down arrow.
  573. case 63233: {
  574. let $nextRow = $(self.rowObject.group)
  575. .eq(-1)
  576. .next('tr')
  577. .eq(0);
  578. let nextRow = $nextRow.get(0);
  579. while (nextRow && $nextRow.is(':hidden')) {
  580. $nextRow = $(nextRow)
  581. .next('tr')
  582. .eq(0);
  583. nextRow = $nextRow.get(0);
  584. }
  585. if (nextRow) {
  586. // Do not allow the onBlur cleanup.
  587. self.safeBlur = false;
  588. self.rowObject.direction = 'down';
  589. keyChange = true;
  590. if ($(item).is('.tabledrag-root')) {
  591. // Swap with the next group (necessarily a top-level one).
  592. groupHeight = 0;
  593. const nextGroup = new self.row(
  594. nextRow,
  595. 'keyboard',
  596. self.indentEnabled,
  597. self.maxDepth,
  598. false,
  599. );
  600. if (nextGroup) {
  601. $(nextGroup.group).each(function() {
  602. groupHeight += $(this).is(':hidden') ? 0 : this.offsetHeight;
  603. });
  604. const nextGroupRow = $(nextGroup.group)
  605. .eq(-1)
  606. .get(0);
  607. self.rowObject.swap('after', nextGroupRow);
  608. // No need to check for indentation, 0 is the only valid one.
  609. window.scrollBy(0, parseInt(groupHeight, 10));
  610. }
  611. } else {
  612. // Swap with the next row.
  613. self.rowObject.swap('after', nextRow);
  614. self.rowObject.interval = null;
  615. self.rowObject.indent(0);
  616. window.scrollBy(0, parseInt(item.offsetHeight, 10));
  617. }
  618. // Regain focus after the DOM manipulation.
  619. handle.trigger('focus');
  620. }
  621. break;
  622. }
  623. }
  624. /* eslint-enable no-fallthrough */
  625. if (self.rowObject && self.rowObject.changed === true) {
  626. $(item).addClass('drag');
  627. if (self.oldRowElement) {
  628. $(self.oldRowElement).removeClass('drag-previous');
  629. }
  630. self.oldRowElement = item;
  631. if (self.striping === true) {
  632. self.restripeTable();
  633. }
  634. self.onDrag();
  635. }
  636. // Returning false if we have an arrow key to prevent scrolling.
  637. if (keyChange) {
  638. return false;
  639. }
  640. });
  641. // Compatibility addition, return false on keypress to prevent unwanted
  642. // scrolling. IE and Safari will suppress scrolling on keydown, but all
  643. // other browsers need to return false on keypress.
  644. // http://www.quirksmode.org/js/keys.html
  645. handle.on('keypress', event => {
  646. /* eslint-disable no-fallthrough */
  647. switch (event.keyCode) {
  648. // Left arrow.
  649. case 37:
  650. // Up arrow.
  651. case 38:
  652. // Right arrow.
  653. case 39:
  654. // Down arrow.
  655. case 40:
  656. return false;
  657. }
  658. /* eslint-enable no-fallthrough */
  659. });
  660. };
  661. /**
  662. * Pointer event initiator, creates drag object and information.
  663. *
  664. * @param {jQuery.Event} event
  665. * The event object that trigger the drag.
  666. * @param {Drupal.tableDrag} self
  667. * The drag handle.
  668. * @param {HTMLElement} item
  669. * The item that is being dragged.
  670. */
  671. Drupal.tableDrag.prototype.dragStart = function(event, self, item) {
  672. // Create a new dragObject recording the pointer information.
  673. self.dragObject = {};
  674. self.dragObject.initOffset = self.getPointerOffset(item, event);
  675. self.dragObject.initPointerCoords = self.pointerCoords(event);
  676. if (self.indentEnabled) {
  677. self.dragObject.indentPointerPos = self.dragObject.initPointerCoords;
  678. }
  679. // If there's a lingering row object from the keyboard, remove its focus.
  680. if (self.rowObject) {
  681. $(self.rowObject.element)
  682. .find('a.tabledrag-handle')
  683. .trigger('blur');
  684. }
  685. // Create a new rowObject for manipulation of this row.
  686. self.rowObject = new self.row(
  687. item,
  688. 'pointer',
  689. self.indentEnabled,
  690. self.maxDepth,
  691. true,
  692. );
  693. // Save the position of the table.
  694. self.table.topY = $(self.table).offset().top;
  695. self.table.bottomY = self.table.topY + self.table.offsetHeight;
  696. // Add classes to the handle and row.
  697. $(item).addClass('drag');
  698. // Set the document to use the move cursor during drag.
  699. $('body').addClass('drag');
  700. if (self.oldRowElement) {
  701. $(self.oldRowElement).removeClass('drag-previous');
  702. }
  703. // Set the initial y coordinate so the direction can be calculated in
  704. // dragRow().
  705. self.oldY = self.pointerCoords(event).y;
  706. };
  707. /**
  708. * Pointer movement handler, bound to document.
  709. *
  710. * @param {jQuery.Event} event
  711. * The pointer event.
  712. * @param {Drupal.tableDrag} self
  713. * The tableDrag instance.
  714. *
  715. * @return {bool|undefined}
  716. * Undefined if no dragObject is defined, false otherwise.
  717. */
  718. Drupal.tableDrag.prototype.dragRow = function(event, self) {
  719. if (self.dragObject) {
  720. self.currentPointerCoords = self.pointerCoords(event);
  721. const y = self.currentPointerCoords.y - self.dragObject.initOffset.y;
  722. const x = self.currentPointerCoords.x - self.dragObject.initOffset.x;
  723. // Check for row swapping and vertical scrolling.
  724. if (y !== self.oldY) {
  725. self.rowObject.direction = y > self.oldY ? 'down' : 'up';
  726. // Update the old value.
  727. self.oldY = y;
  728. // Check if the window should be scrolled (and how fast).
  729. const scrollAmount = self.checkScroll(self.currentPointerCoords.y);
  730. // Stop any current scrolling.
  731. clearInterval(self.scrollInterval);
  732. // Continue scrolling if the mouse has moved in the scroll direction.
  733. if (
  734. (scrollAmount > 0 && self.rowObject.direction === 'down') ||
  735. (scrollAmount < 0 && self.rowObject.direction === 'up')
  736. ) {
  737. self.setScroll(scrollAmount);
  738. }
  739. // If we have a valid target, perform the swap and restripe the table.
  740. const currentRow = self.findDropTargetRow(x, y);
  741. if (currentRow) {
  742. if (self.rowObject.direction === 'down') {
  743. self.rowObject.swap('after', currentRow, self);
  744. } else {
  745. self.rowObject.swap('before', currentRow, self);
  746. }
  747. if (self.striping === true) {
  748. self.restripeTable();
  749. }
  750. }
  751. }
  752. // Similar to row swapping, handle indentations.
  753. if (self.indentEnabled) {
  754. const xDiff =
  755. self.currentPointerCoords.x - self.dragObject.indentPointerPos.x;
  756. // Set the number of indentations the pointer has been moved left or
  757. // right.
  758. const indentDiff = Math.round(xDiff / self.indentAmount);
  759. // Indent the row with our estimated diff, which may be further
  760. // restricted according to the rows around this row.
  761. const indentChange = self.rowObject.indent(indentDiff);
  762. // Update table and pointer indentations.
  763. self.dragObject.indentPointerPos.x +=
  764. self.indentAmount * indentChange * self.rtl;
  765. self.indentCount = Math.max(self.indentCount, self.rowObject.indents);
  766. }
  767. return false;
  768. }
  769. };
  770. /**
  771. * Pointerup behavior.
  772. *
  773. * @param {jQuery.Event} event
  774. * The pointer event.
  775. * @param {Drupal.tableDrag} self
  776. * The tableDrag instance.
  777. */
  778. Drupal.tableDrag.prototype.dropRow = function(event, self) {
  779. let droppedRow;
  780. let $droppedRow;
  781. // Drop row functionality.
  782. if (self.rowObject !== null) {
  783. droppedRow = self.rowObject.element;
  784. $droppedRow = $(droppedRow);
  785. // The row is already in the right place so we just release it.
  786. if (self.rowObject.changed === true) {
  787. // Update the fields in the dropped row.
  788. self.updateFields(droppedRow);
  789. // If a setting exists for affecting the entire group, update all the
  790. // fields in the entire dragged group.
  791. Object.keys(self.tableSettings || {}).forEach(group => {
  792. const rowSettings = self.rowSettings(group, droppedRow);
  793. if (rowSettings.relationship === 'group') {
  794. Object.keys(self.rowObject.children || {}).forEach(n => {
  795. self.updateField(self.rowObject.children[n], group);
  796. });
  797. }
  798. });
  799. self.rowObject.markChanged();
  800. if (self.changed === false) {
  801. $(Drupal.theme('tableDragChangedWarning'))
  802. .insertBefore(self.table)
  803. .hide()
  804. .fadeIn('slow');
  805. self.changed = true;
  806. }
  807. }
  808. if (self.indentEnabled) {
  809. self.rowObject.removeIndentClasses();
  810. }
  811. if (self.oldRowElement) {
  812. $(self.oldRowElement).removeClass('drag-previous');
  813. }
  814. $droppedRow.removeClass('drag').addClass('drag-previous');
  815. self.oldRowElement = droppedRow;
  816. self.onDrop();
  817. self.rowObject = null;
  818. }
  819. // Functionality specific only to pointerup events.
  820. if (self.dragObject !== null) {
  821. self.dragObject = null;
  822. $('body').removeClass('drag');
  823. clearInterval(self.scrollInterval);
  824. }
  825. };
  826. /**
  827. * Get the coordinates from the event (allowing for browser differences).
  828. *
  829. * @param {jQuery.Event} event
  830. * The pointer event.
  831. *
  832. * @return {object}
  833. * An object with `x` and `y` keys indicating the position.
  834. */
  835. Drupal.tableDrag.prototype.pointerCoords = function(event) {
  836. if (event.pageX || event.pageY) {
  837. return { x: event.pageX, y: event.pageY };
  838. }
  839. return {
  840. x: event.clientX + document.body.scrollLeft - document.body.clientLeft,
  841. y: event.clientY + document.body.scrollTop - document.body.clientTop,
  842. };
  843. };
  844. /**
  845. * Get the event offset from the target element.
  846. *
  847. * Given a target element and a pointer event, get the event offset from that
  848. * element. To do this we need the element's position and the target position.
  849. *
  850. * @param {HTMLElement} target
  851. * The target HTML element.
  852. * @param {jQuery.Event} event
  853. * The pointer event.
  854. *
  855. * @return {object}
  856. * An object with `x` and `y` keys indicating the position.
  857. */
  858. Drupal.tableDrag.prototype.getPointerOffset = function(target, event) {
  859. const docPos = $(target).offset();
  860. const pointerPos = this.pointerCoords(event);
  861. return { x: pointerPos.x - docPos.left, y: pointerPos.y - docPos.top };
  862. };
  863. /**
  864. * Find the row the mouse is currently over.
  865. *
  866. * This row is then taken and swapped with the one being dragged.
  867. *
  868. * @param {number} x
  869. * The x coordinate of the mouse on the page (not the screen).
  870. * @param {number} y
  871. * The y coordinate of the mouse on the page (not the screen).
  872. *
  873. * @return {*}
  874. * The drop target row, if found.
  875. */
  876. Drupal.tableDrag.prototype.findDropTargetRow = function(x, y) {
  877. const rows = $(this.table.tBodies[0].rows).not(':hidden');
  878. for (let n = 0; n < rows.length; n++) {
  879. let row = rows[n];
  880. let $row = $(row);
  881. const rowY = $row.offset().top;
  882. let rowHeight;
  883. // Because Safari does not report offsetHeight on table rows, but does on
  884. // table cells, grab the firstChild of the row and use that instead.
  885. // http://jacob.peargrove.com/blog/2006/technical/table-row-offsettop-bug-in-safari.
  886. if (row.offsetHeight === 0) {
  887. rowHeight = parseInt(row.firstChild.offsetHeight, 10) / 2;
  888. }
  889. // Other browsers.
  890. else {
  891. rowHeight = parseInt(row.offsetHeight, 10) / 2;
  892. }
  893. // Because we always insert before, we need to offset the height a bit.
  894. if (y > rowY - rowHeight && y < rowY + rowHeight) {
  895. if (this.indentEnabled) {
  896. // Check that this row is not a child of the row being dragged.
  897. if (
  898. Object.keys(this.rowObject.group).some(
  899. o => this.rowObject.group[o] === row,
  900. )
  901. ) {
  902. return null;
  903. }
  904. }
  905. // Do not allow a row to be swapped with itself.
  906. else if (row === this.rowObject.element) {
  907. return null;
  908. }
  909. // Check that swapping with this row is allowed.
  910. if (!this.rowObject.isValidSwap(row)) {
  911. return null;
  912. }
  913. // We may have found the row the mouse just passed over, but it doesn't
  914. // take into account hidden rows. Skip backwards until we find a
  915. // draggable row.
  916. while ($row.is(':hidden') && $row.prev('tr').is(':hidden')) {
  917. $row = $row.prev('tr:first-of-type');
  918. row = $row.get(0);
  919. }
  920. return row;
  921. }
  922. }
  923. return null;
  924. };
  925. /**
  926. * After the row is dropped, update the table fields.
  927. *
  928. * @param {HTMLElement} changedRow
  929. * DOM object for the row that was just dropped.
  930. */
  931. Drupal.tableDrag.prototype.updateFields = function(changedRow) {
  932. Object.keys(this.tableSettings || {}).forEach(group => {
  933. // Each group may have a different setting for relationship, so we find
  934. // the source rows for each separately.
  935. this.updateField(changedRow, group);
  936. });
  937. };
  938. /**
  939. * After the row is dropped, update a single table field.
  940. *
  941. * @param {HTMLElement} changedRow
  942. * DOM object for the row that was just dropped.
  943. * @param {string} group
  944. * The settings group on which field updates will occur.
  945. */
  946. Drupal.tableDrag.prototype.updateField = function(changedRow, group) {
  947. let rowSettings = this.rowSettings(group, changedRow);
  948. const $changedRow = $(changedRow);
  949. let sourceRow;
  950. let $previousRow;
  951. let previousRow;
  952. let useSibling;
  953. // Set the row as its own target.
  954. if (
  955. rowSettings.relationship === 'self' ||
  956. rowSettings.relationship === 'group'
  957. ) {
  958. sourceRow = changedRow;
  959. }
  960. // Siblings are easy, check previous and next rows.
  961. else if (rowSettings.relationship === 'sibling') {
  962. $previousRow = $changedRow.prev('tr:first-of-type');
  963. previousRow = $previousRow.get(0);
  964. const $nextRow = $changedRow.next('tr:first-of-type');
  965. const nextRow = $nextRow.get(0);
  966. sourceRow = changedRow;
  967. if (
  968. $previousRow.is('.draggable') &&
  969. $previousRow.find(`.${group}`).length
  970. ) {
  971. if (this.indentEnabled) {
  972. if (
  973. $previousRow.find('.js-indentations').length ===
  974. $changedRow.find('.js-indentations').length
  975. ) {
  976. sourceRow = previousRow;
  977. }
  978. } else {
  979. sourceRow = previousRow;
  980. }
  981. } else if (
  982. $nextRow.is('.draggable') &&
  983. $nextRow.find(`.${group}`).length
  984. ) {
  985. if (this.indentEnabled) {
  986. if (
  987. $nextRow.find('.js-indentations').length ===
  988. $changedRow.find('.js-indentations').length
  989. ) {
  990. sourceRow = nextRow;
  991. }
  992. } else {
  993. sourceRow = nextRow;
  994. }
  995. }
  996. }
  997. // Parents, look up the tree until we find a field not in this group.
  998. // Go up as many parents as indentations in the changed row.
  999. else if (rowSettings.relationship === 'parent') {
  1000. $previousRow = $changedRow.prev('tr');
  1001. previousRow = $previousRow;
  1002. while (
  1003. $previousRow.length &&
  1004. $previousRow.find('.js-indentation').length >= this.rowObject.indents
  1005. ) {
  1006. $previousRow = $previousRow.prev('tr');
  1007. previousRow = $previousRow;
  1008. }
  1009. // If we found a row.
  1010. if ($previousRow.length) {
  1011. sourceRow = $previousRow.get(0);
  1012. }
  1013. // Otherwise we went all the way to the left of the table without finding
  1014. // a parent, meaning this item has been placed at the root level.
  1015. else {
  1016. // Use the first row in the table as source, because it's guaranteed to
  1017. // be at the root level. Find the first item, then compare this row
  1018. // against it as a sibling.
  1019. sourceRow = $(this.table)
  1020. .find('tr.draggable:first-of-type')
  1021. .get(0);
  1022. if (sourceRow === this.rowObject.element) {
  1023. sourceRow = $(this.rowObject.group[this.rowObject.group.length - 1])
  1024. .next('tr.draggable')
  1025. .get(0);
  1026. }
  1027. useSibling = true;
  1028. }
  1029. }
  1030. // Because we may have moved the row from one category to another,
  1031. // take a look at our sibling and borrow its sources and targets.
  1032. this.copyDragClasses(sourceRow, changedRow, group);
  1033. rowSettings = this.rowSettings(group, changedRow);
  1034. // In the case that we're looking for a parent, but the row is at the top
  1035. // of the tree, copy our sibling's values.
  1036. if (useSibling) {
  1037. rowSettings.relationship = 'sibling';
  1038. rowSettings.source = rowSettings.target;
  1039. }
  1040. const targetClass = `.${rowSettings.target}`;
  1041. const targetElement = $changedRow.find(targetClass).get(0);
  1042. // Check if a target element exists in this row.
  1043. if (targetElement) {
  1044. const sourceClass = `.${rowSettings.source}`;
  1045. const sourceElement = $(sourceClass, sourceRow).get(0);
  1046. switch (rowSettings.action) {
  1047. case 'depth':
  1048. // Get the depth of the target row.
  1049. targetElement.value = $(sourceElement)
  1050. .closest('tr')
  1051. .find('.js-indentation').length;
  1052. break;
  1053. case 'match':
  1054. // Update the value.
  1055. targetElement.value = sourceElement.value;
  1056. break;
  1057. case 'order': {
  1058. const siblings = this.rowObject.findSiblings(rowSettings);
  1059. if ($(targetElement).is('select')) {
  1060. // Get a list of acceptable values.
  1061. const values = [];
  1062. $(targetElement)
  1063. .find('option')
  1064. .each(function() {
  1065. values.push(this.value);
  1066. });
  1067. const maxVal = values[values.length - 1];
  1068. // Populate the values in the siblings.
  1069. $(siblings)
  1070. .find(targetClass)
  1071. .each(function() {
  1072. // If there are more items than possible values, assign the
  1073. // maximum value to the row.
  1074. if (values.length > 0) {
  1075. this.value = values.shift();
  1076. } else {
  1077. this.value = maxVal;
  1078. }
  1079. });
  1080. } else {
  1081. // Assume a numeric input field.
  1082. let weight =
  1083. parseInt(
  1084. $(siblings[0])
  1085. .find(targetClass)
  1086. .val(),
  1087. 10,
  1088. ) || 0;
  1089. $(siblings)
  1090. .find(targetClass)
  1091. .each(function() {
  1092. this.value = weight;
  1093. weight++;
  1094. });
  1095. }
  1096. break;
  1097. }
  1098. }
  1099. }
  1100. };
  1101. /**
  1102. * Copy all tableDrag related classes from one row to another.
  1103. *
  1104. * Copy all special tableDrag classes from one row's form elements to a
  1105. * different one, removing any special classes that the destination row
  1106. * may have had.
  1107. *
  1108. * @param {HTMLElement} sourceRow
  1109. * The element for the source row.
  1110. * @param {HTMLElement} targetRow
  1111. * The element for the target row.
  1112. * @param {string} group
  1113. * The group selector.
  1114. */
  1115. Drupal.tableDrag.prototype.copyDragClasses = function(
  1116. sourceRow,
  1117. targetRow,
  1118. group,
  1119. ) {
  1120. const sourceElement = $(sourceRow).find(`.${group}`);
  1121. const targetElement = $(targetRow).find(`.${group}`);
  1122. if (sourceElement.length && targetElement.length) {
  1123. targetElement[0].className = sourceElement[0].className;
  1124. }
  1125. };
  1126. /**
  1127. * Check the suggested scroll of the table.
  1128. *
  1129. * @param {number} cursorY
  1130. * The Y position of the cursor.
  1131. *
  1132. * @return {number}
  1133. * The suggested scroll.
  1134. */
  1135. Drupal.tableDrag.prototype.checkScroll = function(cursorY) {
  1136. const de = document.documentElement;
  1137. const b = document.body;
  1138. const windowHeight =
  1139. window.innerHeight ||
  1140. (de.clientHeight && de.clientWidth !== 0
  1141. ? de.clientHeight
  1142. : b.offsetHeight);
  1143. this.windowHeight = windowHeight;
  1144. let scrollY;
  1145. if (document.all) {
  1146. scrollY = !de.scrollTop ? b.scrollTop : de.scrollTop;
  1147. } else {
  1148. scrollY = window.pageYOffset ? window.pageYOffset : window.scrollY;
  1149. }
  1150. this.scrollY = scrollY;
  1151. const trigger = this.scrollSettings.trigger;
  1152. let delta = 0;
  1153. // Return a scroll speed relative to the edge of the screen.
  1154. if (cursorY - scrollY > windowHeight - trigger) {
  1155. delta = trigger / (windowHeight + scrollY - cursorY);
  1156. delta = delta > 0 && delta < trigger ? delta : trigger;
  1157. return delta * this.scrollSettings.amount;
  1158. }
  1159. if (cursorY - scrollY < trigger) {
  1160. delta = trigger / (cursorY - scrollY);
  1161. delta = delta > 0 && delta < trigger ? delta : trigger;
  1162. return -delta * this.scrollSettings.amount;
  1163. }
  1164. };
  1165. /**
  1166. * Set the scroll for the table.
  1167. *
  1168. * @param {number} scrollAmount
  1169. * The amount of scroll to apply to the window.
  1170. */
  1171. Drupal.tableDrag.prototype.setScroll = function(scrollAmount) {
  1172. const self = this;
  1173. this.scrollInterval = setInterval(() => {
  1174. // Update the scroll values stored in the object.
  1175. self.checkScroll(self.currentPointerCoords.y);
  1176. const aboveTable = self.scrollY > self.table.topY;
  1177. const belowTable = self.scrollY + self.windowHeight < self.table.bottomY;
  1178. if (
  1179. (scrollAmount > 0 && belowTable) ||
  1180. (scrollAmount < 0 && aboveTable)
  1181. ) {
  1182. window.scrollBy(0, scrollAmount);
  1183. }
  1184. }, this.scrollSettings.interval);
  1185. };
  1186. /**
  1187. * Command to restripe table properly.
  1188. */
  1189. Drupal.tableDrag.prototype.restripeTable = function() {
  1190. // :even and :odd are reversed because jQuery counts from 0 and
  1191. // we count from 1, so we're out of sync.
  1192. // Match immediate children of the parent element to allow nesting.
  1193. $(this.table)
  1194. .find('> tbody > tr.draggable, > tr.draggable')
  1195. .filter(':visible')
  1196. .filter(':odd')
  1197. .removeClass('odd')
  1198. .addClass('even')
  1199. .end()
  1200. .filter(':even')
  1201. .removeClass('even')
  1202. .addClass('odd');
  1203. };
  1204. /**
  1205. * Stub function. Allows a custom handler when a row begins dragging.
  1206. *
  1207. * @return {null}
  1208. * Returns null when the stub function is used.
  1209. */
  1210. Drupal.tableDrag.prototype.onDrag = function() {
  1211. return null;
  1212. };
  1213. /**
  1214. * Stub function. Allows a custom handler when a row is dropped.
  1215. *
  1216. * @return {null}
  1217. * Returns null when the stub function is used.
  1218. */
  1219. Drupal.tableDrag.prototype.onDrop = function() {
  1220. return null;
  1221. };
  1222. /**
  1223. * Constructor to make a new object to manipulate a table row.
  1224. *
  1225. * @param {HTMLElement} tableRow
  1226. * The DOM element for the table row we will be manipulating.
  1227. * @param {string} method
  1228. * The method in which this row is being moved. Either 'keyboard' or
  1229. * 'mouse'.
  1230. * @param {bool} indentEnabled
  1231. * Whether the containing table uses indentations. Used for optimizations.
  1232. * @param {number} maxDepth
  1233. * The maximum amount of indentations this row may contain.
  1234. * @param {bool} addClasses
  1235. * Whether we want to add classes to this row to indicate child
  1236. * relationships.
  1237. */
  1238. Drupal.tableDrag.prototype.row = function(
  1239. tableRow,
  1240. method,
  1241. indentEnabled,
  1242. maxDepth,
  1243. addClasses,
  1244. ) {
  1245. const $tableRow = $(tableRow);
  1246. this.element = tableRow;
  1247. this.method = method;
  1248. this.group = [tableRow];
  1249. this.groupDepth = $tableRow.find('.js-indentation').length;
  1250. this.changed = false;
  1251. this.table = $tableRow.closest('table')[0];
  1252. this.indentEnabled = indentEnabled;
  1253. this.maxDepth = maxDepth;
  1254. // Direction the row is being moved.
  1255. this.direction = '';
  1256. if (this.indentEnabled) {
  1257. this.indents = $tableRow.find('.js-indentation').length;
  1258. this.children = this.findChildren(addClasses);
  1259. this.group = $.merge(this.group, this.children);
  1260. // Find the depth of this entire group.
  1261. for (let n = 0; n < this.group.length; n++) {
  1262. this.groupDepth = Math.max(
  1263. $(this.group[n]).find('.js-indentation').length,
  1264. this.groupDepth,
  1265. );
  1266. }
  1267. }
  1268. };
  1269. /**
  1270. * Find all children of rowObject by indentation.
  1271. *
  1272. * @param {bool} addClasses
  1273. * Whether we want to add classes to this row to indicate child
  1274. * relationships.
  1275. *
  1276. * @return {Array}
  1277. * An array of children of the row.
  1278. */
  1279. Drupal.tableDrag.prototype.row.prototype.findChildren = function(addClasses) {
  1280. const parentIndentation = this.indents;
  1281. let currentRow = $(this.element, this.table).next('tr.draggable');
  1282. const rows = [];
  1283. let child = 0;
  1284. function rowIndentation(indentNum, el) {
  1285. const self = $(el);
  1286. if (child === 1 && indentNum === parentIndentation) {
  1287. self.addClass('tree-child-first');
  1288. }
  1289. if (indentNum === parentIndentation) {
  1290. self.addClass('tree-child');
  1291. } else if (indentNum > parentIndentation) {
  1292. self.addClass('tree-child-horizontal');
  1293. }
  1294. }
  1295. while (currentRow.length) {
  1296. // A greater indentation indicates this is a child.
  1297. if (currentRow.find('.js-indentation').length > parentIndentation) {
  1298. child++;
  1299. rows.push(currentRow[0]);
  1300. if (addClasses) {
  1301. currentRow.find('.js-indentation').each(rowIndentation);
  1302. }
  1303. } else {
  1304. break;
  1305. }
  1306. currentRow = currentRow.next('tr.draggable');
  1307. }
  1308. if (addClasses && rows.length) {
  1309. $(rows[rows.length - 1])
  1310. .find(`.js-indentation:nth-child(${parentIndentation + 1})`)
  1311. .addClass('tree-child-last');
  1312. }
  1313. return rows;
  1314. };
  1315. /**
  1316. * Ensure that two rows are allowed to be swapped.
  1317. *
  1318. * @param {HTMLElement} row
  1319. * DOM object for the row being considered for swapping.
  1320. *
  1321. * @return {bool}
  1322. * Whether the swap is a valid swap or not.
  1323. */
  1324. Drupal.tableDrag.prototype.row.prototype.isValidSwap = function(row) {
  1325. const $row = $(row);
  1326. if (this.indentEnabled) {
  1327. let prevRow;
  1328. let nextRow;
  1329. if (this.direction === 'down') {
  1330. prevRow = row;
  1331. nextRow = $row.next('tr').get(0);
  1332. } else {
  1333. prevRow = $row.prev('tr').get(0);
  1334. nextRow = row;
  1335. }
  1336. this.interval = this.validIndentInterval(prevRow, nextRow);
  1337. // We have an invalid swap if the valid indentations interval is empty.
  1338. if (this.interval.min > this.interval.max) {
  1339. return false;
  1340. }
  1341. }
  1342. // Do not let an un-draggable first row have anything put before it.
  1343. if (this.table.tBodies[0].rows[0] === row && $row.is(':not(.draggable)')) {
  1344. return false;
  1345. }
  1346. return true;
  1347. };
  1348. /**
  1349. * Perform the swap between two rows.
  1350. *
  1351. * @param {string} position
  1352. * Whether the swap will occur 'before' or 'after' the given row.
  1353. * @param {HTMLElement} row
  1354. * DOM element what will be swapped with the row group.
  1355. */
  1356. Drupal.tableDrag.prototype.row.prototype.swap = function(position, row) {
  1357. // Makes sure only DOM object are passed to Drupal.detachBehaviors().
  1358. this.group.forEach(row => {
  1359. Drupal.detachBehaviors(row, drupalSettings, 'move');
  1360. });
  1361. $(row)[position](this.group);
  1362. // Makes sure only DOM object are passed to Drupal.attachBehaviors()s.
  1363. this.group.forEach(row => {
  1364. Drupal.attachBehaviors(row, drupalSettings);
  1365. });
  1366. this.changed = true;
  1367. this.onSwap(row);
  1368. };
  1369. /**
  1370. * Determine the valid indentations interval for the row at a given position.
  1371. *
  1372. * @param {?HTMLElement} prevRow
  1373. * DOM object for the row before the tested position
  1374. * (or null for first position in the table).
  1375. * @param {?HTMLElement} nextRow
  1376. * DOM object for the row after the tested position
  1377. * (or null for last position in the table).
  1378. *
  1379. * @return {object}
  1380. * An object with the keys `min` and `max` to indicate the valid indent
  1381. * interval.
  1382. */
  1383. Drupal.tableDrag.prototype.row.prototype.validIndentInterval = function(
  1384. prevRow,
  1385. nextRow,
  1386. ) {
  1387. const $prevRow = $(prevRow);
  1388. let maxIndent;
  1389. // Minimum indentation:
  1390. // Do not orphan the next row.
  1391. const minIndent = nextRow ? $(nextRow).find('.js-indentation').length : 0;
  1392. // Maximum indentation:
  1393. if (
  1394. !prevRow ||
  1395. $prevRow.is(':not(.draggable)') ||
  1396. $(this.element).is('.tabledrag-root')
  1397. ) {
  1398. // Do not indent:
  1399. // - the first row in the table,
  1400. // - rows dragged below a non-draggable row,
  1401. // - 'root' rows.
  1402. maxIndent = 0;
  1403. } else {
  1404. // Do not go deeper than as a child of the previous row.
  1405. maxIndent =
  1406. $prevRow.find('.js-indentation').length +
  1407. ($prevRow.is('.tabledrag-leaf') ? 0 : 1);
  1408. // Limit by the maximum allowed depth for the table.
  1409. if (this.maxDepth) {
  1410. maxIndent = Math.min(
  1411. maxIndent,
  1412. this.maxDepth - (this.groupDepth - this.indents),
  1413. );
  1414. }
  1415. }
  1416. return { min: minIndent, max: maxIndent };
  1417. };
  1418. /**
  1419. * Indent a row within the legal bounds of the table.
  1420. *
  1421. * @param {number} indentDiff
  1422. * The number of additional indentations proposed for the row (can be
  1423. * positive or negative). This number will be adjusted to nearest valid
  1424. * indentation level for the row.
  1425. *
  1426. * @return {number}
  1427. * The number of indentations applied.
  1428. */
  1429. Drupal.tableDrag.prototype.row.prototype.indent = function(indentDiff) {
  1430. const $group = $(this.group);
  1431. // Determine the valid indentations interval if not available yet.
  1432. if (!this.interval) {
  1433. const prevRow = $(this.element)
  1434. .prev('tr')
  1435. .get(0);
  1436. const nextRow = $group
  1437. .eq(-1)
  1438. .next('tr')
  1439. .get(0);
  1440. this.interval = this.validIndentInterval(prevRow, nextRow);
  1441. }
  1442. // Adjust to the nearest valid indentation.
  1443. let indent = this.indents + indentDiff;
  1444. indent = Math.max(indent, this.interval.min);
  1445. indent = Math.min(indent, this.interval.max);
  1446. indentDiff = indent - this.indents;
  1447. for (let n = 1; n <= Math.abs(indentDiff); n++) {
  1448. // Add or remove indentations.
  1449. if (indentDiff < 0) {
  1450. $group.find('.js-indentation:first-of-type').remove();
  1451. this.indents--;
  1452. } else {
  1453. $group
  1454. .find('td:first-of-type')
  1455. .prepend(Drupal.theme('tableDragIndentation'));
  1456. this.indents++;
  1457. }
  1458. }
  1459. if (indentDiff) {
  1460. // Update indentation for this row.
  1461. this.changed = true;
  1462. this.groupDepth += indentDiff;
  1463. this.onIndent();
  1464. }
  1465. return indentDiff;
  1466. };
  1467. /**
  1468. * Find all siblings for a row.
  1469. *
  1470. * According to its subgroup or indentation. Note that the passed-in row is
  1471. * included in the list of siblings.
  1472. *
  1473. * @param {object} rowSettings
  1474. * The field settings we're using to identify what constitutes a sibling.
  1475. *
  1476. * @return {Array}
  1477. * An array of siblings.
  1478. */
  1479. Drupal.tableDrag.prototype.row.prototype.findSiblings = function(
  1480. rowSettings,
  1481. ) {
  1482. const siblings = [];
  1483. const directions = ['prev', 'next'];
  1484. const rowIndentation = this.indents;
  1485. let checkRowIndentation;
  1486. for (let d = 0; d < directions.length; d++) {
  1487. let checkRow = $(this.element)[directions[d]]();
  1488. while (checkRow.length) {
  1489. // Check that the sibling contains a similar target field.
  1490. if (checkRow.find(`.${rowSettings.target}`)) {
  1491. // Either add immediately if this is a flat table, or check to ensure
  1492. // that this row has the same level of indentation.
  1493. if (this.indentEnabled) {
  1494. checkRowIndentation = checkRow.find('.js-indentation').length;
  1495. }
  1496. if (!this.indentEnabled || checkRowIndentation === rowIndentation) {
  1497. siblings.push(checkRow[0]);
  1498. } else if (checkRowIndentation < rowIndentation) {
  1499. // No need to keep looking for siblings when we get to a parent.
  1500. break;
  1501. }
  1502. } else {
  1503. break;
  1504. }
  1505. checkRow = checkRow[directions[d]]();
  1506. }
  1507. // Since siblings are added in reverse order for previous, reverse the
  1508. // completed list of previous siblings. Add the current row and continue.
  1509. if (directions[d] === 'prev') {
  1510. siblings.reverse();
  1511. siblings.push(this.element);
  1512. }
  1513. }
  1514. return siblings;
  1515. };
  1516. /**
  1517. * Remove indentation helper classes from the current row group.
  1518. */
  1519. Drupal.tableDrag.prototype.row.prototype.removeIndentClasses = function() {
  1520. Object.keys(this.children || {}).forEach(n => {
  1521. $(this.children[n])
  1522. .find('.js-indentation')
  1523. .removeClass('tree-child')
  1524. .removeClass('tree-child-first')
  1525. .removeClass('tree-child-last')
  1526. .removeClass('tree-child-horizontal');
  1527. });
  1528. };
  1529. /**
  1530. * Add an asterisk or other marker to the changed row.
  1531. */
  1532. Drupal.tableDrag.prototype.row.prototype.markChanged = function() {
  1533. const marker = Drupal.theme('tableDragChangedMarker');
  1534. const cell = $(this.element).find('td:first-of-type');
  1535. if (cell.find('abbr.tabledrag-changed').length === 0) {
  1536. cell.append(marker);
  1537. }
  1538. };
  1539. /**
  1540. * Stub function. Allows a custom handler when a row is indented.
  1541. *
  1542. * @return {null}
  1543. * Returns null when the stub function is used.
  1544. */
  1545. Drupal.tableDrag.prototype.row.prototype.onIndent = function() {
  1546. return null;
  1547. };
  1548. /**
  1549. * Stub function. Allows a custom handler when a row is swapped.
  1550. *
  1551. * @param {HTMLElement} swappedRow
  1552. * The element for the swapped row.
  1553. *
  1554. * @return {null}
  1555. * Returns null when the stub function is used.
  1556. */
  1557. Drupal.tableDrag.prototype.row.prototype.onSwap = function(swappedRow) {
  1558. return null;
  1559. };
  1560. $.extend(
  1561. Drupal.theme,
  1562. /** @lends Drupal.theme */ {
  1563. /**
  1564. * @return {string}
  1565. * Markup for the marker.
  1566. */
  1567. tableDragChangedMarker() {
  1568. return `<abbr class="warning tabledrag-changed" title="${Drupal.t(
  1569. 'Changed',
  1570. )}">*</abbr>`;
  1571. },
  1572. /**
  1573. * @return {string}
  1574. * Markup for the indentation.
  1575. */
  1576. tableDragIndentation() {
  1577. return '<div class="js-indentation indentation">&nbsp;</div>';
  1578. },
  1579. /**
  1580. * @return {string}
  1581. * Markup for the warning.
  1582. */
  1583. tableDragChangedWarning() {
  1584. return `<div class="tabledrag-changed-warning messages messages--warning" role="alert">${Drupal.theme(
  1585. 'tableDragChangedMarker',
  1586. )} ${Drupal.t('You have unsaved changes.')}</div>`;
  1587. },
  1588. },
  1589. );
  1590. })(jQuery, Drupal, drupalSettings);