tabledrag.es6.js 52 KB

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