tabledrag.es6.js 60 KB

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