UNPKG

@visactor/vue-vtable

Version:

The vue version of VTable

1,180 lines (1,166 loc) 59 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@visactor/vtable'), require('vue'), require('@visactor/vutils'), require('@visactor/vtable/es/vrender')) : typeof define === 'function' && define.amd ? define(['exports', '@visactor/vtable', 'vue', '@visactor/vutils', '@visactor/vtable/es/vrender'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.VueVTable = {}, global.VTable, global.Vue, global.VUtils, global.VTable.vrender)); })(this, (function (exports, VTable, vue, vutils, vrender) { 'use strict'; function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var VTable__namespace = /*#__PURE__*/_interopNamespaceDefault(VTable); function toCamelCase(str) { return str.replace(/-([a-z])/g, g => g[1].toUpperCase()); } function convertPropsToCamelCase(props) { const newProps = {}; for (const key in props) { if (props.hasOwnProperty(key)) { const camelCaseKey = toCamelCase(key); newProps[camelCaseKey] = props[key]; } } return newProps; } function flattenVNodes(vnodes) { return vnodes.flatMap(vnode => (Array.isArray(vnode.children) ? flattenVNodes(vnode.children) : vnode)); } function isEventProp(key, props) { return key.startsWith('on') && vutils.isFunction(props[key]); } function createCustomLayout(children, isHeader, args) { const componentMap = { Group: VTable__namespace.CustomLayout.Group, Image: VTable__namespace.CustomLayout.Image, Text: VTable__namespace.CustomLayout.Text, Tag: VTable__namespace.CustomLayout.Tag, Radio: VTable__namespace.CustomLayout.Radio, CheckBox: VTable__namespace.CustomLayout.CheckBox }; function createComponent(child) { if (!child) { return null; } const { type, children: childChildren } = child; const props = convertPropsToCamelCase(child.props); const componentName = type?.symbol || type?.name; const ComponentClass = componentMap[componentName]; if (!ComponentClass) { return null; } const component = new ComponentClass({ ...props }); bindComponentEvents(component, props); const subChildren = resolveChildren(childChildren); if (vutils.isObject(props?.vue)) { const { element } = props.vue; let targetVNode = element ?? subChildren.find(node => node?.type !== Symbol.for('v-cmt')); if (vue.isVNode(targetVNode)) { targetVNode = !targetVNode.key ? vue.cloneVNode(targetVNode, { key: `row_${args.row}_col_${args.col}` }) : targetVNode; } else { targetVNode = null; } Object.assign(child.props.vue, { element: targetVNode, container: isHeader ? args?.table?.headerDomContainer : args?.table?.bodyDomContainer }); return component; } subChildren.forEach((subChild) => { const subComponent = createComponent(subChild); if (subComponent) { component.add(subComponent); } else if (subChild.type === Symbol.for('v-fgt')) { subChild.children.forEach((nestedChild) => { const nestedComponent = createComponent(nestedChild); if (nestedComponent) { component.add(nestedComponent); } }); } }); return component; } function resolveChildren(childChildren) { return childChildren?.default?.() || childChildren || []; } function bindComponentEvents(component, props) { Object.keys(props).forEach(key => { if (isEventProp(key, props)) { let eventName; if (key.startsWith('on')) { eventName = key.slice(2).toLowerCase(); } else { eventName = toCamelCase(key.slice(2)).toLowerCase(); } component.addEventListener(eventName, props[key]); } }); } return { rootComponent: createComponent(children) }; } function createCustomLayoutHandler(children, isHeader) { return (args) => { const { table, row, col, rect } = args; const record = table.getCellOriginRecord(col, row); const { height, width } = rect ?? table.getCellRect(col, row); const customLayoutKey = isHeader ? 'headerCustomLayout' : 'customLayout'; if (!children[customLayoutKey]) { return null; } const rootContainer = children[customLayoutKey]({ table, row, col, rect, record, height, width })[0]; const { rootComponent } = createCustomLayout(rootContainer, isHeader, args); return { rootContainer: rootComponent, renderDefault: false }; }; } class DynamicRenderEditor { wrapContainer; tableContainer; currentValue; nodeMap; currentContext; constructor(currentContext) { this.currentContext = currentContext; this.tableContainer = null; this.currentValue = null; this.wrapContainer = null; this.nodeMap = new Map(); } registerNode(tableId, key, getNode) { if (!vutils.isValid(tableId) || !vutils.isValid(key) || typeof getNode !== 'function') { return; } if (!this.nodeMap.has(tableId)) { this.nodeMap.set(tableId, new Map()); } this.nodeMap.get(tableId).set(key, getNode); } getNode(tableId, key) { return this.nodeMap.get(tableId)?.get(key); } removeNode(tableId) { this.nodeMap.delete(tableId); } release(tableId) { if (!vutils.isValid(tableId)) { this.nodeMap.clear(); } else { this.removeNode(tableId); } } async onStart(editorContext) { const { value } = editorContext; this.setValue(value); if (!(await this.createElement(editorContext))) { return; } } async createElement(editorContext) { const { row, col, value, table, container, referencePosition } = editorContext; if (!container) { return false; } const define = table.getBodyColumnDefine(col, row); const { editConfig } = define || {}; const { id } = table; const key = this.getColumnKeyField(define); if (!vutils.isValid(key) || !vutils.isValid(id)) { return false; } if (typeof editConfig?.editBefore === 'function') { const v = await editConfig.editBefore(editorContext); if (!v) { table.showTooltip(col, row, { content: editConfig.disablePrompt || 'This field is not allowed to be edited', referencePosition: { rect: referencePosition?.rect, placement: VTable.TYPES.Placement.top }, style: { bgColor: 'black', color: 'white', arrowMark: true }, disappearDelay: 1000 }); return false; } } const record = table?.getCellOriginRecord(col, row); const vnode = this.getNode(id, key)?.({ row, col, value, record, table, onChange: (value) => this.setValue(value) })?.find((node) => node?.type !== Symbol.for('v-cmt')); if (!vnode || !vue.isVNode(vnode)) { return false; } this.checkToPassAppContext(vnode, table); const wrapContainer = document.createElement('div'); wrapContainer.style.position = 'absolute'; wrapContainer.style.width = '100%'; wrapContainer.style.boxSizing = 'border-box'; const { bgColor } = table.getCellStyle(col, row) || {}; wrapContainer.style.backgroundColor = bgColor || '#FFFFFF'; this.wrapContainer = wrapContainer; this.tableContainer = container; this.tableContainer.appendChild(wrapContainer); vue.render(vnode, wrapContainer); if (referencePosition?.rect) { this.adjustPosition(referencePosition.rect); } return true; } checkToPassAppContext(vnode, table) { try { const userAppContext = table.options?.customConfig?.getVueUserAppContext?.() ?? this.currentContext; if (!!userAppContext?.components && !!userAppContext?.directives) { vnode.appContext = userAppContext; } } catch (error) { } } getColumnKeyField(column) { const { field, key } = column || {}; return vutils.isValid(key) ? key : field; } getValue() { return this.currentValue; } setValue(value) { this.currentValue = value; } adjustPosition(rect) { if (this.wrapContainer) { this.wrapContainer.style.top = `${rect.top}px`; this.wrapContainer.style.left = `${rect.left}px`; this.wrapContainer.style.width = `${rect.width}px`; this.wrapContainer.style.height = `${rect.height}px`; } } async validateValue(value, oldValue, editCell, table) { const { col, row } = editCell || {}; if (!vutils.isValid(col) || !vutils.isValid(row)) { return true; } const define = table.getBodyColumnDefine(col, row); const { editConfig } = define || {}; if (typeof editConfig?.validateValue === 'function') { const validate = await editConfig.validateValue({ col, row, value, oldValue, table }); if (validate === false) { const rect = table.getVisibleCellRangeRelativeRect({ col, row }); table.showTooltip(col, row, { content: editConfig.invalidPrompt || 'invalid', referencePosition: { rect, placement: VTable.TYPES.Placement.top }, style: { bgColor: 'red', color: 'white', arrowMark: true }, disappearDelay: 1000 }); return false; } return validate; } return true; } onEnd() { if (this.wrapContainer && this.tableContainer) { vue.render(null, this.wrapContainer); this.tableContainer.removeChild(this.wrapContainer); } this.wrapContainer = null; this.tableContainer = null; } isEditorElement(target) { return this.wrapContainer?.contains(target) || this.isClickEditorElement(target); } isClickEditorElement(target) { while (target) { if (target.classList && target.classList.contains('table-editor-element')) { return true; } target = target.parentNode; } return false; } } const DYNAMIC_RENDER_EDITOR = 'dynamic-render-editor'; function checkRenderEditor(column, getEditCustomNode) { const { editor } = column || {}; const key = getRenderEditorColumnKeyField(column); if (!vutils.isValid(key) || editor !== DYNAMIC_RENDER_EDITOR) { return false; } if (typeof getEditCustomNode === 'function') { column.getEditCustomNode = getEditCustomNode; return true; } return typeof column.getEditCustomNode === 'function'; } function getRenderEditorColumnKeyField(column) { const { field, key } = column || {}; return vutils.isValid(key) ? key : field; } function getRenderEditor(create, currentContext) { let renderEditor = VTable.register.editor(DYNAMIC_RENDER_EDITOR); if (!renderEditor && !!create) { renderEditor = new DynamicRenderEditor(currentContext); VTable.register.editor(DYNAMIC_RENDER_EDITOR, renderEditor); } return renderEditor; } function extractPivotSlotOptions(vnodes) { const options = { columns: [], columnHeaderTitle: [], rows: [], rowHeaderTitle: [], indicators: [], corner: {}, tooltip: {}, menu: {} }; const typeMapping = { PivotColumnDimension: 'columns', PivotColumnHeaderTitle: 'columnHeaderTitle', PivotRowDimension: 'rows', PivotRowHeaderTitle: 'rowHeaderTitle', PivotCorner: 'corner', PivotIndicator: 'indicators', Tooltip: 'tooltip', Menu: 'menu' }; vnodes.forEach(vnode => { vnode.props = convertPropsToCamelCase(vnode.props); const typeName = vnode.type?.symbol || vnode.type?.name; const optionKey = typeMapping[typeName]; if (optionKey) { if (Array.isArray(options[optionKey])) { if (vnode.props.hasOwnProperty('objectHandler')) { options[optionKey].push(vnode.props.objectHandler); } else { options[optionKey].push(vnode.props); } } else { options[optionKey] = vnode.props; } } }); return options; } function extractListSlotOptions(vnodes) { const options = { columns: [], tooltip: {}, menu: {} }; const typeMapping = { ListColumn: 'columns', Tooltip: 'tooltip', Menu: 'menu' }; vnodes.forEach(vnode => { vnode.props = convertPropsToCamelCase(vnode.props); const typeName = vnode.type?.symbol || vnode.type?.name; const optionKey = typeMapping[typeName]; if (optionKey) { if (optionKey === 'columns' && vnode.children) { if (vnode.children.customLayout) { vnode.props.customLayout = createCustomLayoutHandler(vnode.children); } if (vnode.children.headerCustomLayout) { vnode.props.headerCustomLayout = createCustomLayoutHandler(vnode.children, true); } checkRenderEditor(vnode.props, vnode.children.edit); } if (Array.isArray(options[optionKey])) { options[optionKey].push(vnode.props); } else { options[optionKey] = vnode.props; } } }); return options; } function mergeSlotOptions(propsOptions, slotOptions) { return { ...propsOptions, columns: slotOptions.columns && slotOptions.columns.length ? slotOptions.columns : propsOptions.columns, columnHeaderTitle: slotOptions.columnHeaderTitle && slotOptions.columnHeaderTitle.length ? slotOptions.columnHeaderTitle : propsOptions.columnHeaderTitle, rows: slotOptions.rows && slotOptions.rows.length ? slotOptions.rows : propsOptions.rows, rowHeaderTitle: slotOptions.rowHeaderTitle && slotOptions.rowHeaderTitle.length ? slotOptions.rowHeaderTitle : propsOptions.rowHeaderTitle, indicators: slotOptions.indicators && slotOptions.indicators.length ? slotOptions.indicators : propsOptions.indicators, corner: Object.keys(propsOptions.corner || {}).length ? propsOptions.corner : slotOptions.corner, tooltip: Object.keys(slotOptions.tooltip || {}).length ? slotOptions.tooltip : propsOptions.tooltip, menu: Object.keys(slotOptions.menu || {}).length ? slotOptions.menu : propsOptions.menu }; } const EVENT_TYPE = { ...VTable.ListTable.EVENT_TYPE, ...VTable.PivotTable.EVENT_TYPE, ...VTable.PivotChart.EVENT_TYPE }; const TABLE_EVENTS = { onClickCell: EVENT_TYPE.CLICK_CELL, onDblClickCell: EVENT_TYPE.DBLCLICK_CELL, onMouseDownCell: EVENT_TYPE.MOUSEDOWN_CELL, onMouseUpCell: EVENT_TYPE.MOUSEUP_CELL, onSelectedCell: EVENT_TYPE.SELECTED_CELL, onKeyDown: EVENT_TYPE.KEYDOWN, onMouseEnterTable: EVENT_TYPE.MOUSEENTER_TABLE, onMouseLeaveTable: EVENT_TYPE.MOUSELEAVE_TABLE, onMouseDownTable: EVENT_TYPE.MOUSEDOWN_TABLE, onMouseMoveCell: EVENT_TYPE.MOUSEMOVE_CELL, onMouseEnterCell: EVENT_TYPE.MOUSEENTER_CELL, onMouseLeaveCell: EVENT_TYPE.MOUSELEAVE_CELL, onContextMenuCell: EVENT_TYPE.CONTEXTMENU_CELL, onResizeColumn: EVENT_TYPE.RESIZE_COLUMN, onResizeColumnEnd: EVENT_TYPE.RESIZE_COLUMN_END, onChangeHeaderPosition: EVENT_TYPE.CHANGE_HEADER_POSITION, onChangeHeaderPositionStart: EVENT_TYPE.CHANGE_HEADER_POSITION_START, onChangeHeaderPositionFail: EVENT_TYPE.CHANGE_HEADER_POSITION_FAIL, onSortClick: EVENT_TYPE.SORT_CLICK, onFreezeClick: EVENT_TYPE.FREEZE_CLICK, onScroll: EVENT_TYPE.SCROLL, onDropdownMenuClick: EVENT_TYPE.DROPDOWN_MENU_CLICK, onMouseOverChartSymbol: EVENT_TYPE.MOUSEOVER_CHART_SYMBOL, onDragSelectEnd: EVENT_TYPE.DRAG_SELECT_END, onDropdownIconClick: EVENT_TYPE.DROPDOWN_ICON_CLICK, onDropdownMenuClear: EVENT_TYPE.DROPDOWN_MENU_CLEAR, onTreeHierarchyStateChange: EVENT_TYPE.TREE_HIERARCHY_STATE_CHANGE, onShowMenu: EVENT_TYPE.SHOW_MENU, onHideMenu: EVENT_TYPE.HIDE_MENU, onIconClick: EVENT_TYPE.ICON_CLICK, onLegendItemClick: EVENT_TYPE.LEGEND_ITEM_CLICK, onLegendItemHover: EVENT_TYPE.LEGEND_ITEM_HOVER, onLegendItemUnHover: EVENT_TYPE.LEGEND_ITEM_UNHOVER, onLegendChange: EVENT_TYPE.LEGEND_CHANGE, onMouseEnterAxis: EVENT_TYPE.MOUSEENTER_AXIS, onMouseLeaveAxis: EVENT_TYPE.MOUSELEAVE_AXIS, onCheckboxStateChange: EVENT_TYPE.CHECKBOX_STATE_CHANGE, onRadioStateChange: EVENT_TYPE.RADIO_STATE_CHANGE, onAfterRender: EVENT_TYPE.AFTER_RENDER, onInitialized: EVENT_TYPE.INITIALIZED, onPivotSortClick: EVENT_TYPE.PIVOT_SORT_CLICK, onDrillMenuClick: EVENT_TYPE.DRILLMENU_CLICK, onVChartEventType: EVENT_TYPE.VCHART_EVENT_TYPE, onChangeCellValue: EVENT_TYPE.CHANGE_CELL_VALUE, onMousedownFillHandle: EVENT_TYPE.MOUSEDOWN_FILL_HANDLE, onDragFillHandleEnd: EVENT_TYPE.DRAG_FILL_HANDLE_END, onDblclickFillHandle: EVENT_TYPE.DBLCLICK_FILL_HANDLE, onScrollVerticalEnd: EVENT_TYPE.SCROLL_VERTICAL_END, onScrollHorizontalEnd: EVENT_TYPE.SCROLL_HORIZONTAL_END, onChangCellValue: EVENT_TYPE.CHANGE_CELL_VALUE, onEmptyTipClick: EVENT_TYPE.EMPTY_TIP_CLICK, onEmptyTipDblClick: EVENT_TYPE.EMPTY_TIP_DBLCLICK, onButtonClick: EVENT_TYPE.BUTTON_CLICK, onBeforeCacheChartImage: EVENT_TYPE.BEFORE_CACHE_CHART_IMAGE, onPastedData: EVENT_TYPE.PASTED_DATA, onSelectedClear: EVENT_TYPE.SELECTED_CLEAR }; const TABLE_EVENTS_KEYS = Object.keys(TABLE_EVENTS); function useEditorRender(props, tableRef) { const instance = vue.getCurrentInstance(); const validColumns = vue.computed(() => { const columns = props.options?.columns; if (!vutils.isArray(columns)) { return []; } return columns.filter(col => !!vutils.isObject(col) && !!checkRenderEditor(col)); }); vue.watchEffect(() => { resolveRenderEditor(); }); vue.onBeforeUnmount(() => { releaseRenderEditor(); }); function resolveRenderEditor() { const id = getTableId(); if (!vutils.isValid(id)) { return; } let renderEditor = getRenderEditor(); if (renderEditor) { renderEditor.removeNode(id); } else if (validColumns.value.length > 0) { renderEditor = getRenderEditor(true, instance?.appContext); } validColumns.value.forEach(column => { const { getEditCustomNode } = column; const key = getRenderEditorColumnKeyField(column); renderEditor.registerNode(id, key, getEditCustomNode); delete column.editCustomNode; }); } function releaseRenderEditor() { const id = getTableId(); if (!vutils.isValid(id)) { return; } const renderEditor = getRenderEditor(); renderEditor?.release(id); } function getTableId() { return tableRef.value?.id; } } class VTableVueAttributePlugin extends vrender.HtmlAttributePlugin { name = 'VTableVueAttributePlugin'; renderQueue = new Set(); isRendering = false; MAX_CACHE_COUNT = 100; accessQueue = []; VIEWPORT_BUFFER = 100; BUFFER_ZONE = 500; styleUpdateQueue = new Map(); styleUpdateRequested = false; eventHandlers = new WeakMap(); currentContext; constructor(currentContext) { super(); this.currentContext = currentContext; } renderGraphicHTML(graphic) { if (!this.checkNeedRender(graphic)) { return; } this.renderQueue.add(graphic); this.scheduleRender(); } scheduleRender() { if (this.isRendering) { return; } this.isRendering = true; requestAnimationFrame(() => { this.renderQueue.forEach(graphic => { try { this.doRenderGraphic(graphic); } catch (error) { const { id } = this.getGraphicOptions(graphic) || {}; this.removeElement(id, true); } }); this.renderQueue.clear(); this.isRendering = false; }); } doRenderGraphic(graphic) { const { id, options } = this.getGraphicOptions(graphic); if (!id) { return; } const stage = graphic.stage; const { element, container: expectedContainer } = options; const actualContainer = expectedContainer ? checkFrozenContainer(graphic) : expectedContainer; let targetMap = this.htmlMap?.[id]; if (targetMap && actualContainer && actualContainer !== targetMap.container) { this.removeElement(id); targetMap = null; } this.checkToPassAppContext(element, graphic); if (!targetMap || !this.checkDom(targetMap.wrapContainer)) { this.checkAndClearCache(graphic); const { wrapContainer, nativeContainer, reuse } = this.getWrapContainer(stage, actualContainer, { id, options }); if (wrapContainer) { const dataRenderId = `${this.renderId}`; wrapContainer.id = id; wrapContainer.setAttribute('data-vue-renderId', dataRenderId); wrapContainer.style.display = 'none'; if (!reuse) { vue.render(element, wrapContainer); } targetMap = { wrapContainer, nativeContainer, container: actualContainer, renderId: this.renderId, graphic, isInViewport: true, lastPosition: null, lastStyle: {} }; this.htmlMap[id] = targetMap; } } if (targetMap) { targetMap.renderId = this.renderId; targetMap.lastAccessed = Date.now(); this.updateAccessQueue(id); this.updateStyleOfWrapContainer(graphic, stage, targetMap.wrapContainer, targetMap.nativeContainer); } } getGraphicOptions(graphic) { const { vue } = graphic?.attribute || {}; if (!vue) { return null; } const id = vutils.isNil(vue.id) ? graphic.id ?? graphic._uid : vue.id; return { id: `vue_${id}`, options: vue }; } checkToPassAppContext(vnode, graphic) { try { const customConfig = this.getCustomConfig(graphic); const userAppContext = customConfig?.getVueUserAppContext?.() ?? this.currentContext; if (!!userAppContext?.components && !!userAppContext?.directives) { vnode.appContext = userAppContext; } } catch (error) { } } getCustomConfig(graphic) { const target = getTargetGroup(graphic); return target?.stage?.table?.options?.customConfig; } checkNeedRender(graphic) { const { id, options } = this.getGraphicOptions(graphic) || {}; if (!id) { return false; } const stage = graphic.stage; if (!stage) { return false; } const { element } = options; if (!element) { return false; } const isInViewport = this.checkInViewport(graphic); return isInViewport; } checkInViewport(graphic) { return this.checkInViewportByZone(graphic, this.VIEWPORT_BUFFER); } checkInBuffer(graphic) { return this.checkInViewportByZone(graphic, this.BUFFER_ZONE); } checkInViewportByZone(graphic, buffer = 0) { const { stage, globalAABBBounds: cBounds } = graphic; if (!stage) { return false; } const { AABBBounds: vBounds } = stage; const eBounds = { x1: vBounds.x1 - buffer, x2: vBounds.x2 + buffer, y1: vBounds.y1 - buffer, y2: vBounds.y2 + buffer }; const isIntersecting = cBounds.x1 < eBounds.x2 && cBounds.x2 > eBounds.x1 && cBounds.y1 < eBounds.y2 && cBounds.y2 > eBounds.y1; return isIntersecting; } updateAccessQueue(id) { const index = this.accessQueue.indexOf(id); if (index > -1) { this.accessQueue.splice(index, 1); } this.accessQueue.unshift(id); } checkAndClearCache(graphic) { const { viewportNodes, bufferNodes, cacheNodes } = this.classifyNodes(); const total = viewportNodes.length + bufferNodes.length + cacheNodes.length; const customConfig = this.getCustomConfig(graphic); const maxTotal = customConfig?.maxDomCacheCount ?? this.MAX_CACHE_COUNT; if (total <= maxTotal) { return; } const exceedingCount = total - maxTotal; let toRemove = cacheNodes.slice(0, exceedingCount); if (toRemove.length < exceedingCount) { const bufferCandidates = bufferNodes .sort((a, b) => this.htmlMap[a].lastAccessed - this.htmlMap[b].lastAccessed) .slice(0, exceedingCount - toRemove.length); toRemove = toRemove.concat(bufferCandidates); } toRemove.forEach(id => this.removeElement(id, true)); } classifyNodes() { const viewportNodes = []; const bufferNodes = []; const cacheNodes = []; Object.keys(this.htmlMap).forEach(id => { const node = this.htmlMap[id]; if (node.isInViewport) { viewportNodes.push(id); } else if (this.checkInBuffer(node.graphic)) { bufferNodes.push(id); } else { cacheNodes.push(id); } }); return { viewportNodes, bufferNodes, cacheNodes }; } checkDom(dom) { if (!dom) { return false; } return document.contains(dom); } removeAllDom(g) { if (this.htmlMap) { Object.keys(this.htmlMap).forEach(key => { this.removeElement(key, true); }); this.htmlMap = null; } } removeElement(id, clear) { const record = this.htmlMap?.[id]; if (!record) { return; } const { wrapContainer } = record; if (!wrapContainer) { return; } if (!clear) { wrapContainer.remove(); record.isInViewport = false; const index = this.accessQueue.indexOf(id); if (index > -1) { this.accessQueue.splice(index, 1); } } else { vue.render(null, wrapContainer); this.checkDom(wrapContainer) && super.removeElement(id); delete this.htmlMap[id]; } this.removeWrapContainerEventListener(wrapContainer); } getWrapContainer(stage, userContainer, domParams) { let nativeContainer; if (userContainer) { nativeContainer = typeof userContainer === 'string' ? vrender.application.global.getElementById(userContainer) : userContainer; } else { nativeContainer = stage.window.getContainer(); } const { id } = domParams || {}; const record = this.htmlMap?.[id]; if (record && !record.isInViewport) { const { wrapContainer } = record; if (!this.checkDom(wrapContainer)) { nativeContainer.appendChild(wrapContainer); } return { reuse: true, wrapContainer, nativeContainer }; } return { wrapContainer: vrender.application.global.createDom({ tagName: 'div', parent: nativeContainer }), nativeContainer }; } updateStyleOfWrapContainer(graphic, stage, wrapContainer, nativeContainer) { const { attribute, type } = graphic; const { vue: options, width, height, visible, display, ...rest } = attribute || {}; const { x: left, y: top } = this.calculatePosition(graphic, options.anchorType); const { left: offsetX, top: offsetTop } = this.calculateOffset(stage, nativeContainer, left, top); const { id } = this.getGraphicOptions(graphic) || {}; const record = id ? this.htmlMap[id] : null; if (!record) { return; } const positionChanged = !record.lastPosition || record.lastPosition.x !== offsetX || record.lastPosition.y !== offsetTop; if (!positionChanged) { return; } const { pointerEvents } = options; const calculateStyle = this.parseDefaultStyleFromGraphic(graphic); const style = this.convertCellStyle(graphic); Object.assign(calculateStyle, { width: `${width}px`, height: `${height}px`, overflow: 'hidden', ...(style || {}), ...(rest || {}), transform: `translate(${offsetX}px, ${offsetTop}px)`, boxSizing: 'border-box', display: visible !== false ? display || 'block' : 'none', pointerEvents: pointerEvents === true ? 'all' : pointerEvents || 'none', position: 'absolute' }); if (calculateStyle.pointerEvents !== 'none') { this.checkToAddEventListener(wrapContainer); } if (type === 'text' && options.anchorType === 'position') { Object.assign(calculateStyle, this.getTransformOfText(graphic)); } this.applyUserStyles(options, calculateStyle, { offsetX, offsetTop, graphic, wrapContainer }); const styleChanged = !vutils.isEqual(record.lastStyle, calculateStyle); if (styleChanged) { this.styleUpdateQueue.set(wrapContainer.id, calculateStyle); this.requestStyleUpdate(); record.lastStyle = calculateStyle; } } checkToAddEventListener(wrapContainer) { if (!this.eventHandlers.has(wrapContainer)) { const handler = (e) => { e.preventDefault(); this.onWheel(e); }; wrapContainer.addEventListener('wheel', handler, { passive: false }); this.eventHandlers.set(wrapContainer, handler); } } requestStyleUpdate() { if (!this.styleUpdateRequested) { this.styleUpdateRequested = true; requestAnimationFrame(() => { this.styleUpdateQueue.forEach((changes, id) => { const container = this.htmlMap?.[id]?.wrapContainer; if (container) { Object.assign(container.style, changes); } }); this.styleUpdateQueue.clear(); this.styleUpdateRequested = false; }); } } convertCellStyle(graphic) { const { col, row, stage } = getTargetGroup(graphic); const style = stage?.table?.getCellStyle(col, row); if (!vutils.isObject(style)) { return; } const { lineHeight, padding, ...rest } = style; return { ...rest, padding: vutils.isArray(padding) ? padding.map(value => `${value}px`).join(' ') : padding }; } calculatePosition(graphic, anchorType) { const bounds = graphic.globalAABBBounds; if (anchorType === 'position' || bounds.empty()) { const matrix = graphic.globalTransMatrix; return { x: matrix.e, y: matrix.f }; } return vutils.calculateAnchorOfBounds(bounds, anchorType || 'top-left'); } calculateOffset(stage, nativeContainer, x, y) { const containerTL = vrender.application.global.getElementTopLeft(nativeContainer, false); const windowTL = stage.window.getTopLeft(false); return { left: x + windowTL.left - containerTL.left, top: y + windowTL.top - containerTL.top }; } applyUserStyles(options, baseStyle, context) { if (vutils.isFunction(options.style)) { const userStyle = options.style({ top: context.offsetTop, left: context.offsetX, width: context.graphic.globalAABBBounds.width(), height: context.graphic.globalAABBBounds.height() }, context.graphic, context.wrapContainer); Object.assign(baseStyle, userStyle); } else if (vutils.isObject(options.style)) { Object.assign(baseStyle, options.style); } else if (vutils.isString(options.style)) { Object.assign(baseStyle, vutils.styleStringToObject(options.style)); } } } function checkFrozenContainer(graphic) { const { col, row, stage } = getTargetGroup(graphic); let container = graphic.attribute.vue?.container; const { table } = stage; if (container === table.bodyDomContainer) { if (col < table.frozenColCount && row >= table.rowCount - table.bottomFrozenRowCount) { container = table.bottomFrozenBodyDomContainer; } else if (col >= table.colCount - table.rightFrozenColCount && row >= table.rowCount - table.bottomFrozenRowCount) { container = table.rightFrozenBottomDomContainer; } else if (row >= table.rowCount - table.bottomFrozenRowCount) { container = table.bottomFrozenBodyDomContainer; } else if (col < table.frozenColCount) { container = table.frozenBodyDomContainer; } else if (col >= table.colCount - table.rightFrozenColCount) { container = table.rightFrozenBodyDomContainer; } } else if (container === table.headerDomContainer) { if (col < table.frozenColCount) { container = table.frozenHeaderDomContainer; } else if (col >= table.colCount - table.rightFrozenColCount) { container = table.rightFrozenHeaderDomContainer; } } return container; } function getTargetGroup(target) { while (target?.parent) { if (target.name === 'custom-container') { return target; } target = target.parent; } return { col: -1, row: -1, stage: null }; } function useCellRender(props, tableRef) { const instance = vue.getCurrentInstance(); const createReactContainer = props?.options?.customConfig?.createReactContainer; vue.watchEffect(() => { if (!createReactContainer) { return; } const pluginService = tableRef.value?.scenegraph?.stage?.pluginService; if (!pluginService) { return; } const exist = pluginService.findPluginsByName('VTableVueAttributePlugin'); if (vutils.isArray(exist) && !!exist.length) { return; } const plugin = new VTableVueAttributePlugin(instance?.appContext); pluginService.register(plugin); }); } var _sfc_main$3 = vue.defineComponent({ __name: 'base-table', props: { type: { type: String, required: false }, options: { type: null, required: false }, records: { type: Array, required: false }, width: { type: [Number, String], required: false, default: '100%' }, height: { type: [Number, String], required: false, default: '100%' }, onReady: { type: Function, required: false }, onError: { type: Function, required: false }, keepColumnWidthChange: { type: Boolean, required: false }, onClickCell: { type: null, required: false }, onDblClickCell: { type: null, required: false }, onMouseDownCell: { type: null, required: false }, onMouseUpCell: { type: null, required: false }, onSelectedCell: { type: null, required: false }, onKeyDown: { type: null, required: false }, onMouseEnterTable: { type: null, required: false }, onMouseLeaveTable: { type: null, required: false }, onMouseDownTable: { type: null, required: false }, onMouseMoveCell: { type: null, required: false }, onMouseEnterCell: { type: null, required: false }, onMouseLeaveCell: { type: null, required: false }, onContextMenuCell: { type: null, required: false }, onResizeColumn: { type: null, required: false }, onResizeColumnEnd: { type: null, required: false }, onChangeHeaderPosition: { type: null, required: false }, onChangeHeaderPositionStart: { type: null, required: false }, onChangeHeaderPositionFail: { type: null, required: false }, onSortClick: { type: null, required: false }, onFreezeClick: { type: null, required: false }, onScroll: { type: null, required: false }, onDropdownMenuClick: { type: null, required: false }, onMouseOverChartSymbol: { type: null, required: false }, onDragSelectEnd: { type: null, required: false }, onDropdownIconClick: { type: null, required: false }, onDropdownMenuClear: { type: null, required: false }, onTreeHierarchyStateChange: { type: null, required: false }, onShowMenu: { type: null, required: false }, onHideMenu: { type: null, required: false }, onIconClick: { type: null, required: false }, onLegendItemClick: { type: null, required: false }, onLegendItemHover: { type: null, required: false }, onLegendItemUnHover: { type: null, required: false }, onLegendChange: { type: null, required: false }, onMouseEnterAxis: { type: null, required: false }, onMouseLeaveAxis: { type: null, required: false }, onCheckboxStateChange: { type: null, required: false }, onRadioStateChange: { type: null, required: false }, onAfterRender: { type: null, required: false }, onInitialized: { type: null, required: false }, onPivotSortClick: { type: null, required: false }, onDrillMenuClick: { type: null, required: false }, onVChartEventType: { type: null, required: false }, onChangeCellValue: { type: null, required: false }, onMousedownFillHandle: { type: null, required: false }, onDragFillHandleEnd: { type: null, required: false }, onDblclickFillHandle: { type: null, required: false }, onScrollVerticalEnd: { type: null, required: false }, onScrollHorizontalEnd: { type: null, required: false }, onChangCellValue: { type: null, required: false }, onEmptyTipClick: { type: null, required: false }, onEmptyTipDblClick: { type: null, required: false }, onButtonClick: { type: null, required: false }, onBeforeCacheChartImage: { type: null, required: false }, onPastedData: { type: null, required: false } }, emits: TABLE_EVENTS_KEYS, setup(__props, { expose: __expose, emit: __emit }) { const props = __props; const vTableContainer = vue.ref(null); const vTableInstance = vue.shallowRef(null); const columnWidths = vue.ref(new Map()); const pivotColumnWidths = vue.ref([]); const pivotHeaderColumnWidths = vue.ref([]); useEditorRender(props, vTableInstance); useCellRender(props, vTableInstance); __expose({ vTableInstance }); const containerWidth = vue.computed(() => (typeof props.width === 'number' ? `${props.width}px` : props.width)); const containerHeight = vue.computed(() => (typeof props.height === 'number' ? `${props.height}px` : props.height)); const emit = __emit; const bindEvents = (instance) => { TABLE_EVENTS_KEYS.forEach(eventKey => { const vueEventHandler = (event) => { emit(eventKey, event); }; instance.on(TABLE_EVENTS[eventKey], vueEventHandler); }); }; const createTableInstance = (Type, options) => { const vtable = new Type(vTableContainer.value, options); vTableInstance.value = vtable; columnWidths.value.clear(); pivotColumnWidths.value = []; pivotHeaderColumnWidths.value = []; vtable.on('resize_column_end', (args) => { if (!props.keepColumnWidthChange) { return; } const { col, colWidths } = args; const width = colWidths[col]; if (vtable.isPivotTable()) { const path = vtable.getCellHeaderPaths(col, vtable.columnHeaderLevelCount); let dimensions = null; if (path.cellLocation === 'rowHeader') { dimensions = path.rowHeaderPaths; } else { dimensions = path.colHeaderPaths; } let found = false; for (let i = 0; i < pivotColumnWidths.value.length; i++) { const item = pivotColumnWidths.value[i]; if (JSON.stringify(item.dimensions) === JSON.stringify(dimensions)) { item.width = width; found = true; } } if (!found) { pivotColumnWidths.value.push({ dimensions, width }); } } else { const define = vtable.getBodyColumnDefine(col, 0); if (define?.key) { columnWidths.value.set(define.key, width); } } }); }; const createVTable = () => { if (!vTableContainer.value) { return; } if (vTableInstance.value) { vTableInstance.value.release(); } const getRecords = () => { return props.records !== undefined && props.records !== null && props.records.length > 0 ? props.records : props.options.records; }; try { switch (props.type) { case 'list': createTableInstance(VTable.ListTable, { ...props.options, records: getRecords() }); break; case 'pivot': createTableInstance(VTable.PivotTable, { ...props.options, records: getRecords() }); break; case 'chart': createTableInstance(VTable.PivotChart, { ...props.options, records: getRecords() }); break; } bindEvents(vTableInstance.value); props.onReady?.(vTableInstance.value, true); } catch (err) { props.onError?.(err); } }; const updateVTable = (newOptions) => { if (!vTableInstance.value) { return; } try { if (props.keepColumnWidthChange) { const columnWidthConfig = updateWidthCache(columnWidths.value, pivotColumnWidths.value, vTableInstance.value); newOptions = { ...newOptions, columnWidthConfig: columnWidthConfig, columnWidthConfigForRowHeader: columnWidthConfig }; } switch (props.type) { case 'list': if (vTableInstance.value instanceof VTable.ListTable) { vTableInstance.value.updateOption(newOptions); } break; case 'pivot': if (vTableInstance.value instanceof VTable.PivotTable) { vTableInstance.value.updateOption(newOptions); } break; case 'chart': if (vTableInstance.value instanceof VTable.PivotChart) { vTableInstance.value.updateOption(newOptions); } break;