From 4b4725a38693482b582b2e10229f4ad9fea44be0 Mon Sep 17 00:00:00 2001 From: zzxming Date: Sat, 14 Dec 2024 16:17:47 +0800 Subject: [PATCH] feat: table cell scale --- README.md | 41 +++-- docs/index.js | 15 +- src/index.ts | 9 ++ src/modules/table-align.ts | 2 +- src/modules/table-resize/index.ts | 1 + .../table-resize/table-resize-scale.ts | 143 ++++++++++++++++++ src/modules/table-selection.ts | 23 +-- src/style/index.less | 3 + src/style/table-resize-scale.less | 24 +++ src/utils/bem.ts | 6 +- src/utils/index.ts | 1 + src/utils/position.ts | 21 +++ src/utils/types.ts | 5 + 13 files changed, 249 insertions(+), 45 deletions(-) create mode 100644 src/modules/table-resize/table-resize-scale.ts create mode 100644 src/style/table-resize-scale.less create mode 100644 src/utils/position.ts diff --git a/README.md b/README.md index 6052535..f17dd6d 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ npm install quill-table-up ```js import Quill from 'quill'; -import TableUp, { TableAlign, TableMenuContextmenu, TableResizeBox, TableSelection, TableVirtualScrollbar } from 'quill-table-up'; +import TableUp, { TableAlign, TableMenuContextmenu, TableResizeBox, TableResizeScale, TableSelection, TableVirtualScrollbar } from 'quill-table-up'; import 'quill/dist/quill.snow.css'; import 'quill-table-up/index.css'; // If using the default customSelect option. You need to import this css @@ -47,6 +47,7 @@ const quill = new Quill('#editor', { scrollbar: TableVirtualScrollbar, align: TableAlign, resize: TableResizeBox, + resizeScale: TableResizeScale, customSelect: defaultCustomSelect, selection: TableSelection, selectionOptions: { @@ -61,21 +62,23 @@ const quill = new Quill('#editor', { ### TableUp Options -| attribute | description | type | default | -| ---------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------- | -| full | if set `true`. width max will be 100% | `boolean` | `false` | -| texts | the text used to create the table | `TableTextOptions` | `defaultTexts` | -| customSelect | display a custom select to custom row and column number add a table. module provides default selector `defaultCustomSelect` | `(tableModule: TableUp, picker: Picker) => Promise \| HTMLElement` | - | -| customBtn | display a custom button to custom row and column number add a table. it only when use `defaultCustomSelect` will effect | `boolean` | `false` | -| selection | table selection handler. module provides `TableSelection` | `Constructor` | - | -| selectionOptions | table selection options | `TableSelectionOptions` | - | -| icon | picker svg icon string. it will set with `innerHTML` | `string` | `origin table icon` | -| resize | table cell resize handler. module provides `TableResizeLine` and `TableResizeBox` | `Constructor` | - | -| scrollbar | table virtual scrollbar handler. module provides `TableVirtualScrollbar` | `Constructor` | - | -| align | table alignment handler. module provides `TableAlign` | `Constructor` | - | -| resizeOptions | table cell resize handler options | `any` | - | -| alignOptions | table alignment handler options | `any` | - | -| scrollbarOptions | table virtual scrollbar handler options | `any` | - | +| attribute | description | type | default | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------- | +| full | if set `true`. width max will be 100% | `boolean` | `false` | +| texts | the text used to create the table | `TableTextOptions` | `defaultTexts` | +| customSelect | display a custom select to custom row and column number add a table. module provides default selector `defaultCustomSelect` | `(tableModule: TableUp, picker: Picker) => Promise \| HTMLElement` | - | +| customBtn | display a custom button to custom row and column number add a table. it only when use `defaultCustomSelect` will effect | `boolean` | `false` | +| selection | table selection handler. module provides `TableSelection` | `Constructor` | - | +| selectionOptions | table selection options | `TableSelectionOptions` | - | +| icon | picker svg icon string. it will set with `innerHTML` | `string` | `origin table icon` | +| resize | table cell resize handler. module provides `TableResizeLine` and `TableResizeBox` | `Constructor` | - | +| resizeScale | equal scale table cell handler. module provides `TableResizeScale` | `Constructor` | - | +| scrollbar | table virtual scrollbar handler. module provides `TableVirtualScrollbar` | `Constructor` | - | +| align | table alignment handler. module provides `TableAlign` | `Constructor` | - | +| resizeOptions | table cell resize handler options | `any` | - | +| resizeScaleOptions | equal scale table cell handler options | `TableResizeScaleOptions` | - | +| alignOptions | table alignment handler options | `any` | - | +| scrollbarOptions | table virtual scrollbar handler options | `any` | - | > I'm not suggest to use `TableVirtualScrollbar` and `TableResizeLine` at same time, because it will make the virtual scrollbar display blink. Just like the first editor in [demo](https://zzxming.github.io/quill-table-up/) @@ -99,6 +102,12 @@ const defaultTexts = { +### TableResizeScale Options + +| attribute | description | type | default | +| --------- | ------------------------ | -------- | ------- | +| blockSize | resize handle block size | `number` | `12` | + ### TableSelection Options | attribute | description | type | default | diff --git a/docs/index.js b/docs/index.js index a35f629..50c49fe 100644 --- a/docs/index.js +++ b/docs/index.js @@ -1,6 +1,17 @@ /* eslint-disable no-undef */ const Quill = window.Quill; -const { default: TableUp, TableAlign, TableVirtualScrollbar, TableResizeLine, TableResizeBox, TableMenuContextmenu, TableMenuSelect, defaultCustomSelect, TableSelection } = window.TableUp; +const { + default: TableUp, + TableAlign, + TableVirtualScrollbar, + TableResizeLine, + TableResizeBox, + TableMenuContextmenu, + TableMenuSelect, + TableResizeScale, + defaultCustomSelect, + TableSelection, +} = window.TableUp; Quill.register({ [`modules/${TableUp.moduleName}`]: TableUp, @@ -33,6 +44,7 @@ const quill1 = new Quill('#editor1', { scrollbar: TableVirtualScrollbar, align: TableAlign, resize: TableResizeLine, + resizeScale: TableResizeScale, customSelect: defaultCustomSelect, customBtn: true, selection: TableSelection, @@ -109,6 +121,7 @@ const quill2 = new Quill('#editor2', { scrollbar: TableVirtualScrollbar, align: TableAlign, resize: TableResizeBox, + resizeScale: TableResizeScale, customSelect: defaultCustomSelect, selection: TableSelection, selectionOptions: { diff --git a/src/index.ts b/src/index.ts index c126151..9a5cd51 100644 --- a/src/index.ts +++ b/src/index.ts @@ -197,6 +197,8 @@ export class TableUp { tableResize?: InternalModule; tableScrollbar?: InternalModule; tableAlign?: InternalModule; + tableResizeScale?: InternalModule; + get statics(): any { return this.constructor; } @@ -346,6 +348,7 @@ export class TableUp { alignOptions: {}, scrollbarOptions: {}, resizeOptions: {}, + resizeScaleOptions: {}, } as TableUpOptions, options); }; @@ -529,6 +532,9 @@ export class TableUp { if (this.options.resize) { this.tableResize = new this.options.resize(this, table, quill, this.options.resizeOptions); } + if (this.options.resizeScale) { + this.tableResizeScale = new this.options.resizeScale(this, table, quill, this.options.resizeScaleOptions); + } } } @@ -549,6 +555,9 @@ export class TableUp { this.tableResize.destroy(); this.tableResize = undefined; } + if (this.tableResizeScale) { + this.tableResizeScale.destroy(); + } this.table = undefined; } diff --git a/src/modules/table-align.ts b/src/modules/table-align.ts index d5d0ae8..9c6147c 100644 --- a/src/modules/table-align.ts +++ b/src/modules/table-align.ts @@ -89,7 +89,7 @@ export class TableAlign { this.show(); computePosition(this.tableWrapperBlot.domNode, this.alignBox, { placement: 'top', - middleware: [flip(), shift({ limiter: limitShift() }), offset(8)], + middleware: [flip(), shift({ limiter: limitShift() }), offset(16)], }).then(({ x, y }) => { Object.assign(this.alignBox!.style, { left: `${x}px`, diff --git a/src/modules/table-resize/index.ts b/src/modules/table-resize/index.ts index c272bc7..011a395 100644 --- a/src/modules/table-resize/index.ts +++ b/src/modules/table-resize/index.ts @@ -1,4 +1,5 @@ export * from './table-resize-box'; export * from './table-resize-common'; export * from './table-resize-line'; +export * from './table-resize-scale'; export * from './utils'; diff --git a/src/modules/table-resize/table-resize-scale.ts b/src/modules/table-resize/table-resize-scale.ts new file mode 100644 index 0000000..99c5613 --- /dev/null +++ b/src/modules/table-resize/table-resize-scale.ts @@ -0,0 +1,143 @@ +import type TableUp from '../..'; +import type { TableColFormat, TableMainFormat, TableRowFormat, TableWrapperFormat } from '../../formats'; +import type { TableResizeScaleOptions } from '../../utils'; +import Quill from 'quill'; +import { addScrollEvent, clearScrollEvent, createBEM, tableUpSize } from '../../utils'; + +export class TableResizeScale { + scrollHandler: [HTMLElement, (e: Event) => void][] = []; + tableMainBlot: TableMainFormat | null = null; + tableWrapperBlot: TableWrapperFormat | null = null; + bem = createBEM('scale'); + startX: number = 0; + startY: number = 0; + options: TableResizeScaleOptions; + root?: HTMLElement; + block?: HTMLElement; + resizeobserver: ResizeObserver = new ResizeObserver(() => this.update()); + constructor(public tableModule: TableUp, table: HTMLElement, public quill: Quill, options: Partial) { + this.options = this.resolveOptions(options); + this.tableMainBlot = Quill.find(table) as TableMainFormat; + + if (this.tableMainBlot && !this.tableMainBlot.full) { + this.tableWrapperBlot = this.tableMainBlot.parent as TableWrapperFormat; + this.buildResizer(); + this.show(); + } + } + + resolveOptions(options: Partial) { + return Object.assign({ + blockSize: 12, + }, options); + } + + buildResizer() { + if (!this.tableMainBlot || !this.tableWrapperBlot) return; + this.root = this.tableModule.addContainer(this.bem.b()); + this.root.classList.add(this.bem.is('hidden')); + this.block = document.createElement('div'); + this.block.classList.add(this.bem.be('block')); + Object.assign(this.block.style, { + width: `${this.options.blockSize}px`, + height: `${this.options.blockSize}px`, + }); + this.root.appendChild(this.block); + + let originColWidth: { blot: TableColFormat; width: number }[] = []; + let originRowHeight: { blot: TableRowFormat; height: number }[] = []; + const handleMouseMove = (e: MouseEvent) => { + // divide equally by col count/row count + const diffX = e.clientX - this.startX; + const diffY = e.clientY - this.startY; + const itemWidth = Math.floor(diffX / originColWidth.length); + const itemHeight = Math.floor(diffY / originRowHeight.length); + + for (const { blot, width } of originColWidth) { + blot.width = Math.max(width + itemWidth, tableUpSize.colMinWidthPx); + } + for (const { blot, height } of originRowHeight) { + blot.setHeight(`${Math.max(height + itemHeight, tableUpSize.rowMinHeightPx)}px`); + } + }; + const handleMouseUp = () => { + originColWidth = []; + originRowHeight = []; + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + this.block.addEventListener('mousedown', (e) => { + if (!this.tableMainBlot || this.isTableOutofEditor()) return; + this.startX = e.clientX; + this.startY = e.clientY; + // save the origin width and height to calculate result width and height + originColWidth = this.tableMainBlot.getCols().map(col => ({ blot: col, width: col.width })); + originRowHeight = this.tableMainBlot.getRows().map(row => ({ blot: row, height: row.domNode.getBoundingClientRect().height })); + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + }); + this.block.addEventListener('dragstart', e => e.preventDefault()); + + this.resizeobserver.observe(this.tableMainBlot.domNode); + addScrollEvent.call(this, this.quill.root, () => this.update()); + addScrollEvent.call(this, this.tableWrapperBlot.domNode, () => this.update()); + } + + isTableOutofEditor(): boolean { + if (!this.tableMainBlot || !this.tableWrapperBlot || this.tableMainBlot.full) return false; + // if tableMain width larger than tableWrapper. reset tableMain width equal editor width + const tableRect = this.tableMainBlot.domNode.getBoundingClientRect(); + const tableWrapperRect = this.tableWrapperBlot.domNode.getBoundingClientRect(); + // equal scale + if (tableRect.width > tableWrapperRect.width) { + for (const col of this.tableMainBlot.getCols()) { + col.width = Math.floor((col.width / tableRect.width) * tableWrapperRect.width); + } + this.tableMainBlot.colWidthFillTable(); + return true; + } + return false; + } + + update() { + if (!this.block || !this.root || !this.tableMainBlot || !this.tableWrapperBlot) return false; + const tableRect = this.tableMainBlot.domNode.getBoundingClientRect(); + const tableWrapperRect = this.tableWrapperBlot.domNode.getBoundingClientRect(); + const editorRect = this.quill.root.getBoundingClientRect(); + const { scrollTop, scrollLeft } = this.tableWrapperBlot.domNode; + const blockSize = this.options.blockSize * 2; + const rootWidth = Math.min(tableRect.width, tableWrapperRect.width) + blockSize; + const rootHeight = Math.min(tableRect.height, tableWrapperRect.height) + blockSize; + Object.assign(this.root.style, { + width: `${rootWidth}px`, + height: `${rootHeight}px`, + left: `${tableWrapperRect.x - editorRect.x - this.options.blockSize}px`, + top: `${tableWrapperRect.y - editorRect.y - this.options.blockSize}px`, + }); + Object.assign(this.block.style, { + left: `${tableRect.width + blockSize - scrollLeft}px`, + top: `${rootHeight - scrollTop}px`, + }); + } + + show() { + if (this.root) { + this.root.classList.remove(this.bem.is('hidden')); + this.update(); + } + } + + hide() { + if (this.root) { + this.root.classList.add(this.bem.is('hidden')); + } + } + + destroy() { + this.hide(); + if (this.root) { + this.root.remove(); + } + clearScrollEvent.call(this); + } +} diff --git a/src/modules/table-selection.ts b/src/modules/table-selection.ts index e6081fd..397bea3 100644 --- a/src/modules/table-selection.ts +++ b/src/modules/table-selection.ts @@ -3,7 +3,7 @@ import type { TableCellInnerFormat, TableMainFormat } from '../formats'; import type { InternalModule, RelactiveRect, TableSelectionOptions } from '../utils'; import Quill from 'quill'; import { TableCellFormat } from '../formats'; -import { addScrollEvent, clearScrollEvent } from '../utils'; +import { addScrollEvent, clearScrollEvent, getRelativeRect, isRectanglesIntersect } from '../utils'; const ERROR_LIMIT = 2; export class TableSelection { @@ -259,24 +259,3 @@ export class TableSelection { return null; } } - -function isRectanglesIntersect(a: Omit, b: Omit, tolerance = 4) { - const { x: minAx, y: minAy, x1: maxAx, y1: maxAy } = a; - const { x: minBx, y: minBy, x1: maxBx, y1: maxBy } = b; - const notOverlapX = maxAx <= minBx + tolerance || minAx + tolerance >= maxBx; - const notOverlapY = maxAy <= minBy + tolerance || minAy + tolerance >= maxBy; - return !(notOverlapX || notOverlapY); -} - -function getRelativeRect(targetRect: Omit, container: HTMLElement) { - const containerRect = container.getBoundingClientRect(); - - return { - x: targetRect.x - containerRect.x - container.scrollLeft, - y: targetRect.y - containerRect.y - container.scrollTop, - x1: targetRect.x - containerRect.x - container.scrollLeft + targetRect.width, - y1: targetRect.y - containerRect.y - container.scrollTop + targetRect.height, - width: targetRect.width, - height: targetRect.height, - }; -} diff --git a/src/style/index.less b/src/style/index.less index cef9936..18e61e9 100644 --- a/src/style/index.less +++ b/src/style/index.less @@ -432,6 +432,7 @@ @import './tooltip.less'; @import './dialog.less'; + @import './color-picker.less'; .@{namespace}-tooltip { @@ -442,3 +443,5 @@ padding: 8px 0px; } } + +@import './table-resize-scale.less'; diff --git a/src/style/table-resize-scale.less b/src/style/table-resize-scale.less new file mode 100644 index 0000000..47cb02e --- /dev/null +++ b/src/style/table-resize-scale.less @@ -0,0 +1,24 @@ +@import './variables.less'; + +.@{namespace}-scale { + position: absolute; + top: 0; + left: 0; + overflow: hidden; + pointer-events: none; + &__block { + position: absolute; + top: 0; + left: 0; + transform: translate(-100%, -100%); + width: 12px; + height: 12px; + background-color: #f1f5f9; + border: 1px solid #808080; + cursor: nw-resize; + pointer-events: all; + } + &.is-hidden { + display: none; + } +} diff --git a/src/utils/bem.ts b/src/utils/bem.ts index ff495bc..98b0026 100644 --- a/src/utils/bem.ts +++ b/src/utils/bem.ts @@ -1,5 +1,4 @@ import { cssNamespace } from './constants'; -import { isBoolean } from './is'; export const createBEM = (b: string, n: string = cssNamespace) => { const prefix = n ? `${n}-` : ''; @@ -19,9 +18,6 @@ export const createBEM = (b: string, n: string = cssNamespace) => { /** --n-v */ cv: (v?: string) => v ? `--${prefix}${v}` : '', /** is-n */ - is: (n: string, status: (boolean | undefined)[] | boolean) => { - const state = isBoolean(status) ? status : status.every(Boolean); - return state ? `is-${n}` : ''; - }, + is: (n: string) => `is-${n}`, }; }; diff --git a/src/utils/index.ts b/src/utils/index.ts index f1b2c30..477aaae 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -2,5 +2,6 @@ export * from './bem'; export * from './components'; export * from './constants'; export * from './is'; +export * from './position'; export * from './types'; export * from './utils'; diff --git a/src/utils/position.ts b/src/utils/position.ts new file mode 100644 index 0000000..edd8921 --- /dev/null +++ b/src/utils/position.ts @@ -0,0 +1,21 @@ +import type { RelactiveRect } from './types'; + +export function isRectanglesIntersect(a: Omit, b: Omit, tolerance = 4) { + const { x: minAx, y: minAy, x1: maxAx, y1: maxAy } = a; + const { x: minBx, y: minBy, x1: maxBx, y1: maxBy } = b; + const notOverlapX = maxAx <= minBx + tolerance || minAx + tolerance >= maxBx; + const notOverlapY = maxAy <= minBy + tolerance || minAy + tolerance >= maxBy; + return !(notOverlapX || notOverlapY); +} + +export function getRelativeRect(targetRect: Omit, container: HTMLElement): RelactiveRect { + const containerRect = container.getBoundingClientRect(); + return { + x: targetRect.x - containerRect.x - container.scrollLeft, + y: targetRect.y - containerRect.y - container.scrollTop, + x1: targetRect.x - containerRect.x - container.scrollLeft + targetRect.width, + y1: targetRect.y - containerRect.y - container.scrollTop + targetRect.height, + width: targetRect.width, + height: targetRect.height, + }; +} diff --git a/src/utils/types.ts b/src/utils/types.ts index 70b22e8..09138a6 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -32,6 +32,9 @@ export interface TableSelectionOptions { tableMenu?: Constructor]>; tableMenuOptions: TableMenuOptions; } +export interface TableResizeScaleOptions { + blockSize: number; +} export interface TableCreatorTextOptions { customBtnText: string; confirmText: string; @@ -61,6 +64,8 @@ export interface TableUpOptions { scrollbarOptions: any; align?: Constructor; alignOptions: any; + resizeScale?: Constructor]>; + resizeScaleOptions: Partial; } export interface TableColValue { tableId: string;