ExamplesPlaygroundReference Source

coral-spectrum/coral-component-table/src/scripts/Table.js

  1. /**
  2. * Copyright 2019 Adobe. All rights reserved.
  3. * This file is licensed to you under the Apache License, Version 2.0 (the "License");
  4. * you may not use this file except in compliance with the License. You may obtain a copy
  5. * of the License at http://www.apache.org/licenses/LICENSE-2.0
  6. *
  7. * Unless required by applicable law or agreed to in writing, software distributed under
  8. * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
  9. * OF ANY KIND, either express or implied. See the License for the specific language
  10. * governing permissions and limitations under the License.
  11. */
  12.  
  13. import {BaseComponent} from '../../../coral-base-component';
  14. import {DragAction} from '../../../coral-dragaction';
  15. import TableColumn from './TableColumn';
  16. import TableCell from './TableCell';
  17. import TableRow from './TableRow';
  18. import TableHead from './TableHead';
  19. import TableBody from './TableBody';
  20. import TableFoot from './TableFoot';
  21. import '../../../coral-component-button';
  22. import {Checkbox} from '../../../coral-component-checkbox';
  23. import base from '../templates/base';
  24. import {SelectableCollection} from '../../../coral-collection';
  25. import {Decorator} from '../../../coral-decorator';
  26. import {
  27. isTableHeaderCell,
  28. isTableCell,
  29. isTableRow,
  30. isTableBody,
  31. getCellByIndex,
  32. getColumns,
  33. getCells,
  34. getContentCells,
  35. getHeaderCells,
  36. getRows,
  37. getSiblingsOf,
  38. getIndexOf,
  39. divider
  40. } from './TableUtil';
  41. import {events, transform, validate, commons, i18n, Keys} from '../../../coral-utils';
  42.  
  43. const CLASSNAME = '_coral-Table-wrapper';
  44.  
  45. /**
  46. Enumeration for {@link Table} variants
  47.  
  48. @typedef {Object} TableVariantEnum
  49.  
  50. @property {String} DEFAULT
  51. A default table.
  52. @property {String} QUIET
  53. A quiet table with transparent borders and background.
  54. @property {String} LIST
  55. Not supported. Falls back to DEFAULT.
  56. */
  57. const variant = {
  58. DEFAULT: 'default',
  59. QUIET: 'quiet',
  60. LIST: 'list'
  61. };
  62.  
  63. const ALL_VARIANT_CLASSES = [];
  64. for (const variantValue in variant) {
  65. ALL_VARIANT_CLASSES.push(`${CLASSNAME}--${variant[variantValue]}`);
  66. }
  67.  
  68. const IS_DISABLED = 'is-disabled';
  69. const IS_SORTED = 'is-sorted';
  70. const IS_UNSELECTABLE = 'is-unselectable';
  71. const IS_FIRST_ITEM_DRAGGED = 'is-draggedFirstItem';
  72. const IS_LAST_ITEM_DRAGGED = 'is-draggedLastItem';
  73. const IS_DRAGGING_CLASS = 'is-dragging';
  74. const IS_BEFORE_CLASS = 'is-before';
  75. const IS_AFTER_CLASS = 'is-after';
  76. const IS_LAYOUTING = 'is-layouting';
  77. const IS_READY = 'is-ready';
  78. const KEY_SPACE = Keys.keyToCode('space');
  79.  
  80. /**
  81. @class Coral.Table
  82. @classdesc A Table component is a container component to display and manipulate data in two dimensions.
  83. To define table actions on specific elements, handles can be used.
  84. A handle is given a special attribute :
  85. - <code>[coral-table-select]</code>. Select/unselect all table items.
  86. - <code>[coral-table-rowselect]</code>. Select/unselect the table item.
  87. - <code>[coral-table-roworder]</code>. Drag to order the table item.
  88. - <code>[coral-table-rowlock]</code>. Lock/unlock the table item.
  89. @htmltag coral-table
  90. @htmlbasetag table
  91. @extends {HTMLTableElement}
  92. @extends {BaseComponent}
  93. */
  94. const Table = Decorator(class extends BaseComponent(HTMLTableElement) {
  95. /** @ignore */
  96. constructor() {
  97. super();
  98.  
  99. // Templates
  100. this._elements = {
  101. head: this.querySelector('thead[is="coral-table-head"]') || new TableHead(),
  102. body: this.querySelector('tbody[is="coral-table-body"]') || new TableBody(),
  103. foot: this.querySelector('tfoot[is="coral-table-foot"]') || new TableFoot(),
  104. columns: this.querySelector('colgroup') || document.createElement('colgroup')
  105. };
  106. base.call(this._elements, {commons});
  107.  
  108. // Events
  109. this._delegateEvents({
  110. // Table specific
  111. 'global:coral-commons:_webfontactive': '_resetLayout',
  112. 'change [coral-table-select]': '_onSelectAll',
  113. 'capture:scroll [handle="container"]': '_onScroll',
  114.  
  115. // Head specific
  116. 'click thead[is="coral-table-head"] th[is="coral-table-headercell"]': '_onHeaderCellSort',
  117. 'coral-dragaction:dragstart thead[is="coral-table-head"] th[is="coral-table-headercell"]': '_onHeaderCellDragStart',
  118. 'coral-dragaction:drag thead[is="coral-table-head"] tr[is="coral-table-row"] > th[is="coral-table-headercell"]': '_onHeaderCellDrag',
  119. 'coral-dragaction:dragend thead[is="coral-table-head"] tr[is="coral-table-row"] > th[is="coral-table-headercell"]': '_onHeaderCellDragEnd',
  120. // a11y
  121. 'key:enter th[is="coral-table-headercell"]': '_onHeaderCellSort',
  122. 'key:enter th[is="coral-table-headercell"] coral-table-headercell-content': '_onHeaderCellSort',
  123. 'key:space th[is="coral-table-headercell"]': '_onHeaderCellSort',
  124. 'key:space th[is="coral-table-headercell"] coral-table-headercell-content': '_onHeaderCellSort',
  125.  
  126. // Body specific
  127. 'click tbody[is="coral-table-body"] [coral-table-rowlock]': '_onRowLock',
  128. 'click tbody[is="coral-table-body"] [coral-table-rowselect]': '_onRowSelect',
  129. 'click tbody[is="coral-table-body"] tr[is="coral-table-row"][selectable] [coral-table-cellselect]': '_onCellSelect',
  130. 'capture:mousedown tbody[is="coral-table-body"] [coral-table-roworder]:not([disabled])': '_onRowOrder',
  131. 'capture:touchstart tbody[is="coral-table-body"] [coral-table-roworder]:not([disabled])': '_onRowOrder',
  132. 'coral-dragaction:dragstart tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onRowDragStart',
  133. 'coral-dragaction:drag tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onRowDrag',
  134. 'coral-dragaction:dragover tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onRowDragOver',
  135. 'coral-dragaction:dragend tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onRowDragEnd',
  136. // a11y dnd
  137. 'key:space tbody[is="coral-table-body"] [coral-table-roworder]:not([disabled])': '_onKeyboardDrag',
  138. 'click tbody[is="coral-table-body"] tr[is="coral-table-row"] [coral-table-roworder]:not([disabled])': '_onDragHandleClick',
  139. 'coral-dragaction:dragonkeyspace tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onRowDragOnKeySpace',
  140. 'coral-dragaction:dragoveronkeyarrowdown tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onRowDragOverOnKeyArrowDown',
  141. 'coral-dragaction:dragoveronkeyarrowup tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onRowDragOverOnKeyArrowUp',
  142. 'coral-dragaction:dragendonkey tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onRowDragOverOnKeyEnter',
  143. // a11y
  144. 'mousedown tbody[is="coral-table-body"] [coral-table-rowselect]': '_onRowDown',
  145. 'key:enter tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onRowSelect',
  146. 'key:space tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onRowSelect',
  147. 'key:pageup tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onFocusPreviousItem',
  148. 'key:pagedown tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onFocusNextItem',
  149. 'key:left tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onFocusPreviousItem',
  150. 'key:right tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onFocusNextItem',
  151. 'key:up tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onFocusPreviousItem',
  152. 'key:down tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onFocusNextItem',
  153. 'key:home tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onFocusFirstItem',
  154. 'key:end tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onFocusLastItem',
  155. 'key:shift+pageup tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onSelectPreviousItem',
  156. 'key:shift+pagedown tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onSelectNextItem',
  157. 'key:shift+left tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onSelectPreviousItem',
  158. 'key:shift+right tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onSelectNextItem',
  159. 'key:shift+up tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onSelectPreviousItem',
  160. 'key:shift+down tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onSelectNextItem',
  161.  
  162. // Private
  163. 'coral-table-row:_multiplechanged': '_onRowMultipleChanged',
  164. 'coral-table-row:_beforeselectedchanged': '_onBeforeRowSelectionChanged',
  165. 'coral-table-row:_selectedchanged': '_onRowSelectionChanged',
  166. 'coral-table-row:_lockedchanged': '_onRowLockedChanged',
  167. 'coral-table-row:_change': '_onRowChange',
  168. 'coral-table-row:_contentchanged': '_onRowContentChanged',
  169. 'coral-table-headercell:_contentchanged': '_resetLayout',
  170. 'coral-table-head:_contentchanged': '_onHeadContentChanged',
  171. 'coral-table-body:_contentchanged': '_onBodyContentChanged',
  172. 'coral-table-body:_empty': '_onBodyEmpty',
  173. 'coral-table-column:_alignmentchanged': '_onAlignmentChanged',
  174. 'coral-table-column:_fixedwidthchanged': '_onFixedWidthChanged',
  175. 'coral-table-column:_orderablechanged': '_onColumnOrderableChanged',
  176. 'coral-table-column:_sortablechanged': '_onColumnSortableChanged',
  177. 'coral-table-column:_sortabledirectionchanged': '_onColumnSortableDirectionChanged',
  178. 'coral-table-column:_hiddenchanged': '_onColumnHiddenChanged',
  179. 'coral-table-column:_beforecolumnsort': '_onBeforeColumnSort',
  180. 'coral-table-column:_sort': '_onColumnSort',
  181. 'coral-table-head:_stickychanged': '_onHeadStickyChanged'
  182. });
  183.  
  184. // Required for coral-table:change event
  185. this._oldSelection = [];
  186. // References selected items in their selection order and is only used for keyboard selection
  187. this._lastSelectedItems = {
  188. items: [],
  189. direction: null
  190. };
  191.  
  192. // Don't sort by default
  193. this._allowSorting = false;
  194.  
  195. // Debounce timer
  196. this._timeout = null;
  197. // Debounce wait in milliseconds
  198. this._wait = 50;
  199.  
  200. // Used by resizing detector
  201. this._resetLayout = this._resetLayout.bind(this);
  202. // Init observer
  203. this._toggleObserver(true);
  204. }
  205.  
  206. /**
  207. The head of the table.
  208.  
  209. @type {TableHead}
  210. @contentzone
  211. */
  212. get head() {
  213. return this._getContentZone(this._elements.head);
  214. }
  215.  
  216. set head(value) {
  217. this._setContentZone('head', value, {
  218. handle: 'head',
  219. tagName: 'thead',
  220. insert: function (head) {
  221. // Using the native table API allows to position the head element at the correct position.
  222. this._elements.table.tHead = head;
  223.  
  224. // To init the head observer
  225. head.setAttribute('_observe', 'on');
  226. }
  227. });
  228. }
  229.  
  230. /**
  231. The body of the table. Multiple bodies are not supported.
  232.  
  233. @type {TableBody}
  234. @contentzone
  235. */
  236. get body() {
  237. return this._getContentZone(this._elements.body);
  238. }
  239.  
  240. set body(value) {
  241. this._setContentZone('body', value, {
  242. handle: 'body',
  243. tagName: 'tbody',
  244. insert: function (body) {
  245. this._elements.table.appendChild(body);
  246. this.items._container = body;
  247.  
  248. // To init the body observer
  249. body.setAttribute('_observe', 'on');
  250. }
  251. });
  252. }
  253.  
  254. /**
  255. The foot of the table.
  256.  
  257. @type {TableFoot}
  258. @contentzone
  259. */
  260. get foot() {
  261. return this._getContentZone(this._elements.foot);
  262. }
  263.  
  264. set foot(value) {
  265. this._setContentZone('foot', value, {
  266. handle: 'foot',
  267. tagName: 'tfoot',
  268. insert: function (foot) {
  269. // Using the native table API allows to position the foot element at the correct position.
  270. this._elements.table.tFoot = foot;
  271. }
  272. });
  273. }
  274.  
  275. /**
  276. The columns of the table.
  277.  
  278. @type {TableColumn}
  279. @contentzone
  280. */
  281. get columns() {
  282. return this._getContentZone(this._elements.columns);
  283. }
  284.  
  285. set columns(value) {
  286. this._setContentZone('columns', value, {
  287. handle: 'columns',
  288. tagName: 'colgroup',
  289. insert: function (content) {
  290. this._elements.table.appendChild(content);
  291. }
  292. });
  293. }
  294.  
  295. /**
  296. The table's variant. See {@link TableVariantEnum}.
  297.  
  298. @type {String}
  299. @default TableVariantEnum.DEFAULT
  300. @htmlattribute variant
  301. @htmlattributereflected
  302. */
  303. get variant() {
  304. return this._variant || variant.DEFAULT;
  305. }
  306.  
  307. set variant(value) {
  308. value = transform.string(value).toLowerCase();
  309. this._variant = validate.enumeration(variant)(value) && value || variant.DEFAULT;
  310. this._reflectAttribute('variant', this._variant);
  311.  
  312. this.classList.remove(...ALL_VARIANT_CLASSES);
  313. this.classList.add(`${CLASSNAME}--${this._variant}`);
  314. }
  315.  
  316. /**
  317. Whether the items are selectable.
  318.  
  319. @type {Boolean}
  320. @default false
  321. @htmlattribute selectable
  322. @htmlattributereflected
  323. */
  324. get selectable() {
  325. return this._selectable || false;
  326. }
  327.  
  328. set selectable(value) {
  329. this._selectable = transform.booleanAttr(value);
  330. this._reflectAttribute('selectable', this._selectable);
  331.  
  332. const rows = getRows([this.body]);
  333.  
  334. if (this._selectable) {
  335. rows.forEach((row) => {
  336. row.setAttribute('_selectable', '');
  337. });
  338. } else {
  339. // Clear selection
  340. rows.forEach((row) => {
  341. row.removeAttribute('_selectable');
  342. });
  343.  
  344. this.trigger('coral-table:change', {
  345. selection: [],
  346. oldSelection: this._oldSelection
  347. });
  348.  
  349. // Sync used collection
  350. this._oldSelection = [];
  351. this._lastSelectedItems.items = [];
  352. }
  353.  
  354. // a11y
  355. this._toggleFocusable();
  356. }
  357.  
  358. /**
  359. Whether the table is orderable. If the table is sorted, ordering handles are hidden.
  360.  
  361. @type {Boolean}
  362. @default false
  363. @htmlattribute orderable
  364. @htmlattributereflected
  365. */
  366. get orderable() {
  367. return this._orderable || false;
  368. }
  369.  
  370. set orderable(value) {
  371. this._orderable = transform.booleanAttr(value);
  372. this._reflectAttribute('orderable', this._orderable);
  373.  
  374. getRows([this.body]).forEach((row) => {
  375. row[this._orderable ? 'setAttribute' : 'removeAttribute']('_orderable', '');
  376. });
  377.  
  378. // a11y
  379. this._toggleFocusable();
  380. }
  381.  
  382. /**
  383. Whether multiple items can be selected.
  384.  
  385. @type {Boolean}
  386. @default false
  387. @htmlattribute multiple
  388. @htmlattributereflected
  389. */
  390. get multiple() {
  391. return this._multiple || false;
  392. }
  393.  
  394. set multiple(value) {
  395. this._multiple = transform.booleanAttr(value);
  396. this._reflectAttribute('multiple', this._multiple);
  397.  
  398. this._elements.table.setAttribute('aria-multiselectable', this._multiple);
  399.  
  400. // Deselect all except last
  401. if (!this.multiple) {
  402. const selection = this.selectedItems;
  403.  
  404. if (selection.length > 1) {
  405. selection.forEach((row, i) => {
  406. // Don't trigger too many events
  407. row.set('selected', i === selection.length - 1, true);
  408. });
  409.  
  410. // Synchronise the table select handle
  411. const newSelection = this.selectedItems;
  412.  
  413. if (newSelection.length) {
  414. this._setSelectAllHandleState('indeterminate');
  415. } else {
  416. this._setSelectAllHandleState('unchecked');
  417. }
  418.  
  419. this.trigger('coral-table:change', {
  420. selection: newSelection,
  421. oldSelection: selection
  422. });
  423.  
  424. // Sync used collection
  425. this._oldSelection = newSelection;
  426. this._lastSelectedItems.items = newSelection;
  427. }
  428. }
  429. }
  430.  
  431. /**
  432. Whether the table rows can be locked/unlocked. If rows are locked, they float to the top of the table and aren't
  433. affected by column sorting.
  434.  
  435. @type {Boolean}
  436. @default false
  437. @htmlattribute lockable
  438. @htmlattributereflected
  439. */
  440. get lockable() {
  441. return this._lockable || false;
  442. }
  443.  
  444. set lockable(value) {
  445. this._lockable = transform.booleanAttr(value);
  446. this._reflectAttribute('lockable', this._lockable);
  447.  
  448. getRows([this.body]).forEach((row) => {
  449. row[this._lockable ? 'setAttribute' : 'removeAttribute']('_lockable', '');
  450. });
  451.  
  452. // a11y
  453. this._toggleFocusable();
  454. }
  455.  
  456. /**
  457. Specifies <code>aria-labelledby</code> value.
  458.  
  459. @type {?String}
  460. @default null
  461. @htmlattribute labelledby
  462. */
  463. get labelledBy() {
  464. return this._elements.table.getAttribute('aria-labelledby');
  465. }
  466.  
  467. set labelledBy(value) {
  468. value = transform.string(value);
  469.  
  470. this._elements.table[value ? 'setAttribute' : 'removeAttribute']('aria-labelledby', value);
  471. }
  472.  
  473. /**
  474. Specifies <code>aria-label</code> value.
  475.  
  476. @type {String}
  477. @default null
  478. @htmlattribute labelled
  479. */
  480. get labelled() {
  481. return this._elements.table.getAttribute('aria-label');
  482. }
  483.  
  484. set labelled(value) {
  485. value = transform.string(value);
  486.  
  487. this._elements.table[value ? 'setAttribute' : 'removeAttribute']('aria-label', value);
  488. }
  489.  
  490. /**
  491. Returns an Array containing the selected items.
  492.  
  493. @type {Array.<HTMLElement>}
  494. @readonly
  495. */
  496. get selectedItems() {
  497. return this.items._getAllSelected();
  498. }
  499.  
  500. /**
  501. Returns the first selected item of the table. The value <code>null</code> is returned if no element is
  502. selected.
  503.  
  504. @type {HTMLElement}
  505. @readonly
  506. */
  507. get selectedItem() {
  508. return this.items._getFirstSelected();
  509. }
  510.  
  511. /**
  512. The Collection Interface that allows interacting with the items that the component contains.
  513.  
  514. @type {SelectableCollection}
  515. @readonly
  516. */
  517. get items() {
  518. // Construct the collection on first request
  519. if (!this._items) {
  520. this._items = new SelectableCollection({
  521. host: this,
  522. container: this.body,
  523. itemBaseTagName: 'tr',
  524. itemTagName: 'coral-table-row'
  525. });
  526. }
  527.  
  528. return this._items;
  529. }
  530.  
  531. /** @private */
  532. _onSelectAll(event) {
  533. if (this.selectable) {
  534. let rows = this._getSelectableItems();
  535.  
  536. if (rows.length) {
  537. if (this.multiple) {
  538. const selected = event.target.checked;
  539.  
  540. rows.forEach((row) => {
  541. // Don't trigger too many events
  542. row.set('selected', selected, true);
  543. });
  544.  
  545. rows = selected ? rows : [];
  546.  
  547. // Synchronise the table select handle
  548. this._setSelectAllHandleState(selected ? 'checked' : 'unchecked');
  549.  
  550. this.trigger('coral-table:change', {
  551. selection: rows,
  552. oldSelection: this._oldSelection
  553. });
  554.  
  555. // Sync used collection
  556. this._oldSelection = rows;
  557. this._lastSelectedItems.items = rows;
  558. } else {
  559. // Only select last item
  560. const lastItem = rows[rows.length - 1];
  561. lastItem.selected = !lastItem.selected;
  562. }
  563. }
  564. }
  565. }
  566.  
  567. _triggerChangeEvent() {
  568. if (!this._preventTriggeringEvents) {
  569. const selectedItems = this.selectedItems;
  570. this.trigger('coral-table:change', {
  571. oldSelection: this._oldSelection,
  572. selection: selectedItems
  573. });
  574.  
  575. this._oldSelection = selectedItems;
  576. }
  577. }
  578.  
  579. /** @private */
  580. _onRowOrder(event) {
  581. if (events.isVirtualEvent(event)) {
  582. return;
  583. }
  584.  
  585. const table = this;
  586. const row = event.target.closest('tr[is="coral-table-row"]');
  587.  
  588. if (row && table.orderable) {
  589. if (row.dragAction && row.dragAction.handle) {
  590. this._unwrapDragHandle(row.dragAction.handle);
  591. }
  592.  
  593. const head = table.head;
  594. const body = table.body;
  595. const sticky = head && head.sticky;
  596. const style = row.getAttribute('style');
  597. const index = getIndexOf(row);
  598. const oldBefore = row.nextElementSibling;
  599. const dragAction = new DragAction(row);
  600. const items = getRows([body]);
  601. const tableBoundingClientRect = table.getBoundingClientRect();
  602. const rowBoundingClientRect = row.getBoundingClientRect();
  603.  
  604. if (row === items[0]) {
  605. table.classList.add(IS_FIRST_ITEM_DRAGGED);
  606. } else if (row === items[items.length - 1]) {
  607. table.classList.add(IS_LAST_ITEM_DRAGGED);
  608. }
  609.  
  610. dragAction.axis = 'vertical';
  611. // Handle the scroll in table
  612. dragAction.scroll = false;
  613. // Specify selection handle directly on the row if none found
  614. dragAction.handle = row.querySelector('[coral-table-roworder]');
  615.  
  616. // The row placeholder indicating where the dragged element will be dropped
  617. const placeholder = row.cloneNode(true);
  618. placeholder.classList.add('_coral-Table-row--placeholder');
  619.  
  620. // Prepare the row position before inserting its placeholder
  621. row.style.top = `${rowBoundingClientRect.top - tableBoundingClientRect.top}px`;
  622.  
  623. // Prevent change event from triggering if the cloned node is selected
  624. table._preventTriggeringEvents = true;
  625. body.insertBefore(placeholder, row.nextElementSibling);
  626. window.requestAnimationFrame(() => {
  627. table._preventTriggeringEvents = false;
  628. });
  629.  
  630. // Store the data to avoid re-reading the layout on drag events
  631. const dragData = {
  632. placeholder: placeholder,
  633. index: index,
  634. oldBefore: oldBefore,
  635. // Backup styles to restore them later
  636. style: {
  637. row: style
  638. }
  639. };
  640.  
  641. // Required to handle the scrolling of the sticky table on drag events
  642. if (sticky) {
  643. dragData.sticky = sticky;
  644. dragData.tableTop = tableBoundingClientRect.top;
  645. dragData.tableSize = tableBoundingClientRect.height;
  646. dragData.headSize = parseFloat(table._elements.container.style.marginTop);
  647. dragData.dragElementSize = rowBoundingClientRect.height;
  648. }
  649.  
  650. row.dragAction._dragData = dragData;
  651. }
  652. }
  653.  
  654. /** @private */
  655. _onHeaderCellSort(event) {
  656. const table = this;
  657. const matchedTarget = event.matchedTarget.closest('th');
  658.  
  659. // Don't sort if the column was dragged
  660. if (!matchedTarget._isDragging) {
  661. const column = table._getColumn(matchedTarget);
  662. // Only sort if actually sortable and event not defaultPrevented
  663. if (column && column.sortable) {
  664. event.preventDefault();
  665.  
  666. column._sort();
  667.  
  668. // Restore focus on the header cell in any case
  669. matchedTarget.focus();
  670. }
  671. }
  672. }
  673.  
  674. /** @private */
  675. _onHeaderCellDragStart(event) {
  676. const table = this;
  677. const matchedTarget = event.matchedTarget;
  678. const dragElement = event.detail.dragElement;
  679. const siblingHeaderCellSelector = matchedTarget === dragElement ? 'th[is="coral-table-headercell"]' : 'th[is="coral-table-headercell"] coral-table-headercell-content';
  680. const tableBoundingClientRect = table.getBoundingClientRect();
  681.  
  682. // Store the data to be used on drag events
  683. dragElement.dragAction._dragData = {
  684. draggedColumnIndex: getIndexOf(matchedTarget),
  685. tableLeft: tableBoundingClientRect.left,
  686. tableSize: tableBoundingClientRect.width,
  687. dragElementSize: matchedTarget.getBoundingClientRect().width,
  688. tableScrollWidth: table._elements.container.scrollWidth
  689. };
  690.  
  691. getSiblingsOf(matchedTarget, siblingHeaderCellSelector, 'prevAll').forEach((item) => {
  692. item.classList.add(IS_BEFORE_CLASS);
  693. });
  694.  
  695. getSiblingsOf(matchedTarget, siblingHeaderCellSelector, 'nextAll').forEach((item) => {
  696. item.classList.add(IS_AFTER_CLASS);
  697. });
  698. }
  699.  
  700. /** @private */
  701. _onHeaderCellDrag(event) {
  702. const table = this;
  703. const container = table._elements.container;
  704. const matchedTarget = event.matchedTarget;
  705. const dragElement = event.detail.dragElement;
  706. const dragData = dragElement.dragAction._dragData;
  707. const row = matchedTarget.parentElement;
  708. const isHeaderCellDragged = matchedTarget === dragElement;
  709. const containerScrollLeft = container.scrollLeft;
  710. const documentScrollLeft = document.body.scrollLeft;
  711.  
  712. // Prevent sorting on header cell click if the header cell is being dragged
  713. matchedTarget._isDragging = true;
  714.  
  715. // Scroll left/right if table edge is reached
  716. const position = dragElement.getBoundingClientRect().left - dragData.tableLeft;
  717. const leftScrollLimit = 0;
  718. const rightScrollLimit = dragData.tableSize - dragData.dragElementSize;
  719. const scrollOffset = 10;
  720.  
  721. if (position < leftScrollLimit) {
  722. container.scrollLeft -= scrollOffset;
  723. }
  724. // 2nd condition is required to avoid increasing the container scroll width
  725. else if (position > rightScrollLimit && containerScrollLeft + dragData.tableSize < dragData.tableScrollWidth) {
  726. container.scrollLeft += scrollOffset;
  727. }
  728.  
  729. // Position sibling header cells based on the dragged element
  730. getHeaderCells(row).forEach((headerCell) => {
  731. const draggedHeaderCell = isHeaderCellDragged ? headerCell : headerCell.content;
  732.  
  733. if (!draggedHeaderCell.classList.contains(IS_DRAGGING_CLASS)) {
  734. const offsetLeft = draggedHeaderCell.getBoundingClientRect().left + documentScrollLeft;
  735. const isAfter = event.detail.pageX < offsetLeft + draggedHeaderCell.offsetWidth / 3;
  736.  
  737. draggedHeaderCell.classList.toggle(IS_AFTER_CLASS, isAfter);
  738. draggedHeaderCell.classList.toggle(IS_BEFORE_CLASS, !isAfter);
  739.  
  740. const columnIndex = getIndexOf(headerCell);
  741. const dragElementIndex = getIndexOf(matchedTarget);
  742.  
  743. // Place headercell after
  744. if (draggedHeaderCell.classList.contains(IS_AFTER_CLASS)) {
  745. if (columnIndex < dragElementIndex) {
  746. // Position the header cells based on their siblings position
  747. if (isHeaderCellDragged) {
  748. const nextHeaderCellWidth = draggedHeaderCell.clientWidth;
  749. draggedHeaderCell.style.left = `${nextHeaderCellWidth}px`;
  750. } else {
  751. const nextHeaderCell = getSiblingsOf(headerCell, 'th[is="coral-table-headercell"]', 'next');
  752. const nextHeaderCellLeftOffset = nextHeaderCell.getBoundingClientRect().left + documentScrollLeft;
  753. draggedHeaderCell.style.left = `${nextHeaderCellLeftOffset + containerScrollLeft}px`;
  754. }
  755. } else {
  756. draggedHeaderCell.style.left = '';
  757. }
  758. }
  759.  
  760. // Place headerCell before
  761. if (draggedHeaderCell.classList.contains(IS_BEFORE_CLASS)) {
  762. if (columnIndex > dragElementIndex) {
  763. const prev = getSiblingsOf(headerCell, 'th[is="coral-table-headercell"]', 'prev');
  764.  
  765. // Position the header cells based on their siblings position
  766. if (isHeaderCellDragged) {
  767. const beforeHeaderCellWidth = prev.clientWidth;
  768. draggedHeaderCell.style.left = `${-1 * (beforeHeaderCellWidth)}px`;
  769. } else {
  770. const beforeHeaderCellLeftOffset = prev.getBoundingClientRect().left + documentScrollLeft;
  771. draggedHeaderCell.style.left = `${beforeHeaderCellLeftOffset + containerScrollLeft}px`;
  772. }
  773. } else {
  774. draggedHeaderCell.style.left = '';
  775. }
  776. }
  777. }
  778. });
  779. }
  780.  
  781. /** @private */
  782. _onHeaderCellDragEnd(event) {
  783. const table = this;
  784. const matchedTarget = event.matchedTarget;
  785. const dragElement = event.detail.dragElement;
  786. const dragData = dragElement.dragAction._dragData;
  787. const column = table._getColumn(matchedTarget);
  788. const headRows = getRows([table.head]);
  789. const isHeaderCellDragged = matchedTarget === dragElement;
  790. const row = matchedTarget.parentElement;
  791.  
  792. // Select all cells in table body and foot given the index
  793. const getCellsByIndex = (cellIndex) => {
  794. const cellElements = [];
  795. const rows = getRows([table.body, table.foot]);
  796. rows.forEach((rowElement) => {
  797. const cell = getCellByIndex(rowElement, cellIndex);
  798. if (cell) {
  799. cellElements.push(cell);
  800. }
  801. });
  802. return cellElements;
  803. };
  804.  
  805. const cells = getCellsByIndex(getIndexOf(matchedTarget));
  806. let before = null;
  807. let after = null;
  808.  
  809. // Siblings are either header cell or header cell content based on the current sticky state
  810. if (isHeaderCellDragged) {
  811. before = row.querySelector(`th[is="coral-table-headercell"].${IS_AFTER_CLASS}`);
  812.  
  813. after = row.querySelectorAll(`th[is="coral-table-headercell"].${IS_BEFORE_CLASS}`);
  814. after = after.length ? after[after.length - 1] : null;
  815. } else {
  816. before = row.querySelector(`th[is="coral-table-headercell"] > coral-table-headercell-content.${IS_AFTER_CLASS}`);
  817. before = before ? before.parentNode : null;
  818.  
  819. after = row.querySelectorAll(`th[is="coral-table-headercell"] > coral-table-headercell-content.${IS_BEFORE_CLASS}`);
  820. after = after.length ? after[after.length - 1].parentNode : null;
  821. }
  822.  
  823. // Did header cell order change ?
  824. const swapped = !(before && before.previousElementSibling === matchedTarget || after && after.nextElementSibling === matchedTarget);
  825.  
  826. // Switch whole columns based on the new position of the dragged element
  827. if (swapped) {
  828. const beforeColumn = before ? table._getColumn(before) : null;
  829.  
  830. // Trigger the event on table
  831. const beforeEvent = table.trigger('coral-table:beforecolumndrag', {
  832. column: column,
  833. before: beforeColumn
  834. });
  835.  
  836. const oldBefore = column.nextElementSibling;
  837.  
  838. if (!beforeEvent.defaultPrevented) {
  839. // Insert the headercell at the new position
  840. if (before) {
  841. const beforeIndex = getIndexOf(before);
  842. const beforeCells = getCellsByIndex(beforeIndex);
  843. cells.forEach((cell, i) => {
  844. cell.parentNode.insertBefore(cell, beforeCells[i]);
  845. });
  846.  
  847. // Sync <coral-table-column> by reordering it too
  848. const beforeCol = getColumns(table.columns)[beforeIndex];
  849. if (beforeCol && column) {
  850. table.columns.insertBefore(column, beforeCol);
  851. }
  852.  
  853. row.insertBefore(matchedTarget, before);
  854. }
  855. if (after) {
  856. const afterIndex = getIndexOf(after);
  857. const afterCells = getCellsByIndex(afterIndex);
  858. cells.forEach((cell, i) => {
  859. cell.parentNode.insertBefore(cell, afterCells[i].nextElementSibling);
  860. });
  861.  
  862. // Sync <coral-table-column> by reordering it too
  863. const afterCol = getColumns(table.columns)[afterIndex];
  864. if (afterCol && column) {
  865. table.columns.insertBefore(column, afterCol.nextElementSibling);
  866. }
  867.  
  868. row.insertBefore(matchedTarget, after.nextElementSibling);
  869. }
  870.  
  871. // Trigger the order event if the column position changed
  872. if (dragData.draggedColumnIndex !== getIndexOf(matchedTarget)) {
  873. const newBefore = getColumns(table.columns)[getIndexOf(column) + 1];
  874. table.trigger('coral-table:columndrag', {
  875. column: column,
  876. oldBefore: oldBefore,
  877. before: newBefore || null
  878. });
  879. }
  880. }
  881. }
  882.  
  883. // Restoring default header cells styling
  884. headRows.forEach((rowElement) => {
  885. getHeaderCells(rowElement).forEach((headerCell) => {
  886. headerCell = isHeaderCellDragged ? headerCell : headerCell.content;
  887. headerCell.classList.remove(IS_AFTER_CLASS);
  888. headerCell.classList.remove(IS_BEFORE_CLASS);
  889. headerCell.style.left = '';
  890. });
  891. });
  892.  
  893. // Trigger a relayout
  894. table._resetLayout();
  895.  
  896. window.requestAnimationFrame(() => {
  897. // Allows sorting again after dragging completed
  898. matchedTarget._isDragging = undefined;
  899. // Refocus the dragged element manually
  900. table._toggleElementTabIndex(dragElement, null, true);
  901. });
  902. }
  903.  
  904. /** @private */
  905. _onCellSelect(event) {
  906. const cell = event.target.closest('td[is="coral-table-cell"]');
  907.  
  908. if (cell) {
  909. cell.selected = !cell.selected;
  910. }
  911. }
  912.  
  913. /** @private */
  914. _onRowSelect(event) {
  915. const table = this;
  916. const row = event.target.closest('tr[is="coral-table-row"]');
  917.  
  918. if (row) {
  919. // Ignore selection if the row is locked
  920. if (table.lockable && row.locked) {
  921. return;
  922. }
  923.  
  924. // Restore text-selection
  925. table.classList.remove(IS_UNSELECTABLE);
  926.  
  927. // Prevent row selection when it's the selection handle and the target is an input
  928. if (table.selectable && (Keys.filterInputs(event) || !row.hasAttribute('coral-table-rowselect'))) {
  929. // Pressing space scrolls the sticky table to the bottom if scrollable
  930. if (event.keyCode === KEY_SPACE) {
  931. event.preventDefault();
  932. }
  933.  
  934. if (event.shiftKey) {
  935. let lastSelectedItem = table._lastSelectedItems.items[table._lastSelectedItems.items.length - 1];
  936. const lastSelectedDirection = table._lastSelectedItems.direction;
  937.  
  938. // If no selected items, by default set the first item as last selected item
  939. if (!table.selectedItem) {
  940. const rows = table._getSelectableItems();
  941. if (rows.length) {
  942. lastSelectedItem = rows[0];
  943. lastSelectedItem.set('selected', true, true);
  944. }
  945. }
  946.  
  947. // Don't continue if table has no items or if the last selected item is the clicked item
  948. if (lastSelectedItem && getIndexOf(row) !== getIndexOf(lastSelectedItem)) {
  949. // Range selection direction
  950. const before = getIndexOf(row) < getIndexOf(lastSelectedItem);
  951. const rangeQuery = before ? 'prevUntil' : 'nextUntil';
  952.  
  953. // Store direction
  954. table._lastSelectedItems.direction = before ? 'up' : 'down';
  955.  
  956. if (!row.selected) {
  957. // Store selection range
  958. const selectionRange = getSiblingsOf(lastSelectedItem, 'tr[is="coral-table-row"]:not([selected])', rangeQuery);
  959. selectionRange[before ? 'push' : 'unshift'](lastSelectedItem);
  960.  
  961. // Direction change
  962. if (!before && lastSelectedDirection === 'up' || before && lastSelectedDirection === 'down') {
  963. selectionRange.forEach((item) => {
  964. item.set('selected', false, true);
  965. });
  966. }
  967.  
  968. // Select item
  969. const selectionRangeRow = selectionRange[before ? 0 : selectionRange.length - 1];
  970. selectionRangeRow.set('selected', true, true);
  971. getSiblingsOf(selectionRangeRow, row, rangeQuery).forEach((item) => {
  972. item.set('selected', true, true);
  973. });
  974. } else {
  975. const selection = getSiblingsOf(lastSelectedItem, row, rangeQuery);
  976.  
  977. // If some items are not selected
  978. if (selection.some((item) => !item.hasAttribute('selected'))) {
  979. // Select all items in between
  980. selection.forEach((item) => {
  981. item.set('selected', true, true);
  982. });
  983.  
  984. // Deselect selected item right before/after the selection range
  985. getSiblingsOf(row, 'tr[is="coral-table-row"]:not([selected])', rangeQuery).forEach((item) => {
  986. item.set('selected', false, true);
  987. });
  988. } else {
  989. // Deselect items
  990. selection[before ? 'push' : 'unshift'](lastSelectedItem);
  991. selection.forEach((item) => {
  992. item.set('selected', false, true);
  993. });
  994. }
  995. }
  996. }
  997. } else {
  998. // Remove direction if simple click without shift key pressed
  999. table._lastSelectedItems.direction = null;
  1000. }
  1001.  
  1002. // Select the row that was clicked and keep the row selected if shift key was pressed
  1003. row.selected = event.shiftKey ? true : !row.selected;
  1004.  
  1005. // Don't focus the row if the target isn't the row and focusable
  1006. table._focusItem(row, event.target === event.matchedTarget || event.target.tabIndex < 0);
  1007. }
  1008. }
  1009. }
  1010.  
  1011. /** @private */
  1012. _onRowLock(event) {
  1013. const table = this;
  1014.  
  1015. if (table.lockable) {
  1016. const row = event.target.closest('tr[is="coral-table-row"]');
  1017. if (row) {
  1018. event.preventDefault();
  1019. event.stopPropagation();
  1020. row.locked = !row.locked;
  1021.  
  1022. // Refocus the locked/unlocked item manually
  1023. window.requestAnimationFrame(() => {
  1024. table._focusItem(row, true);
  1025. });
  1026. }
  1027. }
  1028. }
  1029.  
  1030. /** @private */
  1031. _onRowDown(event) {
  1032. const table = this;
  1033.  
  1034. // Prevent text-selection
  1035. if (table.selectedItem && event.shiftKey) {
  1036. table.classList.add(IS_UNSELECTABLE);
  1037.  
  1038. // @polyfill IE
  1039. // Store text selection feature
  1040. const onSelectStart = document.onselectstart;
  1041. // Kill text selection feature
  1042. document.onselectstart = () => false;
  1043. // Restore text selection feature
  1044. window.requestAnimationFrame(() => {
  1045. document.onselectstart = onSelectStart;
  1046. });
  1047. }
  1048. }
  1049.  
  1050. /** @private */
  1051. _onRowDragStart(event) {
  1052. const table = this;
  1053. const head = table.head;
  1054. const body = table.body;
  1055. const dragElement = event.detail.dragElement;
  1056. const dragData = dragElement.dragAction._dragData;
  1057.  
  1058. dragData.style.cells = [];
  1059. getCells(dragElement).forEach((cell) => {
  1060. // Backup styles to restore them later
  1061. dragData.style.cells.push(cell.getAttribute('style'));
  1062. // Cells will shrink otherwise
  1063. cell.style.width = window.getComputedStyle(cell).width;
  1064. });
  1065.  
  1066. if (head && !head.sticky) {
  1067. // @polyfill ie11
  1068. // Element that scrolls the document.
  1069. const scrollingElement = document.scrollingElement || document.documentElement;
  1070. dragElement.style.top = `${dragElement.getBoundingClientRect().top + scrollingElement.scrollTop}px`;
  1071. }
  1072. dragElement.style.position = 'absolute';
  1073.  
  1074. // Setting drop zones allows to listen for coral-dragaction:dragover event
  1075. dragElement.dragAction.dropZone = body.querySelectorAll(`tr[is="coral-table-row"]:not(.${IS_DRAGGING_CLASS})`);
  1076.  
  1077. // We cannot rely on :focus since the row is being moved in the dom while dnd
  1078. dragElement.classList.add('is-focused');
  1079. }
  1080.  
  1081. /** @private */
  1082. _onRowDrag(event) {
  1083. const table = this;
  1084. const body = table.body;
  1085. const dragElement = event.detail.dragElement;
  1086. const dragData = dragElement.dragAction._dragData;
  1087. const firstRow = getRows([body])[0];
  1088.  
  1089. // Insert the placeholder at the top
  1090. if (dragElement.getBoundingClientRect().top <= firstRow.getBoundingClientRect().top) {
  1091. table._preventTriggeringEvents = true;
  1092. body.insertBefore(dragData.placeholder, firstRow);
  1093. window.requestAnimationFrame(() => {
  1094. table._preventTriggeringEvents = false;
  1095. });
  1096. }
  1097.  
  1098. // Scroll up/down if table edge is reached
  1099. if (dragData.sticky) {
  1100. const dragElementTop = dragElement.getBoundingClientRect().top;
  1101. const position = dragElementTop - dragData.tableTop - dragData.headSize;
  1102. const topScrollLimit = 0;
  1103. const bottomScrollLimit = dragData.tableSize - dragData.dragElementSize - dragData.headSize;
  1104. const scrollOffset = 10;
  1105.  
  1106. // Handle the scrollbar position based on the dragged element position.
  1107. // nextFrame is required else Chrome wouldn't take scrollTop changes in account when dragging the first row down
  1108. window.requestAnimationFrame(() => {
  1109. if (position < topScrollLimit) {
  1110. table._elements.container.scrollTop -= scrollOffset;
  1111. } else if (position > bottomScrollLimit) {
  1112. table._elements.container.scrollTop += scrollOffset;
  1113. }
  1114. });
  1115. }
  1116. }
  1117.  
  1118. /** @private */
  1119. _onRowDragOver(event) {
  1120. const table = this;
  1121. const body = table.body;
  1122. const dragElement = event.detail.dragElement;
  1123. const dropElement = event.detail.dropElement;
  1124. const dragData = dragElement.dragAction._dragData;
  1125.  
  1126. // Swap the placeholder
  1127. if (dragElement.getBoundingClientRect().top >= dropElement.getBoundingClientRect().top) {
  1128. table._preventTriggeringEvents = true;
  1129. body.insertBefore(dragData.placeholder, dropElement.nextElementSibling);
  1130. window.requestAnimationFrame(() => {
  1131. table._preventTriggeringEvents = false;
  1132. });
  1133. }
  1134. }
  1135.  
  1136. /** @private */
  1137. _onRowDragEnd(event) {
  1138. const table = this;
  1139. const body = table.body;
  1140. const dragElement = event.detail.dragElement;
  1141. const dragAction = event.detail.dragElement.dragAction;
  1142.  
  1143. const dragData = dragAction._dragData;
  1144. const before = dragData.placeholder ? dragData.placeholder.nextElementSibling : null;
  1145.  
  1146. // Clean up
  1147. table.classList.remove(IS_FIRST_ITEM_DRAGGED);
  1148. table.classList.remove(IS_LAST_ITEM_DRAGGED);
  1149.  
  1150. if (dragData.placeholder && dragData.placeholder.parentNode) {
  1151. dragData.placeholder.parentNode.removeChild(dragData.placeholder);
  1152. }
  1153. dragAction.destroy();
  1154.  
  1155. // Restore specific styling
  1156. dragElement.setAttribute('style', dragData.style.row || '');
  1157. getCells(dragElement).forEach((cell, i) => {
  1158. cell.setAttribute('style', dragData.style.cells[i] || '');
  1159. });
  1160.  
  1161. // Trigger the event on table
  1162. const beforeEvent = table.trigger('coral-table:beforeroworder', {
  1163. row: dragElement,
  1164. before: before
  1165. });
  1166.  
  1167. if (!beforeEvent.defaultPrevented) {
  1168. // Did row order change ?
  1169. const rows = getRows([body]).filter((item) => item !== dragElement);
  1170.  
  1171. if (dragData.index !== rows.indexOf(dragData.placeholder)) {
  1172. // Insert the row at the new position and prevent change event from triggering
  1173. table._preventTriggeringEvents = true;
  1174. body.insertBefore(dragElement, before);
  1175. window.requestAnimationFrame(() => {
  1176. table._preventTriggeringEvents = false;
  1177. });
  1178.  
  1179. // Trigger the order event if the row position changed
  1180. table.trigger('coral-table:roworder', {
  1181. row: dragElement,
  1182. oldBefore: dragData.oldBefore,
  1183. before: before
  1184. });
  1185. }
  1186. }
  1187.  
  1188. // Refocus the dragged element manually
  1189. window.requestAnimationFrame(() => {
  1190. dragElement.classList.remove('is-focused');
  1191. table._focusItem(dragElement, true);
  1192. });
  1193. }
  1194.  
  1195. /** @private */
  1196. _wrapDragHandle(handle, callback = () => {}) {
  1197. if(!handle.closest('span[role="application"]')) {
  1198. const span = document.createElement('span');
  1199. span.setAttribute('role', 'application');
  1200. span.setAttribute('aria-label', i18n.get('reordering'));
  1201. handle.parentNode.insertBefore(span, handle);
  1202. span.appendChild(handle);
  1203. handle.selected = true;
  1204. handle.setAttribute('aria-pressed', 'true');
  1205. window.requestAnimationFrame(() => callback());
  1206. }
  1207. }
  1208.  
  1209. /** @private */
  1210. _unwrapDragHandle(handle, callback = () => {}) {
  1211. const span = handle && handle.closest('span[role="application"]');
  1212.  
  1213. if (handle) {
  1214. handle.selected = false;
  1215. handle.removeAttribute('aria-pressed');
  1216. handle.removeAttribute('aria-describedby');
  1217. }
  1218. window.requestAnimationFrame(() => {
  1219. if (span) {
  1220. span.parentNode.insertBefore(handle, span);
  1221. span.remove();
  1222. }
  1223. callback();
  1224. });
  1225. }
  1226.  
  1227. /** @private */
  1228. _onKeyboardDrag(event) {
  1229. const table = this;
  1230. const row = event.target.closest('tr[is="coral-table-row"]');
  1231.  
  1232. if (row && table.orderable) {
  1233. event.preventDefault();
  1234. event.stopPropagation();
  1235.  
  1236. if (row.dragAction && row.dragAction.isKeyboardDragging) {
  1237. return;
  1238. }
  1239.  
  1240. const style = row.getAttribute('style');
  1241. const index = getIndexOf(row);
  1242. const oldBefore = row.nextElementSibling;
  1243. const dragAction = new DragAction(row);
  1244.  
  1245. dragAction.axis = 'vertical';
  1246.  
  1247. // Handle the scroll in table
  1248. dragAction.scroll = false;
  1249.  
  1250. // Specify selection handle directly on the row if none found
  1251. const handle = row.querySelector('[coral-table-roworder]');
  1252. dragAction.handle = handle;
  1253.  
  1254. // Wrap the drag handle button in a span with role="application",
  1255. // to force Windows screen readers into forms mode while dragging.
  1256. if (event.target === handle) {
  1257. this._wrapDragHandle(handle, () => handle.focus());
  1258. }
  1259.  
  1260. // The row placeholder indicating where the dragged element will be dropped
  1261. const placeholder = row.cloneNode(true);
  1262. placeholder.classList.add('_coral-Table-row--placeholder');
  1263.  
  1264. // Store the data to avoid re-reading the layout on drag events
  1265. const dragData = {
  1266. placeholder: placeholder,
  1267. index: index,
  1268. oldBefore: oldBefore,
  1269. // Backup styles to restore them later
  1270. style: {
  1271. row: style
  1272. }
  1273. };
  1274. row.dragAction._dragData = dragData;
  1275. }
  1276. }
  1277.  
  1278. _onDragHandleClick(event) {
  1279. const row = event.target.closest('tr[is="coral-table-row"]');
  1280. if (!row.dragAction) {
  1281. this._onKeyboardDrag(event);
  1282. row.dragAction._isKeyboardDrag = true;
  1283. } else if (row.dragAction._isKeyboardDrag) {
  1284. row.dragAction._isKeyboardDrag = undefined;
  1285. }
  1286. }
  1287.  
  1288. /** @private */
  1289. _onRowDragOnKeySpace(event) {
  1290. event.preventDefault();
  1291.  
  1292. const dragElement = event.detail.dragElement;
  1293. const dragData = dragElement.dragAction._dragData;
  1294.  
  1295. if (dragElement.dragAction._isKeyboardDrag) {
  1296. return;
  1297. }
  1298.  
  1299. dragData.style.cells = [];
  1300. getCells(dragElement).forEach((cell) => {
  1301. // Backup styles to restore them later
  1302. dragData.style.cells.push(cell.getAttribute('style'));
  1303. // Cells will shrink otherwise
  1304. cell.style.width = window.getComputedStyle(cell).width;
  1305. });
  1306. }
  1307.  
  1308. /** @private */
  1309. _onRowDragOverOnKeyArrowDown(event) {
  1310. const table = this;
  1311. const body = table.body;
  1312. const dragElement = event.detail.dragElement;
  1313. const items = getRows([body]);
  1314. const index = getIndexOf(dragElement);
  1315. const dragData = dragElement.dragAction._dragData;
  1316. const handle = dragElement.dragAction.handle;
  1317. const rowHeader = dragElement.rowHeader;
  1318.  
  1319. event.preventDefault();
  1320.  
  1321. // We cannot rely on :focus since the row is being moved in the dom while dnd
  1322. dragElement.classList.add('is-focused');
  1323.  
  1324. if (dragElement === items[items.length - 1]) {
  1325. for (let position = 0; position < items.length - 1; position++) {
  1326. body.appendChild(items[position]);
  1327. }
  1328. body.insertBefore(items[0], items[items.length - 2].nextElementSibling);
  1329. } else {
  1330. body.insertBefore(items[index + 1], items[index]);
  1331. }
  1332.  
  1333. // Restore specific styling
  1334. dragElement.setAttribute('style', dragData.style.row || '');
  1335. getCells(dragElement).forEach((cell, i) => {
  1336. if (dragData.style.cells) {
  1337. cell.setAttribute('style', dragData.style.cells[i] || '');
  1338. }
  1339. });
  1340.  
  1341. if (handle) {
  1342. handle.focus();
  1343. this._announceLiveRegion((rowHeader ? rowHeader.textContent + ' ' : '') + i18n.get('reordered to row {0}', [getIndexOf(dragElement) + 1]), 'assertive');
  1344. }
  1345.  
  1346. dragElement.scrollIntoView({block: 'nearest'});
  1347. }
  1348.  
  1349. /** @private */
  1350. _onRowDragOverOnKeyArrowUp(event) {
  1351. const table = this;
  1352. const body = table.body;
  1353. const dragElement = event.detail.dragElement;
  1354. const items = getRows([body]);
  1355. const index = getIndexOf(dragElement);
  1356. const dragData = dragElement.dragAction._dragData;
  1357. const handle = dragElement.dragAction.handle;
  1358. const rowHeader = dragElement.rowHeader;
  1359.  
  1360. event.preventDefault();
  1361.  
  1362. // We cannot rely on :focus since the row is being moved in the dom while dnd
  1363. dragElement.classList.add('is-focused');
  1364.  
  1365. if (dragElement === items[0]) {
  1366. for (let position = 0; position < items.length - 2; position++) {
  1367. body.insertBefore(items[position + 1], items[0]);
  1368. }
  1369. body.insertBefore(items[items.length - 1], items[1]);
  1370. } else {
  1371. body.insertBefore(items[index - 1], items[index].nextElementSibling);
  1372. }
  1373.  
  1374. // Restore specific styling
  1375. dragElement.setAttribute('style', dragData.style.row || '');
  1376. getCells(dragElement).forEach((cell, i) => {
  1377. if (dragData.style.cells) {
  1378. cell.setAttribute('style', dragData.style.cells[i] || '');
  1379. }
  1380. });
  1381.  
  1382. if (handle) {
  1383. handle.focus();
  1384. this._announceLiveRegion((rowHeader ? rowHeader.textContent + ' ' : '') + i18n.get('reordered to row {0}', [getIndexOf(dragElement) + 1]), 'assertive');
  1385. }
  1386.  
  1387. dragElement.scrollIntoView({block: 'nearest'});
  1388. }
  1389.  
  1390. /** @private */
  1391. _onRowDragOverOnKeyEnter(event) {
  1392. const table = this;
  1393. const dragElement = event.detail.dragElement;
  1394. const dragAction = dragElement.dragAction;
  1395. const dragData = dragAction._dragData;
  1396. const handle = dragAction.handle;
  1397.  
  1398. if (dragAction._isKeyboardDrag) {
  1399. dragAction._isKeyboardDrag = undefined;
  1400. return;
  1401. }
  1402.  
  1403. // Trigger the event on table
  1404. const beforeEvent = table.trigger('coral-table:beforeroworder', {
  1405. row: dragElement,
  1406. before: dragData.oldBefore
  1407. });
  1408.  
  1409. if (!beforeEvent.defaultPrevented && dragData.oldBefore !== dragElement.nextElementSibling) {
  1410. // Trigger the order event if the row position changed
  1411. table.trigger('coral-table:roworder', {
  1412. row: dragElement,
  1413. oldBefore: dragData.oldBefore,
  1414. before: dragElement.nextElementSibling
  1415. });
  1416. }
  1417.  
  1418. dragAction.destroy();
  1419.  
  1420. const isFocusWithinDragElement = dragElement.contains(document.activeElement) || dragElement === document.activeElement;
  1421. const isFocusOnHandle = handle && handle === document.activeElement;
  1422.  
  1423. // Refocus the dragged element manually
  1424. const callback = () => {
  1425. dragElement.classList.remove('is-focused');
  1426. if (isFocusWithinDragElement) {
  1427. table._focusItem(dragElement, true);
  1428. }
  1429. if (isFocusOnHandle) {
  1430. handle.focus();
  1431. }
  1432. };
  1433.  
  1434. this._unwrapDragHandle(handle, callback);
  1435. }
  1436.  
  1437. /** @private */
  1438. _onRowMultipleChanged(event) {
  1439. event.stopImmediatePropagation();
  1440.  
  1441. const table = this;
  1442. const row = event.target;
  1443.  
  1444. // Deselect all except last
  1445. if (!row.multiple) {
  1446. const selectedItems = row.selectedItems;
  1447. table._preventTriggeringEvents = true;
  1448. selectedItems.forEach((cell, i) => {
  1449. cell.selected = i === selectedItems.length - 1;
  1450. });
  1451.  
  1452. window.requestAnimationFrame(() => {
  1453. table._preventTriggeringEvents = false;
  1454.  
  1455. table.trigger('coral-table:rowchange', {
  1456. oldSelection: selectedItems,
  1457. selection: row.selectedItems,
  1458. row: row
  1459. });
  1460. });
  1461. }
  1462. }
  1463.  
  1464. /** @private */
  1465. _onBeforeRowSelectionChanged(event) {
  1466. event.stopImmediatePropagation();
  1467.  
  1468. // In single selection, if the added item is selected, the rest should be deselected
  1469. const selectedItem = this.selectedItem;
  1470. if (!this.multiple && selectedItem && !event.target.selected) {
  1471. selectedItem.set('selected', false, true);
  1472. this._removeLastSelectedItem(selectedItem);
  1473. }
  1474. }
  1475.  
  1476. /** @private */
  1477. _syncSelectAllHandle(selectedItems, items) {
  1478. if (items.length && selectedItems.length === items.length) {
  1479. this._setSelectAllHandleState('checked');
  1480. } else if (!selectedItems.length) {
  1481. this._setSelectAllHandleState('unchecked');
  1482. } else {
  1483. this._setSelectAllHandleState('indeterminate');
  1484. }
  1485. }
  1486.  
  1487. /** @private */
  1488. _setSelectAllHandleState(state) {
  1489. const handle = this.querySelector('[coral-table-select]');
  1490.  
  1491. if (handle) {
  1492.  
  1493. // If the handle is a checkbox but lacks a label, label it with "Select All".
  1494. if (handle.tagName === 'CORAL-CHECKBOX') {
  1495. if (!handle.labelled) {
  1496. handle.setAttribute('labelled', i18n.get('Select All'));
  1497. }
  1498. if (!handle.title) {
  1499. handle.setAttribute('title', i18n.get('Select All'));
  1500. }
  1501. }
  1502.  
  1503. if (state === 'checked') {
  1504. handle.removeAttribute('indeterminate');
  1505. handle.setAttribute('checked', '');
  1506. } else if (state === 'unchecked') {
  1507. handle.removeAttribute('indeterminate');
  1508. handle.removeAttribute('checked');
  1509. } else if (state === 'indeterminate') {
  1510. handle.setAttribute('indeterminate', '');
  1511. }
  1512. }
  1513. }
  1514.  
  1515. /** @private */
  1516. _onRowSelectionChanged(event) {
  1517. event.stopImmediatePropagation();
  1518.  
  1519. this._triggerChangeEvent();
  1520.  
  1521. const table = this;
  1522. const body = table.body;
  1523. const row = event.target;
  1524.  
  1525. // Synchronise the table select handle
  1526. if (body && body.contains(row)) {
  1527. const selection = table.selectedItems;
  1528. const rows = table._getSelectableItems();
  1529.  
  1530. // Sync select all handle
  1531. table._syncSelectAllHandle(selection, rows);
  1532.  
  1533. // Store or remove the row reference
  1534. table[row.selected ? '_addLastSelectedItem' : '_removeLastSelectedItem'](row);
  1535.  
  1536. // Store selected items range
  1537. const lastSelectedItem = table._lastSelectedItems.items[table._lastSelectedItems.items.length - 1];
  1538. const next = table._lastSelectedItems.direction === 'down';
  1539. if (row.selected && lastSelectedItem && lastSelectedItem.selected && getSiblingsOf(lastSelectedItem, 'tr[is="coral-table-row"][selected]', next ? 'next' : 'prev')) {
  1540. getSiblingsOf(lastSelectedItem, 'tr[is="coral-table-row"]:not([selected])', next ? 'nextUntil' : 'prevUntil').forEach((item) => {
  1541. table._addLastSelectedItem(item);
  1542. });
  1543. }
  1544. }
  1545. }
  1546.  
  1547. _onRowLockedChanged(event) {
  1548. event.stopImmediatePropagation();
  1549.  
  1550. const table = this;
  1551. const body = this.body;
  1552. const row = event.target;
  1553.  
  1554. if (body && body.contains(row)) {
  1555. if (row.locked) {
  1556. // Store the row index as reference to place it back if unlocked and its selection state
  1557. row._rowIndex = getIndexOf(row);
  1558.  
  1559. // Insert row at first position of its tbody
  1560. table._preventTriggeringEvents = true;
  1561. body.insertBefore(row, getRows([body])[0]);
  1562. window.requestAnimationFrame(() => {
  1563. table._preventTriggeringEvents = false;
  1564. });
  1565.  
  1566. // Trigger event on table
  1567. table.trigger('coral-table:rowlock', {row});
  1568. } else {
  1569. // Put the row back to its initial position
  1570. if (row._rowIndex >= 0) {
  1571. const beforeRow = getRows([body])[row._rowIndex];
  1572. if (beforeRow) {
  1573. // Insert row at its initial position
  1574. table._preventTriggeringEvents = true;
  1575. body.insertBefore(row, beforeRow.nextElementSibling);
  1576. window.requestAnimationFrame(() => {
  1577. table._preventTriggeringEvents = false;
  1578. });
  1579. }
  1580. }
  1581.  
  1582. // Trigger event on table
  1583. table.trigger('coral-table:rowunlock', {row});
  1584. }
  1585. }
  1586. }
  1587.  
  1588. _onHeadContentChanged(event) {
  1589. event.stopImmediatePropagation();
  1590.  
  1591. const table = this;
  1592. const head = table.head;
  1593. const addedNodes = event.detail.addedNodes;
  1594.  
  1595. for (let i = 0 ; i < addedNodes.length ; i++) {
  1596. const node = addedNodes[i];
  1597.  
  1598. // Sync header cell whether sticky or not
  1599. if (isTableHeaderCell(node)) {
  1600. table._toggleStickyHeaderCell(node, head.sticky);
  1601. }
  1602. }
  1603. }
  1604.  
  1605. /** @private */
  1606. _onBodyContentChanged(event) {
  1607. if (event.stopImmediatePropagation) {
  1608. event.stopImmediatePropagation();
  1609. }
  1610.  
  1611. const table = this;
  1612. const addedNodes = event.detail.addedNodes;
  1613. const removedNodes = event.detail.removedNodes;
  1614. let addedNode = null;
  1615. const selectItem = (item) => {
  1616. item.selected = item === addedNode;
  1617. };
  1618. let changed = false;
  1619.  
  1620. // Sync added nodes
  1621. for (let i = 0 ; i < addedNodes.length ; i++) {
  1622. addedNode = addedNodes[i];
  1623.  
  1624. // Sync row state with table properties
  1625. if (isTableRow(addedNode)) {
  1626. changed = true;
  1627.  
  1628. addedNode._toggleSelectable(table.selectable);
  1629. addedNode._toggleOrderable(table.orderable);
  1630. addedNode._toggleLockable(table.lockable);
  1631.  
  1632. // @compat
  1633. this._toggleSelectionCheckbox(addedNode);
  1634.  
  1635. const selectedItems = table.selectedItems;
  1636. if (addedNode.selected) {
  1637. // In single selection, if the added item is selected, the rest should be deselected
  1638. if (!table.multiple && selectedItems.length > 1) {
  1639. selectedItems.forEach(selectItem);
  1640. }
  1641.  
  1642. table._triggerChangeEvent();
  1643. }
  1644.  
  1645. // Cells are selectable too
  1646. if (addedNode.selectable) {
  1647. addedNode.trigger('coral-table-row:_contentchanged', {
  1648. addedNodes: getContentCells(addedNode),
  1649. removedNodes: []
  1650. });
  1651. }
  1652.  
  1653. // Trigger collection event
  1654. if (!table._preventTriggeringEvents) {
  1655. table.trigger('coral-collection:add', {
  1656. item: addedNode
  1657. });
  1658. }
  1659.  
  1660. // a11y
  1661. table._toggleFocusable();
  1662. }
  1663. }
  1664.  
  1665. // Sync removed nodes
  1666. for (let k = 0 ; k < removedNodes.length ; k++) {
  1667. const removedNode = removedNodes[k];
  1668.  
  1669. if (isTableRow(removedNode)) {
  1670. changed = true;
  1671.  
  1672. // If the focusable item is removed, the first item becomes the new focusable item
  1673. if (removedNode.getAttribute('tabindex') === '0') {
  1674. const firstItem = getRows([table.body])[0];
  1675. if (firstItem) {
  1676. table._focusItem(firstItem);
  1677. }
  1678. }
  1679.  
  1680. if (removedNode.selected) {
  1681. table._triggerChangeEvent();
  1682. }
  1683.  
  1684. // Sync _lastSelectedItems array
  1685. const removedItemIndex = table._lastSelectedItems.items.indexOf(removedNode);
  1686. if (removedItemIndex !== -1) {
  1687. table._lastSelectedItems.items = table._lastSelectedItems.items.splice(removedItemIndex, 1);
  1688. }
  1689.  
  1690. // Trigger collection event
  1691. if (!table._preventTriggeringEvents) {
  1692. table.trigger('coral-collection:remove', {
  1693. item: removedNode
  1694. });
  1695. }
  1696. }
  1697. }
  1698.  
  1699. if (changed) {
  1700. const items = this._getSelectableItems();
  1701. // Sync select all handle if any.
  1702. table._syncSelectAllHandle(table.selectedItems, items);
  1703. // Disable table features if no items.
  1704. table._toggleInteractivity(items.length === 0);
  1705. }
  1706. }
  1707.  
  1708. /** @private */
  1709. _onBodyEmpty(event) {
  1710. event.stopImmediatePropagation();
  1711. this._toggleInteractivity(true);
  1712. }
  1713.  
  1714. /** @private */
  1715. _onRowChange(event) {
  1716. event.stopImmediatePropagation();
  1717.  
  1718. if (!this._preventTriggeringEvents) {
  1719. this.trigger('coral-table:rowchange', {
  1720. oldSelection: event.detail.oldSelection,
  1721. selection: event.detail.selection,
  1722. row: event.target
  1723. });
  1724. }
  1725. }
  1726.  
  1727. /** @private */
  1728. _onRowContentChanged(event) {
  1729. event.stopImmediatePropagation();
  1730.  
  1731. const table = this;
  1732. const row = event.target;
  1733. const addedNodes = event.detail.addedNodes;
  1734. let addedNode = null;
  1735. const removedNodes = event.detail.removedNodes;
  1736. const selectItem = (item) => {
  1737. item.selected = item === addedNode;
  1738. };
  1739.  
  1740. // Sync added nodes
  1741. for (let i = 0 ; i < addedNodes.length ; i++) {
  1742. addedNode = addedNodes[i];
  1743.  
  1744. // Sync row state with table properties
  1745. if (isTableCell(addedNode)) {
  1746. addedNode._toggleSelectable(row.selectable);
  1747.  
  1748. const selectedItems = row.selectedItems;
  1749. if (addedNode.selected) {
  1750. // In single selection, if the added item is selected, the rest should be deselected
  1751. if (!row.multiple && selectedItems.length > 1) {
  1752. selectedItems.forEach(selectItem);
  1753. }
  1754.  
  1755. row._triggerChangeEvent();
  1756. }
  1757.  
  1758. // Trigger collection event
  1759. if (!table._preventTriggeringEvents) {
  1760. row.trigger('coral-collection:add', {
  1761. item: addedNode
  1762. });
  1763. }
  1764. }
  1765. // Add appropriate scope depending on whether headercell is in THEAD or TBODY
  1766. else if (isTableHeaderCell(addedNode)) {
  1767. table._setHeaderCellScope(addedNode, row.parentNode);
  1768. }
  1769. }
  1770.  
  1771. // Sync removed nodes
  1772. for (let k = 0 ; k < removedNodes.length ; k++) {
  1773. const removedNode = removedNodes[k];
  1774.  
  1775. if (isTableCell(removedNode)) {
  1776. if (removedNode.selected) {
  1777. row._triggerChangeEvent();
  1778. }
  1779.  
  1780. // Trigger collection event
  1781. if (!table._preventTriggeringEvents) {
  1782. row.trigger('coral-collection:remove', {
  1783. item: removedNode
  1784. });
  1785. }
  1786. }
  1787. }
  1788. }
  1789.  
  1790. /** @private */
  1791. _toggleInteractivity(disable) {
  1792. const table = this;
  1793. const selectAll = table.querySelector('[coral-table-select]');
  1794.  
  1795. if (selectAll) {
  1796. selectAll.disabled = disable;
  1797. }
  1798.  
  1799. table.classList.toggle(IS_DISABLED, disable);
  1800. }
  1801.  
  1802. _onAlignmentChanged(event) {
  1803. event.stopImmediatePropagation();
  1804.  
  1805. this._resetAlignmentColumns();
  1806. }
  1807.  
  1808. /** @private */
  1809. _onFixedWidthChanged(event) {
  1810. event.stopImmediatePropagation();
  1811.  
  1812. const table = this;
  1813. const head = table.head;
  1814. const column = event.target;
  1815.  
  1816. if (head) {
  1817. const headRows = getRows([head]);
  1818. const columnIndex = getIndexOf(event.target);
  1819.  
  1820. headRows.forEach((row) => {
  1821. const headerCell = getCellByIndex(row, columnIndex);
  1822. if (headerCell && headerCell.tagName === 'TH') {
  1823. headerCell[column.fixedWidth ? 'setAttribute' : 'removeAttribute']('fixedwidth', '');
  1824. }
  1825. });
  1826. }
  1827.  
  1828. table._resetLayout();
  1829. }
  1830.  
  1831. /** @private */
  1832. _onColumnOrderableChanged(event) {
  1833. event.stopImmediatePropagation();
  1834.  
  1835. const table = this;
  1836. const head = this.head;
  1837. const column = event.target;
  1838. const headerCell = table._getColumnHeaderCell(column);
  1839.  
  1840. if (headerCell) {
  1841. // Move the drag handle
  1842. table._toggleDragActionHandle(headerCell, head && head.sticky);
  1843.  
  1844. table._resetLayout();
  1845. }
  1846. }
  1847.  
  1848. /** @private */
  1849. _onColumnSortableChanged(event) {
  1850. event.stopImmediatePropagation();
  1851.  
  1852. const table = this;
  1853. const head = this.head;
  1854. const column = event.target;
  1855. const headerCell = table._getColumnHeaderCell(column);
  1856.  
  1857. if (headerCell) {
  1858. // For icons (chevron up/down) styling
  1859. headerCell[column.sortable ? 'setAttribute' : 'removeAttribute']('sortable', '');
  1860.  
  1861. // Toggle tab index. Sortable headercells are focusable.
  1862. table._toggleHeaderCellTabIndex(headerCell, head && head.sticky);
  1863.  
  1864. table._resetLayout();
  1865. }
  1866. }
  1867.  
  1868. _onColumnSortableDirectionChanged(event) {
  1869. event.stopImmediatePropagation();
  1870.  
  1871. const table = this;
  1872. const column = event.target;
  1873. const sortableDirection = TableColumn.sortableDirection;
  1874.  
  1875. // Hide coral-table-roworder handles if table is sorted
  1876. table.classList.toggle(IS_SORTED, table._isSorted());
  1877.  
  1878. const headerCell = table._getColumnHeaderCell(column);
  1879. if (headerCell) {
  1880. // For icons (chevron up/down) styling
  1881. headerCell.setAttribute('sortabledirection', column.sortableDirection);
  1882. headerCell.setAttribute('aria-sort', column.sortableDirection === sortableDirection.DEFAULT ? 'none' : column.sortableDirection);
  1883.  
  1884. if (column.sortableDirection === sortableDirection.DEFAULT) {
  1885. this._announceLiveRegion();
  1886. } else {
  1887. const textContent = headerCell.content.textContent.trim();
  1888. if (textContent.length) {
  1889. // Set live region to true so that sort description string will be announced.
  1890. this._announceLiveRegion(i18n.get(`sorted by column {0} in ${column.sortableDirection} order`, textContent));
  1891. }
  1892. }
  1893. }
  1894. }
  1895.  
  1896. /** @private */
  1897. _announceLiveRegion(text, politeness = 'polite') {
  1898.  
  1899. if (this._liveRegionTimeout) {
  1900. window.clearTimeout(this._liveRegionTimeout);
  1901. }
  1902.  
  1903. if (!text || !text.length) {
  1904. this._elements.liveRegion.innerText = '';
  1905. return;
  1906. }
  1907.  
  1908. // Set live region to true so that text string will be announced.
  1909. this._elements.liveRegion.setAttribute('aria-live', politeness);
  1910. this._elements.liveRegion.removeAttribute('aria-hidden');
  1911. if (this._isSorted()) {
  1912. this._elements.liveRegion.innerText = text;
  1913. } else {
  1914. this._liveRegionTimeout = window.setTimeout(() => this._elements.liveRegion.innerText = text, 100);
  1915. }
  1916.  
  1917. // @a11y wait 2.5 seconds to give screen reader enough time to announce the live region before silencing the it.
  1918. window.setTimeout(() => {
  1919. this._elements.liveRegion.setAttribute('aria-live', 'off');
  1920. this._elements.liveRegion.setAttribute('aria-hidden', 'true');
  1921. if (!this._isSorted()) {
  1922. this._elements.liveRegion.innerText = '';
  1923. }
  1924. }, 2500);
  1925. }
  1926.  
  1927. _onColumnHiddenChanged(event) {
  1928. event.stopImmediatePropagation();
  1929.  
  1930. this._resetHiddenColumns(true);
  1931. }
  1932.  
  1933. _onBeforeColumnSort(event) {
  1934. event.stopImmediatePropagation();
  1935.  
  1936. const table = this;
  1937. const column = event.target;
  1938. const newSortableDirection = event.detail.newSortableDirection;
  1939.  
  1940. const beforeEvent = table.trigger('coral-table:beforecolumnsort', {
  1941. column: column,
  1942. direction: newSortableDirection
  1943. });
  1944.  
  1945. if (!beforeEvent.defaultPrevented) {
  1946. column.sortableDirection = newSortableDirection;
  1947. }
  1948. }
  1949.  
  1950. _onColumnSort(event) {
  1951. event.stopImmediatePropagation();
  1952.  
  1953. // Don't sort yet
  1954. if (!this._allowSorting) {
  1955. return;
  1956. }
  1957.  
  1958. const table = this;
  1959. const body = table.body;
  1960. const column = event.target;
  1961. const columnIndex = getIndexOf(column);
  1962. const colHeaderCell = table._getColumnHeaderCell(column);
  1963. const onInitialization = event.detail.onInitialization;
  1964. const sortableDirection = event.detail.sortableDirection;
  1965. const sortableType = event.detail.sortableType;
  1966.  
  1967. const rows = getRows([body]);
  1968. const cells = [];
  1969.  
  1970. // Prevent change event from triggering when sorting
  1971. if (table) {
  1972. table._preventTriggeringEvents = true;
  1973. }
  1974.  
  1975. // Store a reference of the default row index for default sortable direction
  1976. rows.forEach((row, i) => {
  1977. if (typeof row._defaultRowIndex === 'undefined') {
  1978. row._defaultRowIndex = i;
  1979. }
  1980.  
  1981. const cell = getCellByIndex(row, columnIndex);
  1982. if (cell) {
  1983. cells.push(cell);
  1984. }
  1985. });
  1986.  
  1987. if (column.sortableDirection === sortableDirection.ASCENDING) {
  1988. // Remove sortable direction on sibling columns
  1989. getSiblingsOf(column, 'col[is="coral-table-column"]').forEach((col) => {
  1990. col._preventSort = true;
  1991. col.setAttribute('sortabledirection', sortableDirection.DEFAULT);
  1992. col._preventSort = false;
  1993. });
  1994.  
  1995. if (colHeaderCell) {
  1996. // For icons (chevron up/down) styling
  1997. getSiblingsOf(colHeaderCell, 'th[is="coral-table-headercell"]').forEach((headerCell) => {
  1998. headerCell.setAttribute('sortabledirection', sortableDirection.DEFAULT);
  1999. headerCell.setAttribute('aria-sort', 'none');
  2000. });
  2001. }
  2002.  
  2003. // Use cell value to sort and fallback if not specified
  2004. cells.sort((a, b) => {
  2005. if (column.sortableType === sortableType.ALPHANUMERIC) {
  2006. const aText = a.value ? a.value : a.textContent;
  2007. const bText = b.value ? b.value : b.textContent;
  2008. return aText.localeCompare(bText);
  2009. } else if (column.sortableType === sortableType.NUMBER) {
  2010. // Remove all spaces and replace commas with dots for decimal values
  2011. const aNumber = parseFloat(a.value ? a.value : a.textContent.replace(/\s+/g, '').replace(/,/g, '.'));
  2012. const bNumber = parseFloat(b.value ? b.value : b.textContent.replace(/\s+/g, '').replace(/,/g, '.'));
  2013. return aNumber > bNumber ? 1 : -1;
  2014. } else if (column.sortableType === sortableType.DATE) {
  2015. const aDate = a.value ? new Date(parseInt(a.value, 10)) : new Date(a.textContent);
  2016. const bDate = b.value ? new Date(parseInt(b.value, 10)) : new Date(b.textContent);
  2017. return aDate > bDate ? 1 : -1;
  2018. }
  2019. });
  2020.  
  2021. // Only sort if not custom sorting
  2022. if (column.sortableType !== sortableType.CUSTOM) {
  2023. if (body) {
  2024. // Insert the row at the new position if actually sorted
  2025. cells.forEach((cell) => {
  2026. const row = cell.parentElement;
  2027. // Prevent locked row to be sorted
  2028. if (!row.locked) {
  2029. body.appendChild(row);
  2030. }
  2031. });
  2032. }
  2033.  
  2034. // Trigger on table
  2035. table.trigger('coral-table:columnsort', {column});
  2036. }
  2037.  
  2038. // Table is in a sorted state. Disable orderable actions
  2039. rows.forEach((row) => {
  2040. if (row.dragAction) {
  2041. row.dragAction.destroy();
  2042. }
  2043. });
  2044. } else if (column.sortableDirection === sortableDirection.DESCENDING) {
  2045. getSiblingsOf(column, 'col[is="coral-table-column"]').forEach((col) => {
  2046. col._preventSort = true;
  2047. col.setAttribute('sortabledirection', sortableDirection.DEFAULT);
  2048. col._preventSort = false;
  2049. });
  2050.  
  2051. if (colHeaderCell) {
  2052. getSiblingsOf(colHeaderCell, 'th[is="coral-table-headercell"]').forEach((headerCell) => {
  2053. headerCell.setAttribute('sortabledirection', sortableDirection.DEFAULT);
  2054. headerCell.setAttribute('aria-sort', 'none');
  2055. });
  2056. }
  2057.  
  2058. cells.sort((a, b) => {
  2059. if (column.sortableType === sortableType.ALPHANUMERIC) {
  2060. const aText = a.value ? a.value : a.textContent;
  2061. const bText = b.value ? b.value : b.textContent;
  2062. return bText.localeCompare(aText);
  2063. } else if (column.sortableType === sortableType.NUMBER) {
  2064. // Remove all spaces and replace commas with dots for decimal values
  2065. const aNumber = parseFloat(a.value ? a.value : a.textContent.replace(/\s+/g, '').replace(/,/g, '.'));
  2066. const bNumber = parseFloat(b.value ? b.value : b.textContent.replace(/\s+/g, '').replace(/,/g, '.'));
  2067. return aNumber < bNumber ? 1 : -1;
  2068. } else if (column.sortableType === sortableType.DATE) {
  2069. const aDate = a.value ? new Date(parseInt(a.value, 10)) : new Date(a.textContent);
  2070. const bDate = b.value ? new Date(parseInt(b.value, 10)) : new Date(b.textContent);
  2071. return aDate < bDate ? 1 : -1;
  2072. }
  2073. });
  2074.  
  2075. // Only sort if not custom sorting
  2076. if (column.sortableType !== sortableType.CUSTOM) {
  2077. if (body) {
  2078. // Insert the row at the new position if actually sorted
  2079. cells.forEach((cell) => {
  2080. const row = cell.parentElement;
  2081. // Prevent locked row to be sorted
  2082. if (!row.locked) {
  2083. body.appendChild(row);
  2084. }
  2085. });
  2086. }
  2087.  
  2088. // Trigger on table
  2089. table.trigger('coral-table:columnsort', {column});
  2090. }
  2091.  
  2092. // Table is in a sorted state. Disable orderable actions
  2093. rows.forEach((row) => {
  2094. if (row.dragAction) {
  2095. row.dragAction.destroy();
  2096. }
  2097. });
  2098. } else if (column.sortableDirection === sortableDirection.DEFAULT && !onInitialization) {
  2099. // Only sort if not custom sorting
  2100. if (column.sortableType !== sortableType.CUSTOM) {
  2101. // Put rows back to their initial position
  2102. rows.sort((a, b) => a._defaultRowIndex > b._defaultRowIndex ? 1 : -1);
  2103.  
  2104. rows.forEach((row) => {
  2105. // Prevent locked row to be sorted
  2106. if (body && !row.locked) {
  2107. body.appendChild(row);
  2108. }
  2109. });
  2110.  
  2111. // Trigger on table
  2112. table.trigger('coral-table:columnsort', {column});
  2113. }
  2114. }
  2115.  
  2116. // Allow triggering change events again after sorting
  2117. window.requestAnimationFrame(() => {
  2118. // a11y initialize column sort aria-describedby
  2119. if (onInitialization && column.sortableDirection !== sortableDirection.DEFAULT) {
  2120. const textContent = colHeaderCell.content.textContent.trim();
  2121. if (textContent.length) {
  2122. this._elements.liveRegion.innerText = i18n.get(`sorted by column {0} in ${column.sortableDirection} order`, textContent);
  2123. }
  2124. }
  2125.  
  2126. table._preventTriggeringEvents = false;
  2127. });
  2128. }
  2129.  
  2130. _onHeadStickyChanged(event) {
  2131. event.stopImmediatePropagation();
  2132.  
  2133. // a11y
  2134. this._toggleFocusable();
  2135.  
  2136. const table = this;
  2137. const head = event.target;
  2138.  
  2139. // Wait next frame before reading and changing header cell layout
  2140. window.requestAnimationFrame(() => {
  2141. // Defines the head height
  2142. const tableHeight = head.sticky ? `${head.getBoundingClientRect().height}px` : null;
  2143. table._resetContainerLayout(tableHeight, table._elements.container.style.height);
  2144.  
  2145. getRows([head]).forEach((row) => {
  2146. getHeaderCells(row).forEach((headerCell) => {
  2147. table._toggleStickyHeaderCell(headerCell, head.sticky);
  2148. });
  2149. });
  2150.  
  2151. // Make sure sticky styling is applied
  2152. table.classList.toggle(`${CLASSNAME}--sticky`, head.sticky);
  2153.  
  2154. // Layout sticky head
  2155. table._preventResetLayout = false;
  2156. table._resetLayout();
  2157. });
  2158. }
  2159.  
  2160. /** @private */
  2161. _getColumnHeaderCell(column) {
  2162. const table = this;
  2163. const head = table.head;
  2164. let headerCell = null;
  2165.  
  2166. if (head) {
  2167. const headRows = getRows([head]);
  2168. const columnIndex = getIndexOf(column);
  2169. if (headRows.length) {
  2170. headerCell = getCellByIndex(headRows[headRows.length - 1], columnIndex);
  2171. headerCell = headerCell && headerCell.tagName === 'TH' ? headerCell : null;
  2172. }
  2173. }
  2174.  
  2175. return headerCell;
  2176. }
  2177.  
  2178. /** @private */
  2179. _getColumn(headerCell) {
  2180. // Get the corresponding column
  2181. return getColumns(this.columns)[getIndexOf(headerCell)] || null;
  2182. }
  2183.  
  2184. /** @private */
  2185. _toggleStickyHeaderCell(headerCell, sticky) {
  2186. // Set the size
  2187. this._layoutStickyCell(headerCell, sticky);
  2188.  
  2189. // Define DragAction on the sticky cell instead of the headercell
  2190. this._toggleDragActionHandle(headerCell, sticky);
  2191.  
  2192. // Toggle tab index. Sortable headercells are focusable.
  2193. this._toggleHeaderCellTabIndex(headerCell, sticky);
  2194. }
  2195.  
  2196. _layoutStickyCell(headerCell, sticky) {
  2197. if (sticky) {
  2198. const computedStyle = window.getComputedStyle(headerCell);
  2199.  
  2200. // Don't allow the column to shrink less than its minimum allowed
  2201. if (!headerCell.style.minWidth) {
  2202. let hasVisibleChildNodes = false;
  2203. // In most cases, there's text content
  2204. if (headerCell.textContent.trim().length) {
  2205. hasVisibleChildNodes = true;
  2206. }
  2207. // Verify if there are any visible nodes without text content which could take layout space
  2208. else {
  2209. const headerCellChildren = headerCell.content.children;
  2210. for (let i = 0 ; i < headerCellChildren.length && !hasVisibleChildNodes ; i++) {
  2211. if (headerCellChildren[0].offsetParent) {
  2212. hasVisibleChildNodes = true;
  2213. }
  2214. }
  2215. }
  2216.  
  2217. if (hasVisibleChildNodes) {
  2218. const width = headerCell.content.getBoundingClientRect().width;
  2219. // Don't set the width if the header cell is hidden
  2220. if (width > 0) {
  2221. headerCell.style.minWidth = `${width}px`;
  2222. }
  2223. }
  2224. }
  2225.  
  2226. const cellWidth = parseFloat(computedStyle.width);
  2227. const cellPadding = parseFloat(computedStyle.paddingLeft) + parseFloat(computedStyle.paddingRight);
  2228. const borderRightWidth = parseFloat(computedStyle.borderRightWidth);
  2229.  
  2230. // Reflect headercell size on sticky cell
  2231. headerCell.content.style.width = `${cellWidth + cellPadding + borderRightWidth}px`;
  2232. } else {
  2233. // Restore headercell style
  2234. headerCell.style.minWidth = '';
  2235. headerCell.content.style.width = '';
  2236. headerCell.content.style.height = '';
  2237. headerCell.content.style.top = '';
  2238. headerCell.content.style.marginLeft = '';
  2239. headerCell.content.style.paddingTop = '';
  2240. }
  2241. }
  2242.  
  2243. /** @private */
  2244. _toggleDragActionHandle(headerCell, sticky) {
  2245. const column = this._getColumn(headerCell);
  2246.  
  2247. if (headerCell.dragAction) {
  2248. headerCell.dragAction.destroy();
  2249. }
  2250. if (headerCell.content.dragAction) {
  2251. headerCell.content.dragAction.destroy();
  2252. }
  2253.  
  2254. if (column && column.orderable) {
  2255. const dragAction = new DragAction(sticky ? headerCell.content : headerCell);
  2256. dragAction.axis = 'horizontal';
  2257. // Handle the scroll in table
  2258. dragAction.scroll = false;
  2259. headerCell.setAttribute('orderable', '');
  2260. } else {
  2261. headerCell.removeAttribute('orderable');
  2262. }
  2263. }
  2264.  
  2265. /** @private */
  2266. _toggleFocusable() {
  2267. const firstItem = getRows([this.body])[0];
  2268. if (!firstItem) {
  2269. return;
  2270. }
  2271.  
  2272. const focusableItem = this._getFocusableItem();
  2273. if (this.selectable || this.lockable || this.orderable || (this.head && this.head.sticky)) {
  2274. // First item is focusable by default but don't remove the tabindex of the existing focusable item
  2275. if (!focusableItem) {
  2276. this._toggleElementTabIndex(firstItem);
  2277. }
  2278. } else if (focusableItem) {
  2279. // Basic table is not focusable
  2280. focusableItem.removeAttribute('tabindex');
  2281. }
  2282. }
  2283.  
  2284. /** @private */
  2285. _toggleElementTabIndex(element, oldFocusable, forceFocus) {
  2286. if (oldFocusable) {
  2287. oldFocusable.removeAttribute('tabindex');
  2288. }
  2289.  
  2290. element.setAttribute('tabindex', '0');
  2291. if (forceFocus) {
  2292. element.focus();
  2293. }
  2294. }
  2295.  
  2296. /** @private */
  2297. _toggleHeaderCellTabIndex(headerCell, sticky) {
  2298. const column = this._getColumn(headerCell);
  2299. const sortable = column && (column.sortable || column.orderable);
  2300. headerCell.content[sortable ? 'setAttribute' : 'removeAttribute']('tabindex', '0');
  2301. headerCell.content[sortable ? 'setAttribute' : 'removeAttribute']('role', 'button');
  2302. }
  2303.  
  2304. /** @private */
  2305. _isSorted() {
  2306. let column = null;
  2307. const isSorted = getColumns(this.columns).some((col) => {
  2308. column = col;
  2309. return col.sortableDirection !== TableColumn.sortableDirection.DEFAULT;
  2310. });
  2311.  
  2312. return isSorted ? column : false;
  2313. }
  2314.  
  2315. /** @private */
  2316. _focusEdgeItem(event, first) {
  2317. const items = getRows([this.body]);
  2318. if (items.length) {
  2319. event.preventDefault();
  2320.  
  2321. let item = this._getFocusableItem();
  2322. if (item) {
  2323. item.removeAttribute('tabindex');
  2324. }
  2325.  
  2326. item = items[first ? 0 : items.length - 1];
  2327. item.setAttribute('tabindex', '0');
  2328. item.focus();
  2329. }
  2330. }
  2331.  
  2332. /** @private */
  2333. _focusSiblingItem(event, next) {
  2334. const item = this._getFocusableItem();
  2335. if (item) {
  2336. event.preventDefault();
  2337.  
  2338. const siblingItem = getSiblingsOf(item, 'tr[is="coral-table-row"]', next ? 'next' : 'prev');
  2339. if (siblingItem) {
  2340. item.removeAttribute('tabindex');
  2341. siblingItem.setAttribute('tabindex', '0');
  2342. siblingItem.focus();
  2343. }
  2344. }
  2345. }
  2346.  
  2347. /** @private */
  2348. _selectSiblingItem(next) {
  2349. if (this.selectable && this.multiple) {
  2350. const selectedItems = this.selectedItems;
  2351. let lastSelectedItem = this._lastSelectedItems.items[this._lastSelectedItems.items.length - 1];
  2352.  
  2353. if (selectedItems.length) {
  2354. // Prevent selection if we reached the edge
  2355. if (next && lastSelectedItem.matches(':last-of-type') || !next && lastSelectedItem.matches(':first-of-type')) {
  2356. return;
  2357. }
  2358.  
  2359. // Target sibling item
  2360. const sibling = getSiblingsOf(lastSelectedItem, 'tr[is="coral-table-row"]', next ? 'next' : 'prev');
  2361. if (!sibling.hasAttribute('selected')) {
  2362. lastSelectedItem = sibling;
  2363. }
  2364.  
  2365. // Store last selection
  2366. this._lastSelectedItems.direction = next ? 'down' : 'up';
  2367.  
  2368. // Toggle selection
  2369. lastSelectedItem.selected = !lastSelectedItem.selected;
  2370. } else if (getRows([this.body]).length) {
  2371. const focusableItem = this._getFocusableItem();
  2372.  
  2373. // Store last selection
  2374. this._lastSelectedItems.direction = next ? 'down' : 'up';
  2375.  
  2376. // Select focusable item by default if no items selected
  2377. focusableItem.selected = true;
  2378. }
  2379. }
  2380.  
  2381. // Focus last selected item
  2382. window.requestAnimationFrame(() => {
  2383. const itemToFocus = this._lastSelectedItems.items[this._lastSelectedItems.items.length - 1];
  2384. if (itemToFocus) {
  2385. this._focusItem(itemToFocus, true);
  2386. }
  2387. });
  2388. }
  2389.  
  2390. /** @private */
  2391. _getFocusableItem() {
  2392. return this.body && this.body.querySelector('tr[is="coral-table-row"][tabindex="0"]');
  2393. }
  2394.  
  2395. /** @private */
  2396. _getFocusableHeaderCell() {
  2397. return this.head && this.head.querySelector('th[is="coral-table-headercell"][tabindex="0"], th[is="coral-table-headercell"] > coral-table-headercell-content[tabindex="0"]');
  2398. }
  2399.  
  2400. /** @private */
  2401. _addLastSelectedItem(item) {
  2402. if (this._lastSelectedItems.items.indexOf(item) === -1) {
  2403. this._lastSelectedItems.items.push(item);
  2404. } else {
  2405. // Push it at the end
  2406. this._removeLastSelectedItem(item);
  2407. this._addLastSelectedItem(item);
  2408. }
  2409. }
  2410.  
  2411. /** @private */
  2412. _removeLastSelectedItem(item) {
  2413. this._lastSelectedItems.items.splice(this._lastSelectedItems.items.indexOf(item), 1);
  2414. }
  2415.  
  2416. /** @private */
  2417. _focusItem(item, forceFocus) {
  2418. this._toggleElementTabIndex(item, this._getFocusableItem(), forceFocus);
  2419. }
  2420.  
  2421. /** @private */
  2422. _onFocusFirstItem(event) {
  2423. this._focusEdgeItem(event, true);
  2424. }
  2425.  
  2426. /** @private */
  2427. _onFocusLastItem(event) {
  2428. this._focusEdgeItem(event, false);
  2429. }
  2430.  
  2431. /** @private */
  2432. _onFocusNextItem(event) {
  2433. this._focusSiblingItem(event, true);
  2434. }
  2435.  
  2436. /** @private */
  2437. _onFocusPreviousItem(event) {
  2438. this._focusSiblingItem(event, false);
  2439. }
  2440.  
  2441. /** @private */
  2442. _onSelectNextItem() {
  2443. this._selectSiblingItem(true);
  2444. }
  2445.  
  2446. /** @private */
  2447. _onSelectPreviousItem() {
  2448. this._selectSiblingItem(false);
  2449. }
  2450.  
  2451. /**
  2452. * Call the layout method of table component
  2453. *
  2454. * @param {Boolean} forced
  2455. * If true call the layout method immediately, else wait for timeout
  2456. */
  2457. resetLayout(forced) {
  2458. forced = transform.boolean(forced);
  2459. if (forced === true) {
  2460. this._doResetLayout();
  2461. this._preventResetLayout = false;
  2462. } else {
  2463. this._resetLayout();
  2464. }
  2465. }
  2466.  
  2467. /** @private */
  2468. _doResetLayout() {
  2469. this.classList.add(IS_LAYOUTING);
  2470. this._resizeStickyHead();
  2471. this._resizeContainer();
  2472. this.classList.remove(IS_LAYOUTING);
  2473. }
  2474.  
  2475. /** @private */
  2476. _resetLayout() {
  2477. if (this._preventResetLayout) {
  2478. return;
  2479. }
  2480.  
  2481. // Debounce
  2482. if (this._timeout !== null) {
  2483. window.clearTimeout(this._timeout);
  2484. }
  2485.  
  2486. this._timeout = window.setTimeout(() => {
  2487. this._timeout = null;
  2488. this._doResetLayout();
  2489. // Mark table as ready
  2490. this.classList.add(IS_READY);
  2491. }, this._wait);
  2492. }
  2493.  
  2494. /** @private */
  2495. _resizeStickyHead() {
  2496. const table = this;
  2497. const head = table.head;
  2498. if (head && head.sticky) {
  2499. getRows([head]).forEach((row) => {
  2500. getHeaderCells(row).forEach((headerCell) => {
  2501. table._layoutStickyCell(headerCell, true);
  2502. });
  2503. });
  2504. }
  2505. }
  2506.  
  2507. /** @private */
  2508. _resizeContainer() {
  2509. const table = this;
  2510. const head = table.head;
  2511.  
  2512. if (head && head.sticky) {
  2513. let calculatedHeadSize = 0;
  2514. let previousRowHeight = 0;
  2515.  
  2516. // Reset head layout
  2517. getRows([head]).forEach((row, i) => {
  2518. const headerCells = getHeaderCells(row);
  2519.  
  2520. if (headerCells.length) {
  2521. const computedStyle = window.getComputedStyle(headerCells[0].content);
  2522. let rowHeight = 0;
  2523.  
  2524. const stickyHeaderCellMinHeight = parseFloat(computedStyle.minHeight);
  2525. // Divider 'row' or 'cell' adds a border top
  2526. const borderTop = parseFloat(computedStyle.borderTopWidth);
  2527.  
  2528. headerCells.forEach((headerCell) => {
  2529. // Reset to default
  2530. headerCell.content.style.height = '';
  2531. // The highest header cell defines the row height
  2532. rowHeight = Math.max(rowHeight, headerCell.content.getBoundingClientRect().height);
  2533. });
  2534.  
  2535. // Add the row height to the table head height
  2536. calculatedHeadSize += rowHeight;
  2537.  
  2538. headerCells.forEach((headerCell) => {
  2539. // Expand the header cell height to the row height
  2540. if (rowHeight - borderTop !== stickyHeaderCellMinHeight) {
  2541. headerCell.content.style.height = `${rowHeight}px`;
  2542. }
  2543.  
  2544. // Vertically align text in sticky cell by getting the label height
  2545. if (headerCell.content.textContent.trim().length && !headerCell.content.querySelector('coral-checkbox[coral-table-select]')) {
  2546. const stickyHeaderCellHeight = headerCell.content.getBoundingClientRect().height;
  2547. const span = document.createElement('span');
  2548.  
  2549. // Prevents a recursive table relayout that is triggered from changing the header cell content
  2550. table._preventResetLayout = true;
  2551.  
  2552. while (headerCell.content.firstChild) {
  2553. span.appendChild(headerCell.content.firstChild);
  2554. }
  2555. headerCell.content.appendChild(span);
  2556.  
  2557. const labelHeight = span.getBoundingClientRect().height;
  2558. const paddingTop = (stickyHeaderCellHeight - labelHeight) / 2;
  2559.  
  2560. while (span.firstChild) {
  2561. headerCell.content.appendChild(span.firstChild);
  2562. }
  2563. headerCell.content.removeChild(span);
  2564.  
  2565. headerCell.content.style.paddingTop = `${paddingTop}px`;
  2566.  
  2567. window.requestAnimationFrame(() => {
  2568. table._preventResetLayout = false;
  2569. });
  2570. }
  2571.  
  2572. // Position the sticky cell
  2573. previousRowHeight = previousRowHeight || rowHeight;
  2574. headerCell.content.style.top = `${i > 0 ? previousRowHeight * i + borderTop * (i - 1) : 0}px`;
  2575. });
  2576. }
  2577. });
  2578.  
  2579. const containerComputedStyle = window.getComputedStyle(this._elements.container);
  2580. const borderTopWidth = parseFloat(containerComputedStyle.borderTopWidth);
  2581. const borderBottomWidth = parseFloat(containerComputedStyle.borderBottomWidth);
  2582.  
  2583. const containerBorderSize = borderTopWidth + borderBottomWidth;
  2584. const containerMarginTop = `${calculatedHeadSize}px`;
  2585. const containerHeight = `calc(100% - ${calculatedHeadSize + containerBorderSize}px)`;
  2586. this._resetContainerLayout(containerMarginTop, containerHeight);
  2587. } else {
  2588. this._resetContainerLayout();
  2589. }
  2590. }
  2591.  
  2592. /** @private */
  2593. _resetContainerLayout(marginTop, height) {
  2594. this._elements.container.style.marginTop = marginTop || '';
  2595. this._elements.container.style.height = height || '';
  2596. }
  2597.  
  2598. /** @private */
  2599. _resetHiddenColumns(resetLayout) {
  2600. this.id = this.id || commons.getUID();
  2601.  
  2602. // Delete styles
  2603. this._elements.hiddenStyle.innerHTML = '';
  2604.  
  2605. // Render styles for each column
  2606. getColumns(this.columns).forEach((column) => {
  2607. if (column.hidden) {
  2608. const columnIndex = getIndexOf(column) + 1;
  2609.  
  2610. this._elements.hiddenStyle.innerHTML += `
  2611. #${this.id} ._coral-Table-cell:nth-child(${columnIndex}),
  2612. #${this.id} ._coral-Table-headerCell:nth-child(${columnIndex}) {
  2613. display: none;
  2614. }
  2615. `;
  2616. }
  2617. });
  2618.  
  2619. if (resetLayout) {
  2620. this._resetLayout();
  2621. }
  2622. }
  2623.  
  2624. _resetAlignmentColumns() {
  2625. this.id = this.id || commons.getUID();
  2626.  
  2627. // Delete styles
  2628. this._elements.alignmentStyle.innerHTML = '';
  2629.  
  2630. getColumns(this.columns).forEach((column) => {
  2631. const columnAlignment = column.alignment;
  2632. const columnIndex = getIndexOf(column) + 1;
  2633.  
  2634. this._elements.alignmentStyle.innerHTML += `
  2635. #${this.id} ._coral-Table-cell:nth-child(${columnIndex}),
  2636. #${this.id} ._coral-Table-headerCell:nth-child(${columnIndex}) {
  2637. text-align: ${columnAlignment};
  2638. }
  2639. `;
  2640. });
  2641. }
  2642.  
  2643. /** @private */
  2644. _onScroll() {
  2645. const table = this;
  2646. const head = table.head;
  2647.  
  2648. // Ignore if only vertical scroll
  2649. const scrollLeft = table._elements.container.scrollLeft;
  2650. if (table._lastScrollLeft === scrollLeft) {
  2651. return;
  2652. }
  2653. table._lastScrollLeft = scrollLeft;
  2654.  
  2655. if (head && head.sticky) {
  2656. // Trigger a reflow that will reposition the sticky cells for FF only.
  2657. head.style.margin = '1px';
  2658.  
  2659. window.requestAnimationFrame(() => {
  2660. head.style.margin = '';
  2661.  
  2662. // In other browsers e.g Chrome or IE, we need to adjust the position of the sticky cells manually
  2663. if (!table._preventLayoutStickyCellOnScroll) {
  2664. const firstHeaderCell = head.querySelector('tr[is="coral-table-row"] th[is="coral-table-headercell"]');
  2665.  
  2666. if (firstHeaderCell) {
  2667. // Verify if the sticky cells need to be adjusted. If the first one didn't move, we can assume that they
  2668. // all need to be adjusted. By default, the left offset is 1px because of the table border.
  2669. if (table._layoutStickyCellOnScroll || firstHeaderCell.content.offsetLeft === 1) {
  2670. table._layoutStickyCellOnScroll = true;
  2671.  
  2672. getRows([head]).forEach((row) => {
  2673. getHeaderCells(row).forEach((headerCell) => {
  2674. const paddingLeft = parseFloat(window.getComputedStyle(headerCell).paddingLeft);
  2675. headerCell.content.style.marginLeft = `-${scrollLeft + paddingLeft}px`;
  2676. });
  2677. });
  2678. } else {
  2679. // We don't need to layout the sticky cells manually
  2680. table._preventLayoutStickyCellOnScroll = true;
  2681. }
  2682. }
  2683. }
  2684. });
  2685. }
  2686. }
  2687.  
  2688. // @compat
  2689. _toggleSelectionCheckbox(row) {
  2690. const cells = getContentCells(row);
  2691. const renderCheckbox = (cell, process) => {
  2692. // Support cloneNode
  2693. cell._checkbox = cell._checkbox || cell.querySelector('coral-checkbox');
  2694.  
  2695. // Render checkbox if none
  2696. if (!cell._checkbox) {
  2697. cell._checkbox = new Checkbox();
  2698. }
  2699.  
  2700. process(cell._checkbox);
  2701.  
  2702. // Add checkbox
  2703. cell.insertBefore(cell._checkbox, cell.firstChild);
  2704. };
  2705.  
  2706. cells.forEach((cell, i) => {
  2707. const isRowSelect = i === 0 && cell.hasAttribute('coral-table-rowselect');
  2708. const isCellSelect = cell.hasAttribute('coral-table-cellselect') || cell.querySelector('coral-checkbox[coral-table-cellselect]');
  2709.  
  2710. if (isRowSelect || isCellSelect) {
  2711. let handle;
  2712. if (isRowSelect) {
  2713. handle = 'coral-table-rowselect';
  2714. }
  2715. if (isCellSelect) {
  2716. handle = 'coral-table-cellselect';
  2717. }
  2718.  
  2719. renderCheckbox(cell, (checkbox) => {
  2720. // Define selection handle
  2721. if (isRowSelect) {
  2722. cell.classList.add('_coral-Table-cell--check');
  2723. cell.removeAttribute(handle);
  2724. checkbox.setAttribute(handle, '');
  2725. } else {
  2726. cell.setAttribute(handle, '');
  2727. checkbox.removeAttribute(handle);
  2728. }
  2729.  
  2730. // Sync selection
  2731. const isSelected = (isRowSelect ? row : cell).hasAttribute('selected');
  2732. checkbox[isSelected ? 'setAttribute' : 'removeAttribute']('checked', '');
  2733. });
  2734. }
  2735. });
  2736. }
  2737.  
  2738. /** @private */
  2739. _setHeaderCellScope(headerCell, tableSection) {
  2740. // Add appropriate scope depending on whether header cell is in THEAD or TBODY
  2741. const scope = tableSection.nodeName === 'THEAD' || tableSection.nodeName === 'TFOOT' ? 'col' : 'row';
  2742. if (scope === 'col') {
  2743. headerCell.setAttribute('role', 'columnheader');
  2744. } else {
  2745. headerCell.setAttribute('role', 'rowheader');
  2746. }
  2747. headerCell.setAttribute('scope', scope);
  2748.  
  2749. if(headerCell.hasAttribute('sortable') && headerCell.content){
  2750. headerCell.content.setAttribute('role', 'button');
  2751. }
  2752. }
  2753.  
  2754. /** @private */
  2755. _handleMutations(mutations) {
  2756. mutations.forEach((mutation) => {
  2757. // Sync added nodes
  2758. for (let k = 0 ; k < mutation.addedNodes.length ; k++) {
  2759. const addedNode = mutation.addedNodes[k];
  2760.  
  2761. if (isTableBody(addedNode)) {
  2762. this._onBodyContentChanged({
  2763. target: addedNode,
  2764. detail: {
  2765. addedNodes: getRows([addedNode]),
  2766. removedNodes: []
  2767. }
  2768. });
  2769. }
  2770. }
  2771.  
  2772. // Sync removed nodes
  2773. for (let k = 0 ; k < mutation.removedNodes.length ; k++) {
  2774. const removedNode = mutation.removedNodes[k];
  2775.  
  2776. if (isTableBody(removedNode)) {
  2777. // Always make sure there's a body content zone
  2778. if (!this.body) {
  2779. this.body = new TableBody();
  2780. }
  2781.  
  2782. this._onBodyContentChanged({
  2783. target: removedNode,
  2784. detail: {
  2785. addedNodes: [],
  2786. removedNodes: getRows([removedNode])
  2787. }
  2788. });
  2789. }
  2790. }
  2791. });
  2792.  
  2793. this._resetLayout();
  2794. }
  2795.  
  2796. _getSelectableItems() {
  2797. return this.items._getSelectableItems().filter(item => !item.querySelector('[coral-table-rowselect][disabled]'));
  2798. }
  2799.  
  2800. _toggleObserver(enable) {
  2801. this._observer = this._observer || new MutationObserver(this._handleMutations.bind(this));
  2802.  
  2803. if (enable) {
  2804. this._observer.observe(this, {
  2805. childList: true,
  2806. subtree: true
  2807. });
  2808. } else {
  2809. this._observer.disconnect();
  2810. }
  2811. }
  2812.  
  2813. get _contentZones() {
  2814. return {
  2815. tbody: 'body',
  2816. thead: 'head',
  2817. tfoot: 'foot',
  2818. colgroup: 'columns'
  2819. };
  2820. }
  2821.  
  2822. /**
  2823. Returns {@link Table} variants.
  2824.  
  2825. @return {TableVariantEnum}
  2826. */
  2827. static get variant() {
  2828. return variant;
  2829. }
  2830.  
  2831. /**
  2832. Returns divider options for {@link TableHead}, {@link TableBody} and {@link TableFoot}.
  2833.  
  2834. @return {TableSectionDividerEnum}
  2835. */
  2836. static get divider() {
  2837. return divider;
  2838. }
  2839.  
  2840. static get _attributePropertyMap() {
  2841. return commons.extend(super._attributePropertyMap, {
  2842. labelledby: 'labelledBy'
  2843. });
  2844. }
  2845.  
  2846. /** @ignore */
  2847. static get observedAttributes() {
  2848. return super.observedAttributes.concat(['variant', 'selectable', 'orderable', 'labelled', 'labelledby', 'multiple', 'lockable']);
  2849. }
  2850.  
  2851. /** @ignore */
  2852. render() {
  2853. super.render();
  2854.  
  2855. this.classList.add(CLASSNAME);
  2856.  
  2857. // Wrapper should have role="presentation" because it wraps another table
  2858. this.setAttribute('role', 'presentation');
  2859.  
  2860. // Default reflected attribute
  2861. if (!this._variant) {
  2862. this.variant = variant.DEFAULT;
  2863. }
  2864.  
  2865. const head = this._elements.head;
  2866. const body = this._elements.body;
  2867. const foot = this._elements.foot;
  2868. const columns = this._elements.columns;
  2869.  
  2870. // Disable observer while rendering template
  2871. this._toggleObserver(false);
  2872. this._elements.head.setAttribute('_observe', 'off');
  2873. this._elements.body.setAttribute('_observe', 'off');
  2874.  
  2875. // Render template
  2876. const frag = document.createDocumentFragment();
  2877. frag.appendChild(this._elements.container);
  2878. frag.appendChild(this._elements.liveRegion);
  2879.  
  2880. // cloneNode support
  2881. const wrapper = this.querySelector('._coral-Table-wrapper-container');
  2882. if (wrapper) {
  2883. wrapper.remove();
  2884. }
  2885.  
  2886. let liveRegion = this.querySelector('._coral-Table-liveRegion');
  2887. if (liveRegion) {
  2888. liveRegion.remove();
  2889. }
  2890.  
  2891. // Append frag
  2892. this.appendChild(frag);
  2893.  
  2894. // Call content zone inserts
  2895. this.head = head;
  2896. this.body = body;
  2897. this.foot = foot;
  2898. this.columns = columns;
  2899.  
  2900. // Set header cell scope
  2901. getRows([this._elements.table]).forEach((row) => {
  2902. getHeaderCells(row).forEach((headerCell) => {
  2903. this._setHeaderCellScope(headerCell, row.parentNode);
  2904. });
  2905. });
  2906.  
  2907. // With a thead and tfoot,
  2908. if (this.head && this.foot) {
  2909. const headRows = getRows([this.head]);
  2910. const footRows = getRows([this.foot]);
  2911. // if the number of rows in the thead and tfoot match
  2912. if (headRows.length === footRows.length) {
  2913. let redundantFooter = true;
  2914. // and the textContent of each thead header cell matches the textContent of each tfoot header cell in the same column
  2915. headRows.forEach((row, rowIndex) => getHeaderCells(row).forEach((headerCell, cellIndex) => {
  2916. const footerCell = getHeaderCells(footRows[rowIndex])[cellIndex];
  2917. if (!footerCell || headerCell.textContent.trim() !== footerCell.textContent.trim()) {
  2918. redundantFooter = false;
  2919. }
  2920. }));
  2921. // the tfoot is redundant and should be hidden to prevent double or triple voicing of table headers.
  2922. if (redundantFooter) {
  2923. this.foot.setAttribute('aria-hidden', 'true');
  2924. }
  2925. }
  2926. }
  2927.  
  2928. // Detect table size changes
  2929. commons.addResizeListener(this, this._resetLayout);
  2930.  
  2931. // Disable table features if no items.
  2932. const items = this._getSelectableItems();
  2933. this._toggleInteractivity(items.length === 0);
  2934.  
  2935. // Sync selection state
  2936. if (this.selectable) {
  2937. const selectedItems = this.selectedItems;
  2938.  
  2939. // Sync select all handle if any
  2940. this._syncSelectAllHandle(selectedItems, items);
  2941.  
  2942. // Sync used collections
  2943. if (selectedItems.length) {
  2944. this._oldSelection = selectedItems;
  2945. this._lastSelectedItems.items = selectedItems;
  2946. }
  2947. }
  2948.  
  2949. // Sync sorted
  2950. this._allowSorting = true;
  2951. const column = this._isSorted();
  2952. if (column) {
  2953. column._doSort && column._doSort(true);
  2954. }
  2955.  
  2956. // @compat
  2957. if (this.body) {
  2958. const rows = getRows([this.body]);
  2959. // Use the first column as selection column
  2960. rows.forEach(row => this._toggleSelectionCheckbox(row));
  2961. }
  2962.  
  2963. // Enable observer again
  2964. this._toggleObserver(true);
  2965.  
  2966. // Mark table as ready
  2967. if (!this.head || this.head && !this.head.hasAttribute('sticky')) {
  2968. this.classList.add(IS_READY);
  2969. }
  2970. }
  2971.  
  2972. /**
  2973. Triggered before a {@link Table} column gets sorted by user interaction. Can be used to cancel column sorting and define
  2974. custom sorting.
  2975.  
  2976. @typedef {CustomEvent} coral-table:beforecolumnsort
  2977.  
  2978. @property {TableColumn} detail.column
  2979. The column to be sorted.
  2980. @property {String} detail.direction
  2981. The requested sorting direction for the column.
  2982. */
  2983.  
  2984. /**
  2985. Triggered when a {@link Table} column is sorted.
  2986.  
  2987. @typedef {CustomEvent} coral-table:columnsort
  2988.  
  2989. @param {TableColumn} detail.column
  2990. The sorted column.
  2991. */
  2992.  
  2993. /**
  2994. Triggered before a {@link Table} column is dragged. Can be used to cancel column dragging.
  2995.  
  2996. @typedef {CustomEvent} coral-table:beforecolumndrag
  2997.  
  2998. @property {TableColumn} detail.column
  2999. The dragged column.
  3000. @property {TableColumn} detail.before
  3001. The column will be inserted before this sibling column.
  3002. If <code>null</code>, the column is inserted at the end.
  3003. */
  3004.  
  3005. /**
  3006. Triggered when a {@link Table} column is dragged.
  3007.  
  3008. @typedef {CustomEvent} coral-table:columndrag
  3009.  
  3010. @property {TableColumn} detail.column
  3011. The dragged column.
  3012. @property {TableColumn} detail.oldBefore
  3013. The column next sibling before the swap.
  3014. If <code>null</code>, the column was the last item.
  3015. @property {TableColumn} detail.before
  3016. The column is inserted before this sibling column.
  3017. If <code>null</code>, the column is inserted at the end.
  3018. */
  3019.  
  3020. /**
  3021. Triggered before a {@link Table} row is ordered. Can be used to cancel row ordering.
  3022.  
  3023. @typedef {CustomEvent} coral-table:beforeroworder
  3024.  
  3025. @property {TableRow} detail.row
  3026. The row to be ordered.
  3027. @property {TableRow} detail.before
  3028. The row will be inserted before this sibling row.
  3029. If <code>null</code>, the row is inserted at the end.
  3030. */
  3031.  
  3032. /**
  3033. Triggered when a {@link Table} row is ordered.
  3034.  
  3035. @typedef {CustomEvent} coral-table:roworder
  3036.  
  3037. @property {TableRow} detail.row
  3038. The ordered row.
  3039. @property {TableRow} detail.oldBefore
  3040. The row next sibling before the swap.
  3041. If <code>null</code>, the row was the last item.
  3042. @param {TableRow} detail.before
  3043. The row is inserted before this sibling row.
  3044. If <code>null</code>, the row is inserted at the end.
  3045. */
  3046.  
  3047. /**
  3048. Triggered when a {@linked Table} row is locked.
  3049.  
  3050. @typedef {CustomEvent} coral-table:rowlock
  3051.  
  3052. @property {TableRow} detail.row
  3053. The locked row.
  3054. */
  3055.  
  3056. /**
  3057. Triggered when {@link Table} a row is locked.
  3058.  
  3059. @typedef {CustomEvent} coral-table:rowunlock
  3060.  
  3061. @property {TableRow} detail.row
  3062. The unlocked row.
  3063. */
  3064.  
  3065. /**
  3066. Triggered when a {@link Table} row selection changed.
  3067.  
  3068. @typedef {CustomEvent} coral-table:rowchange
  3069.  
  3070. @property {Array.<TableCell>} detail.oldSelection
  3071. The old item selection. When {@link TableRow#multiple}, it includes an Array.
  3072. @property {Array.<TableCell>} detail.selection
  3073. The item selection. When {@link Coral.Table.Row#multiple}, it includes an Array.
  3074. @property {TableRow} detail.row
  3075. The targeted row.
  3076. */
  3077.  
  3078. /**
  3079. Triggered when the {@link Table} selection changed.
  3080.  
  3081. @typedef {CustomEvent} coral-table:change
  3082.  
  3083. @property {Array.<TableRow>} detail.oldSelection
  3084. The old item selection. When {@link Table#multiple}, it includes an Array.
  3085. @property {Array.<TableRow>} detail.selection
  3086. The item selection. When {@link Table#multiple}, it includes an Array.
  3087. */
  3088. });
  3089.  
  3090. export default Table;