coral-spectrum/coral-component-table/src/scripts/Table.js
- /**
- * Copyright 2019 Adobe. All rights reserved.
- * This file is licensed to you under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License. You may obtain a copy
- * of the License at http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under
- * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
- * OF ANY KIND, either express or implied. See the License for the specific language
- * governing permissions and limitations under the License.
- */
-
- import {BaseComponent} from '../../../coral-base-component';
- import {DragAction} from '../../../coral-dragaction';
- import TableColumn from './TableColumn';
- import TableCell from './TableCell';
- import TableRow from './TableRow';
- import TableHead from './TableHead';
- import TableBody from './TableBody';
- import TableFoot from './TableFoot';
- import '../../../coral-component-button';
- import {Checkbox} from '../../../coral-component-checkbox';
- import base from '../templates/base';
- import {SelectableCollection} from '../../../coral-collection';
- import {Decorator} from '../../../coral-decorator';
- import {
- isTableHeaderCell,
- isTableCell,
- isTableRow,
- isTableBody,
- getCellByIndex,
- getColumns,
- getCells,
- getContentCells,
- getHeaderCells,
- getRows,
- getSiblingsOf,
- getIndexOf,
- divider
- } from './TableUtil';
- import {events, transform, validate, commons, i18n, Keys} from '../../../coral-utils';
-
- const CLASSNAME = '_coral-Table-wrapper';
-
- /**
- Enumeration for {@link Table} variants
-
- @typedef {Object} TableVariantEnum
-
- @property {String} DEFAULT
- A default table.
- @property {String} QUIET
- A quiet table with transparent borders and background.
- @property {String} LIST
- Not supported. Falls back to DEFAULT.
- */
- const variant = {
- DEFAULT: 'default',
- QUIET: 'quiet',
- LIST: 'list'
- };
-
- const ALL_VARIANT_CLASSES = [];
- for (const variantValue in variant) {
- ALL_VARIANT_CLASSES.push(`${CLASSNAME}--${variant[variantValue]}`);
- }
-
- const IS_DISABLED = 'is-disabled';
- const IS_SORTED = 'is-sorted';
- const IS_UNSELECTABLE = 'is-unselectable';
- const IS_FIRST_ITEM_DRAGGED = 'is-draggedFirstItem';
- const IS_LAST_ITEM_DRAGGED = 'is-draggedLastItem';
- const IS_DRAGGING_CLASS = 'is-dragging';
- const IS_BEFORE_CLASS = 'is-before';
- const IS_AFTER_CLASS = 'is-after';
- const IS_LAYOUTING = 'is-layouting';
- const IS_READY = 'is-ready';
- const KEY_SPACE = Keys.keyToCode('space');
-
- /**
- @class Coral.Table
- @classdesc A Table component is a container component to display and manipulate data in two dimensions.
- To define table actions on specific elements, handles can be used.
- A handle is given a special attribute :
- - <code>[coral-table-select]</code>. Select/unselect all table items.
- - <code>[coral-table-rowselect]</code>. Select/unselect the table item.
- - <code>[coral-table-roworder]</code>. Drag to order the table item.
- - <code>[coral-table-rowlock]</code>. Lock/unlock the table item.
- @htmltag coral-table
- @htmlbasetag table
- @extends {HTMLTableElement}
- @extends {BaseComponent}
- */
- const Table = Decorator(class extends BaseComponent(HTMLTableElement) {
- /** @ignore */
- constructor() {
- super();
-
- // Templates
- this._elements = {
- head: this.querySelector('thead[is="coral-table-head"]') || new TableHead(),
- body: this.querySelector('tbody[is="coral-table-body"]') || new TableBody(),
- foot: this.querySelector('tfoot[is="coral-table-foot"]') || new TableFoot(),
- columns: this.querySelector('colgroup') || document.createElement('colgroup')
- };
- base.call(this._elements, {commons});
-
- // Events
- this._delegateEvents({
- // Table specific
- 'global:coral-commons:_webfontactive': '_resetLayout',
- 'change [coral-table-select]': '_onSelectAll',
- 'capture:scroll [handle="container"]': '_onScroll',
-
- // Head specific
- 'click thead[is="coral-table-head"] th[is="coral-table-headercell"]': '_onHeaderCellSort',
- 'coral-dragaction:dragstart thead[is="coral-table-head"] th[is="coral-table-headercell"]': '_onHeaderCellDragStart',
- 'coral-dragaction:drag thead[is="coral-table-head"] tr[is="coral-table-row"] > th[is="coral-table-headercell"]': '_onHeaderCellDrag',
- 'coral-dragaction:dragend thead[is="coral-table-head"] tr[is="coral-table-row"] > th[is="coral-table-headercell"]': '_onHeaderCellDragEnd',
- // a11y
- 'key:enter th[is="coral-table-headercell"]': '_onHeaderCellSort',
- 'key:enter th[is="coral-table-headercell"] coral-table-headercell-content': '_onHeaderCellSort',
- 'key:space th[is="coral-table-headercell"]': '_onHeaderCellSort',
- 'key:space th[is="coral-table-headercell"] coral-table-headercell-content': '_onHeaderCellSort',
-
- // Body specific
- 'click tbody[is="coral-table-body"] [coral-table-rowlock]': '_onRowLock',
- 'click tbody[is="coral-table-body"] [coral-table-rowselect]': '_onRowSelect',
- 'click tbody[is="coral-table-body"] tr[is="coral-table-row"][selectable] [coral-table-cellselect]': '_onCellSelect',
- 'capture:mousedown tbody[is="coral-table-body"] [coral-table-roworder]:not([disabled])': '_onRowOrder',
- 'capture:touchstart tbody[is="coral-table-body"] [coral-table-roworder]:not([disabled])': '_onRowOrder',
- 'coral-dragaction:dragstart tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onRowDragStart',
- 'coral-dragaction:drag tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onRowDrag',
- 'coral-dragaction:dragover tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onRowDragOver',
- 'coral-dragaction:dragend tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onRowDragEnd',
- // a11y dnd
- 'key:space tbody[is="coral-table-body"] [coral-table-roworder]:not([disabled])': '_onKeyboardDrag',
- 'click tbody[is="coral-table-body"] tr[is="coral-table-row"] [coral-table-roworder]:not([disabled])': '_onDragHandleClick',
- 'coral-dragaction:dragonkeyspace tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onRowDragOnKeySpace',
- 'coral-dragaction:dragoveronkeyarrowdown tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onRowDragOverOnKeyArrowDown',
- 'coral-dragaction:dragoveronkeyarrowup tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onRowDragOverOnKeyArrowUp',
- 'coral-dragaction:dragendonkey tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onRowDragOverOnKeyEnter',
- // a11y
- 'mousedown tbody[is="coral-table-body"] [coral-table-rowselect]': '_onRowDown',
- 'key:enter tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onRowSelect',
- 'key:space tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onRowSelect',
- 'key:pageup tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onFocusPreviousItem',
- 'key:pagedown tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onFocusNextItem',
- 'key:left tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onFocusPreviousItem',
- 'key:right tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onFocusNextItem',
- 'key:up tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onFocusPreviousItem',
- 'key:down tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onFocusNextItem',
- 'key:home tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onFocusFirstItem',
- 'key:end tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onFocusLastItem',
- 'key:shift+pageup tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onSelectPreviousItem',
- 'key:shift+pagedown tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onSelectNextItem',
- 'key:shift+left tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onSelectPreviousItem',
- 'key:shift+right tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onSelectNextItem',
- 'key:shift+up tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onSelectPreviousItem',
- 'key:shift+down tbody[is="coral-table-body"] tr[is="coral-table-row"]': '_onSelectNextItem',
-
- // Private
- 'coral-table-row:_multiplechanged': '_onRowMultipleChanged',
- 'coral-table-row:_beforeselectedchanged': '_onBeforeRowSelectionChanged',
- 'coral-table-row:_selectedchanged': '_onRowSelectionChanged',
- 'coral-table-row:_lockedchanged': '_onRowLockedChanged',
- 'coral-table-row:_change': '_onRowChange',
- 'coral-table-row:_contentchanged': '_onRowContentChanged',
- 'coral-table-headercell:_contentchanged': '_resetLayout',
- 'coral-table-head:_contentchanged': '_onHeadContentChanged',
- 'coral-table-body:_contentchanged': '_onBodyContentChanged',
- 'coral-table-body:_empty': '_onBodyEmpty',
- 'coral-table-column:_alignmentchanged': '_onAlignmentChanged',
- 'coral-table-column:_fixedwidthchanged': '_onFixedWidthChanged',
- 'coral-table-column:_orderablechanged': '_onColumnOrderableChanged',
- 'coral-table-column:_sortablechanged': '_onColumnSortableChanged',
- 'coral-table-column:_sortabledirectionchanged': '_onColumnSortableDirectionChanged',
- 'coral-table-column:_hiddenchanged': '_onColumnHiddenChanged',
- 'coral-table-column:_beforecolumnsort': '_onBeforeColumnSort',
- 'coral-table-column:_sort': '_onColumnSort',
- 'coral-table-head:_stickychanged': '_onHeadStickyChanged'
- });
-
- // Required for coral-table:change event
- this._oldSelection = [];
- // References selected items in their selection order and is only used for keyboard selection
- this._lastSelectedItems = {
- items: [],
- direction: null
- };
-
- // Don't sort by default
- this._allowSorting = false;
-
- // Debounce timer
- this._timeout = null;
- // Debounce wait in milliseconds
- this._wait = 50;
-
- // Used by resizing detector
- this._resetLayout = this._resetLayout.bind(this);
- // Init observer
- this._toggleObserver(true);
- }
-
- /**
- The head of the table.
-
- @type {TableHead}
- @contentzone
- */
- get head() {
- return this._getContentZone(this._elements.head);
- }
-
- set head(value) {
- this._setContentZone('head', value, {
- handle: 'head',
- tagName: 'thead',
- insert: function (head) {
- // Using the native table API allows to position the head element at the correct position.
- this._elements.table.tHead = head;
-
- // To init the head observer
- head.setAttribute('_observe', 'on');
- }
- });
- }
-
- /**
- The body of the table. Multiple bodies are not supported.
-
- @type {TableBody}
- @contentzone
- */
- get body() {
- return this._getContentZone(this._elements.body);
- }
-
- set body(value) {
- this._setContentZone('body', value, {
- handle: 'body',
- tagName: 'tbody',
- insert: function (body) {
- this._elements.table.appendChild(body);
- this.items._container = body;
-
- // To init the body observer
- body.setAttribute('_observe', 'on');
- }
- });
- }
-
- /**
- The foot of the table.
-
- @type {TableFoot}
- @contentzone
- */
- get foot() {
- return this._getContentZone(this._elements.foot);
- }
-
- set foot(value) {
- this._setContentZone('foot', value, {
- handle: 'foot',
- tagName: 'tfoot',
- insert: function (foot) {
- // Using the native table API allows to position the foot element at the correct position.
- this._elements.table.tFoot = foot;
- }
- });
- }
-
- /**
- The columns of the table.
-
- @type {TableColumn}
- @contentzone
- */
- get columns() {
- return this._getContentZone(this._elements.columns);
- }
-
- set columns(value) {
- this._setContentZone('columns', value, {
- handle: 'columns',
- tagName: 'colgroup',
- insert: function (content) {
- this._elements.table.appendChild(content);
- }
- });
- }
-
- /**
- The table's variant. See {@link TableVariantEnum}.
-
- @type {String}
- @default TableVariantEnum.DEFAULT
- @htmlattribute variant
- @htmlattributereflected
- */
- get variant() {
- return this._variant || variant.DEFAULT;
- }
-
- set variant(value) {
- value = transform.string(value).toLowerCase();
- this._variant = validate.enumeration(variant)(value) && value || variant.DEFAULT;
- this._reflectAttribute('variant', this._variant);
-
- this.classList.remove(...ALL_VARIANT_CLASSES);
- this.classList.add(`${CLASSNAME}--${this._variant}`);
- }
-
- /**
- Whether the items are selectable.
-
- @type {Boolean}
- @default false
- @htmlattribute selectable
- @htmlattributereflected
- */
- get selectable() {
- return this._selectable || false;
- }
-
- set selectable(value) {
- this._selectable = transform.booleanAttr(value);
- this._reflectAttribute('selectable', this._selectable);
-
- const rows = getRows([this.body]);
-
- if (this._selectable) {
- rows.forEach((row) => {
- row.setAttribute('_selectable', '');
- });
- } else {
- // Clear selection
- rows.forEach((row) => {
- row.removeAttribute('_selectable');
- });
-
- this.trigger('coral-table:change', {
- selection: [],
- oldSelection: this._oldSelection
- });
-
- // Sync used collection
- this._oldSelection = [];
- this._lastSelectedItems.items = [];
- }
-
- // a11y
- this._toggleFocusable();
- }
-
- /**
- Whether the table is orderable. If the table is sorted, ordering handles are hidden.
-
- @type {Boolean}
- @default false
- @htmlattribute orderable
- @htmlattributereflected
- */
- get orderable() {
- return this._orderable || false;
- }
-
- set orderable(value) {
- this._orderable = transform.booleanAttr(value);
- this._reflectAttribute('orderable', this._orderable);
-
- getRows([this.body]).forEach((row) => {
- row[this._orderable ? 'setAttribute' : 'removeAttribute']('_orderable', '');
- });
-
- // a11y
- this._toggleFocusable();
- }
-
- /**
- Whether multiple items can be selected.
-
- @type {Boolean}
- @default false
- @htmlattribute multiple
- @htmlattributereflected
- */
- get multiple() {
- return this._multiple || false;
- }
-
- set multiple(value) {
- this._multiple = transform.booleanAttr(value);
- this._reflectAttribute('multiple', this._multiple);
-
- this._elements.table.setAttribute('aria-multiselectable', this._multiple);
-
- // Deselect all except last
- if (!this.multiple) {
- const selection = this.selectedItems;
-
- if (selection.length > 1) {
- selection.forEach((row, i) => {
- // Don't trigger too many events
- row.set('selected', i === selection.length - 1, true);
- });
-
- // Synchronise the table select handle
- const newSelection = this.selectedItems;
-
- if (newSelection.length) {
- this._setSelectAllHandleState('indeterminate');
- } else {
- this._setSelectAllHandleState('unchecked');
- }
-
- this.trigger('coral-table:change', {
- selection: newSelection,
- oldSelection: selection
- });
-
- // Sync used collection
- this._oldSelection = newSelection;
- this._lastSelectedItems.items = newSelection;
- }
- }
- }
-
- /**
- Whether the table rows can be locked/unlocked. If rows are locked, they float to the top of the table and aren't
- affected by column sorting.
-
- @type {Boolean}
- @default false
- @htmlattribute lockable
- @htmlattributereflected
- */
- get lockable() {
- return this._lockable || false;
- }
-
- set lockable(value) {
- this._lockable = transform.booleanAttr(value);
- this._reflectAttribute('lockable', this._lockable);
-
- getRows([this.body]).forEach((row) => {
- row[this._lockable ? 'setAttribute' : 'removeAttribute']('_lockable', '');
- });
-
- // a11y
- this._toggleFocusable();
- }
-
- /**
- Specifies <code>aria-labelledby</code> value.
-
- @type {?String}
- @default null
- @htmlattribute labelledby
- */
- get labelledBy() {
- return this._elements.table.getAttribute('aria-labelledby');
- }
-
- set labelledBy(value) {
- value = transform.string(value);
-
- this._elements.table[value ? 'setAttribute' : 'removeAttribute']('aria-labelledby', value);
- }
-
- /**
- Specifies <code>aria-label</code> value.
-
- @type {String}
- @default null
- @htmlattribute labelled
- */
- get labelled() {
- return this._elements.table.getAttribute('aria-label');
- }
-
- set labelled(value) {
- value = transform.string(value);
-
- this._elements.table[value ? 'setAttribute' : 'removeAttribute']('aria-label', value);
- }
-
- /**
- Returns an Array containing the selected items.
-
- @type {Array.<HTMLElement>}
- @readonly
- */
- get selectedItems() {
- return this.items._getAllSelected();
- }
-
- /**
- Returns the first selected item of the table. The value <code>null</code> is returned if no element is
- selected.
-
- @type {HTMLElement}
- @readonly
- */
- get selectedItem() {
- return this.items._getFirstSelected();
- }
-
- /**
- The Collection Interface that allows interacting with the items that the component contains.
-
- @type {SelectableCollection}
- @readonly
- */
- get items() {
- // Construct the collection on first request
- if (!this._items) {
- this._items = new SelectableCollection({
- host: this,
- container: this.body,
- itemBaseTagName: 'tr',
- itemTagName: 'coral-table-row'
- });
- }
-
- return this._items;
- }
-
- /** @private */
- _onSelectAll(event) {
- if (this.selectable) {
- let rows = this._getSelectableItems();
-
- if (rows.length) {
- if (this.multiple) {
- const selected = event.target.checked;
-
- rows.forEach((row) => {
- // Don't trigger too many events
- row.set('selected', selected, true);
- });
-
- rows = selected ? rows : [];
-
- // Synchronise the table select handle
- this._setSelectAllHandleState(selected ? 'checked' : 'unchecked');
-
- this.trigger('coral-table:change', {
- selection: rows,
- oldSelection: this._oldSelection
- });
-
- // Sync used collection
- this._oldSelection = rows;
- this._lastSelectedItems.items = rows;
- } else {
- // Only select last item
- const lastItem = rows[rows.length - 1];
- lastItem.selected = !lastItem.selected;
- }
- }
- }
- }
-
- _triggerChangeEvent() {
- if (!this._preventTriggeringEvents) {
- const selectedItems = this.selectedItems;
- this.trigger('coral-table:change', {
- oldSelection: this._oldSelection,
- selection: selectedItems
- });
-
- this._oldSelection = selectedItems;
- }
- }
-
- /** @private */
- _onRowOrder(event) {
- if (events.isVirtualEvent(event)) {
- return;
- }
-
- const table = this;
- const row = event.target.closest('tr[is="coral-table-row"]');
-
- if (row && table.orderable) {
- if (row.dragAction && row.dragAction.handle) {
- this._unwrapDragHandle(row.dragAction.handle);
- }
-
- const head = table.head;
- const body = table.body;
- const sticky = head && head.sticky;
- const style = row.getAttribute('style');
- const index = getIndexOf(row);
- const oldBefore = row.nextElementSibling;
- const dragAction = new DragAction(row);
- const items = getRows([body]);
- const tableBoundingClientRect = table.getBoundingClientRect();
- const rowBoundingClientRect = row.getBoundingClientRect();
-
- if (row === items[0]) {
- table.classList.add(IS_FIRST_ITEM_DRAGGED);
- } else if (row === items[items.length - 1]) {
- table.classList.add(IS_LAST_ITEM_DRAGGED);
- }
-
- dragAction.axis = 'vertical';
- // Handle the scroll in table
- dragAction.scroll = false;
- // Specify selection handle directly on the row if none found
- dragAction.handle = row.querySelector('[coral-table-roworder]');
-
- // The row placeholder indicating where the dragged element will be dropped
- const placeholder = row.cloneNode(true);
- placeholder.classList.add('_coral-Table-row--placeholder');
-
- // Prepare the row position before inserting its placeholder
- row.style.top = `${rowBoundingClientRect.top - tableBoundingClientRect.top}px`;
-
- // Prevent change event from triggering if the cloned node is selected
- table._preventTriggeringEvents = true;
- body.insertBefore(placeholder, row.nextElementSibling);
- window.requestAnimationFrame(() => {
- table._preventTriggeringEvents = false;
- });
-
- // Store the data to avoid re-reading the layout on drag events
- const dragData = {
- placeholder: placeholder,
- index: index,
- oldBefore: oldBefore,
- // Backup styles to restore them later
- style: {
- row: style
- }
- };
-
- // Required to handle the scrolling of the sticky table on drag events
- if (sticky) {
- dragData.sticky = sticky;
- dragData.tableTop = tableBoundingClientRect.top;
- dragData.tableSize = tableBoundingClientRect.height;
- dragData.headSize = parseFloat(table._elements.container.style.marginTop);
- dragData.dragElementSize = rowBoundingClientRect.height;
- }
-
- row.dragAction._dragData = dragData;
- }
- }
-
- /** @private */
- _onHeaderCellSort(event) {
- const table = this;
- const matchedTarget = event.matchedTarget.closest('th');
-
- // Don't sort if the column was dragged
- if (!matchedTarget._isDragging) {
- const column = table._getColumn(matchedTarget);
- // Only sort if actually sortable and event not defaultPrevented
- if (column && column.sortable) {
- event.preventDefault();
-
- column._sort();
-
- // Restore focus on the header cell in any case
- matchedTarget.focus();
- }
- }
- }
-
- /** @private */
- _onHeaderCellDragStart(event) {
- const table = this;
- const matchedTarget = event.matchedTarget;
- const dragElement = event.detail.dragElement;
- const siblingHeaderCellSelector = matchedTarget === dragElement ? 'th[is="coral-table-headercell"]' : 'th[is="coral-table-headercell"] coral-table-headercell-content';
- const tableBoundingClientRect = table.getBoundingClientRect();
-
- // Store the data to be used on drag events
- dragElement.dragAction._dragData = {
- draggedColumnIndex: getIndexOf(matchedTarget),
- tableLeft: tableBoundingClientRect.left,
- tableSize: tableBoundingClientRect.width,
- dragElementSize: matchedTarget.getBoundingClientRect().width,
- tableScrollWidth: table._elements.container.scrollWidth
- };
-
- getSiblingsOf(matchedTarget, siblingHeaderCellSelector, 'prevAll').forEach((item) => {
- item.classList.add(IS_BEFORE_CLASS);
- });
-
- getSiblingsOf(matchedTarget, siblingHeaderCellSelector, 'nextAll').forEach((item) => {
- item.classList.add(IS_AFTER_CLASS);
- });
- }
-
- /** @private */
- _onHeaderCellDrag(event) {
- const table = this;
- const container = table._elements.container;
- const matchedTarget = event.matchedTarget;
- const dragElement = event.detail.dragElement;
- const dragData = dragElement.dragAction._dragData;
- const row = matchedTarget.parentElement;
- const isHeaderCellDragged = matchedTarget === dragElement;
- const containerScrollLeft = container.scrollLeft;
- const documentScrollLeft = document.body.scrollLeft;
-
- // Prevent sorting on header cell click if the header cell is being dragged
- matchedTarget._isDragging = true;
-
- // Scroll left/right if table edge is reached
- const position = dragElement.getBoundingClientRect().left - dragData.tableLeft;
- const leftScrollLimit = 0;
- const rightScrollLimit = dragData.tableSize - dragData.dragElementSize;
- const scrollOffset = 10;
-
- if (position < leftScrollLimit) {
- container.scrollLeft -= scrollOffset;
- }
- // 2nd condition is required to avoid increasing the container scroll width
- else if (position > rightScrollLimit && containerScrollLeft + dragData.tableSize < dragData.tableScrollWidth) {
- container.scrollLeft += scrollOffset;
- }
-
- // Position sibling header cells based on the dragged element
- getHeaderCells(row).forEach((headerCell) => {
- const draggedHeaderCell = isHeaderCellDragged ? headerCell : headerCell.content;
-
- if (!draggedHeaderCell.classList.contains(IS_DRAGGING_CLASS)) {
- const offsetLeft = draggedHeaderCell.getBoundingClientRect().left + documentScrollLeft;
- const isAfter = event.detail.pageX < offsetLeft + draggedHeaderCell.offsetWidth / 3;
-
- draggedHeaderCell.classList.toggle(IS_AFTER_CLASS, isAfter);
- draggedHeaderCell.classList.toggle(IS_BEFORE_CLASS, !isAfter);
-
- const columnIndex = getIndexOf(headerCell);
- const dragElementIndex = getIndexOf(matchedTarget);
-
- // Place headercell after
- if (draggedHeaderCell.classList.contains(IS_AFTER_CLASS)) {
- if (columnIndex < dragElementIndex) {
- // Position the header cells based on their siblings position
- if (isHeaderCellDragged) {
- const nextHeaderCellWidth = draggedHeaderCell.clientWidth;
- draggedHeaderCell.style.left = `${nextHeaderCellWidth}px`;
- } else {
- const nextHeaderCell = getSiblingsOf(headerCell, 'th[is="coral-table-headercell"]', 'next');
- const nextHeaderCellLeftOffset = nextHeaderCell.getBoundingClientRect().left + documentScrollLeft;
- draggedHeaderCell.style.left = `${nextHeaderCellLeftOffset + containerScrollLeft}px`;
- }
- } else {
- draggedHeaderCell.style.left = '';
- }
- }
-
- // Place headerCell before
- if (draggedHeaderCell.classList.contains(IS_BEFORE_CLASS)) {
- if (columnIndex > dragElementIndex) {
- const prev = getSiblingsOf(headerCell, 'th[is="coral-table-headercell"]', 'prev');
-
- // Position the header cells based on their siblings position
- if (isHeaderCellDragged) {
- const beforeHeaderCellWidth = prev.clientWidth;
- draggedHeaderCell.style.left = `${-1 * (beforeHeaderCellWidth)}px`;
- } else {
- const beforeHeaderCellLeftOffset = prev.getBoundingClientRect().left + documentScrollLeft;
- draggedHeaderCell.style.left = `${beforeHeaderCellLeftOffset + containerScrollLeft}px`;
- }
- } else {
- draggedHeaderCell.style.left = '';
- }
- }
- }
- });
- }
-
- /** @private */
- _onHeaderCellDragEnd(event) {
- const table = this;
- const matchedTarget = event.matchedTarget;
- const dragElement = event.detail.dragElement;
- const dragData = dragElement.dragAction._dragData;
- const column = table._getColumn(matchedTarget);
- const headRows = getRows([table.head]);
- const isHeaderCellDragged = matchedTarget === dragElement;
- const row = matchedTarget.parentElement;
-
- // Select all cells in table body and foot given the index
- const getCellsByIndex = (cellIndex) => {
- const cellElements = [];
- const rows = getRows([table.body, table.foot]);
- rows.forEach((rowElement) => {
- const cell = getCellByIndex(rowElement, cellIndex);
- if (cell) {
- cellElements.push(cell);
- }
- });
- return cellElements;
- };
-
- const cells = getCellsByIndex(getIndexOf(matchedTarget));
- let before = null;
- let after = null;
-
- // Siblings are either header cell or header cell content based on the current sticky state
- if (isHeaderCellDragged) {
- before = row.querySelector(`th[is="coral-table-headercell"].${IS_AFTER_CLASS}`);
-
- after = row.querySelectorAll(`th[is="coral-table-headercell"].${IS_BEFORE_CLASS}`);
- after = after.length ? after[after.length - 1] : null;
- } else {
- before = row.querySelector(`th[is="coral-table-headercell"] > coral-table-headercell-content.${IS_AFTER_CLASS}`);
- before = before ? before.parentNode : null;
-
- after = row.querySelectorAll(`th[is="coral-table-headercell"] > coral-table-headercell-content.${IS_BEFORE_CLASS}`);
- after = after.length ? after[after.length - 1].parentNode : null;
- }
-
- // Did header cell order change ?
- const swapped = !(before && before.previousElementSibling === matchedTarget || after && after.nextElementSibling === matchedTarget);
-
- // Switch whole columns based on the new position of the dragged element
- if (swapped) {
- const beforeColumn = before ? table._getColumn(before) : null;
-
- // Trigger the event on table
- const beforeEvent = table.trigger('coral-table:beforecolumndrag', {
- column: column,
- before: beforeColumn
- });
-
- const oldBefore = column.nextElementSibling;
-
- if (!beforeEvent.defaultPrevented) {
- // Insert the headercell at the new position
- if (before) {
- const beforeIndex = getIndexOf(before);
- const beforeCells = getCellsByIndex(beforeIndex);
- cells.forEach((cell, i) => {
- cell.parentNode.insertBefore(cell, beforeCells[i]);
- });
-
- // Sync <coral-table-column> by reordering it too
- const beforeCol = getColumns(table.columns)[beforeIndex];
- if (beforeCol && column) {
- table.columns.insertBefore(column, beforeCol);
- }
-
- row.insertBefore(matchedTarget, before);
- }
- if (after) {
- const afterIndex = getIndexOf(after);
- const afterCells = getCellsByIndex(afterIndex);
- cells.forEach((cell, i) => {
- cell.parentNode.insertBefore(cell, afterCells[i].nextElementSibling);
- });
-
- // Sync <coral-table-column> by reordering it too
- const afterCol = getColumns(table.columns)[afterIndex];
- if (afterCol && column) {
- table.columns.insertBefore(column, afterCol.nextElementSibling);
- }
-
- row.insertBefore(matchedTarget, after.nextElementSibling);
- }
-
- // Trigger the order event if the column position changed
- if (dragData.draggedColumnIndex !== getIndexOf(matchedTarget)) {
- const newBefore = getColumns(table.columns)[getIndexOf(column) + 1];
- table.trigger('coral-table:columndrag', {
- column: column,
- oldBefore: oldBefore,
- before: newBefore || null
- });
- }
- }
- }
-
- // Restoring default header cells styling
- headRows.forEach((rowElement) => {
- getHeaderCells(rowElement).forEach((headerCell) => {
- headerCell = isHeaderCellDragged ? headerCell : headerCell.content;
- headerCell.classList.remove(IS_AFTER_CLASS);
- headerCell.classList.remove(IS_BEFORE_CLASS);
- headerCell.style.left = '';
- });
- });
-
- // Trigger a relayout
- table._resetLayout();
-
- window.requestAnimationFrame(() => {
- // Allows sorting again after dragging completed
- matchedTarget._isDragging = undefined;
- // Refocus the dragged element manually
- table._toggleElementTabIndex(dragElement, null, true);
- });
- }
-
- /** @private */
- _onCellSelect(event) {
- const cell = event.target.closest('td[is="coral-table-cell"]');
-
- if (cell) {
- cell.selected = !cell.selected;
- }
- }
-
- /** @private */
- _onRowSelect(event) {
- const table = this;
- const row = event.target.closest('tr[is="coral-table-row"]');
-
- if (row) {
- // Ignore selection if the row is locked
- if (table.lockable && row.locked) {
- return;
- }
-
- // Restore text-selection
- table.classList.remove(IS_UNSELECTABLE);
-
- // Prevent row selection when it's the selection handle and the target is an input
- if (table.selectable && (Keys.filterInputs(event) || !row.hasAttribute('coral-table-rowselect'))) {
- // Pressing space scrolls the sticky table to the bottom if scrollable
- if (event.keyCode === KEY_SPACE) {
- event.preventDefault();
- }
-
- if (event.shiftKey) {
- let lastSelectedItem = table._lastSelectedItems.items[table._lastSelectedItems.items.length - 1];
- const lastSelectedDirection = table._lastSelectedItems.direction;
-
- // If no selected items, by default set the first item as last selected item
- if (!table.selectedItem) {
- const rows = table._getSelectableItems();
- if (rows.length) {
- lastSelectedItem = rows[0];
- lastSelectedItem.set('selected', true, true);
- }
- }
-
- // Don't continue if table has no items or if the last selected item is the clicked item
- if (lastSelectedItem && getIndexOf(row) !== getIndexOf(lastSelectedItem)) {
- // Range selection direction
- const before = getIndexOf(row) < getIndexOf(lastSelectedItem);
- const rangeQuery = before ? 'prevUntil' : 'nextUntil';
-
- // Store direction
- table._lastSelectedItems.direction = before ? 'up' : 'down';
-
- if (!row.selected) {
- // Store selection range
- const selectionRange = getSiblingsOf(lastSelectedItem, 'tr[is="coral-table-row"]:not([selected])', rangeQuery);
- selectionRange[before ? 'push' : 'unshift'](lastSelectedItem);
-
- // Direction change
- if (!before && lastSelectedDirection === 'up' || before && lastSelectedDirection === 'down') {
- selectionRange.forEach((item) => {
- item.set('selected', false, true);
- });
- }
-
- // Select item
- const selectionRangeRow = selectionRange[before ? 0 : selectionRange.length - 1];
- selectionRangeRow.set('selected', true, true);
- getSiblingsOf(selectionRangeRow, row, rangeQuery).forEach((item) => {
- item.set('selected', true, true);
- });
- } else {
- const selection = getSiblingsOf(lastSelectedItem, row, rangeQuery);
-
- // If some items are not selected
- if (selection.some((item) => !item.hasAttribute('selected'))) {
- // Select all items in between
- selection.forEach((item) => {
- item.set('selected', true, true);
- });
-
- // Deselect selected item right before/after the selection range
- getSiblingsOf(row, 'tr[is="coral-table-row"]:not([selected])', rangeQuery).forEach((item) => {
- item.set('selected', false, true);
- });
- } else {
- // Deselect items
- selection[before ? 'push' : 'unshift'](lastSelectedItem);
- selection.forEach((item) => {
- item.set('selected', false, true);
- });
- }
- }
- }
- } else {
- // Remove direction if simple click without shift key pressed
- table._lastSelectedItems.direction = null;
- }
-
- // Select the row that was clicked and keep the row selected if shift key was pressed
- row.selected = event.shiftKey ? true : !row.selected;
-
- // Don't focus the row if the target isn't the row and focusable
- table._focusItem(row, event.target === event.matchedTarget || event.target.tabIndex < 0);
- }
- }
- }
-
- /** @private */
- _onRowLock(event) {
- const table = this;
-
- if (table.lockable) {
- const row = event.target.closest('tr[is="coral-table-row"]');
- if (row) {
- event.preventDefault();
- event.stopPropagation();
- row.locked = !row.locked;
-
- // Refocus the locked/unlocked item manually
- window.requestAnimationFrame(() => {
- table._focusItem(row, true);
- });
- }
- }
- }
-
- /** @private */
- _onRowDown(event) {
- const table = this;
-
- // Prevent text-selection
- if (table.selectedItem && event.shiftKey) {
- table.classList.add(IS_UNSELECTABLE);
-
- // @polyfill IE
- // Store text selection feature
- const onSelectStart = document.onselectstart;
- // Kill text selection feature
- document.onselectstart = () => false;
- // Restore text selection feature
- window.requestAnimationFrame(() => {
- document.onselectstart = onSelectStart;
- });
- }
- }
-
- /** @private */
- _onRowDragStart(event) {
- const table = this;
- const head = table.head;
- const body = table.body;
- const dragElement = event.detail.dragElement;
- const dragData = dragElement.dragAction._dragData;
-
- dragData.style.cells = [];
- getCells(dragElement).forEach((cell) => {
- // Backup styles to restore them later
- dragData.style.cells.push(cell.getAttribute('style'));
- // Cells will shrink otherwise
- cell.style.width = window.getComputedStyle(cell).width;
- });
-
- if (head && !head.sticky) {
- // @polyfill ie11
- // Element that scrolls the document.
- const scrollingElement = document.scrollingElement || document.documentElement;
- dragElement.style.top = `${dragElement.getBoundingClientRect().top + scrollingElement.scrollTop}px`;
- }
- dragElement.style.position = 'absolute';
-
- // Setting drop zones allows to listen for coral-dragaction:dragover event
- dragElement.dragAction.dropZone = body.querySelectorAll(`tr[is="coral-table-row"]:not(.${IS_DRAGGING_CLASS})`);
-
- // We cannot rely on :focus since the row is being moved in the dom while dnd
- dragElement.classList.add('is-focused');
- }
-
- /** @private */
- _onRowDrag(event) {
- const table = this;
- const body = table.body;
- const dragElement = event.detail.dragElement;
- const dragData = dragElement.dragAction._dragData;
- const firstRow = getRows([body])[0];
-
- // Insert the placeholder at the top
- if (dragElement.getBoundingClientRect().top <= firstRow.getBoundingClientRect().top) {
- table._preventTriggeringEvents = true;
- body.insertBefore(dragData.placeholder, firstRow);
- window.requestAnimationFrame(() => {
- table._preventTriggeringEvents = false;
- });
- }
-
- // Scroll up/down if table edge is reached
- if (dragData.sticky) {
- const dragElementTop = dragElement.getBoundingClientRect().top;
- const position = dragElementTop - dragData.tableTop - dragData.headSize;
- const topScrollLimit = 0;
- const bottomScrollLimit = dragData.tableSize - dragData.dragElementSize - dragData.headSize;
- const scrollOffset = 10;
-
- // Handle the scrollbar position based on the dragged element position.
- // nextFrame is required else Chrome wouldn't take scrollTop changes in account when dragging the first row down
- window.requestAnimationFrame(() => {
- if (position < topScrollLimit) {
- table._elements.container.scrollTop -= scrollOffset;
- } else if (position > bottomScrollLimit) {
- table._elements.container.scrollTop += scrollOffset;
- }
- });
- }
- }
-
- /** @private */
- _onRowDragOver(event) {
- const table = this;
- const body = table.body;
- const dragElement = event.detail.dragElement;
- const dropElement = event.detail.dropElement;
- const dragData = dragElement.dragAction._dragData;
-
- // Swap the placeholder
- if (dragElement.getBoundingClientRect().top >= dropElement.getBoundingClientRect().top) {
- table._preventTriggeringEvents = true;
- body.insertBefore(dragData.placeholder, dropElement.nextElementSibling);
- window.requestAnimationFrame(() => {
- table._preventTriggeringEvents = false;
- });
- }
- }
-
- /** @private */
- _onRowDragEnd(event) {
- const table = this;
- const body = table.body;
- const dragElement = event.detail.dragElement;
- const dragAction = event.detail.dragElement.dragAction;
-
- const dragData = dragAction._dragData;
- const before = dragData.placeholder ? dragData.placeholder.nextElementSibling : null;
-
- // Clean up
- table.classList.remove(IS_FIRST_ITEM_DRAGGED);
- table.classList.remove(IS_LAST_ITEM_DRAGGED);
-
- if (dragData.placeholder && dragData.placeholder.parentNode) {
- dragData.placeholder.parentNode.removeChild(dragData.placeholder);
- }
- dragAction.destroy();
-
- // Restore specific styling
- dragElement.setAttribute('style', dragData.style.row || '');
- getCells(dragElement).forEach((cell, i) => {
- cell.setAttribute('style', dragData.style.cells[i] || '');
- });
-
- // Trigger the event on table
- const beforeEvent = table.trigger('coral-table:beforeroworder', {
- row: dragElement,
- before: before
- });
-
- if (!beforeEvent.defaultPrevented) {
- // Did row order change ?
- const rows = getRows([body]).filter((item) => item !== dragElement);
-
- if (dragData.index !== rows.indexOf(dragData.placeholder)) {
- // Insert the row at the new position and prevent change event from triggering
- table._preventTriggeringEvents = true;
- body.insertBefore(dragElement, before);
- window.requestAnimationFrame(() => {
- table._preventTriggeringEvents = false;
- });
-
- // Trigger the order event if the row position changed
- table.trigger('coral-table:roworder', {
- row: dragElement,
- oldBefore: dragData.oldBefore,
- before: before
- });
- }
- }
-
- // Refocus the dragged element manually
- window.requestAnimationFrame(() => {
- dragElement.classList.remove('is-focused');
- table._focusItem(dragElement, true);
- });
- }
-
- /** @private */
- _wrapDragHandle(handle, callback = () => {}) {
- if(!handle.closest('span[role="application"]')) {
- const span = document.createElement('span');
- span.setAttribute('role', 'application');
- span.setAttribute('aria-label', i18n.get('reordering'));
- handle.parentNode.insertBefore(span, handle);
- span.appendChild(handle);
- handle.selected = true;
- handle.setAttribute('aria-pressed', 'true');
- window.requestAnimationFrame(() => callback());
- }
- }
-
- /** @private */
- _unwrapDragHandle(handle, callback = () => {}) {
- const span = handle && handle.closest('span[role="application"]');
-
- if (handle) {
- handle.selected = false;
- handle.removeAttribute('aria-pressed');
- handle.removeAttribute('aria-describedby');
- }
- window.requestAnimationFrame(() => {
- if (span) {
- span.parentNode.insertBefore(handle, span);
- span.remove();
- }
- callback();
- });
- }
-
- /** @private */
- _onKeyboardDrag(event) {
- const table = this;
- const row = event.target.closest('tr[is="coral-table-row"]');
-
- if (row && table.orderable) {
- event.preventDefault();
- event.stopPropagation();
-
- if (row.dragAction && row.dragAction.isKeyboardDragging) {
- return;
- }
-
- const style = row.getAttribute('style');
- const index = getIndexOf(row);
- const oldBefore = row.nextElementSibling;
- const dragAction = new DragAction(row);
-
- dragAction.axis = 'vertical';
-
- // Handle the scroll in table
- dragAction.scroll = false;
-
- // Specify selection handle directly on the row if none found
- const handle = row.querySelector('[coral-table-roworder]');
- dragAction.handle = handle;
-
- // Wrap the drag handle button in a span with role="application",
- // to force Windows screen readers into forms mode while dragging.
- if (event.target === handle) {
- this._wrapDragHandle(handle, () => handle.focus());
- }
-
- // The row placeholder indicating where the dragged element will be dropped
- const placeholder = row.cloneNode(true);
- placeholder.classList.add('_coral-Table-row--placeholder');
-
- // Store the data to avoid re-reading the layout on drag events
- const dragData = {
- placeholder: placeholder,
- index: index,
- oldBefore: oldBefore,
- // Backup styles to restore them later
- style: {
- row: style
- }
- };
- row.dragAction._dragData = dragData;
- }
- }
-
- _onDragHandleClick(event) {
- const row = event.target.closest('tr[is="coral-table-row"]');
- if (!row.dragAction) {
- this._onKeyboardDrag(event);
- row.dragAction._isKeyboardDrag = true;
- } else if (row.dragAction._isKeyboardDrag) {
- row.dragAction._isKeyboardDrag = undefined;
- }
- }
-
- /** @private */
- _onRowDragOnKeySpace(event) {
- event.preventDefault();
-
- const dragElement = event.detail.dragElement;
- const dragData = dragElement.dragAction._dragData;
-
- if (dragElement.dragAction._isKeyboardDrag) {
- return;
- }
-
- dragData.style.cells = [];
- getCells(dragElement).forEach((cell) => {
- // Backup styles to restore them later
- dragData.style.cells.push(cell.getAttribute('style'));
- // Cells will shrink otherwise
- cell.style.width = window.getComputedStyle(cell).width;
- });
- }
-
- /** @private */
- _onRowDragOverOnKeyArrowDown(event) {
- const table = this;
- const body = table.body;
- const dragElement = event.detail.dragElement;
- const items = getRows([body]);
- const index = getIndexOf(dragElement);
- const dragData = dragElement.dragAction._dragData;
- const handle = dragElement.dragAction.handle;
- const rowHeader = dragElement.rowHeader;
-
- event.preventDefault();
-
- // We cannot rely on :focus since the row is being moved in the dom while dnd
- dragElement.classList.add('is-focused');
-
- if (dragElement === items[items.length - 1]) {
- for (let position = 0; position < items.length - 1; position++) {
- body.appendChild(items[position]);
- }
- body.insertBefore(items[0], items[items.length - 2].nextElementSibling);
- } else {
- body.insertBefore(items[index + 1], items[index]);
- }
-
- // Restore specific styling
- dragElement.setAttribute('style', dragData.style.row || '');
- getCells(dragElement).forEach((cell, i) => {
- if (dragData.style.cells) {
- cell.setAttribute('style', dragData.style.cells[i] || '');
- }
- });
-
- if (handle) {
- handle.focus();
- this._announceLiveRegion((rowHeader ? rowHeader.textContent + ' ' : '') + i18n.get('reordered to row {0}', [getIndexOf(dragElement) + 1]), 'assertive');
- }
-
- dragElement.scrollIntoView({block: 'nearest'});
- }
-
- /** @private */
- _onRowDragOverOnKeyArrowUp(event) {
- const table = this;
- const body = table.body;
- const dragElement = event.detail.dragElement;
- const items = getRows([body]);
- const index = getIndexOf(dragElement);
- const dragData = dragElement.dragAction._dragData;
- const handle = dragElement.dragAction.handle;
- const rowHeader = dragElement.rowHeader;
-
- event.preventDefault();
-
- // We cannot rely on :focus since the row is being moved in the dom while dnd
- dragElement.classList.add('is-focused');
-
- if (dragElement === items[0]) {
- for (let position = 0; position < items.length - 2; position++) {
- body.insertBefore(items[position + 1], items[0]);
- }
- body.insertBefore(items[items.length - 1], items[1]);
- } else {
- body.insertBefore(items[index - 1], items[index].nextElementSibling);
- }
-
- // Restore specific styling
- dragElement.setAttribute('style', dragData.style.row || '');
- getCells(dragElement).forEach((cell, i) => {
- if (dragData.style.cells) {
- cell.setAttribute('style', dragData.style.cells[i] || '');
- }
- });
-
- if (handle) {
- handle.focus();
- this._announceLiveRegion((rowHeader ? rowHeader.textContent + ' ' : '') + i18n.get('reordered to row {0}', [getIndexOf(dragElement) + 1]), 'assertive');
- }
-
- dragElement.scrollIntoView({block: 'nearest'});
- }
-
- /** @private */
- _onRowDragOverOnKeyEnter(event) {
- const table = this;
- const dragElement = event.detail.dragElement;
- const dragAction = dragElement.dragAction;
- const dragData = dragAction._dragData;
- const handle = dragAction.handle;
-
- if (dragAction._isKeyboardDrag) {
- dragAction._isKeyboardDrag = undefined;
- return;
- }
-
- // Trigger the event on table
- const beforeEvent = table.trigger('coral-table:beforeroworder', {
- row: dragElement,
- before: dragData.oldBefore
- });
-
- if (!beforeEvent.defaultPrevented && dragData.oldBefore !== dragElement.nextElementSibling) {
- // Trigger the order event if the row position changed
- table.trigger('coral-table:roworder', {
- row: dragElement,
- oldBefore: dragData.oldBefore,
- before: dragElement.nextElementSibling
- });
- }
-
- dragAction.destroy();
-
- const isFocusWithinDragElement = dragElement.contains(document.activeElement) || dragElement === document.activeElement;
- const isFocusOnHandle = handle && handle === document.activeElement;
-
- // Refocus the dragged element manually
- const callback = () => {
- dragElement.classList.remove('is-focused');
- if (isFocusWithinDragElement) {
- table._focusItem(dragElement, true);
- }
- if (isFocusOnHandle) {
- handle.focus();
- }
- };
-
- this._unwrapDragHandle(handle, callback);
- }
-
- /** @private */
- _onRowMultipleChanged(event) {
- event.stopImmediatePropagation();
-
- const table = this;
- const row = event.target;
-
- // Deselect all except last
- if (!row.multiple) {
- const selectedItems = row.selectedItems;
- table._preventTriggeringEvents = true;
- selectedItems.forEach((cell, i) => {
- cell.selected = i === selectedItems.length - 1;
- });
-
- window.requestAnimationFrame(() => {
- table._preventTriggeringEvents = false;
-
- table.trigger('coral-table:rowchange', {
- oldSelection: selectedItems,
- selection: row.selectedItems,
- row: row
- });
- });
- }
- }
-
- /** @private */
- _onBeforeRowSelectionChanged(event) {
- event.stopImmediatePropagation();
-
- // In single selection, if the added item is selected, the rest should be deselected
- const selectedItem = this.selectedItem;
- if (!this.multiple && selectedItem && !event.target.selected) {
- selectedItem.set('selected', false, true);
- this._removeLastSelectedItem(selectedItem);
- }
- }
-
- /** @private */
- _syncSelectAllHandle(selectedItems, items) {
- if (items.length && selectedItems.length === items.length) {
- this._setSelectAllHandleState('checked');
- } else if (!selectedItems.length) {
- this._setSelectAllHandleState('unchecked');
- } else {
- this._setSelectAllHandleState('indeterminate');
- }
- }
-
- /** @private */
- _setSelectAllHandleState(state) {
- const handle = this.querySelector('[coral-table-select]');
-
- if (handle) {
-
- // If the handle is a checkbox but lacks a label, label it with "Select All".
- if (handle.tagName === 'CORAL-CHECKBOX') {
- if (!handle.labelled) {
- handle.setAttribute('labelled', i18n.get('Select All'));
- }
- if (!handle.title) {
- handle.setAttribute('title', i18n.get('Select All'));
- }
- }
-
- if (state === 'checked') {
- handle.removeAttribute('indeterminate');
- handle.setAttribute('checked', '');
- } else if (state === 'unchecked') {
- handle.removeAttribute('indeterminate');
- handle.removeAttribute('checked');
- } else if (state === 'indeterminate') {
- handle.setAttribute('indeterminate', '');
- }
- }
- }
-
- /** @private */
- _onRowSelectionChanged(event) {
- event.stopImmediatePropagation();
-
- this._triggerChangeEvent();
-
- const table = this;
- const body = table.body;
- const row = event.target;
-
- // Synchronise the table select handle
- if (body && body.contains(row)) {
- const selection = table.selectedItems;
- const rows = table._getSelectableItems();
-
- // Sync select all handle
- table._syncSelectAllHandle(selection, rows);
-
- // Store or remove the row reference
- table[row.selected ? '_addLastSelectedItem' : '_removeLastSelectedItem'](row);
-
- // Store selected items range
- const lastSelectedItem = table._lastSelectedItems.items[table._lastSelectedItems.items.length - 1];
- const next = table._lastSelectedItems.direction === 'down';
- if (row.selected && lastSelectedItem && lastSelectedItem.selected && getSiblingsOf(lastSelectedItem, 'tr[is="coral-table-row"][selected]', next ? 'next' : 'prev')) {
- getSiblingsOf(lastSelectedItem, 'tr[is="coral-table-row"]:not([selected])', next ? 'nextUntil' : 'prevUntil').forEach((item) => {
- table._addLastSelectedItem(item);
- });
- }
- }
- }
-
- _onRowLockedChanged(event) {
- event.stopImmediatePropagation();
-
- const table = this;
- const body = this.body;
- const row = event.target;
-
- if (body && body.contains(row)) {
- if (row.locked) {
- // Store the row index as reference to place it back if unlocked and its selection state
- row._rowIndex = getIndexOf(row);
-
- // Insert row at first position of its tbody
- table._preventTriggeringEvents = true;
- body.insertBefore(row, getRows([body])[0]);
- window.requestAnimationFrame(() => {
- table._preventTriggeringEvents = false;
- });
-
- // Trigger event on table
- table.trigger('coral-table:rowlock', {row});
- } else {
- // Put the row back to its initial position
- if (row._rowIndex >= 0) {
- const beforeRow = getRows([body])[row._rowIndex];
- if (beforeRow) {
- // Insert row at its initial position
- table._preventTriggeringEvents = true;
- body.insertBefore(row, beforeRow.nextElementSibling);
- window.requestAnimationFrame(() => {
- table._preventTriggeringEvents = false;
- });
- }
- }
-
- // Trigger event on table
- table.trigger('coral-table:rowunlock', {row});
- }
- }
- }
-
- _onHeadContentChanged(event) {
- event.stopImmediatePropagation();
-
- const table = this;
- const head = table.head;
- const addedNodes = event.detail.addedNodes;
-
- for (let i = 0 ; i < addedNodes.length ; i++) {
- const node = addedNodes[i];
-
- // Sync header cell whether sticky or not
- if (isTableHeaderCell(node)) {
- table._toggleStickyHeaderCell(node, head.sticky);
- }
- }
- }
-
- /** @private */
- _onBodyContentChanged(event) {
- if (event.stopImmediatePropagation) {
- event.stopImmediatePropagation();
- }
-
- const table = this;
- const addedNodes = event.detail.addedNodes;
- const removedNodes = event.detail.removedNodes;
- let addedNode = null;
- const selectItem = (item) => {
- item.selected = item === addedNode;
- };
- let changed = false;
-
- // Sync added nodes
- for (let i = 0 ; i < addedNodes.length ; i++) {
- addedNode = addedNodes[i];
-
- // Sync row state with table properties
- if (isTableRow(addedNode)) {
- changed = true;
-
- addedNode._toggleSelectable(table.selectable);
- addedNode._toggleOrderable(table.orderable);
- addedNode._toggleLockable(table.lockable);
-
- // @compat
- this._toggleSelectionCheckbox(addedNode);
-
- const selectedItems = table.selectedItems;
- if (addedNode.selected) {
- // In single selection, if the added item is selected, the rest should be deselected
- if (!table.multiple && selectedItems.length > 1) {
- selectedItems.forEach(selectItem);
- }
-
- table._triggerChangeEvent();
- }
-
- // Cells are selectable too
- if (addedNode.selectable) {
- addedNode.trigger('coral-table-row:_contentchanged', {
- addedNodes: getContentCells(addedNode),
- removedNodes: []
- });
- }
-
- // Trigger collection event
- if (!table._preventTriggeringEvents) {
- table.trigger('coral-collection:add', {
- item: addedNode
- });
- }
-
- // a11y
- table._toggleFocusable();
- }
- }
-
- // Sync removed nodes
- for (let k = 0 ; k < removedNodes.length ; k++) {
- const removedNode = removedNodes[k];
-
- if (isTableRow(removedNode)) {
- changed = true;
-
- // If the focusable item is removed, the first item becomes the new focusable item
- if (removedNode.getAttribute('tabindex') === '0') {
- const firstItem = getRows([table.body])[0];
- if (firstItem) {
- table._focusItem(firstItem);
- }
- }
-
- if (removedNode.selected) {
- table._triggerChangeEvent();
- }
-
- // Sync _lastSelectedItems array
- const removedItemIndex = table._lastSelectedItems.items.indexOf(removedNode);
- if (removedItemIndex !== -1) {
- table._lastSelectedItems.items = table._lastSelectedItems.items.splice(removedItemIndex, 1);
- }
-
- // Trigger collection event
- if (!table._preventTriggeringEvents) {
- table.trigger('coral-collection:remove', {
- item: removedNode
- });
- }
- }
- }
-
- if (changed) {
- const items = this._getSelectableItems();
- // Sync select all handle if any.
- table._syncSelectAllHandle(table.selectedItems, items);
- // Disable table features if no items.
- table._toggleInteractivity(items.length === 0);
- }
- }
-
- /** @private */
- _onBodyEmpty(event) {
- event.stopImmediatePropagation();
- this._toggleInteractivity(true);
- }
-
- /** @private */
- _onRowChange(event) {
- event.stopImmediatePropagation();
-
- if (!this._preventTriggeringEvents) {
- this.trigger('coral-table:rowchange', {
- oldSelection: event.detail.oldSelection,
- selection: event.detail.selection,
- row: event.target
- });
- }
- }
-
- /** @private */
- _onRowContentChanged(event) {
- event.stopImmediatePropagation();
-
- const table = this;
- const row = event.target;
- const addedNodes = event.detail.addedNodes;
- let addedNode = null;
- const removedNodes = event.detail.removedNodes;
- const selectItem = (item) => {
- item.selected = item === addedNode;
- };
-
- // Sync added nodes
- for (let i = 0 ; i < addedNodes.length ; i++) {
- addedNode = addedNodes[i];
-
- // Sync row state with table properties
- if (isTableCell(addedNode)) {
- addedNode._toggleSelectable(row.selectable);
-
- const selectedItems = row.selectedItems;
- if (addedNode.selected) {
- // In single selection, if the added item is selected, the rest should be deselected
- if (!row.multiple && selectedItems.length > 1) {
- selectedItems.forEach(selectItem);
- }
-
- row._triggerChangeEvent();
- }
-
- // Trigger collection event
- if (!table._preventTriggeringEvents) {
- row.trigger('coral-collection:add', {
- item: addedNode
- });
- }
- }
- // Add appropriate scope depending on whether headercell is in THEAD or TBODY
- else if (isTableHeaderCell(addedNode)) {
- table._setHeaderCellScope(addedNode, row.parentNode);
- }
- }
-
- // Sync removed nodes
- for (let k = 0 ; k < removedNodes.length ; k++) {
- const removedNode = removedNodes[k];
-
- if (isTableCell(removedNode)) {
- if (removedNode.selected) {
- row._triggerChangeEvent();
- }
-
- // Trigger collection event
- if (!table._preventTriggeringEvents) {
- row.trigger('coral-collection:remove', {
- item: removedNode
- });
- }
- }
- }
- }
-
- /** @private */
- _toggleInteractivity(disable) {
- const table = this;
- const selectAll = table.querySelector('[coral-table-select]');
-
- if (selectAll) {
- selectAll.disabled = disable;
- }
-
- table.classList.toggle(IS_DISABLED, disable);
- }
-
- _onAlignmentChanged(event) {
- event.stopImmediatePropagation();
-
- this._resetAlignmentColumns();
- }
-
- /** @private */
- _onFixedWidthChanged(event) {
- event.stopImmediatePropagation();
-
- const table = this;
- const head = table.head;
- const column = event.target;
-
- if (head) {
- const headRows = getRows([head]);
- const columnIndex = getIndexOf(event.target);
-
- headRows.forEach((row) => {
- const headerCell = getCellByIndex(row, columnIndex);
- if (headerCell && headerCell.tagName === 'TH') {
- headerCell[column.fixedWidth ? 'setAttribute' : 'removeAttribute']('fixedwidth', '');
- }
- });
- }
-
- table._resetLayout();
- }
-
- /** @private */
- _onColumnOrderableChanged(event) {
- event.stopImmediatePropagation();
-
- const table = this;
- const head = this.head;
- const column = event.target;
- const headerCell = table._getColumnHeaderCell(column);
-
- if (headerCell) {
- // Move the drag handle
- table._toggleDragActionHandle(headerCell, head && head.sticky);
-
- table._resetLayout();
- }
- }
-
- /** @private */
- _onColumnSortableChanged(event) {
- event.stopImmediatePropagation();
-
- const table = this;
- const head = this.head;
- const column = event.target;
- const headerCell = table._getColumnHeaderCell(column);
-
- if (headerCell) {
- // For icons (chevron up/down) styling
- headerCell[column.sortable ? 'setAttribute' : 'removeAttribute']('sortable', '');
-
- // Toggle tab index. Sortable headercells are focusable.
- table._toggleHeaderCellTabIndex(headerCell, head && head.sticky);
-
- table._resetLayout();
- }
- }
-
- _onColumnSortableDirectionChanged(event) {
- event.stopImmediatePropagation();
-
- const table = this;
- const column = event.target;
- const sortableDirection = TableColumn.sortableDirection;
-
- // Hide coral-table-roworder handles if table is sorted
- table.classList.toggle(IS_SORTED, table._isSorted());
-
- const headerCell = table._getColumnHeaderCell(column);
- if (headerCell) {
- // For icons (chevron up/down) styling
- headerCell.setAttribute('sortabledirection', column.sortableDirection);
- headerCell.setAttribute('aria-sort', column.sortableDirection === sortableDirection.DEFAULT ? 'none' : column.sortableDirection);
-
- if (column.sortableDirection === sortableDirection.DEFAULT) {
- this._announceLiveRegion();
- } else {
- const textContent = headerCell.content.textContent.trim();
- if (textContent.length) {
- // Set live region to true so that sort description string will be announced.
- this._announceLiveRegion(i18n.get(`sorted by column {0} in ${column.sortableDirection} order`, textContent));
- }
- }
- }
- }
-
- /** @private */
- _announceLiveRegion(text, politeness = 'polite') {
-
- if (this._liveRegionTimeout) {
- window.clearTimeout(this._liveRegionTimeout);
- }
-
- if (!text || !text.length) {
- this._elements.liveRegion.innerText = '';
- return;
- }
-
- // Set live region to true so that text string will be announced.
- this._elements.liveRegion.setAttribute('aria-live', politeness);
- this._elements.liveRegion.removeAttribute('aria-hidden');
- if (this._isSorted()) {
- this._elements.liveRegion.innerText = text;
- } else {
- this._liveRegionTimeout = window.setTimeout(() => this._elements.liveRegion.innerText = text, 100);
- }
-
- // @a11y wait 2.5 seconds to give screen reader enough time to announce the live region before silencing the it.
- window.setTimeout(() => {
- this._elements.liveRegion.setAttribute('aria-live', 'off');
- this._elements.liveRegion.setAttribute('aria-hidden', 'true');
- if (!this._isSorted()) {
- this._elements.liveRegion.innerText = '';
- }
- }, 2500);
- }
-
- _onColumnHiddenChanged(event) {
- event.stopImmediatePropagation();
-
- this._resetHiddenColumns(true);
- }
-
- _onBeforeColumnSort(event) {
- event.stopImmediatePropagation();
-
- const table = this;
- const column = event.target;
- const newSortableDirection = event.detail.newSortableDirection;
-
- const beforeEvent = table.trigger('coral-table:beforecolumnsort', {
- column: column,
- direction: newSortableDirection
- });
-
- if (!beforeEvent.defaultPrevented) {
- column.sortableDirection = newSortableDirection;
- }
- }
-
- _onColumnSort(event) {
- event.stopImmediatePropagation();
-
- // Don't sort yet
- if (!this._allowSorting) {
- return;
- }
-
- const table = this;
- const body = table.body;
- const column = event.target;
- const columnIndex = getIndexOf(column);
- const colHeaderCell = table._getColumnHeaderCell(column);
- const onInitialization = event.detail.onInitialization;
- const sortableDirection = event.detail.sortableDirection;
- const sortableType = event.detail.sortableType;
-
- const rows = getRows([body]);
- const cells = [];
-
- // Prevent change event from triggering when sorting
- if (table) {
- table._preventTriggeringEvents = true;
- }
-
- // Store a reference of the default row index for default sortable direction
- rows.forEach((row, i) => {
- if (typeof row._defaultRowIndex === 'undefined') {
- row._defaultRowIndex = i;
- }
-
- const cell = getCellByIndex(row, columnIndex);
- if (cell) {
- cells.push(cell);
- }
- });
-
- if (column.sortableDirection === sortableDirection.ASCENDING) {
- // Remove sortable direction on sibling columns
- getSiblingsOf(column, 'col[is="coral-table-column"]').forEach((col) => {
- col._preventSort = true;
- col.setAttribute('sortabledirection', sortableDirection.DEFAULT);
- col._preventSort = false;
- });
-
- if (colHeaderCell) {
- // For icons (chevron up/down) styling
- getSiblingsOf(colHeaderCell, 'th[is="coral-table-headercell"]').forEach((headerCell) => {
- headerCell.setAttribute('sortabledirection', sortableDirection.DEFAULT);
- headerCell.setAttribute('aria-sort', 'none');
- });
- }
-
- // Use cell value to sort and fallback if not specified
- cells.sort((a, b) => {
- if (column.sortableType === sortableType.ALPHANUMERIC) {
- const aText = a.value ? a.value : a.textContent;
- const bText = b.value ? b.value : b.textContent;
- return aText.localeCompare(bText);
- } else if (column.sortableType === sortableType.NUMBER) {
- // Remove all spaces and replace commas with dots for decimal values
- const aNumber = parseFloat(a.value ? a.value : a.textContent.replace(/\s+/g, '').replace(/,/g, '.'));
- const bNumber = parseFloat(b.value ? b.value : b.textContent.replace(/\s+/g, '').replace(/,/g, '.'));
- return aNumber > bNumber ? 1 : -1;
- } else if (column.sortableType === sortableType.DATE) {
- const aDate = a.value ? new Date(parseInt(a.value, 10)) : new Date(a.textContent);
- const bDate = b.value ? new Date(parseInt(b.value, 10)) : new Date(b.textContent);
- return aDate > bDate ? 1 : -1;
- }
- });
-
- // Only sort if not custom sorting
- if (column.sortableType !== sortableType.CUSTOM) {
- if (body) {
- // Insert the row at the new position if actually sorted
- cells.forEach((cell) => {
- const row = cell.parentElement;
- // Prevent locked row to be sorted
- if (!row.locked) {
- body.appendChild(row);
- }
- });
- }
-
- // Trigger on table
- table.trigger('coral-table:columnsort', {column});
- }
-
- // Table is in a sorted state. Disable orderable actions
- rows.forEach((row) => {
- if (row.dragAction) {
- row.dragAction.destroy();
- }
- });
- } else if (column.sortableDirection === sortableDirection.DESCENDING) {
- getSiblingsOf(column, 'col[is="coral-table-column"]').forEach((col) => {
- col._preventSort = true;
- col.setAttribute('sortabledirection', sortableDirection.DEFAULT);
- col._preventSort = false;
- });
-
- if (colHeaderCell) {
- getSiblingsOf(colHeaderCell, 'th[is="coral-table-headercell"]').forEach((headerCell) => {
- headerCell.setAttribute('sortabledirection', sortableDirection.DEFAULT);
- headerCell.setAttribute('aria-sort', 'none');
- });
- }
-
- cells.sort((a, b) => {
- if (column.sortableType === sortableType.ALPHANUMERIC) {
- const aText = a.value ? a.value : a.textContent;
- const bText = b.value ? b.value : b.textContent;
- return bText.localeCompare(aText);
- } else if (column.sortableType === sortableType.NUMBER) {
- // Remove all spaces and replace commas with dots for decimal values
- const aNumber = parseFloat(a.value ? a.value : a.textContent.replace(/\s+/g, '').replace(/,/g, '.'));
- const bNumber = parseFloat(b.value ? b.value : b.textContent.replace(/\s+/g, '').replace(/,/g, '.'));
- return aNumber < bNumber ? 1 : -1;
- } else if (column.sortableType === sortableType.DATE) {
- const aDate = a.value ? new Date(parseInt(a.value, 10)) : new Date(a.textContent);
- const bDate = b.value ? new Date(parseInt(b.value, 10)) : new Date(b.textContent);
- return aDate < bDate ? 1 : -1;
- }
- });
-
- // Only sort if not custom sorting
- if (column.sortableType !== sortableType.CUSTOM) {
- if (body) {
- // Insert the row at the new position if actually sorted
- cells.forEach((cell) => {
- const row = cell.parentElement;
- // Prevent locked row to be sorted
- if (!row.locked) {
- body.appendChild(row);
- }
- });
- }
-
- // Trigger on table
- table.trigger('coral-table:columnsort', {column});
- }
-
- // Table is in a sorted state. Disable orderable actions
- rows.forEach((row) => {
- if (row.dragAction) {
- row.dragAction.destroy();
- }
- });
- } else if (column.sortableDirection === sortableDirection.DEFAULT && !onInitialization) {
- // Only sort if not custom sorting
- if (column.sortableType !== sortableType.CUSTOM) {
- // Put rows back to their initial position
- rows.sort((a, b) => a._defaultRowIndex > b._defaultRowIndex ? 1 : -1);
-
- rows.forEach((row) => {
- // Prevent locked row to be sorted
- if (body && !row.locked) {
- body.appendChild(row);
- }
- });
-
- // Trigger on table
- table.trigger('coral-table:columnsort', {column});
- }
- }
-
- // Allow triggering change events again after sorting
- window.requestAnimationFrame(() => {
- // a11y initialize column sort aria-describedby
- if (onInitialization && column.sortableDirection !== sortableDirection.DEFAULT) {
- const textContent = colHeaderCell.content.textContent.trim();
- if (textContent.length) {
- this._elements.liveRegion.innerText = i18n.get(`sorted by column {0} in ${column.sortableDirection} order`, textContent);
- }
- }
-
- table._preventTriggeringEvents = false;
- });
- }
-
- _onHeadStickyChanged(event) {
- event.stopImmediatePropagation();
-
- // a11y
- this._toggleFocusable();
-
- const table = this;
- const head = event.target;
-
- // Wait next frame before reading and changing header cell layout
- window.requestAnimationFrame(() => {
- // Defines the head height
- const tableHeight = head.sticky ? `${head.getBoundingClientRect().height}px` : null;
- table._resetContainerLayout(tableHeight, table._elements.container.style.height);
-
- getRows([head]).forEach((row) => {
- getHeaderCells(row).forEach((headerCell) => {
- table._toggleStickyHeaderCell(headerCell, head.sticky);
- });
- });
-
- // Make sure sticky styling is applied
- table.classList.toggle(`${CLASSNAME}--sticky`, head.sticky);
-
- // Layout sticky head
- table._preventResetLayout = false;
- table._resetLayout();
- });
- }
-
- /** @private */
- _getColumnHeaderCell(column) {
- const table = this;
- const head = table.head;
- let headerCell = null;
-
- if (head) {
- const headRows = getRows([head]);
- const columnIndex = getIndexOf(column);
- if (headRows.length) {
- headerCell = getCellByIndex(headRows[headRows.length - 1], columnIndex);
- headerCell = headerCell && headerCell.tagName === 'TH' ? headerCell : null;
- }
- }
-
- return headerCell;
- }
-
- /** @private */
- _getColumn(headerCell) {
- // Get the corresponding column
- return getColumns(this.columns)[getIndexOf(headerCell)] || null;
- }
-
- /** @private */
- _toggleStickyHeaderCell(headerCell, sticky) {
- // Set the size
- this._layoutStickyCell(headerCell, sticky);
-
- // Define DragAction on the sticky cell instead of the headercell
- this._toggleDragActionHandle(headerCell, sticky);
-
- // Toggle tab index. Sortable headercells are focusable.
- this._toggleHeaderCellTabIndex(headerCell, sticky);
- }
-
- _layoutStickyCell(headerCell, sticky) {
- if (sticky) {
- const computedStyle = window.getComputedStyle(headerCell);
-
- // Don't allow the column to shrink less than its minimum allowed
- if (!headerCell.style.minWidth) {
- let hasVisibleChildNodes = false;
- // In most cases, there's text content
- if (headerCell.textContent.trim().length) {
- hasVisibleChildNodes = true;
- }
- // Verify if there are any visible nodes without text content which could take layout space
- else {
- const headerCellChildren = headerCell.content.children;
- for (let i = 0 ; i < headerCellChildren.length && !hasVisibleChildNodes ; i++) {
- if (headerCellChildren[0].offsetParent) {
- hasVisibleChildNodes = true;
- }
- }
- }
-
- if (hasVisibleChildNodes) {
- const width = headerCell.content.getBoundingClientRect().width;
- // Don't set the width if the header cell is hidden
- if (width > 0) {
- headerCell.style.minWidth = `${width}px`;
- }
- }
- }
-
- const cellWidth = parseFloat(computedStyle.width);
- const cellPadding = parseFloat(computedStyle.paddingLeft) + parseFloat(computedStyle.paddingRight);
- const borderRightWidth = parseFloat(computedStyle.borderRightWidth);
-
- // Reflect headercell size on sticky cell
- headerCell.content.style.width = `${cellWidth + cellPadding + borderRightWidth}px`;
- } else {
- // Restore headercell style
- headerCell.style.minWidth = '';
- headerCell.content.style.width = '';
- headerCell.content.style.height = '';
- headerCell.content.style.top = '';
- headerCell.content.style.marginLeft = '';
- headerCell.content.style.paddingTop = '';
- }
- }
-
- /** @private */
- _toggleDragActionHandle(headerCell, sticky) {
- const column = this._getColumn(headerCell);
-
- if (headerCell.dragAction) {
- headerCell.dragAction.destroy();
- }
- if (headerCell.content.dragAction) {
- headerCell.content.dragAction.destroy();
- }
-
- if (column && column.orderable) {
- const dragAction = new DragAction(sticky ? headerCell.content : headerCell);
- dragAction.axis = 'horizontal';
- // Handle the scroll in table
- dragAction.scroll = false;
- headerCell.setAttribute('orderable', '');
- } else {
- headerCell.removeAttribute('orderable');
- }
- }
-
- /** @private */
- _toggleFocusable() {
- const firstItem = getRows([this.body])[0];
- if (!firstItem) {
- return;
- }
-
- const focusableItem = this._getFocusableItem();
- if (this.selectable || this.lockable || this.orderable || (this.head && this.head.sticky)) {
- // First item is focusable by default but don't remove the tabindex of the existing focusable item
- if (!focusableItem) {
- this._toggleElementTabIndex(firstItem);
- }
- } else if (focusableItem) {
- // Basic table is not focusable
- focusableItem.removeAttribute('tabindex');
- }
- }
-
- /** @private */
- _toggleElementTabIndex(element, oldFocusable, forceFocus) {
- if (oldFocusable) {
- oldFocusable.removeAttribute('tabindex');
- }
-
- element.setAttribute('tabindex', '0');
- if (forceFocus) {
- element.focus();
- }
- }
-
- /** @private */
- _toggleHeaderCellTabIndex(headerCell, sticky) {
- const column = this._getColumn(headerCell);
- const sortable = column && (column.sortable || column.orderable);
- headerCell.content[sortable ? 'setAttribute' : 'removeAttribute']('tabindex', '0');
- headerCell.content[sortable ? 'setAttribute' : 'removeAttribute']('role', 'button');
- }
-
- /** @private */
- _isSorted() {
- let column = null;
- const isSorted = getColumns(this.columns).some((col) => {
- column = col;
- return col.sortableDirection !== TableColumn.sortableDirection.DEFAULT;
- });
-
- return isSorted ? column : false;
- }
-
- /** @private */
- _focusEdgeItem(event, first) {
- const items = getRows([this.body]);
- if (items.length) {
- event.preventDefault();
-
- let item = this._getFocusableItem();
- if (item) {
- item.removeAttribute('tabindex');
- }
-
- item = items[first ? 0 : items.length - 1];
- item.setAttribute('tabindex', '0');
- item.focus();
- }
- }
-
- /** @private */
- _focusSiblingItem(event, next) {
- const item = this._getFocusableItem();
- if (item) {
- event.preventDefault();
-
- const siblingItem = getSiblingsOf(item, 'tr[is="coral-table-row"]', next ? 'next' : 'prev');
- if (siblingItem) {
- item.removeAttribute('tabindex');
- siblingItem.setAttribute('tabindex', '0');
- siblingItem.focus();
- }
- }
- }
-
- /** @private */
- _selectSiblingItem(next) {
- if (this.selectable && this.multiple) {
- const selectedItems = this.selectedItems;
- let lastSelectedItem = this._lastSelectedItems.items[this._lastSelectedItems.items.length - 1];
-
- if (selectedItems.length) {
- // Prevent selection if we reached the edge
- if (next && lastSelectedItem.matches(':last-of-type') || !next && lastSelectedItem.matches(':first-of-type')) {
- return;
- }
-
- // Target sibling item
- const sibling = getSiblingsOf(lastSelectedItem, 'tr[is="coral-table-row"]', next ? 'next' : 'prev');
- if (!sibling.hasAttribute('selected')) {
- lastSelectedItem = sibling;
- }
-
- // Store last selection
- this._lastSelectedItems.direction = next ? 'down' : 'up';
-
- // Toggle selection
- lastSelectedItem.selected = !lastSelectedItem.selected;
- } else if (getRows([this.body]).length) {
- const focusableItem = this._getFocusableItem();
-
- // Store last selection
- this._lastSelectedItems.direction = next ? 'down' : 'up';
-
- // Select focusable item by default if no items selected
- focusableItem.selected = true;
- }
- }
-
- // Focus last selected item
- window.requestAnimationFrame(() => {
- const itemToFocus = this._lastSelectedItems.items[this._lastSelectedItems.items.length - 1];
- if (itemToFocus) {
- this._focusItem(itemToFocus, true);
- }
- });
- }
-
- /** @private */
- _getFocusableItem() {
- return this.body && this.body.querySelector('tr[is="coral-table-row"][tabindex="0"]');
- }
-
- /** @private */
- _getFocusableHeaderCell() {
- return this.head && this.head.querySelector('th[is="coral-table-headercell"][tabindex="0"], th[is="coral-table-headercell"] > coral-table-headercell-content[tabindex="0"]');
- }
-
- /** @private */
- _addLastSelectedItem(item) {
- if (this._lastSelectedItems.items.indexOf(item) === -1) {
- this._lastSelectedItems.items.push(item);
- } else {
- // Push it at the end
- this._removeLastSelectedItem(item);
- this._addLastSelectedItem(item);
- }
- }
-
- /** @private */
- _removeLastSelectedItem(item) {
- this._lastSelectedItems.items.splice(this._lastSelectedItems.items.indexOf(item), 1);
- }
-
- /** @private */
- _focusItem(item, forceFocus) {
- this._toggleElementTabIndex(item, this._getFocusableItem(), forceFocus);
- }
-
- /** @private */
- _onFocusFirstItem(event) {
- this._focusEdgeItem(event, true);
- }
-
- /** @private */
- _onFocusLastItem(event) {
- this._focusEdgeItem(event, false);
- }
-
- /** @private */
- _onFocusNextItem(event) {
- this._focusSiblingItem(event, true);
- }
-
- /** @private */
- _onFocusPreviousItem(event) {
- this._focusSiblingItem(event, false);
- }
-
- /** @private */
- _onSelectNextItem() {
- this._selectSiblingItem(true);
- }
-
- /** @private */
- _onSelectPreviousItem() {
- this._selectSiblingItem(false);
- }
-
- /**
- * Call the layout method of table component
- *
- * @param {Boolean} forced
- * If true call the layout method immediately, else wait for timeout
- */
- resetLayout(forced) {
- forced = transform.boolean(forced);
- if (forced === true) {
- this._doResetLayout();
- this._preventResetLayout = false;
- } else {
- this._resetLayout();
- }
- }
-
- /** @private */
- _doResetLayout() {
- this.classList.add(IS_LAYOUTING);
- this._resizeStickyHead();
- this._resizeContainer();
- this.classList.remove(IS_LAYOUTING);
- }
-
- /** @private */
- _resetLayout() {
- if (this._preventResetLayout) {
- return;
- }
-
- // Debounce
- if (this._timeout !== null) {
- window.clearTimeout(this._timeout);
- }
-
- this._timeout = window.setTimeout(() => {
- this._timeout = null;
- this._doResetLayout();
- // Mark table as ready
- this.classList.add(IS_READY);
- }, this._wait);
- }
-
- /** @private */
- _resizeStickyHead() {
- const table = this;
- const head = table.head;
- if (head && head.sticky) {
- getRows([head]).forEach((row) => {
- getHeaderCells(row).forEach((headerCell) => {
- table._layoutStickyCell(headerCell, true);
- });
- });
- }
- }
-
- /** @private */
- _resizeContainer() {
- const table = this;
- const head = table.head;
-
- if (head && head.sticky) {
- let calculatedHeadSize = 0;
- let previousRowHeight = 0;
-
- // Reset head layout
- getRows([head]).forEach((row, i) => {
- const headerCells = getHeaderCells(row);
-
- if (headerCells.length) {
- const computedStyle = window.getComputedStyle(headerCells[0].content);
- let rowHeight = 0;
-
- const stickyHeaderCellMinHeight = parseFloat(computedStyle.minHeight);
- // Divider 'row' or 'cell' adds a border top
- const borderTop = parseFloat(computedStyle.borderTopWidth);
-
- headerCells.forEach((headerCell) => {
- // Reset to default
- headerCell.content.style.height = '';
- // The highest header cell defines the row height
- rowHeight = Math.max(rowHeight, headerCell.content.getBoundingClientRect().height);
- });
-
- // Add the row height to the table head height
- calculatedHeadSize += rowHeight;
-
- headerCells.forEach((headerCell) => {
- // Expand the header cell height to the row height
- if (rowHeight - borderTop !== stickyHeaderCellMinHeight) {
- headerCell.content.style.height = `${rowHeight}px`;
- }
-
- // Vertically align text in sticky cell by getting the label height
- if (headerCell.content.textContent.trim().length && !headerCell.content.querySelector('coral-checkbox[coral-table-select]')) {
- const stickyHeaderCellHeight = headerCell.content.getBoundingClientRect().height;
- const span = document.createElement('span');
-
- // Prevents a recursive table relayout that is triggered from changing the header cell content
- table._preventResetLayout = true;
-
- while (headerCell.content.firstChild) {
- span.appendChild(headerCell.content.firstChild);
- }
- headerCell.content.appendChild(span);
-
- const labelHeight = span.getBoundingClientRect().height;
- const paddingTop = (stickyHeaderCellHeight - labelHeight) / 2;
-
- while (span.firstChild) {
- headerCell.content.appendChild(span.firstChild);
- }
- headerCell.content.removeChild(span);
-
- headerCell.content.style.paddingTop = `${paddingTop}px`;
-
- window.requestAnimationFrame(() => {
- table._preventResetLayout = false;
- });
- }
-
- // Position the sticky cell
- previousRowHeight = previousRowHeight || rowHeight;
- headerCell.content.style.top = `${i > 0 ? previousRowHeight * i + borderTop * (i - 1) : 0}px`;
- });
- }
- });
-
- const containerComputedStyle = window.getComputedStyle(this._elements.container);
- const borderTopWidth = parseFloat(containerComputedStyle.borderTopWidth);
- const borderBottomWidth = parseFloat(containerComputedStyle.borderBottomWidth);
-
- const containerBorderSize = borderTopWidth + borderBottomWidth;
- const containerMarginTop = `${calculatedHeadSize}px`;
- const containerHeight = `calc(100% - ${calculatedHeadSize + containerBorderSize}px)`;
- this._resetContainerLayout(containerMarginTop, containerHeight);
- } else {
- this._resetContainerLayout();
- }
- }
-
- /** @private */
- _resetContainerLayout(marginTop, height) {
- this._elements.container.style.marginTop = marginTop || '';
- this._elements.container.style.height = height || '';
- }
-
- /** @private */
- _resetHiddenColumns(resetLayout) {
- this.id = this.id || commons.getUID();
-
- // Delete styles
- this._elements.hiddenStyle.innerHTML = '';
-
- // Render styles for each column
- getColumns(this.columns).forEach((column) => {
- if (column.hidden) {
- const columnIndex = getIndexOf(column) + 1;
-
- this._elements.hiddenStyle.innerHTML += `
- #${this.id} ._coral-Table-cell:nth-child(${columnIndex}),
- #${this.id} ._coral-Table-headerCell:nth-child(${columnIndex}) {
- display: none;
- }
- `;
- }
- });
-
- if (resetLayout) {
- this._resetLayout();
- }
- }
-
- _resetAlignmentColumns() {
- this.id = this.id || commons.getUID();
-
- // Delete styles
- this._elements.alignmentStyle.innerHTML = '';
-
- getColumns(this.columns).forEach((column) => {
- const columnAlignment = column.alignment;
- const columnIndex = getIndexOf(column) + 1;
-
- this._elements.alignmentStyle.innerHTML += `
- #${this.id} ._coral-Table-cell:nth-child(${columnIndex}),
- #${this.id} ._coral-Table-headerCell:nth-child(${columnIndex}) {
- text-align: ${columnAlignment};
- }
- `;
- });
- }
-
- /** @private */
- _onScroll() {
- const table = this;
- const head = table.head;
-
- // Ignore if only vertical scroll
- const scrollLeft = table._elements.container.scrollLeft;
- if (table._lastScrollLeft === scrollLeft) {
- return;
- }
- table._lastScrollLeft = scrollLeft;
-
- if (head && head.sticky) {
- // Trigger a reflow that will reposition the sticky cells for FF only.
- head.style.margin = '1px';
-
- window.requestAnimationFrame(() => {
- head.style.margin = '';
-
- // In other browsers e.g Chrome or IE, we need to adjust the position of the sticky cells manually
- if (!table._preventLayoutStickyCellOnScroll) {
- const firstHeaderCell = head.querySelector('tr[is="coral-table-row"] th[is="coral-table-headercell"]');
-
- if (firstHeaderCell) {
- // Verify if the sticky cells need to be adjusted. If the first one didn't move, we can assume that they
- // all need to be adjusted. By default, the left offset is 1px because of the table border.
- if (table._layoutStickyCellOnScroll || firstHeaderCell.content.offsetLeft === 1) {
- table._layoutStickyCellOnScroll = true;
-
- getRows([head]).forEach((row) => {
- getHeaderCells(row).forEach((headerCell) => {
- const paddingLeft = parseFloat(window.getComputedStyle(headerCell).paddingLeft);
- headerCell.content.style.marginLeft = `-${scrollLeft + paddingLeft}px`;
- });
- });
- } else {
- // We don't need to layout the sticky cells manually
- table._preventLayoutStickyCellOnScroll = true;
- }
- }
- }
- });
- }
- }
-
- // @compat
- _toggleSelectionCheckbox(row) {
- const cells = getContentCells(row);
- const renderCheckbox = (cell, process) => {
- // Support cloneNode
- cell._checkbox = cell._checkbox || cell.querySelector('coral-checkbox');
-
- // Render checkbox if none
- if (!cell._checkbox) {
- cell._checkbox = new Checkbox();
- }
-
- process(cell._checkbox);
-
- // Add checkbox
- cell.insertBefore(cell._checkbox, cell.firstChild);
- };
-
- cells.forEach((cell, i) => {
- const isRowSelect = i === 0 && cell.hasAttribute('coral-table-rowselect');
- const isCellSelect = cell.hasAttribute('coral-table-cellselect') || cell.querySelector('coral-checkbox[coral-table-cellselect]');
-
- if (isRowSelect || isCellSelect) {
- let handle;
- if (isRowSelect) {
- handle = 'coral-table-rowselect';
- }
- if (isCellSelect) {
- handle = 'coral-table-cellselect';
- }
-
- renderCheckbox(cell, (checkbox) => {
- // Define selection handle
- if (isRowSelect) {
- cell.classList.add('_coral-Table-cell--check');
- cell.removeAttribute(handle);
- checkbox.setAttribute(handle, '');
- } else {
- cell.setAttribute(handle, '');
- checkbox.removeAttribute(handle);
- }
-
- // Sync selection
- const isSelected = (isRowSelect ? row : cell).hasAttribute('selected');
- checkbox[isSelected ? 'setAttribute' : 'removeAttribute']('checked', '');
- });
- }
- });
- }
-
- /** @private */
- _setHeaderCellScope(headerCell, tableSection) {
- // Add appropriate scope depending on whether header cell is in THEAD or TBODY
- const scope = tableSection.nodeName === 'THEAD' || tableSection.nodeName === 'TFOOT' ? 'col' : 'row';
- if (scope === 'col') {
- headerCell.setAttribute('role', 'columnheader');
- } else {
- headerCell.setAttribute('role', 'rowheader');
- }
- headerCell.setAttribute('scope', scope);
-
- if(headerCell.hasAttribute('sortable') && headerCell.content){
- headerCell.content.setAttribute('role', 'button');
- }
- }
-
- /** @private */
- _handleMutations(mutations) {
- mutations.forEach((mutation) => {
- // Sync added nodes
- for (let k = 0 ; k < mutation.addedNodes.length ; k++) {
- const addedNode = mutation.addedNodes[k];
-
- if (isTableBody(addedNode)) {
- this._onBodyContentChanged({
- target: addedNode,
- detail: {
- addedNodes: getRows([addedNode]),
- removedNodes: []
- }
- });
- }
- }
-
- // Sync removed nodes
- for (let k = 0 ; k < mutation.removedNodes.length ; k++) {
- const removedNode = mutation.removedNodes[k];
-
- if (isTableBody(removedNode)) {
- // Always make sure there's a body content zone
- if (!this.body) {
- this.body = new TableBody();
- }
-
- this._onBodyContentChanged({
- target: removedNode,
- detail: {
- addedNodes: [],
- removedNodes: getRows([removedNode])
- }
- });
- }
- }
- });
-
- this._resetLayout();
- }
-
- _getSelectableItems() {
- return this.items._getSelectableItems().filter(item => !item.querySelector('[coral-table-rowselect][disabled]'));
- }
-
- _toggleObserver(enable) {
- this._observer = this._observer || new MutationObserver(this._handleMutations.bind(this));
-
- if (enable) {
- this._observer.observe(this, {
- childList: true,
- subtree: true
- });
- } else {
- this._observer.disconnect();
- }
- }
-
- get _contentZones() {
- return {
- tbody: 'body',
- thead: 'head',
- tfoot: 'foot',
- colgroup: 'columns'
- };
- }
-
- /**
- Returns {@link Table} variants.
-
- @return {TableVariantEnum}
- */
- static get variant() {
- return variant;
- }
-
- /**
- Returns divider options for {@link TableHead}, {@link TableBody} and {@link TableFoot}.
-
- @return {TableSectionDividerEnum}
- */
- static get divider() {
- return divider;
- }
-
- static get _attributePropertyMap() {
- return commons.extend(super._attributePropertyMap, {
- labelledby: 'labelledBy'
- });
- }
-
- /** @ignore */
- static get observedAttributes() {
- return super.observedAttributes.concat(['variant', 'selectable', 'orderable', 'labelled', 'labelledby', 'multiple', 'lockable']);
- }
-
- /** @ignore */
- render() {
- super.render();
-
- this.classList.add(CLASSNAME);
-
- // Wrapper should have role="presentation" because it wraps another table
- this.setAttribute('role', 'presentation');
-
- // Default reflected attribute
- if (!this._variant) {
- this.variant = variant.DEFAULT;
- }
-
- const head = this._elements.head;
- const body = this._elements.body;
- const foot = this._elements.foot;
- const columns = this._elements.columns;
-
- // Disable observer while rendering template
- this._toggleObserver(false);
- this._elements.head.setAttribute('_observe', 'off');
- this._elements.body.setAttribute('_observe', 'off');
-
- // Render template
- const frag = document.createDocumentFragment();
- frag.appendChild(this._elements.container);
- frag.appendChild(this._elements.liveRegion);
-
- // cloneNode support
- const wrapper = this.querySelector('._coral-Table-wrapper-container');
- if (wrapper) {
- wrapper.remove();
- }
-
- let liveRegion = this.querySelector('._coral-Table-liveRegion');
- if (liveRegion) {
- liveRegion.remove();
- }
-
- // Append frag
- this.appendChild(frag);
-
- // Call content zone inserts
- this.head = head;
- this.body = body;
- this.foot = foot;
- this.columns = columns;
-
- // Set header cell scope
- getRows([this._elements.table]).forEach((row) => {
- getHeaderCells(row).forEach((headerCell) => {
- this._setHeaderCellScope(headerCell, row.parentNode);
- });
- });
-
- // With a thead and tfoot,
- if (this.head && this.foot) {
- const headRows = getRows([this.head]);
- const footRows = getRows([this.foot]);
- // if the number of rows in the thead and tfoot match
- if (headRows.length === footRows.length) {
- let redundantFooter = true;
- // and the textContent of each thead header cell matches the textContent of each tfoot header cell in the same column
- headRows.forEach((row, rowIndex) => getHeaderCells(row).forEach((headerCell, cellIndex) => {
- const footerCell = getHeaderCells(footRows[rowIndex])[cellIndex];
- if (!footerCell || headerCell.textContent.trim() !== footerCell.textContent.trim()) {
- redundantFooter = false;
- }
- }));
- // the tfoot is redundant and should be hidden to prevent double or triple voicing of table headers.
- if (redundantFooter) {
- this.foot.setAttribute('aria-hidden', 'true');
- }
- }
- }
-
- // Detect table size changes
- commons.addResizeListener(this, this._resetLayout);
-
- // Disable table features if no items.
- const items = this._getSelectableItems();
- this._toggleInteractivity(items.length === 0);
-
- // Sync selection state
- if (this.selectable) {
- const selectedItems = this.selectedItems;
-
- // Sync select all handle if any
- this._syncSelectAllHandle(selectedItems, items);
-
- // Sync used collections
- if (selectedItems.length) {
- this._oldSelection = selectedItems;
- this._lastSelectedItems.items = selectedItems;
- }
- }
-
- // Sync sorted
- this._allowSorting = true;
- const column = this._isSorted();
- if (column) {
- column._doSort && column._doSort(true);
- }
-
- // @compat
- if (this.body) {
- const rows = getRows([this.body]);
- // Use the first column as selection column
- rows.forEach(row => this._toggleSelectionCheckbox(row));
- }
-
- // Enable observer again
- this._toggleObserver(true);
-
- // Mark table as ready
- if (!this.head || this.head && !this.head.hasAttribute('sticky')) {
- this.classList.add(IS_READY);
- }
- }
-
- /**
- Triggered before a {@link Table} column gets sorted by user interaction. Can be used to cancel column sorting and define
- custom sorting.
-
- @typedef {CustomEvent} coral-table:beforecolumnsort
-
- @property {TableColumn} detail.column
- The column to be sorted.
- @property {String} detail.direction
- The requested sorting direction for the column.
- */
-
- /**
- Triggered when a {@link Table} column is sorted.
-
- @typedef {CustomEvent} coral-table:columnsort
-
- @param {TableColumn} detail.column
- The sorted column.
- */
-
- /**
- Triggered before a {@link Table} column is dragged. Can be used to cancel column dragging.
-
- @typedef {CustomEvent} coral-table:beforecolumndrag
-
- @property {TableColumn} detail.column
- The dragged column.
- @property {TableColumn} detail.before
- The column will be inserted before this sibling column.
- If <code>null</code>, the column is inserted at the end.
- */
-
- /**
- Triggered when a {@link Table} column is dragged.
-
- @typedef {CustomEvent} coral-table:columndrag
-
- @property {TableColumn} detail.column
- The dragged column.
- @property {TableColumn} detail.oldBefore
- The column next sibling before the swap.
- If <code>null</code>, the column was the last item.
- @property {TableColumn} detail.before
- The column is inserted before this sibling column.
- If <code>null</code>, the column is inserted at the end.
- */
-
- /**
- Triggered before a {@link Table} row is ordered. Can be used to cancel row ordering.
-
- @typedef {CustomEvent} coral-table:beforeroworder
-
- @property {TableRow} detail.row
- The row to be ordered.
- @property {TableRow} detail.before
- The row will be inserted before this sibling row.
- If <code>null</code>, the row is inserted at the end.
- */
-
- /**
- Triggered when a {@link Table} row is ordered.
-
- @typedef {CustomEvent} coral-table:roworder
-
- @property {TableRow} detail.row
- The ordered row.
- @property {TableRow} detail.oldBefore
- The row next sibling before the swap.
- If <code>null</code>, the row was the last item.
- @param {TableRow} detail.before
- The row is inserted before this sibling row.
- If <code>null</code>, the row is inserted at the end.
- */
-
- /**
- Triggered when a {@linked Table} row is locked.
-
- @typedef {CustomEvent} coral-table:rowlock
-
- @property {TableRow} detail.row
- The locked row.
- */
-
- /**
- Triggered when {@link Table} a row is locked.
-
- @typedef {CustomEvent} coral-table:rowunlock
-
- @property {TableRow} detail.row
- The unlocked row.
- */
-
- /**
- Triggered when a {@link Table} row selection changed.
-
- @typedef {CustomEvent} coral-table:rowchange
-
- @property {Array.<TableCell>} detail.oldSelection
- The old item selection. When {@link TableRow#multiple}, it includes an Array.
- @property {Array.<TableCell>} detail.selection
- The item selection. When {@link Coral.Table.Row#multiple}, it includes an Array.
- @property {TableRow} detail.row
- The targeted row.
- */
-
- /**
- Triggered when the {@link Table} selection changed.
-
- @typedef {CustomEvent} coral-table:change
-
- @property {Array.<TableRow>} detail.oldSelection
- The old item selection. When {@link Table#multiple}, it includes an Array.
- @property {Array.<TableRow>} detail.selection
- The item selection. When {@link Table#multiple}, it includes an Array.
- */
- });
-
- export default Table;