UNPKG

@revolist/revogrid

Version:

Virtual reactive data grid spreadsheet component - RevoGrid.

1,243 lines (1,231 loc) 57.8 kB
/*! * Built by Revolist OU ❤️ */ import { r as registerInstance, h, e as Host, d as createEvent, g as getElement } from './index-Chp_81rd.js'; import { k as getItemByIndex, b as getSourceItem, j as getItemByPosition, J as FOCUS_CLASS, U as codesLetter, u as timeout, R as RESIZE_INTERVAL, K as MOBILE_CLASS, L as CELL_HANDLER_CLASS, S as SELECTION_BORDER_CLASS, D as DataStore, A as ROW_HEADER_TYPE, o as getScrollbarSize, T as TMP_SELECTION_BG_CLASS } from './dimension.helpers-DzxqJQqN.js'; import { g as getPropertyFromEvent, v as verifyTouchTarget } from './events-BvSmBueA.js'; import { g as getRange, M as ColumnService, z as getCellData, N as getCellEditor, b as isRangeSingleCell } from './column.service-Cdz3dYqZ.js'; import { c as isClear, d as isTab, f as isEnterKeyValue, h as isCopy, g as isCut, j as isPaste, k as isAll, l as isEditInput } from './edit.utils-CzfeG98N.js'; import { d as debounce } from './debounce-BfO9dz9v.js'; import { V as ViewportStore, f as calculateRowHeaderSize } from './viewport.store-saAZJHRo.js'; import { H as HEADER_SLOT } from './viewport.helpers-VXhsJZtn.js'; import { L as LocalScrollTimer, a as LocalScrollService, g as getContentSize, t as throttle } from './throttle-BEjFQa2l.js'; const Attribution = class { constructor(hostRef) { registerInstance(this, hostRef); } render() { return (h(Host, { key: '3d66475a019010c24b6c610ccc047e01c35178f9' }, h("a", { key: '777afddffef0309a697b9c14ee73c0001ac22b71', href: "https://rv-grid.com/guide/attribution", target: "_blank", rel: "noopener noreferrer", title: "Made with \u2764\uFE0F by Revolist OU Team", class: "value" }, "RevoGrid"))); } }; const RevoGridExtra = class { constructor(hostRef) { registerInstance(this, hostRef); /** * Nodes to render */ this.nodes = []; /** * Force component to re-render */ this.update = 1; } /** * Refreshes the extra component. Useful if you want to manually * force the component to re-render. */ async refresh() { this.update *= -1; } render() { var _a; return (_a = this.nodes) === null || _a === void 0 ? void 0 : _a.map(node => { // Check if node is a function or a stencil component // If function wrap it in a stencil component with the refresh function if (typeof node === 'function') { const config = {}; const getNodes = () => [node({ refresh: () => { var _a; return (_a = config.refresh) === null || _a === void 0 ? void 0 : _a.call(config); } })]; return (h("revogr-extra", { nodes: getNodes(), ref: (el) => { if (el) { // Update exclusively for current node config.refresh = () => { el.nodes = getNodes(); }; } } })); } return node; }); } }; function collectModelsOfRange(data, store) { const models = {}; for (let i in data) { const rowIndex = parseInt(i, 10); models[rowIndex] = getSourceItem(store, rowIndex); } return models; } function getFocusCellBasedOnEvent(e, data) { // If event default is prevented, return if (e.defaultPrevented) { return null; } // Get coordinates from event object const x = getPropertyFromEvent(e, 'clientX'); const y = getPropertyFromEvent(e, 'clientY'); // If coordinates are not available, return if (x === null || y === null) { return null; } // Get current cell based on coordinates and data const focusCell = getCurrentCell({ x, y }, data); // If current cell is not available, return if (isAfterLast(focusCell, data.lastCell)) { return null; } return focusCell; } /** * Calculate cell based on x, y position */ function getCurrentCell({ x, y }, { el, rows, cols }) { // Get the bounding rectangle of the element const { top, left, height, width } = el.getBoundingClientRect(); // Calculate the cell position relative to the element let cellY = y - top; let cellX = x - left; // Limit the cell position to the element height if (cellY >= height) { cellY = height - 1; } // Limit the cell position to the element width if (cellX >= width) { cellX = width - 1; } // Get the row and column items based on the cell position const rgRow = getItemByPosition(rows, cellY); const rgCol = getItemByPosition(cols, cellX); // Set the row and column index to 0 if they are before the first item if (rgCol.itemIndex < 0) { rgCol.itemIndex = 0; } if (rgRow.itemIndex < 0) { rgRow.itemIndex = 0; } return { x: rgCol.itemIndex, y: rgRow.itemIndex }; } function getCoordinate(range, focus, changes, isMulti = false) { const updateCoordinate = (c, pos = 0) => { const start = { x: range.x, y: range.y }; const end = isMulti ? { x: range.x1, y: range.y1 } : start; const point = end[c] > focus[c] ? end : start; point[c] += pos; return { start, end }; }; if (changes.x) { return updateCoordinate('x', changes['x']); } if (changes.y) { return updateCoordinate('y', changes['y']); } return null; } /** * Check if the x coordinate of the cell position is after or equal to the x coordinate of the last cell position * or if the y coordinate of the cell position is after or equal to the y coordinate of the last cell position */ function isAfterLast({ x, y }, lastCell) { return x >= lastCell.x || y >= lastCell.y; } /** check if out of range */ function isBeforeFirst({ x, y }) { return x < 0 || y < 0; } /** Compare cells, only 1 coordinate difference is possible */ // export function getDirectionCoordinate(initial: Cell, last: Cell): Partial<Cell> | null { // const c: (keyof Cell)[] = ['x', 'y']; // for (let k of c) { // if (initial[k] !== last[k]) { // return { [k]: 1 }; // } // } // return null; // } // export function getLargestAxis(initial: Cell, last: Cell): Partial<Cell> | null { // const cell: Partial<Cell> = {}; // const c: (keyof Cell)[] = ['x', 'y']; // for (let k of c) { // cell[k] = Math.abs(initial[k] - last[k]); // } // if (cell.x > cell.y) { // return { x: 1 }; // } // if (cell.y > cell.x) { // return { y: 1 }; // } // return null; // } function styleByCellProps(styles) { return { left: `${styles.left}px`, top: `${styles.top}px`, width: `${styles.width}px`, height: `${styles.height}px`, }; } function getCell({ x, y, x1, y1 }, dimensionRow, dimensionCol) { const top = getItemByIndex(dimensionRow, y).start; const left = getItemByIndex(dimensionCol, x).start; const bottom = getItemByIndex(dimensionRow, y1).end; const right = getItemByIndex(dimensionCol, x1).end; return { left, right, top, bottom, width: right - left, height: bottom - top, }; } const revogrFocusStyleCss = () => `revogr-focus.focused-cell{box-shadow:-1px 0 0 #0d63e8 inset, 1px 0 0 #0d63e8 inset, 0 -1px 0 #0d63e8 inset, 0 1px 0 #0d63e8 inset;position:absolute;pointer-events:none;z-index:9;display:block !important}`; const RevogrFocus$1 = class RevogrFocus { constructor(hostRef) { registerInstance(this, hostRef); this.beforeFocusRender = createEvent(this, "beforefocusrender", 7); this.beforeScrollIntoView = createEvent(this, "beforescrollintoview", 7); this.afterFocus = createEvent(this, "afterfocus", 7); /** * Focus template custom function. Can be used to render custom focus layer. */ this.focusTemplate = null; this.activeFocus = null; } componentDidRender() { var _a, _b; const currentFocus = this.selectionStore.get('focus'); if (((_a = this.activeFocus) === null || _a === void 0 ? void 0 : _a.x) === (currentFocus === null || currentFocus === void 0 ? void 0 : currentFocus.x) && ((_b = this.activeFocus) === null || _b === void 0 ? void 0 : _b.y) === (currentFocus === null || currentFocus === void 0 ? void 0 : currentFocus.y)) { return; } this.activeFocus = currentFocus; if (currentFocus && this.el) { const beforeScrollIn = this.beforeScrollIntoView.emit({ el: this.el }); if (!beforeScrollIn.defaultPrevented) { this.el.scrollIntoView({ block: 'nearest', inline: 'nearest', }); } const model = getSourceItem(this.dataStore, currentFocus.y); const column = getSourceItem(this.colData, currentFocus.x); this.afterFocus.emit({ model, column, rowType: this.rowType, colType: this.colType, rowIndex: currentFocus.y, colIndex: currentFocus.x, }); } } render() { var _a; const editCell = this.selectionStore.get('edit'); if (editCell) { return; } const focusCell = this.selectionStore.get('focus'); if (!focusCell) { return; } const event = this.beforeFocusRender.emit({ range: Object.assign(Object.assign({}, focusCell), { x1: focusCell.x, y1: focusCell.y }), rowType: this.rowType, colType: this.colType, rowDimension: Object.assign({}, this.dimensionRow.state), colDimension: Object.assign({}, this.dimensionCol.state), }); if (event.defaultPrevented) { return h("slot", null); } const { detail } = event; const cell = getCell(detail.range, event.detail.rowDimension, event.detail.colDimension); const styles = styleByCellProps(cell); const extra = (_a = this.focusTemplate) === null || _a === void 0 ? void 0 : _a.call(this, h, detail); const props = { class: { [FOCUS_CLASS]: true }, style: styles, }; return (h(Host, Object.assign({}, props), h("slot", null), extra)); } get el() { return getElement(this); } }; RevogrFocus$1.style = revogrFocusStyleCss(); const DIRECTION_CODES = [ codesLetter.TAB, codesLetter.ARROW_UP, codesLetter.ARROW_DOWN, codesLetter.ARROW_LEFT, codesLetter.ARROW_RIGHT, ]; class KeyboardService { constructor(sv) { this.sv = sv; } async keyDown(e, canRange, isEditMode, { range, focus }) { // IF EDIT MODE if (isEditMode) { switch (e.code) { case codesLetter.ESCAPE: this.sv.cancel(); break; case codesLetter.TAB: this.keyChangeSelection(e, canRange); break; } return; } // IF NOT EDIT MODE // pressed clear key if (range && isClear(e.code)) { this.sv.clearCell(); return; } // below works with focus only if (!focus) { return; } // tab key means same as arrow right if (isTab(e.code)) { this.keyChangeSelection(e, canRange); return; } // pressed enter if (isEnterKeyValue(e.key)) { this.sv.change(); return; } // copy operation if (isCopy(e)) { return; } // cut operation if (isCut(e)) { return; } // paste operation if (isPaste(e)) { this.sv.internalPaste(); return; } // select all if (isAll(e)) { if (canRange) { this.selectAll(e); } return; } // pressed letter key if (e.key.length === 1) { this.sv.change(e.key); return; } // pressed arrow, change selection position if (await this.keyChangeSelection(e, canRange)) { return; } } selectAll(e) { const range = this.sv.selectionStore.get('range'); const focus = this.sv.selectionStore.get('focus'); // if no range or focus - do nothing if (!range || !focus) { return; } e.preventDefault(); this.sv.selectAll(); } async keyChangeSelection(e, canRange) { const data = this.changeDirectionKey(e, canRange); if (!data) { return false; } // this interval needed for several cases // grid could be resized before next click // at this case to avoid screen jump we use this interval await timeout(RESIZE_INTERVAL + 30); const range = this.sv.selectionStore.get('range'); const focus = this.sv.selectionStore.get('focus'); return this.keyPositionChange(data.changes, range, focus, data.isMulti); } keyPositionChange(changes, range, focus, isMulti = false) { if (!range || !focus) { return false; } const data = getCoordinate(range, focus, changes, isMulti); if (!data) { return false; } const eData = this.sv.getData(); if (isMulti) { if (isAfterLast(data.end, eData.lastCell) || isBeforeFirst(data.start)) { return false; } const range = getRange(data.start, data.end); return this.sv.range(range); } return this.sv.focus(data.start, changes, isAfterLast(data.start, eData.lastCell) ? 1 : isBeforeFirst(data.start) ? -1 : 0); } /** Monitor key direction changes */ changeDirectionKey(e, canRange) { const isMulti = canRange && e.shiftKey; if (DIRECTION_CODES.includes(e.code)) { e.preventDefault(); } if (e.shiftKey) { switch (e.code) { case codesLetter.TAB: return { changes: { x: -1 }, isMulti: false }; } } switch (e.code) { case codesLetter.ARROW_UP: return { changes: { y: -1 }, isMulti }; case codesLetter.ARROW_DOWN: return { changes: { y: 1 }, isMulti }; case codesLetter.ARROW_LEFT: return { changes: { x: -1 }, isMulti }; case codesLetter.TAB: case codesLetter.ARROW_RIGHT: return { changes: { x: 1 }, isMulti }; } } } class AutoFillService { constructor(sv) { this.sv = sv; this.autoFillType = null; this.autoFillInitial = null; this.autoFillStart = null; this.autoFillLast = null; } /** * Render autofill box * @param range * @param selectionFocus */ renderAutofill(range, selectionFocus, isMobile = false) { let handlerStyle; if (range) { handlerStyle = getCell(range, this.sv.dimensionRow.state, this.sv.dimensionCol.state); } else { handlerStyle = getCell(Object.assign(Object.assign({}, selectionFocus), { x1: selectionFocus.x, y1: selectionFocus.y }), this.sv.dimensionRow.state, this.sv.dimensionCol.state); } return (h("div", { class: { [CELL_HANDLER_CLASS]: true, [MOBILE_CLASS]: isMobile, }, style: { left: `${handlerStyle.right}px`, top: `${handlerStyle.bottom}px`, }, onMouseDown: (e) => this.autoFillHandler(e), onTouchStart: (e) => this.autoFillHandler(e) })); } autoFillHandler(e, type = "AutoFill" /* AutoFillType.autoFill */) { let target = null; if (e.target instanceof Element) { target = e.target; } if (!target) { return; } this.selectionStart(target, this.sv.getData(), type); e.preventDefault(); } get isAutoFill() { return !!this.autoFillType; } /** * Process mouse move events */ selectionMouseMove(e) { // initiate mouse move debounce if not present if (!this.onMouseMoveAutofill) { this.onMouseMoveAutofill = debounce((e, data) => this.doAutofillMouseMove(e, data), 5); } if (this.isAutoFill) { this.onMouseMoveAutofill(e, this.sv.getData()); } } getFocus(focus, range) { // there was an issue that it was taking last cell from range but focus was out if (!focus && range) { focus = { x: range.x, y: range.y }; } return focus || null; } /** * Autofill logic: * on mouse move apply based on previous direction (if present) */ doAutofillMouseMove(event, data) { // if no initial - not started if (!this.autoFillInitial) { return; } const x = getPropertyFromEvent(event, 'clientX', MOBILE_CLASS); const y = getPropertyFromEvent(event, 'clientY', MOBILE_CLASS); // skip touch if (x === null || y === null) { return; } const current = getCurrentCell({ x, y }, data); // first time or direction equal to start(same as first time) if (!this.autoFillLast) { if (!this.autoFillLast) { this.autoFillLast = this.autoFillStart; } } // check if not the latest, if latest - do nothing if (isAfterLast(current, data.lastCell)) { return; } this.autoFillLast = current; const isSame = current.x === this.autoFillInitial.x && current.y === this.autoFillInitial.y; // if same as initial - clear if (isSame) { this.sv.setTempRange(null); } else { const area = getRange(this.autoFillInitial, this.autoFillLast); this.sv.setTempRange({ area, type: this.autoFillType, }); } } /** * Range selection started * Mode @param type: * Can be triggered from MouseDown selection on element * Or can be triggered on corner square drag */ selectionStart(target, data, type = "Selection" /* AutoFillType.selection */) { /** Get cell by autofill element */ const { top, left } = target.getBoundingClientRect(); this.autoFillInitial = this.getFocus(data.focus, data.range); this.autoFillType = type; this.autoFillStart = getCurrentCell({ x: left, y: top }, data); } /** * Clear current range selection on mouse up and mouse leave events */ clearAutoFillSelection(focus, oldRange) { // If autofill was active, apply autofill values if (this.autoFillInitial) { // Fetch latest focus this.autoFillInitial = this.getFocus(focus, oldRange); // Apply range data if autofill mode is active if (this.autoFillType === "AutoFill" /* AutoFillType.autoFill */) { const range = getRange(this.autoFillInitial, this.autoFillLast); // If range is present, apply data if (range) { const { defaultPrevented: stopApply, detail: { range: newRange }, } = this.sv.clearRangeDataApply({ range, }); // If data apply was not prevented, apply new range if (!stopApply && oldRange) { this.applyRangeWithData(newRange, oldRange); } else { // If data apply was prevented, clear temporary range this.sv.setTempRange(null); } } } else { // If not autofill mode, apply range only this.applyRangeOnly(this.autoFillInitial, this.autoFillLast); } } // Reset autofill state this.resetAutoFillState(); } /** * Reset autofill state */ resetAutoFillState() { this.autoFillType = null; this.autoFillInitial = null; this.autoFillLast = null; this.autoFillStart = null; } /** * Trigger range apply events and handle responses */ onRangeApply(newData, newRange, oldRange) { this.sv.rangeDataApply({ data: newData, models: collectModelsOfRange(newData, this.sv.dataStore), type: this.sv.dataStore.get('type'), oldRange, newRange }); this.sv.setRange(newRange); } /** Apply range and copy data during range application */ applyRangeWithData(newRange, rangeToCopy) { const rangeData = { type: this.sv.dataStore.get('type'), colType: this.sv.columnService.type, newData: {}, mapping: {}, newRange, oldRange: rangeToCopy, }; const { mapping, changed } = this.sv.columnService.getRangeData(rangeData, this.sv.columnService.columns); rangeData.newData = changed; rangeData.mapping = mapping; let e = this.sv.selectionChanged(rangeData); // if default prevented - clear range if (e.defaultPrevented) { this.sv.setTempRange(null); return; } e = this.sv.rangeCopy(rangeData); if (e.defaultPrevented) { this.sv.setRange(newRange); return; } this.onRangeApply(rangeData.newData, newRange, rangeToCopy); } /** * Update range selection only, * no data change (mouse selection) */ applyRangeOnly(start, end) { // no changes to apply if (!start || !end) { return; } const newRange = getRange(start, end); this.sv.setRange(newRange); } } const revogrOverlayStyleCss = () => `revogr-overlay-selection{display:block;position:relative;width:100%}revogr-overlay-selection .autofill-handle{position:absolute;width:14px;height:14px;margin-left:-13px;margin-top:-13px;z-index:10;cursor:crosshair}revogr-overlay-selection .autofill-handle::before{content:"";position:absolute;right:0;bottom:0;width:10px;height:10px;background:#0d63e8;border:1px solid white;box-sizing:border-box}revogr-overlay-selection.mobile .autofill-handle{position:absolute;width:30px;height:30px;margin-left:-29px;margin-top:-29px;z-index:10;cursor:crosshair}revogr-overlay-selection.mobile .autofill-handle::before{content:"";position:absolute;right:0;bottom:0;width:12px;height:12px;background:#0d63e8;border:1px solid white;box-sizing:border-box}revogr-overlay-selection .selection-border-range{position:absolute;pointer-events:none;z-index:9;box-shadow:-1px 0 0 #0d63e8 inset, 1px 0 0 #0d63e8 inset, 0 -1px 0 #0d63e8 inset, 0 1px 0 #0d63e8 inset}revogr-overlay-selection .selection-border-range .range-handlers{height:100%;background-color:transparent;width:75%;max-width:50px;min-width:20px;left:50%;transform:translateX(-50%);position:absolute}revogr-overlay-selection .selection-border-range .range-handlers>span{pointer-events:auto;height:20px;width:20px;position:absolute;left:50%;transform:translateX(-50%)}revogr-overlay-selection .selection-border-range .range-handlers>span:before,revogr-overlay-selection .selection-border-range .range-handlers>span:after{position:absolute;border-radius:5px;width:15px;height:5px;left:50%;transform:translateX(-50%);background-color:rgba(0, 0, 0, 0.2)}revogr-overlay-selection .selection-border-range .range-handlers>span:first-child{top:-7px}revogr-overlay-selection .selection-border-range .range-handlers>span:first-child:before{content:"";top:0}revogr-overlay-selection .selection-border-range .range-handlers>span:last-child{bottom:-7px}revogr-overlay-selection .selection-border-range .range-handlers>span:last-child:after{content:"";bottom:0}revogr-overlay-selection revogr-edit{z-index:10}`; const OverlaySelection = class { constructor(hostRef) { registerInstance(this, hostRef); this.beforeCopyRegion = createEvent(this, "beforecopyregion", 7); this.beforeRegionPaste = createEvent(this, "beforepasteregion", 7); this.cellEditApply = createEvent(this, "celleditapply", 7); this.beforeFocusCell = createEvent(this, "beforecellfocusinit", 7); this.beforeNextViewportFocus = createEvent(this, "beforenextvpfocus", 7); this.setEdit = createEvent(this, "setedit", 7); this.beforeApplyRange = createEvent(this, "beforeapplyrange", 7); this.beforeSetRange = createEvent(this, "beforesetrange", 7); this.setRange = createEvent(this, "setrange", 7); this.beforeEditRender = createEvent(this, "beforeeditrender", 7); this.selectAll = createEvent(this, "selectall", 7); this.cancelEdit = createEvent(this, "canceledit", 7); this.setTempRange = createEvent(this, "settemprange", 7); this.beforeSetTempRange = createEvent(this, "beforesettemprange", 7); this.applyFocus = createEvent(this, "applyfocus", 7); this.focusCell = createEvent(this, "focuscell", 7); this.beforeRangeDataApply = createEvent(this, "beforerangedataapply", 7); this.selectionChange = createEvent(this, "selectionchangeinit", 7); this.beforeRangeCopyApply = createEvent(this, "beforerangecopyapply", 7); this.rangeEditApply = createEvent(this, "rangeeditapply", 7); this.rangeClipboardCopy = createEvent(this, "clipboardrangecopy", 7); this.rangeClipboardPaste = createEvent(this, "clipboardrangepaste", 7); this.beforeKeyDown = createEvent(this, "beforekeydown", 7); this.beforeKeyUp = createEvent(this, "beforekeyup", 7); this.beforeCellSave = createEvent(this, "beforecellsave", 7); this.cellEditDone = createEvent(this, "celledit", 7); /** * If true applys changes when cell closes if not Escape. */ this.applyChangesOnClose = false; this.keyboardService = null; this.autoFillService = null; this.unsubscribeSelectionStore = []; } // #endregion // #region Listeners onMouseMove(e) { var _a; if (this.selectionStore.get('focus')) { (_a = this.autoFillService) === null || _a === void 0 ? void 0 : _a.selectionMouseMove(e); } } /** * Action finished inside the document. * Pointer left document, clear any active operation. */ onMouseUp() { var _a; // Clear autofill selection // when pointer left document, // clear any active operation. (_a = this.autoFillService) === null || _a === void 0 ? void 0 : _a.clearAutoFillSelection(this.selectionStore.get('focus'), this.selectionStore.get('range')); } /** * Row drag started. * This event is fired when drag action started on cell. */ onCellDrag(e) { var _a; // Invoke drag start on order editor. (_a = this.orderEditor) === null || _a === void 0 ? void 0 : _a.dragStart(e.detail); } /** * Get keyboard down from element. * This event is fired when keyboard key is released. */ onKeyUp(e) { // Emit before key up event. this.beforeKeyUp.emit(Object.assign({ original: e }, this.getData())); } /** * Get keyboard down from element. * This event is fired when keyboard key is pressed. */ onKeyDown(e) { var _a; // Emit before key down event and check if default prevention is set. const proxy = this.beforeKeyDown.emit(Object.assign({ original: e }, this.getData())); if (e.defaultPrevented || proxy.defaultPrevented) { return; } // Invoke key down on keyboard service. (_a = this.keyboardService) === null || _a === void 0 ? void 0 : _a.keyDown(e, this.range, !!this.selectionStore.get('edit'), { focus: this.selectionStore.get('focus'), range: this.selectionStore.get('range'), }); } // #endregion /** * Selection & Keyboard */ selectionServiceSet(selectionStore) { // clear subscriptions this.unsubscribeSelectionStore.forEach(v => v()); this.unsubscribeSelectionStore.length = 0; this.unsubscribeSelectionStore.push(selectionStore.onChange('nextFocus', v => v && this.doFocus(v, v))); this.keyboardService = new KeyboardService({ selectionStore, range: r => !!r && this.triggerRangeEvent(r), focus: (f, changes, focusNextViewport) => { if (focusNextViewport) { this.beforeNextViewportFocus.emit(f); return false; } else { return this.doFocus(f, f, changes); } }, change: val => { if (this.readonly) { return; } this.doEdit(val); }, cancel: async () => { var _a; await ((_a = this.revogrEdit) === null || _a === void 0 ? void 0 : _a.cancelChanges()); this.closeEdit(); }, clearCell: () => !this.readonly && this.clearCell(), internalPaste: () => !this.readonly && this.beforeRegionPaste.emit(), getData: () => this.getData(), selectAll: () => this.selectAll.emit(), }); this.createAutoFillService(); } /** Autofill */ createAutoFillService() { this.autoFillService = new AutoFillService({ dimensionRow: this.dimensionRow, dimensionCol: this.dimensionCol, columnService: this.columnService, dataStore: this.dataStore, clearRangeDataApply: e => this.beforeRangeDataApply.emit(Object.assign(Object.assign(Object.assign({}, e), this.types), { rowDimension: Object.assign({}, this.dimensionRow.state), colDimension: Object.assign({}, this.dimensionCol.state) })), setTempRange: e => { const tempRangeEvent = this.beforeSetTempRange.emit(Object.assign(Object.assign({ tempRange: e }, this.getData()), this.types)); if (tempRangeEvent.defaultPrevented) { return null; } return this.setTempRange.emit(tempRangeEvent.detail.tempRange); }, selectionChanged: e => this.selectionChange.emit(e), rangeCopy: e => this.beforeRangeCopyApply.emit(e), rangeDataApply: e => this.rangeEditApply.emit(e), setRange: e => !!e && this.triggerRangeEvent(e), getData: () => this.getData(), }); } /** Columns */ columnServiceSet() { var _a; (_a = this.columnService) === null || _a === void 0 ? void 0 : _a.destroy(); this.columnService = new ColumnService(this.dataStore, this.colData); this.createAutoFillService(); } connectedCallback() { this.columnServiceSet(); this.selectionServiceSet(this.selectionStore); } disconnectedCallback() { var _a; // clear subscriptions this.unsubscribeSelectionStore.forEach(v => v()); this.unsubscribeSelectionStore.length = 0; (_a = this.columnService) === null || _a === void 0 ? void 0 : _a.destroy(); } async componentWillRender() { var _a, _b; const editCell = this.selectionStore.get('edit'); if (!editCell) { await ((_b = (_a = this.revogrEdit) === null || _a === void 0 ? void 0 : _a.beforeDisconnect) === null || _b === void 0 ? void 0 : _b.call(_a)); } } renderRange(range) { const cell = getCell(range, this.dimensionRow.state, this.dimensionCol.state); const styles = styleByCellProps(cell); return [ h("div", { class: SELECTION_BORDER_CLASS, style: styles }, this.isMobileDevice && (h("div", { class: "range-handlers" }, h("span", { class: MOBILE_CLASS }), h("span", { class: MOBILE_CLASS })))), ]; } renderEditor() { // Check if edit access const editCell = this.selectionStore.get('edit'); // Readonly or Editor closed if (this.readonly || !editCell) { return null; } const enteredOrModelValue = editCell.val || getCellData(this.columnService.rowDataModel(editCell.y, editCell.x).value); const editable = Object.assign(Object.assign({}, editCell), this.columnService.getSaveData(editCell.y, editCell.x, enteredOrModelValue)); const renderEvent = this.beforeEditRender.emit(Object.assign(Object.assign({ range: Object.assign(Object.assign({}, editCell), { x1: editCell.x, y1: editCell.y }) }, this.types), { rowDimension: Object.assign({}, this.dimensionRow.state), colDimension: Object.assign({}, this.dimensionCol.state) })); // Render prevented if (renderEvent.defaultPrevented) { return null; } const cell = getCell(renderEvent.detail.range, renderEvent.detail.rowDimension, renderEvent.detail.colDimension); const styles = styleByCellProps(cell); return (h("revogr-edit", { style: styles, ref: el => (this.revogrEdit = el), additionalData: this.additionalData, editCell: editable, saveOnClose: this.applyChangesOnClose, onCelleditinit: (e) => { this.cellEditDone.emit(e.detail); }, column: this.columnService.rowDataModel(editCell.y, editCell.x), editor: getCellEditor(this.columnService.columns[editCell.x], this.editors) })); } onEditCell(e) { if (e.defaultPrevented) { return; } const saveEv = this.beforeCellSave.emit(e.detail); if (!saveEv.defaultPrevented) { this.cellEdit(saveEv.detail); } // if not clear navigate to next cell after edit if (!saveEv.detail.preventFocus) { this.focusNext(); } } render() { var _a; const nodes = []; const editCell = this.renderEditor(); // Editor if (editCell) { nodes.push(editCell); } else { const range = this.selectionStore.get('range'); const focus = this.selectionStore.get('focus'); // Clipboard if ((range || focus) && this.useClipboard) { nodes.push(h("revogr-clipboard", { readonly: this.readonly, onCopyregion: e => this.onCopy(e.detail), onClearregion: () => !this.readonly && this.clearCell(), ref: e => (this.clipboard = e), onPasteregion: e => this.onPaste(e.detail) })); } // Range if (range) { nodes.push(...this.renderRange(range)); } // Autofill if (focus && !this.readonly && this.range) { nodes.push((_a = this.autoFillService) === null || _a === void 0 ? void 0 : _a.renderAutofill(range, focus, this.isMobileDevice)); } // Order if (this.canDrag) { nodes.push(h("revogr-order-editor", { ref: e => (this.orderEditor = e), dataStore: this.dataStore, dimensionRow: this.dimensionRow, dimensionCol: this.dimensionCol, parent: this.element, rowType: this.types.rowType, onRowdragstartinit: e => this.rowDragStart(e) })); } } return (h(Host, { key: 'd936e8452e84c7a25ecd6502e929f1a5af69467f', class: { mobile: this.isMobileDevice }, onDblClick: (e) => this.onElementDblClick(e), onMouseDown: (e) => this.onElementMouseDown(e), onTouchStart: (e) => this.onElementMouseDown(e, true), onCloseedit: (e) => this.closeEdit(e), // it's done to be able to throw events from different levels, not just from editor onCelledit: (e) => this.onEditCell(e) }, nodes, h("slot", { key: 'cd3525d404aa44fd8d06e7fc459777acb8a9d585', name: "data" }))); } /** * Executes the focus operation on the specified range of cells. */ doFocus(focus, end, changes) { // 1. Trigger beforeFocus event const { defaultPrevented } = this.beforeFocusCell.emit(this.columnService.getSaveData(focus.y, focus.x)); if (defaultPrevented) { return false; } const evData = Object.assign(Object.assign({ range: Object.assign(Object.assign({}, focus), { x1: end.x, y1: end.y }), next: changes }, this.types), { rowDimension: Object.assign({}, this.dimensionRow.state), colDimension: Object.assign({}, this.dimensionCol.state) }); // 2. Trigger apply focus event const applyEvent = this.applyFocus.emit(evData); if (applyEvent.defaultPrevented) { return false; } const { range } = applyEvent.detail; // 3. Trigger focus event return !this.focusCell.emit(Object.assign({ focus: { x: range.x, y: range.y, }, end: { x: range.x1, y: range.y1, } }, applyEvent.detail)).defaultPrevented; } triggerRangeEvent(range) { const type = this.types.rowType; // 1. Apply range const applyEvent = this.beforeApplyRange.emit(Object.assign(Object.assign({ range: Object.assign({}, range) }, this.types), { rowDimension: Object.assign({}, this.dimensionRow.state), colDimension: Object.assign({}, this.dimensionCol.state) })); if (applyEvent.defaultPrevented) { return false; } const data = this.columnService.getRangeTransformedToProps(applyEvent.detail.range, this.dataStore); // 2. Before set range let e = this.beforeSetRange.emit(data); if (e.defaultPrevented) { return false; } // 3. Set range e = this.setRange.emit(Object.assign(Object.assign({}, applyEvent.detail.range), { type })); if (e.defaultPrevented) { return false; } return !e.defaultPrevented; } /** * Open Editor on DblClick */ onElementDblClick(e) { // DblClick prevented outside - Editor will not open if (e.defaultPrevented) { return; } // Get data from the component const data = this.getData(); const focusCell = getFocusCellBasedOnEvent(e, data); if (!focusCell) { return; } this.doEdit(); } /** * Handle mouse down event on Host element */ onElementMouseDown(e, touch = false) { var _a; // Get the target element from the event object const targetElement = e.target; // Ignore focus if clicked input if (isEditInput(targetElement) || e.defaultPrevented) { return; } // Get data from the component const data = this.getData(); const focusCell = getFocusCellBasedOnEvent(e, data); if (!focusCell) { return; } // Set focus on the current cell this.focus(focusCell, this.range && e.shiftKey); // Initiate autofill selection if (this.range) { targetElement && ((_a = this.autoFillService) === null || _a === void 0 ? void 0 : _a.selectionStart(targetElement, this.getData())); // Prevent default behavior for mouse events, // but only if target element is not a mobile input if (!touch) { e.preventDefault(); } else if (verifyTouchTarget(e.touches[0], MOBILE_CLASS)) { // Prevent default behavior for touch events // if target element is a mobile input e.preventDefault(); } } } /** * Start cell editing */ doEdit(val = '') { var _a; if (this.canEdit()) { const focus = this.selectionStore.get('focus'); if (!focus) { return; } const data = this.columnService.getSaveData(focus.y, focus.x); (_a = this.setEdit) === null || _a === void 0 ? void 0 : _a.emit(Object.assign(Object.assign({}, data), { val })); } } /** * Close editor event triggered * @param details - if it requires focus next */ async closeEdit(e) { this.cancelEdit.emit(); if (e === null || e === void 0 ? void 0 : e.detail) { await this.focusNext(); } } /** * Edit finished. * Close Editor and save. */ cellEdit(e) { const dataToSave = this.columnService.getSaveData(e.rgRow, e.rgCol, e.val); this.cellEditApply.emit(dataToSave); } getRegion() { const focus = this.selectionStore.get('focus'); let range = this.selectionStore.get('range'); if (!range) { range = getRange(focus, focus); } return range; } onCopy(e) { var _a; const range = this.getRegion(); const canCopyEvent = this.beforeCopyRegion.emit(range); if (canCopyEvent.defaultPrevented) { return false; } let rangeData; if (range) { const { data, mapping } = this.columnService.copyRangeArray(range, this.dataStore); const event = this.rangeClipboardCopy.emit(Object.assign({ range, data, mapping }, this.types)); if (!event.defaultPrevented) { rangeData = event.detail.data; } } (_a = this.clipboard) === null || _a === void 0 ? void 0 : _a.doCopy(e, rangeData); return true; } onPaste(data) { var _a; const focus = this.selectionStore.get('focus'); const isEditing = this.selectionStore.get('edit') !== null; if (!focus || isEditing) { return; } let { changed, range } = this.columnService.getTransformedDataToApply(focus, data); const { defaultPrevented: canPaste } = this.rangeClipboardPaste.emit(Object.assign({ data: changed, models: collectModelsOfRange(changed, this.dataStore), range }, this.types)); if (canPaste) { return; } (_a = this.autoFillService) === null || _a === void 0 ? void 0 : _a.onRangeApply(changed, range, range); } async focusNext() { var _a; const canFocus = await ((_a = this.keyboardService) === null || _a === void 0 ? void 0 : _a.keyChangeSelection(new KeyboardEvent('keydown', { code: codesLetter.ARROW_DOWN, }), this.range)); if (!canFocus) { this.closeEdit(); } } clearCell() { var _a; const range = this.selectionStore.get('range'); if (range && !isRangeSingleCell(range)) { const data = this.columnService.getRangeStaticData(range, ''); (_a = this.autoFillService) === null || _a === void 0 ? void 0 : _a.onRangeApply(data, range, range); } else if (this.canEdit()) { const focused = this.selectionStore.get('focus'); if (!focused) { return; } const cell = this.columnService.getSaveData(focused.y, focused.x); this.cellEdit({ rgRow: focused.y, rgCol: focused.x, val: '', type: cell.type, prop: cell.prop, }); } } rowDragStart({ detail }) { detail.text = getCellData(this.columnService.rowDataModel(detail.cell.y, detail.cell.x).value); } /** * Verify if edit allowed. */ canEdit() { var _a; if (this.readonly) { return false; } const focus = this.selectionStore.get('focus'); return focus && !((_a = this.columnService) === null || _a === void 0 ? void 0 : _a.isReadOnly(focus.y, focus.x)); } get edited() { return this.selectionStore.get('edit'); } /** * Sets the focus on a cell and optionally edits a range. */ focus(cell, isRangeEdit = false) { if (!cell) return false; const end = cell; const start = this.selectionStore.get('focus'); if (isRangeEdit && start) { const range = getRange(start, end); if (range) { return this.triggerRangeEvent(range); } } return this.doFocus(cell, end); } get types() { return { rowType: this.dataStore.get('type'), colType: this.columnService.type, }; } /** * Collect data */ getData() { return { el: this.element, rows: this.dimensionRow.state, cols: this.dimensionCol.state, lastCell: this.lastCell, focus: this.selectionStore.get('focus'), range: this.selectionStore.get('range'), edit: this.selectionStore.get('edit'), }; } get element() { return getElement(this); } static get watchers() { return { "selectionStore": [{ "selectionServiceSet": 0 }], "dimensionRow": [{ "createAutoFillService": 0 }], "dimensionCol": [{ "createAutoFillService": 0 }], "dataStore": [{ "columnServiceSet": 0 }], "colData": [{ "columnServiceSet": 0 }] }; } }; OverlaySelection.style = revogrOverlayStyleCss(); const RowHeaderRender = s => (__, { rowIndex: i }) => s + i; const RevogrRowHeaders = class { constructor(hostRef) { registerInstance(this, hostRef); this.scrollViewport = createEvent(this, "scrollview", 3); this.elementToScroll = createEvent(this, "ref", 3); /** * Prevent rendering until job is done. * Can be used for initial rendering performance improvement. * When several plugins require initial rendering this will prevent double initial rendering. */ this.jobsBeforeRender = []; } render() { const dataViews = []; const viewport = new ViewportStore('colPinStart'); /** render viewports rows */ let totalLength = 1; // todo: this part could be optimized to avoid to often re-render dataPorts can be cached for (let data of this.dataPorts) { const itemCount = data.dataStore.get('items').length; // initiate row data const dataStore = new DataStore(data.type, Object.assign({}, data.dataStore.state)); // initiate column data const colData = new DataStore('colPinStart'); const column = Object.assign({ cellTemplate: RowHeaderRender(totalLength) }, this.rowHeaderColumn); colData.updateData([column]); dataViews.push(h("revogr-data", Object.assign({}, data, { colType: "rowHeaders", jobsBeforeRender: this.jobsBeforeRender, rowClass: this.rowClass, dataStore: dataStore.store, colData: colData.store, viewportCol: viewport.store, readonly: true, range: false }))); totalLength += itemCount; } const colSize = calculateRowHeaderSize(totalLength, this.rowHeaderColumn); viewport.setViewport({ realCount: 1, virtualSize: 0, items: [ { size: colSize, start: 0, end: colSize, itemIndex: 0, }, ], }); const viewportScroll = { contentHeight: this.height, contentWidth: 0, style: { minWidth: `${colSize}px` }, colType: 'rowHeaders', ref: (el) => this.elementToScroll.emit(el), onScrollviewport: (e) => this.scrollViewport.emit(e.detail), }; const viewportHeader = Object.assign(Object.assign({}, this.headerProp), { // groups not present on row headers groups: [], colData: typeof this.rowHeaderColumn === 'object' ? [this.rowHeaderColumn] : [], viewportCol: viewport.store, canResize: false, type: ROW_HEADER_TYPE, // parent, slot: HEADER_SLOT }); return (h(Host, { class: { [ROW_HEADER_TYPE]: true }, key: ROW_HEADER_TYPE }, h("revogr-viewport-scroll", Object.assign({ key: 'c401e82e02e4bdb7afb25f2f49c6776f2e115c81' }, viewportScroll, { "row-header": true }), h("revogr-header", Object.assign({ key: '3c73d27bd96e23a34fc0cf47eda4d2e65751df98' }, viewportHeader)), dataViews))); } }; /** * Autohide scroll for MacOS when scroll is visible only for 1 sec */ class AutohideScrollPlugin { constructor(element) { this.element = element; this.autohideScrollTimeout = 0; } /** * When scroll size updates set it up for autohide */ setScrollSize(s) { if (!s) { this.element.setAttribute('autohide', 'true'); } else { this.element.removeAttribute('autohide'); } } /** * On each scroll check if it's time to show */ checkScroll({ scrollSize, contentSize, virtualSize, }) { const hasScroll = contentSize > virtualSize; const isHidden = !scrollSize && hasScroll; if (isHidden) { this.element.setAttribute('visible', 'true'); this.autohideS