UNPKG

@hashicorp/design-system-components

Version:
318 lines (300 loc) 10.9 kB
import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; import { modifier } from 'ember-modifier'; import { requestAnimationFrameWaiter } from './utils.js'; import { BORDER_WIDTH } from './index.js'; import { precompileTemplate } from '@ember/template-compilation'; import { g, i, n } from 'decorator-transforms/runtime'; import { setComponentTemplate } from '@ember/component'; var TEMPLATE = precompileTemplate("{{!\n Copyright (c) HashiCorp, Inc.\n SPDX-License-Identifier: MPL-2.0\n}}\n\n{{! template-lint-disable no-pointer-down-event-binding }}\n<div\n class={{this.classNames}}\n draggable=\"false\"\n role=\"slider\"\n aria-orientation=\"horizontal\"\n aria-valuenow={{or @column.pxAppliedWidth @column.thElement.offsetWidth}}\n aria-valuemin={{@column.pxMinWidth}}\n aria-valuemax={{@column.pxMaxWidth}}\n tabindex=\"0\"\n aria-label={{hds-t\n \"hds.components.advanced-table.th-resize-handle.aria-label\"\n columnLabel=@column.label\n default=(concat \"Resize \" @column.label \" column\")\n }}\n {{this._registerHandleElement}}\n {{on \"pointerdown\" this.startResize}}\n {{on \"keydown\" this.handleKeydown}}\n {{style height=this.height}}\n ...attributes\n/>"); /** * Copyright (c) HashiCorp, Inc. * SPDX-License-Identifier: MPL-2.0 */ const KEYBOARD_RESIZE_STEP = 10; function calculateEffectiveDelta(deltaX, col, startColW, nextCol, startNextColW) { const colMin = col.pxMinWidth ?? 0; const colMax = col.pxMaxWidth ?? Infinity; const nextMin = nextCol.pxMinWidth ?? 0; const nextMax = nextCol.pxMaxWidth ?? Infinity; let effectiveDelta = 0; // expanding col, shrinking nextCol if (deltaX > 0) { const maxCanExpandCol = colMax - startColW; const maxCanShrinkNext = startNextColW - nextMin; effectiveDelta = Math.min(deltaX, maxCanExpandCol, maxCanShrinkNext); effectiveDelta = Math.max(0, effectiveDelta); } // shrinking col, expanding nextCol else if (deltaX < 0) { const absDeltaX = -deltaX; const maxCanShrinkCol = startColW - colMin; let maxCanExpandNext; if (startNextColW > nextMax) { maxCanExpandNext = Infinity; } else { maxCanExpandNext = nextMax - startNextColW; } effectiveDelta = -Math.min(absDeltaX, maxCanShrinkCol, maxCanExpandNext); effectiveDelta = Math.min(0, effectiveDelta); } return effectiveDelta; } class HdsAdvancedTableThResizeHandle extends Component { static { g(this.prototype, "resizing", [tracked], function () { return null; }); } #resizing = (i(this, "resizing"), void 0); static { g(this.prototype, "_transientDelta", [tracked], function () { return 0; }); } #_transientDelta = (i(this, "_transientDelta"), void 0); // track the width change as it is changing, applied when resizing stops static { g(this.prototype, "_isUpdateQueued", [tracked], function () { return false; }); } #_isUpdateQueued = (i(this, "_isUpdateQueued"), void 0); static { g(this.prototype, "_lastPointerEvent", [tracked], function () { return null; }); } #_lastPointerEvent = (i(this, "_lastPointerEvent"), void 0); _handleElement; _boundResize; _boundStopResize; _registerHandleElement = modifier(element => { this._handleElement = element; }); constructor(owner, args) { super(owner, args); this._boundResize = this._resize.bind(this); this._boundStopResize = this._stopResize.bind(this); } get height() { const { tableHeight } = this.args; if (tableHeight === undefined) { return; } return `${tableHeight - BORDER_WIDTH * 2}px`; } get classNames() { const classes = ['hds-advanced-table__th-resize-handle']; if (this.resizing !== null) { classes.push('hds-advanced-table__th-resize-handle--resizing'); } return classes.join(' '); } _applyTransientWidths() { const { column } = this.args; const { next: nextColumn } = column.siblings; column.width = column.appliedWidth; if (nextColumn !== undefined) { nextColumn.width = nextColumn.appliedWidth; } } onColumnResize(key, width) { const { onColumnResize } = this.args; if (typeof onColumnResize === 'function' && key !== undefined) { onColumnResize(key, width); } } static { n(this.prototype, "onColumnResize", [action]); } handleKeydown(event) { const validKeys = ['ArrowLeft', 'ArrowRight']; if (!validKeys.includes(event.key)) { return; } event.preventDefault(); event.stopPropagation(); const { column } = this.args; const { next: nextColumn } = column.siblings; if (nextColumn === undefined) { return; } column.table.setTransientColumnWidths({ roundValues: true }); const startColumnPxWidth = Math.round(column.pxAppliedWidth ?? 0); const startNextColumnPxWidth = Math.round(nextColumn.pxAppliedWidth ?? 0); const deltaX = event.key === 'ArrowRight' ? KEYBOARD_RESIZE_STEP : -KEYBOARD_RESIZE_STEP; this._applyResizeDelta(deltaX, column, startColumnPxWidth, nextColumn, startNextColumnPxWidth); // ensure the resize handle remains visible during keyboard navigation. this._handleElement.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' }); // use a microtask to commit the final state after the render pass. queueMicrotask(() => { // reset transient values this._setWidthDebts(); this._applyTransientWidths(); column.table.resetTransientColumnWidths(); this._transientDelta = 0; this.onColumnResize(column.key, column.width); }); } static { n(this.prototype, "handleKeydown", [action]); } startResize(event) { if (event.button !== 0) { return; } event.preventDefault(); event.stopPropagation(); const { column } = this.args; const { next: nextColumn } = column.siblings; column.table.setTransientColumnWidths(); this.resizing = { startX: event.clientX, startColumnPxWidth: Math.round(column.pxAppliedWidth ?? 0), startNextColumnPxWidth: Math.round(nextColumn?.pxAppliedWidth ?? 0) }; window.addEventListener('pointermove', this._boundResize); window.addEventListener('pointerup', this._boundStopResize); } static { n(this.prototype, "startResize", [action]); } _setColumnWidth(column, width) { if (width > column.pxMaxWidth || width < column.pxMinWidth) { column.pxTransientWidth = width; } else { column.setPxTransientWidth(width); } } _applyResizeDelta(deltaX, column, startColumnPxWidth, nextColumn, startNextColumnPxWidth) { const canResizeNeighbor = nextColumn !== undefined && startNextColumnPxWidth !== undefined; if (canResizeNeighbor) { const effectiveDelta = calculateEffectiveDelta(deltaX, column, startColumnPxWidth, nextColumn, startNextColumnPxWidth); // set the width for the current column this._setColumnWidth(column, Math.round(startColumnPxWidth + effectiveDelta)); // the actual new column width may differ from the intended width due to min/max constraints. const actualNewColumnWidth = column.pxAppliedWidth ?? startColumnPxWidth; const actualAppliedDelta = actualNewColumnWidth - startColumnPxWidth; // set the width for the next sibling column this._setColumnWidth(nextColumn, Math.round(startNextColumnPxWidth - actualAppliedDelta)); this._transientDelta = actualAppliedDelta; } else { column.setPxTransientWidth(Math.round(startColumnPxWidth + deltaX)); } } _resize(event) { this._lastPointerEvent = event; if (this._isUpdateQueued) { return; } this._isUpdateQueued = true; requestAnimationFrameWaiter(() => { if (this.resizing === null || this._lastPointerEvent === null) { this._isUpdateQueued = false; return; } const event = this._lastPointerEvent; event.preventDefault(); const { column } = this.args; const { next: nextColumn } = column.siblings; const { startX, startColumnPxWidth, startNextColumnPxWidth } = this.resizing; const deltaX = event.clientX - startX; this._applyResizeDelta(deltaX, column, startColumnPxWidth, // Width at the start of the drag nextColumn, startNextColumnPxWidth // Width of next col at the start of the drag ); this._isUpdateQueued = false; }); } _stopResize() { const { column } = this.args; window.removeEventListener('pointermove', this._boundResize); window.removeEventListener('pointerup', this._boundStopResize); this._setWidthDebts(); this._applyTransientWidths(); // reset the transient width column.table.resetTransientColumnWidths(); // reset the resizing state this.resizing = null; this._transientDelta = 0; this.onColumnResize(column.key, column.appliedWidth); } _addDebt(borrower, lenderKey, amount) { borrower.widthDebts = { ...borrower.widthDebts, [lenderKey]: (borrower.widthDebts[lenderKey] ?? 0) + amount }; } _setWidthDebts() { const { column } = this.args; const { next: nextColumn } = column.siblings; const delta = this._transientDelta; if (delta === 0 || nextColumn === undefined || nextColumn.key === undefined || column.key === undefined) { return; } // Determine the borrower, lender, and the amount of width transferred const borrower = delta > 0 ? column : nextColumn; const lender = delta > 0 ? nextColumn : column; let amount = Math.abs(delta); if (borrower.key === undefined || lender.key === undefined) { return; } // Check if the lender already has a debt to the borrower. // If so, this transaction is a "payment" against that existing debt. const existingDebt = lender.widthDebts[borrower.key] ?? 0; if (existingDebt > 0) { const paymentAmount = Math.min(amount, existingDebt); // Reduce the lender's debt by the payment amount lender.widthDebts[borrower.key] = existingDebt - paymentAmount; if (lender.widthDebts[borrower.key] <= 0) { delete lender.widthDebts[borrower.key]; } // The amount of the new debt is reduced by the amount paid amount = amount - paymentAmount; } // If there is still a remaining amount, create a new debt for the borrower. if (amount > 0) { this._addDebt(borrower, lender.key, amount); } } } setComponentTemplate(TEMPLATE, HdsAdvancedTableThResizeHandle); export { HdsAdvancedTableThResizeHandle as default }; //# sourceMappingURL=th-resize-handle.js.map