@revolist/revogrid
Version:
Virtual reactive data grid spreadsheet component - RevoGrid.
1,219 lines (1,206 loc) • 58.7 kB
JavaScript
/*!
* Built by Revolist OU ❤️
*/
'use strict';
var index = require('./index-Dq8Xzj5l.js');
var dimension_helpers = require('./dimension.helpers-CaIsYC99.js');
var events = require('./events-DeLDyZlb.js');
var column_service = require('./column.service-DvQDqxxx.js');
var edit_utils = require('./edit.utils-CwMzSIVF.js');
var debounce = require('./debounce-CcpHiH2p.js');
var viewport_store = require('./viewport.store-Dcjud-a-.js');
var viewport_helpers = require('./viewport.helpers-BAovztDd.js');
var throttle = require('./throttle-CI4MsAqs.js');
const Attribution = class {
constructor(hostRef) {
index.registerInstance(this, hostRef);
}
render() {
return (index.h(index.Host, { key: '3d66475a019010c24b6c610ccc047e01c35178f9' }, index.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) {
index.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 (index.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] = dimension_helpers.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 = events.getPropertyFromEvent(e, 'clientX');
const y = events.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 = dimension_helpers.getItemByPosition(rows, cellY);
const rgCol = dimension_helpers.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 = dimension_helpers.getItemByIndex(dimensionRow, y).start;
const left = dimension_helpers.getItemByIndex(dimensionCol, x).start;
const bottom = dimension_helpers.getItemByIndex(dimensionRow, y1).end;
const right = dimension_helpers.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) {
index.registerInstance(this, hostRef);
this.beforeFocusRender = index.createEvent(this, "beforefocusrender", 7);
this.beforeScrollIntoView = index.createEvent(this, "beforescrollintoview", 7);
this.afterFocus = index.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 = dimension_helpers.getSourceItem(this.dataStore, currentFocus.y);
const column = dimension_helpers.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 index.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, index.h, detail);
const props = {
class: { [dimension_helpers.FOCUS_CLASS]: true },
style: styles,
};
return (index.h(index.Host, Object.assign({}, props), index.h("slot", null), extra));
}
get el() { return index.getElement(this); }
};
RevogrFocus$1.style = revogrFocusStyleCss();
const DIRECTION_CODES = [
dimension_helpers.codesLetter.TAB,
dimension_helpers.codesLetter.ARROW_UP,
dimension_helpers.codesLetter.ARROW_DOWN,
dimension_helpers.codesLetter.ARROW_LEFT,
dimension_helpers.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 dimension_helpers.codesLetter.ESCAPE:
this.sv.cancel();
break;
case dimension_helpers.codesLetter.TAB:
this.keyChangeSelection(e, canRange);
break;
}
return;
}
// IF NOT EDIT MODE
// pressed clear key
if (range && edit_utils.isClear(e.code)) {
this.sv.clearCell();
return;
}
// below works with focus only
if (!focus) {
return;
}
// tab key means same as arrow right
if (edit_utils.isTab(e.code)) {
this.keyChangeSelection(e, canRange);
return;
}
// pressed enter
if (edit_utils.isEnterKeyValue(e.key)) {
this.sv.change();
return;
}
// copy operation
if (edit_utils.isCopy(e)) {
return;
}
// cut operation
if (edit_utils.isCut(e)) {
return;
}
// paste operation
if (edit_utils.isPaste(e)) {
this.sv.internalPaste();
return;
}
// select all
if (edit_utils.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 dimension_helpers.timeout(dimension_helpers.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 = column_service.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 dimension_helpers.codesLetter.TAB:
return { changes: { x: -1 }, isMulti: false };
}
}
switch (e.code) {
case dimension_helpers.codesLetter.ARROW_UP:
return { changes: { y: -1 }, isMulti };
case dimension_helpers.codesLetter.ARROW_DOWN:
return { changes: { y: 1 }, isMulti };
case dimension_helpers.codesLetter.ARROW_LEFT:
return { changes: { x: -1 }, isMulti };
case dimension_helpers.codesLetter.TAB:
case dimension_helpers.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 (index.h("div", { class: {
[dimension_helpers.CELL_HANDLER_CLASS]: true,
[dimension_helpers.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.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 = events.getPropertyFromEvent(event, 'clientX', dimension_helpers.MOBILE_CLASS);
const y = events.getPropertyFromEvent(event, 'clientY', dimension_helpers.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 = column_service.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 = column_service.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 = column_service.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) {
index.registerInstance(this, hostRef);
this.beforeCopyRegion = index.createEvent(this, "beforecopyregion", 7);
this.beforeRegionPaste = index.createEvent(this, "beforepasteregion", 7);
this.cellEditApply = index.createEvent(this, "celleditapply", 7);
this.beforeFocusCell = index.createEvent(this, "beforecellfocusinit", 7);
this.beforeNextViewportFocus = index.createEvent(this, "beforenextvpfocus", 7);
this.setEdit = index.createEvent(this, "setedit", 7);
this.beforeApplyRange = index.createEvent(this, "beforeapplyrange", 7);
this.beforeSetRange = index.createEvent(this, "beforesetrange", 7);
this.setRange = index.createEvent(this, "setrange", 7);
this.beforeEditRender = index.createEvent(this, "beforeeditrender", 7);
this.selectAll = index.createEvent(this, "selectall", 7);
this.cancelEdit = index.createEvent(this, "canceledit", 7);
this.setTempRange = index.createEvent(this, "settemprange", 7);
this.beforeSetTempRange = index.createEvent(this, "beforesettemprange", 7);
this.applyFocus = index.createEvent(this, "applyfocus", 7);
this.focusCell = index.createEvent(this, "focuscell", 7);
this.beforeRangeDataApply = index.createEvent(this, "beforerangedataapply", 7);
this.selectionChange = index.createEvent(this, "selectionchangeinit", 7);
this.beforeRangeCopyApply = index.createEvent(this, "beforerangecopyapply", 7);
this.rangeEditApply = index.createEvent(this, "rangeeditapply", 7);
this.rangeClipboardCopy = index.createEvent(this, "clipboardrangecopy", 7);
this.rangeClipboardPaste = index.createEvent(this, "clipboardrangepaste", 7);
this.beforeKeyDown = index.createEvent(this, "beforekeydown", 7);
this.beforeKeyUp = index.createEvent(this, "beforekeyup", 7);
this.beforeCellSave = index.createEvent(this, "beforecellsave", 7);
this.cellEditDone = index.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 column_service.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 [
index.h("div", { class: dimension_helpers.SELECTION_BORDER_CLASS, style: styles }, this.isMobileDevice && (index.h("div", { class: "range-handlers" }, index.h("span", { class: dimension_helpers.MOBILE_CLASS }), index.h("span", { class: dimension_helpers.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 ||
column_service.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 (index.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: column_service.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(index.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(index.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 (index.h(index.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, index.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 (edit_utils.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 (events.verifyTouchTarget(e.touches[0], dimension_helpers.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 = column_service.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: dimension_helpers.codesLetter.ARROW_DOWN,
}), this.range));
if (!canFocus) {
this.closeEdit();
}
}
clearCell() {
var _a;
const range = this.selectionStore.get('range');
if (range && !column_service.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 = column_service.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 = column_service.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 index.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) {
index.registerInstance(this, hostRef);
this.scrollViewport = index.createEvent(this, "scrollview", 3);
this.elementToScroll = index.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 viewport_store.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 dimension_helpers.DataStore(data.type, Object.assign({}, data.dataStore.state));
// initiate column data
const colData = new dimension_helpers.DataStore('colPinStart');
const column = Object.assign({ cellTemplate: RowHeaderRender(totalLength) }, this.rowHeaderColumn);
colData.updateData([column]);
dataViews.push(index.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 = viewport_store.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: dimension_helpers.ROW_HEADER_TYPE,
// parent,
slot: viewport_helpers.HEADER_SLOT
});
return (index.h(index.Host, { class: { [dimension_helpers.ROW_HEADER_TYPE]: true }, key: dimension_helpers.ROW_HEADER_TYPE }, index.h("revogr-viewport-scroll", Object.assign({ key: 'c401e82e02e4bdb7afb25f2f49c6776f2e115c81' }, viewportScroll, { "row-header": true }), index.h("revogr-header", Object.assign({ key: '3c73d27bd96e23a34fc0cf47eda4d2e65751df98' }, viewportHeader)), dataViews)));
}
};
/**
* Autohide scroll for MacOS when scroll is visible only for 1 sec