From cff224fb24a244a55b8386ed1fb35cdd8e5f7a73 Mon Sep 17 00:00:00 2001 From: zhangw Date: Tue, 7 Jan 2025 10:31:50 +0800 Subject: [PATCH] feat(facade): add comment & rich-text & data-validation & edit API (#4423) Co-authored-by: Wenzhao Hu Co-authored-by: jocs --- examples/src/sheets/main.ts | 1 + packages/core/src/common/error.ts | 7 + .../src/docs/data-model/rich-text-builder.ts | 1990 +++++++++++++++++ .../data-model/text-x/build-utils/index.ts | 3 +- .../text-x/build-utils/text-x-utils.ts | 33 +- packages/core/src/facade/f-doc.ts | 28 + packages/core/src/facade/f-enum.ts | 73 + packages/core/src/facade/f-event.ts | 208 +- packages/core/src/facade/f-univer.ts | 240 +- packages/core/src/facade/f-util.ts | 50 +- packages/core/src/index.ts | 4 +- packages/core/src/shared/rectangle.ts | 340 ++- packages/core/src/sheets/column-manager.ts | 14 +- packages/core/src/sheets/row-manager.ts | 14 +- packages/core/src/sheets/typedef.ts | 10 + packages/core/src/sheets/workbook.ts | 18 +- packages/core/src/sheets/worksheet.ts | 18 +- .../core/src/types/interfaces/i-style-data.ts | 20 +- packages/data-validation/src/index.ts | 3 +- .../src/models/data-validation-model.ts | 2 +- .../docs/src/utils/custom-range-factory.ts | 2 +- packages/facade/package.json | 1 + packages/facade/src/apis/everything.ts | 1 + .../src/services/find-replace.service.ts | 9 +- .../src/facade/f-event.ts | 324 +++ .../src/facade/f-univer.ts | 204 +- .../src/facade/f-worksheet.ts | 32 + .../src/facade/index.ts | 2 + .../src/facade/f-text-finder.ts | 10 - .../src/facade/f-univer.ts | 3 - .../commands/remove-hyper-link.command.ts | 2 +- .../sheets-hyper-link/src/facade/f-range.ts | 50 +- .../src/facade/f-workbook.ts | 26 +- .../src/facade/f-worksheet.ts | 39 + .../sheets-hyper-link/src/facade/index.ts | 2 + .../src/facade/f-event.ts | 324 +++ .../src/facade/f-range.ts | 61 +- .../src/facade/f-thread-comment.ts | 379 +++- .../src/facade/f-univer.ts | 260 +++ .../src/facade/f-workbook.ts | 54 +- .../src/facade/f-worksheet.ts | 47 +- .../sheets-thread-comment/src/facade/index.ts | 7 +- .../src/controllers/clipboard/utils.ts | 63 + packages/sheets-ui/src/facade/f-event.ts | 202 +- packages/sheets-ui/src/facade/f-univer.ts | 145 +- packages/sheets-ui/src/facade/index.ts | 4 +- .../src/views/cell-alert/CellAlertPopup.tsx | 5 + packages/sheets-zen-editor/package.json | 9 +- .../sheets-zen-editor/src/facade/f-univer.ts | 172 ++ .../src/facade/f-workbook.ts | 65 + .../sheets-zen-editor/src/facade/index.ts | 21 + packages/sheets/src/basics/cell-style.ts | 16 + packages/sheets/src/facade/f-event.ts | 104 +- packages/sheets/src/facade/f-range.ts | 300 ++- packages/sheets/src/facade/f-univer.ts | 105 +- packages/sheets/src/facade/f-workbook.ts | 34 +- packages/sheets/src/facade/f-worksheet.ts | 81 + packages/thread-comment/src/common/utils.ts | 4 +- pnpm-lock.yaml | 6 + 59 files changed, 5994 insertions(+), 257 deletions(-) create mode 100644 packages/core/src/docs/data-model/rich-text-builder.ts create mode 100644 packages/core/src/facade/f-doc.ts create mode 100644 packages/sheets-data-validation/src/facade/f-event.ts create mode 100644 packages/sheets-hyper-link/src/facade/f-worksheet.ts create mode 100644 packages/sheets-thread-comment/src/facade/f-event.ts create mode 100644 packages/sheets-thread-comment/src/facade/f-univer.ts create mode 100644 packages/sheets-zen-editor/src/facade/f-univer.ts create mode 100644 packages/sheets-zen-editor/src/facade/f-workbook.ts create mode 100644 packages/sheets-zen-editor/src/facade/index.ts diff --git a/examples/src/sheets/main.ts b/examples/src/sheets/main.ts index 736a6659a26..24449fbe6c2 100644 --- a/examples/src/sheets/main.ts +++ b/examples/src/sheets/main.ts @@ -58,6 +58,7 @@ import '@univerjs/sheets-thread-comment/facade'; import '@univerjs/sheets-conditional-formatting/facade'; import '@univerjs/sheets-find-replace/facade'; import '@univerjs/sheets-drawing-ui/facade'; +import '@univerjs/sheets-zen-editor/facade'; import '../global.css'; /* eslint-disable-next-line node/prefer-global/process */ diff --git a/packages/core/src/common/error.ts b/packages/core/src/common/error.ts index 30b45bad8d6..c735651d1fb 100644 --- a/packages/core/src/common/error.ts +++ b/packages/core/src/common/error.ts @@ -20,3 +20,10 @@ export class CustomCommandExecutionError extends Error { this.name = 'CustomCommandExecutionError'; } } + +export class CanceledError extends Error { + constructor() { + super('Canceled by facade'); + this.name = 'CanceledError'; + } +} diff --git a/packages/core/src/docs/data-model/rich-text-builder.ts b/packages/core/src/docs/data-model/rich-text-builder.ts new file mode 100644 index 00000000000..b66975008a8 --- /dev/null +++ b/packages/core/src/docs/data-model/rich-text-builder.ts @@ -0,0 +1,1990 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Nullable } from '../../shared'; +import type { BaselineOffset, HorizontalAlign, TextDecoration, TextDirection } from '../../types/enum'; +import { generateRandomId, Tools } from '../../shared'; +import { BooleanNumber } from '../../types/enum'; +import { CustomRangeType, type IBorderData, type IColorStyle, type IDocumentBody, type IDocumentData, type INumberUnit, type IParagraphBorder, type IParagraphStyle, type IShading, type ITabStop, type ITextDecoration, type ITextStyle, type NamedStyleType, type SpacingRule } from '../../types/interfaces'; +import { DocumentDataModel } from './document-data-model'; +import { BuildTextUtils } from './text-x/build-utils'; +import { TextX } from './text-x/text-x'; +import { getBodySlice } from './text-x/utils'; + +/** + * Represents a read-only font style value object. + * This class provides access to font style properties without modification capabilities. + */ +export class TextStyleValue { + protected _style: ITextStyle; + + /** + * Creates an instance of TextStyleValue. + * @param {ITextStyle} style style object + * @returns {TextStyleValue} font style instance + * @example + * ```ts + * const style = TextStyleValue.create({ ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE }); + * console.log(style); + * ``` + */ + static create(style: ITextStyle = {}) { + return new TextStyleValue(style); + } + + /** + * Creates a new TextStyleValue instance + * @param {ITextStyle} style The initial style object + * @example + * ```ts + * const style = TextStyleValue.create({ ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE }); + * console.log(style); + * ``` + */ + constructor(style: ITextStyle = {}) { + this._style = style; + } + + /** + * Gets the font family + * @returns {Nullable} The font family name or undefined + * @example + * ```ts + * const style = TextStyleValue.create({ ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE }); + * console.log(style.fontFamily); + * ``` + */ + get fontFamily(): Nullable { + return this._style.ff; + } + + /** + * Gets the font size in points + * @returns {number | undefined} The font size or undefined + * @example + * ```ts + * const style = TextStyleValue.create({ ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE }); + * console.log(style.fontSize); + * ``` + */ + get fontSize(): number | undefined { + return this._style.fs; + } + + /** + * Gets whether the text is italic + * @returns {boolean} True if italic, false otherwise + * @example + * ```ts + * const style = TextStyleValue.create({ ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE }); + * console.log(style.italic); + * ``` + */ + get italic(): boolean { + return this._style.it === BooleanNumber.TRUE; + } + + /** + * Gets whether the text is bold + * @returns {boolean} True if bold, false otherwise + * @example + * ```ts + * const style = TextStyleValue.create({ ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE }); + * console.log(style.bold); + * ``` + */ + get bold(): boolean { + return this._style.bl === BooleanNumber.TRUE; + } + + /** + * Gets the underline decoration + * @returns {TextDecorationBuilder | undefined} The underline decoration or undefined + * @example + * ```ts + * const style = TextStyleValue.create({ ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE }); + * console.log(style.underline); + * ``` + */ + get underline(): TextDecorationBuilder | undefined { + return this._style.ul && TextDecorationBuilder.create(this._style.ul); + } + + /** + * Gets the bottom border line decoration + * @returns {TextDecorationBuilder | undefined} The bottom border line decoration or undefined + * @example + * ```ts + * const style = TextStyleValue.create({ ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE }); + * console.log(style.bottomBorderLine); + * ``` + */ + get bottomBorderLine(): TextDecorationBuilder | undefined { + return this._style.bbl && TextDecorationBuilder.create(this._style.bbl); + } + + /** + * Gets the strikethrough decoration + * @returns {TextDecorationBuilder | undefined} The strikethrough decoration or undefined + * @example + * ```ts + * const style = TextStyleValue.create({ ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE }); + * console.log(style.strikethrough); + * ``` + */ + get strikethrough(): TextDecorationBuilder | undefined { + return this._style.st && TextDecorationBuilder.create(this._style.st); + } + + /** + * Gets the overline decoration + * @returns {TextDecorationBuilder | undefined} The overline decoration or undefined + * @example + * ```ts + * const style = TextStyleValue.create({ ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE }); + * console.log(style.overline); + * ``` + */ + get overline(): TextDecorationBuilder | undefined { + return this._style.ol && TextDecorationBuilder.create(this._style.ol); + } + + /** + * Gets the background color + * @returns {Nullable} The background color or null/undefined + * @example + * ```ts + * const style = TextStyleValue.create({ ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE }); + * console.log(style.background); + * ``` + */ + get background(): Nullable { + return this._style.bg; + } + + /** + * Gets the border settings + * @returns {Nullable} The border settings or null/undefined + * @example + * ```ts + * const style = TextStyleValue.create({ ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE }); + * console.log(style.border); + * ``` + */ + get border(): Nullable { + return this._style.bd; + } + + /** + * Gets the text color + * @returns {Nullable} The text color or null/undefined + * @example + * ```ts + * const style = TextStyleValue.create({ ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE }); + * console.log(style.color); + * ``` + */ + get color(): Nullable { + return this._style.cl; + } + + /** + * Gets the vertical alignment (subscript/superscript) + * @returns {Nullable} The vertical alignment or null/undefined + * @example + * ```ts + * const style = TextStyleValue.create({ ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE }); + * console.log(style.verticalAlign); + * ``` + */ + get verticalAlign(): Nullable { + return this._style.va; + } + + /** + * Gets the number format pattern + * @returns {Nullable<{ pattern: string }>} The number format pattern or null/undefined + * @example + * ```ts + * const style = TextStyleValue.create({ ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE }); + * console.log(style.numberFormat); + * ``` + */ + get numberFormat(): Nullable<{ pattern: string }> { + return this._style.n; + } + + /** + * Creates a copy of this font style as a builder + * @returns {TextStyleBuilder} A new TextStyleBuilder instance with the same style + * @example + * ```ts + * const style = TextStyleValue.create({ ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE }); + * const copy = style.copy(); + * console.log(copy); + * ``` + */ + copy(): TextStyleBuilder { + return TextStyleBuilder.create(Tools.deepClone(this._style)); + } + + /** + * Gets the raw style object + * @returns {ITextStyle} The underlying style object + * @example + * ```ts + * const style = TextStyleValue.create({ ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE }); + * console.log(style.getValue()); + * ``` + */ + getValue(): ITextStyle { + return { ...this._style }; + } +} + +/** + * Builder class for creating and modifying font styles. + * Extends TextStyleValue to provide setter methods for all style properties. + */ +export class TextStyleBuilder extends TextStyleValue { + /** + * Creates a new TextStyleBuilder instance + * @param {ITextStyle} style Initial style object + * @returns {TextStyleBuilder} A new TextStyleBuilder instance + * @example + * ```ts + * const style = TextStyleBuilder.create({ ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE }); + * console.log(style); + * ``` + */ + static override create(style: ITextStyle = {}) { + return new TextStyleBuilder(style); + } + + /** + * Creates a new TextStyleBuilder instance + * @param {ITextStyle} style The initial style object + * @example + * ```ts + * const style = new TextStyleBuilder({ ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE }); + * console.log(style); + * ``` + */ + constructor(style: ITextStyle = {}) { + super(style); + } + + /** + * Sets the font family + * @param {string} family The font family name + * @returns {TextStyleBuilder} The builder instance for chaining + * @example + * ```ts + * const style = TextStyleBuilder.create({ ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE }); + * style.setFontFamily('Times New Roman'); + * console.log(style.fontFamily); + * ``` + */ + setFontFamily(family: string): TextStyleBuilder { + this._style.ff = family; + return this; + } + + /** + * Sets the font size in points + * @param {number} size The font size + * @returns {TextStyleBuilder} The builder instance for chaining + * @example + * ```ts + * const style = TextStyleBuilder.create({ ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE }); + * style.setFontSize(14); + * console.log(style.fontSize); + * ``` + */ + setFontSize(size: number): TextStyleBuilder { + this._style.fs = size; + return this; + } + + /** + * Sets the italic style + * @param {boolean} value True to make italic, false otherwise + * @returns {TextStyleBuilder} The builder instance for chaining + * @example + * ```ts + * const style = TextStyleBuilder.create({ ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE }); + * style.setItalic(true); + * console.log(style.italic); + * ``` + */ + setItalic(value: boolean): TextStyleBuilder { + this._style.it = value ? 1 : 0; + return this; + } + + /** + * Sets the bold style + * @param {boolean} value True to make bold, false otherwise + * @returns {TextStyleBuilder} The builder instance for chaining + * @example + * ```ts + * const style = TextStyleBuilder.create({ ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE }); + * style.setBold(true); + * console.log(style.bold); + * ``` + */ + setBold(value: boolean): TextStyleBuilder { + this._style.bl = value ? 1 : 0; + return this; + } + + /** + * Sets the underline decoration + * @param {TextDecorationBuilder} decoration The underline decoration settings + * @returns {TextStyleBuilder} The builder instance for chaining + * @example + * ```ts + * const style = TextStyleBuilder.create({ ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE }); + * style.setUnderline({ type: 'single', color: '#FF0000' }); + * console.log(style.underline); + * ``` + */ + setUnderline(decoration: TextDecorationBuilder): TextStyleBuilder { + this._style.ul = decoration.build(); + return this; + } + + /** + * Sets the bottom border line decoration + * @param {TextDecorationBuilder} decoration The bottom border line decoration settings + * @returns {TextStyleBuilder} The builder instance for chaining + * @example + * ```ts + * const style = TextStyleBuilder.create({ ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE }); + * style.setBottomBorderLine({ type: 'single', color: '#FF0000' }); + * console.log(style.bottomBorderLine); + * ``` + */ + setBottomBorderLine(decoration: TextDecorationBuilder): TextStyleBuilder { + this._style.bbl = decoration.build(); + return this; + } + + /** + * Sets the strikethrough decoration + * @param {TextDecorationBuilder} decoration The strikethrough decoration settings + * @returns {TextStyleBuilder} The builder instance for chaining + */ + setStrikethrough(decoration: TextDecorationBuilder): TextStyleBuilder { + this._style.st = decoration.build(); + return this; + } + + /** + * Sets the overline decoration + * @param {TextDecorationBuilder} decoration The overline decoration settings + * @returns {TextStyleBuilder} The builder instance for chaining + */ + setOverline(decoration: TextDecorationBuilder): TextStyleBuilder { + this._style.ol = decoration.build(); + return this; + } + + /** + * Sets the background color + * @param {IColorStyle | null} color The background color or null to remove + * @returns {TextStyleBuilder} The builder instance for chaining + */ + setBackground(color: IColorStyle | null): TextStyleBuilder { + this._style.bg = color; + return this; + } + + /** + * Sets the border settings + * @param {IBorderData | null} border The border settings or null to remove + * @returns {TextStyleBuilder} The builder instance for chaining + */ + setBorder(border: IBorderData | null): TextStyleBuilder { + this._style.bd = border; + return this; + } + + /** + * Sets the text color + * @param {IColorStyle | null} color The text color or null to remove + * @returns {TextStyleBuilder} The builder instance for chaining + */ + setColor(color: IColorStyle | null): TextStyleBuilder { + this._style.cl = color; + return this; + } + + /** + * Sets the vertical alignment (subscript/superscript) + * @param {BaselineOffset | null} offset The vertical alignment or null to remove + * @returns {TextStyleBuilder} The builder instance for chaining + */ + setVerticalAlign(offset: BaselineOffset | null): TextStyleBuilder { + this._style.va = offset; + return this; + } + + /** + * Creates a copy of this font style builder + * @returns {TextStyleBuilder} A new TextStyleBuilder instance with the same style + */ + override copy(): TextStyleBuilder { + return TextStyleBuilder.create(Tools.deepClone(this._style)); + } + + /** + * Builds and returns the final style object + * @returns {ITextStyle} The complete style object + */ + build(): ITextStyle { + return this.getValue(); + } +} + +/** + * Builder class for creating and modifying text decorations. + * Provides a fluent interface for setting text decoration properties. + */ +export class TextDecorationBuilder { + protected _decoration: ITextDecoration; + + /** + * Creates an instance of TextDecorationBuilder. + * @param {ITextDecoration} decoration Initial decoration object + * @returns {TextDecorationBuilder} text decoration builder instance + * @example + * ```ts + * const decoration = TextDecorationBuilder.create({ s: 1, t: TextDecoration.SINGLE }); + * console.log(decoration); + * ``` + */ + static create(decoration: ITextDecoration = { s: 1 }) { + return new TextDecorationBuilder(decoration); + } + + /** + * Creates a new TextDecorationBuilder instance + * @param {ITextDecoration} decoration The initial decoration object + * @example + * ```ts + * const decoration = new TextDecorationBuilder({ s: 1, t: TextDecoration.SINGLE }); + * ``` + */ + constructor(decoration: ITextDecoration = { s: 1 }) { + this._decoration = decoration; + } + + /** + * Gets whether the decoration is shown + * @returns {boolean} True if the decoration is shown + */ + get show(): boolean { + return this._decoration.s === BooleanNumber.TRUE; + } + + /** + * Gets whether the decoration color follows the font color + * @returns {boolean} True if the decoration color follows the font color + */ + get followFontColor(): boolean { + return this._decoration.c === BooleanNumber.TRUE; + } + + /** + * Gets the decoration color + * @returns {Nullable} The decoration color + */ + get color(): Nullable { + return this._decoration.cl; + } + + /** + * Gets the decoration line type + * @returns {Nullable} The decoration line type + */ + get type(): Nullable { + return this._decoration.t; + } + + /** + * Sets whether the decoration is shown + * @param {boolean} value True to show the decoration + * @returns {TextDecorationBuilder} The builder instance for chaining + * @example + * ```ts + * decoration.setShow(true); + * ``` + */ + setShow(value: boolean): TextDecorationBuilder { + this._decoration.s = value ? 1 : 0; + return this; + } + + /** + * Sets whether the decoration color follows the font color + * @param {boolean} value True to follow font color + * @returns {TextDecorationBuilder} The builder instance for chaining + * @example + * ```ts + * decoration.setFollowFontColor(false); + * ``` + */ + setFollowFontColor(value: boolean): TextDecorationBuilder { + this._decoration.c = value ? 1 : 0; + return this; + } + + /** + * Sets the decoration color + * @param {IColorStyle} color The color style + * @returns {TextDecorationBuilder} The builder instance for chaining + * @example + * ```ts + * decoration.setColor({ rgb: '#FF0000' }); + * ``` + */ + setColor(color: IColorStyle): TextDecorationBuilder { + this._decoration.cl = color; + return this; + } + + /** + * Sets the decoration line type + * @param {TextDecoration} type The line type + * @returns {TextDecorationBuilder} The builder instance for chaining + * @example + * ```ts + * decoration.setLineType(TextDecoration.SINGLE); + * ``` + */ + setLineType(type: TextDecoration): TextDecorationBuilder { + this._decoration.t = type; + return this; + } + + /** + * Creates a copy of this text decoration builder + * @returns {TextDecorationBuilder} A new TextDecorationBuilder instance with the same decoration + * @example + * ```ts + * const copy = decoration.copy(); + * ``` + */ + copy(): TextDecorationBuilder { + return TextDecorationBuilder.create(Tools.deepClone(this._decoration)); + } + + /** + * Builds and returns the final decoration object + * @returns {ITextDecoration} The complete text decoration object + * @example + * ```ts + * const style = decoration.build(); + * ``` + */ + build(): ITextDecoration { + return { ...this._decoration }; + } +} + +export class ParagraphStyleValue { + protected _style: IParagraphStyle; + + /** + * Creates a new ParagraphStyleValue instance + * @param {IParagraphStyle} style The initial style object + * @returns A new ParagraphStyleValue instance + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * ``` + */ + static create(style: IParagraphStyle = {}) { + return new ParagraphStyleValue(style); + } + + constructor(style: IParagraphStyle = {}) { + this._style = style; + } + + /** + * Gets the first line indent + * @returns {Nullable} The first line indent + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * console.log(style.indentFirstLine); + * ``` + */ + get indentFirstLine(): Nullable { + return this._style.indentFirstLine; + } + + /** + * Gets the hanging indent + * @returns {Nullable} The hanging indent + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * console.log(style.hanging); + * ``` + */ + get hanging(): Nullable { + return this._style.hanging; + } + + /** + * Gets the indent start + * @returns {Nullable} The indent start + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * console.log(style.indentStart); + * ``` + */ + get indentStart(): Nullable { + return this._style.indentStart; + } + + /** + * Gets the indent end + * @returns {Nullable} The indent end + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * console.log(style.indentEnd); + * ``` + */ + get tabStops(): Nullable { + return this._style.tabStops; + } + + /** + * Gets the indent end + * @returns {Nullable} The indent end + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * console.log(style.indentEnd); + * ``` + */ + get indentEnd(): Nullable { + return this._style.indentEnd; + } + + /** + * Gets the text style + * @returns {Nullable} The text style + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * console.log(style.textStyle); + * ``` + */ + get textStyle(): Nullable { + return this._style.textStyle; + } + + /** + * Gets the heading id + * @returns {Nullable} The heading id + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * console.log(style.headingId); + * ``` + */ + get headingId(): Nullable { + return this._style.headingId; + } + + /** + * Gets the named style type + * @returns {Nullable} The named style type + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * console.log(style.namedStyleType); + * ``` + */ + get namedStyleType(): Nullable { + return this._style.namedStyleType; + } + + /** + * Gets the horizontal align + * @returns {Nullable} The horizontal align + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * console.log(style.horizontalAlign); + * ``` + */ + get horizontalAlign(): Nullable { + return this._style.horizontalAlign; + } + + /** + * Gets the line spacing + * @returns {Nullable} The line spacing + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * console.log(style.lineSpacing); + * ``` + */ + get lineSpacing(): Nullable { + return this._style.lineSpacing; + } + + /** + * Gets the text direction + * @returns {Nullable} The text direction + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * console.log(style.direction); + * ``` + */ + get direction(): Nullable { + return this._style.direction; + } + + /** + * Gets the spacing rule + * @returns {Nullable} The spacing rule + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * console.log(style.spacingRule); + * ``` + */ + get spacingRule(): Nullable { + return this._style.spacingRule; + } + + /** + * Gets the snap to grid + * @returns {Nullable} The snap to grid + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * console.log(style.snapToGrid); + * ``` + */ + get snapToGrid(): Nullable { + return this._style.snapToGrid; + } + + /** + * Gets the space above + * @returns {Nullable} The space above + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * console.log(style.spaceAbove); + * ``` + */ + get spaceAbove(): Nullable { + return this._style.spaceAbove; + } + + /** + * Gets the space below + * @returns {Nullable} The space below + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * console.log(style.spaceBelow); + * ``` + */ + get spaceBelow(): Nullable { + return this._style.spaceBelow; + } + + /** + * Gets the border between + * @returns {Nullable} The border between + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * console.log(style.borderBetween); + * ``` + */ + get borderBetween(): Nullable { + return this._style.borderBetween; + } + + /** + * Gets the border top + * @returns {Nullable} The border top + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * console.log(style.borderTop); + * ``` + */ + get borderTop(): Nullable { + return this._style.borderTop; + } + + /** + * Gets the border bottom + * @returns {Nullable} The border bottom + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * console.log(style.borderBottom); + * ``` + */ + get borderBottom(): Nullable { + return this._style.borderBottom; + } + + /** + * Gets the border left + * @returns {Nullable} The border left + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * console.log(style.borderLeft); + * ``` + */ + get borderLeft(): Nullable { + return this._style.borderLeft; + } + + /** + * Gets the border right + * @returns {Nullable} The border right + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * console.log(style.borderRight); + * ``` + */ + get borderRight(): Nullable { + return this._style.borderRight; + } + + /** + * Gets the keep lines + * @returns {boolean} The keep lines + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * console.log(style.keepLines); + * ``` + */ + get keepLines(): boolean { + return this._style.keepLines === BooleanNumber.TRUE; + } + + /** + * Gets the keep next + * @returns {boolean} The keep next + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * console.log(style.keepNext); + * ``` + */ + get keepNext(): boolean { + return this._style.keepNext === BooleanNumber.TRUE; + } + + /** + * Gets the word wrap + * @returns {boolean} The word wrap + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * console.log(style.wordWrap); + * ``` + */ + get wordWrap(): boolean { + return this._style.wordWrap === BooleanNumber.TRUE; + } + + /** + * Gets the widow control + * @returns {boolean} The widow control + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * console.log(style.widowControl); + * ``` + */ + get widowControl(): boolean { + return this._style.widowControl === BooleanNumber.TRUE; + } + + /** + * Gets the shading + * @returns {Nullable} The shading + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * console.log(style.shading); + * ``` + */ + get shading(): Nullable { + return this._style.shading; + } + + /** + * Gets the suppress hyphenation + * @returns {boolean} The suppress hyphenation + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * console.log(style.suppressHyphenation); + * ``` + */ + get suppressHyphenation(): boolean { + return this._style.suppressHyphenation === BooleanNumber.TRUE; + } + + /** + * Creates a copy of the paragraph style + * @returns {ParagraphStyleBuilder} The copy + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * const copy = style.copy(); + * ``` + */ + copy(): ParagraphStyleBuilder { + return ParagraphStyleBuilder.create(Tools.deepClone(this._style)); + } + + /** + * Gets the value + * @returns {IParagraphStyle} The value + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * console.log(style.getValue()); + * ``` + */ + getValue(): IParagraphStyle { + return this._style; + } +} + +/** + * Paragraph style builder + */ +export class ParagraphStyleBuilder extends ParagraphStyleValue { + /** + * Creates a new paragraph style builder + * @param style The paragraph style + * @returns A new paragraph style builder + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * const copy = style.copy(); + * ``` + */ + static override create(style: IParagraphStyle = {}) { + return new ParagraphStyleBuilder(style); + } + + constructor(style: IParagraphStyle = {}) { + super(style); + } + + /** + * Sets the indent first line + * @param value The indent first line + * @returns {ParagraphStyleBuilder} The paragraph style builder + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * const copy = style.copy(); + * copy.setIndentFirstLine(10); + * ``` + */ + setIndentFirstLine(value: INumberUnit): ParagraphStyleBuilder { + this._style.indentFirstLine = value; + return this; + } + + /** + * Sets the hanging + * @param value The hanging + * @returns {ParagraphStyleBuilder} The paragraph style builder + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * const copy = style.copy(); + * copy.setHanging(10); + * ``` + */ + setHanging(value: INumberUnit): ParagraphStyleBuilder { + this._style.hanging = value; + return this; + } + + /** + * Sets the indent start + * @param value The indent start + * @returns {ParagraphStyleBuilder} The paragraph style builder + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * const copy = style.copy(); + * copy.setIndentStart(10); + * ``` + */ + setIndentStart(value: INumberUnit): ParagraphStyleBuilder { + this._style.indentStart = value; + return this; + } + + /** + * Sets the tab stops + * @param value The tab stops + * @returns {ParagraphStyleBuilder} The paragraph style builder + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * const copy = style.copy(); + * copy.setTabStops([{ value: 10 }]); + * ``` + */ + setTabStops(value: ITabStop[]): ParagraphStyleBuilder { + this._style.tabStops = value; + return this; + } + + /** + * Sets the indent end + * @param value The indent end + * @returns {ParagraphStyleBuilder} The paragraph style builder + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * const copy = style.copy(); + * copy.setIndentEnd(10); + * ``` + */ + setIndentEnd(value: INumberUnit): ParagraphStyleBuilder { + this._style.indentEnd = value; + return this; + } + + /** + * Sets the text style + * @param value The text style + * @returns {ParagraphStyleBuilder} The paragraph style builder + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * const copy = style.copy(); + * copy.setTextStyle({ ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE }); + * ``` + */ + setTextStyle(value: ITextStyle): ParagraphStyleBuilder { + this._style.textStyle = value; + return this; + } + + /** + * Sets the heading id + * @param value The heading id + * @returns {ParagraphStyleBuilder} The paragraph style builder + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * const copy = style.copy(); + * copy.setHeadingId('test'); + * ``` + */ + setHeadingId(value: string): ParagraphStyleBuilder { + this._style.headingId = value; + return this; + } + + /** + * Sets the named style type + * @param value The named style type + * @returns {ParagraphStyleBuilder} The paragraph style builder + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * const copy = style.copy(); + * copy.setNamedStyleType(NamedStyleType.CHAPTER); + * ``` + */ + setNamedStyleType(value: NamedStyleType): ParagraphStyleBuilder { + this._style.namedStyleType = value; + return this; + } + + /** + * Sets the vertical align + * @param value The vertical align + * @returns {ParagraphStyleBuilder} The paragraph style builder + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * const copy = style.copy(); + * copy.setVerticalAlign(VerticalAlign.CENTER); + * ``` + */ + setHorizontalAlign(value: HorizontalAlign): ParagraphStyleBuilder { + this._style.horizontalAlign = value; + return this; + } + + /** + * Sets the line spacing + * @param value The line spacing + * @returns {ParagraphStyleBuilder} The paragraph style builder + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * const copy = style.copy(); + * copy.setLineSpacing(10); + * ``` + */ + setLineSpacing(value: number): ParagraphStyleBuilder { + this._style.lineSpacing = value; + return this; + } + + /** + * Sets the text direction + * @param value The text direction + * @returns {ParagraphStyleBuilder} The paragraph style builder + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * const copy = style.copy(); + * copy.setTextDirection(TextDirection.RIGHT_TO_LEFT); + * ``` + */ + setDirection(value: TextDirection): ParagraphStyleBuilder { + this._style.direction = value; + return this; + } + + /** + * Sets the spacing rule + * @param value The spacing rule + * @returns {ParagraphStyleBuilder} The paragraph style builder + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * const copy = style.copy(); + * copy.setSpacingRule(SpacingRule.AUTO); + * ``` + */ + setSpacingRule(value: SpacingRule): ParagraphStyleBuilder { + this._style.spacingRule = value; + return this; + } + + /** + * Sets the snap to grid + * @param value The snap to grid + * @returns {ParagraphStyleBuilder} The paragraph style builder + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * const copy = style.copy(); + * copy.setSnapToGrid(true); + * ``` + */ + setSnapToGrid(value: boolean): ParagraphStyleBuilder { + this._style.snapToGrid = value ? 1 : 0; + return this; + } + + /** + * Sets the space above + * @param value The space above + * @returns {ParagraphStyleBuilder} The paragraph style builder + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * const copy = style.copy(); + * copy.setSpaceAbove(10); + * ``` + */ + setSpaceAbove(value: INumberUnit): ParagraphStyleBuilder { + this._style.spaceAbove = value; + return this; + } + + /** + * Sets the space below + * @param value The space below + * @returns {ParagraphStyleBuilder} The paragraph style builder + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * const copy = style.copy(); + * copy.setSpaceBelow(10); + * ``` + */ + setSpaceBelow(value: INumberUnit): ParagraphStyleBuilder { + this._style.spaceBelow = value; + return this; + } + + /** + * Sets the border between + * @param {IParagraphBorder} value The border between + * @returns {ParagraphStyleBuilder} The paragraph style builder + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * const copy = style.copy(); + * copy.setBorderBetween({ color: 'red', width: 1 }); + * ``` + */ + setBorderBetween(value: IParagraphBorder): ParagraphStyleBuilder { + this._style.borderBetween = value; + return this; + } + + /** + * Sets the border top + * @param {IParagraphBorder} value The border top + * @returns {ParagraphStyleBuilder} The paragraph style builder + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * const copy = style.copy(); + * copy.setBorderTop({ color: 'red', width: 1 }); + * ``` + */ + setBorderTop(value: IParagraphBorder): ParagraphStyleBuilder { + this._style.borderTop = value; + return this; + } + + /** + * Sets the border bottom + * @param {IParagraphBorder} value The border bottom + * @returns {ParagraphStyleBuilder} The paragraph style builder + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * const copy = style.copy(); + * copy.setBorderBottom({ color: 'red', width: 1 }); + * ``` + */ + setBorderBottom(value: IParagraphBorder): ParagraphStyleBuilder { + this._style.borderBottom = value; + return this; + } + + /** + * Sets the border left + * @param {IParagraphBorder} value The border left + * @returns {ParagraphStyleBuilder} The paragraph style builder + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * const copy = style.copy(); + * copy.setBorderLeft({ color: 'red', width: 1 }); + * ``` + */ + setBorderLeft(value: IParagraphBorder): ParagraphStyleBuilder { + this._style.borderLeft = value; + return this; + } + + /** + * Sets the border right + * @param {IParagraphBorder} value The border right + * @returns {ParagraphStyleBuilder} The paragraph style builder + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * const copy = style.copy(); + * copy.setBorderRight({ color: 'red', width: 1 }); + * ``` + */ + setBorderRight(value: IParagraphBorder): ParagraphStyleBuilder { + this._style.borderRight = value; + return this; + } + + /** + * Sets the keep lines + * @param value The keep lines + * @returns {ParagraphStyleBuilder} The paragraph style builder + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * const copy = style.copy(); + * copy.setKeepLines(true); + * ``` + */ + setKeepLines(value: boolean): ParagraphStyleBuilder { + this._style.keepLines = value ? 1 : 0; + return this; + } + + /** + * Sets the keep next + * @param value The keep next + * @returns {ParagraphStyleBuilder} The paragraph style builder + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * const copy = style.copy(); + * copy.setKeepNext(true); + * ``` + */ + setKeepNext(value: boolean): ParagraphStyleBuilder { + this._style.keepNext = value ? 1 : 0; + return this; + } + + /** + * Sets the word wrap + * @param value The word wrap + * @returns {ParagraphStyleBuilder} The paragraph style builder + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * const copy = style.copy(); + * copy.setWordWrap(true); + * ``` + */ + setWordWrap(value: boolean): ParagraphStyleBuilder { + this._style.wordWrap = value ? 1 : 0; + return this; + } + + /** + * Sets the widow control + * @param {boolean} value The widow control value + * @returns {ParagraphStyleBuilder} The paragraph style builder + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * const copy = style.copy(); + * copy.setWidowControl(true); + * ``` + */ + setWidowControl(value: boolean): ParagraphStyleBuilder { + this._style.widowControl = value ? 1 : 0; + return this; + } + + /** + * Sets the shading style + * @param {IShading} value The shading configuration + * @returns {ParagraphStyleBuilder} The paragraph style builder + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * const copy = style.copy(); + * copy.setShading({ backgroundColor: '#f0f0f0' }); + * ``` + */ + setShading(value: IShading): ParagraphStyleBuilder { + this._style.shading = value; + return this; + } + + /** + * Sets whether to suppress hyphenation + * @param {boolean} value The suppress hyphenation value + * @returns {ParagraphStyleBuilder} The paragraph style builder + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * const copy = style.copy(); + * copy.setSuppressHyphenation(true); + * ``` + */ + setSuppressHyphenation(value: boolean): ParagraphStyleBuilder { + this._style.suppressHyphenation = value ? 1 : 0; + return this; + } + + /** + * Creates a copy of the current paragraph style builder + * @returns {ParagraphStyleBuilder} A new instance of ParagraphStyleBuilder with the same settings + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * const copy = style.copy(); + * ``` + */ + override copy(): ParagraphStyleBuilder { + return ParagraphStyleBuilder.create(Tools.deepClone(this._style)); + } + + /** + * Builds and returns the final paragraph style configuration + * @returns {IParagraphStyle} The constructed paragraph style object + * @example + * ```ts + * const style = ParagraphStyleValue.create({ textStyle: { ff: 'Arial', fs: 12, it: univerAPI.Enum.BooleanNumber.TRUE, bl: univerAPI.Enum.BooleanNumber.TRUE } }); + * const finalStyle = style.build(); + * ``` + */ + build(): IParagraphStyle { + return this.getValue(); + } +} + +/** + * Represents a rich text value + */ +export class RichTextValue { + protected _data: IDocumentData; + + /** + * Creates a new RichTextValue instance + * @param {IDocumentData} data The initial data for the rich text value + * @returns {RichTextValue} A new RichTextValue instance + */ + public static create(data: IDocumentData): RichTextValue { + return new RichTextValue(data); + } + + /** + * Creates a new RichTextValue instance + * @param {IDocumentBody} data The initial data for the rich text value + * @returns {RichTextValue} A new RichTextValue instance + */ + public static createByBody(data: IDocumentBody): RichTextValue { + return new RichTextValue({ body: data, id: 'd', documentStyle: {} }); + } + + constructor(data: IDocumentData) { + if (!data.body) { + throw new Error('Invalid document data, body is required'); + } + this._data = data; + } + + /** + * Creates a copy of the current RichTextValue instance + * @returns {RichTextValue} A new instance of RichTextValue with the same data + */ + copy(): RichTextBuilder { + return RichTextBuilder.create(Tools.deepClone(this._data)); + } + + /** + * Slices the current RichTextValue instance + * @param {number} start The start index + * @param {number} end The end index + * @returns {RichTextBuilder} A new instance of RichTextBuilder with the sliced data + */ + slice(start: number, end: number): RichTextBuilder { + const { body, ...ext } = this._data; + return RichTextBuilder.create({ + ...Tools.deepClone(ext), + body: getBodySlice(body!, start, end), + }); + } + + /** + * Converts the current RichTextValue instance to plain text + * @returns {string} The plain text representation of the current RichTextValue instance + */ + toPlainText(): string { + return BuildTextUtils.transform.getPlainText(this._data.body?.dataStream ?? ''); + } + + /** + * Gets the paragraph style of the current RichTextValue instance + * @returns {ParagraphStyleValue} The paragraph style of the current RichTextValue instance + */ + getParagraphStyle(): ParagraphStyleValue { + return ParagraphStyleValue.create(this._data.body?.paragraphs?.[0].paragraphStyle); + } + + /** + * Gets the paragraph bullet of the current RichTextValue instance + * @returns {ParagraphBulletValue} The paragraph bullet of the current RichTextValue instance + */ + getParagraphBullet() { + return this._data.body?.paragraphs?.[0].bullet; + } + + /** + * Gets the paragraphs of the current RichTextValue instance + * @returns {RichTextValue[]} The paragraphs of the current RichTextValue instance + */ + getParagraphs(): RichTextValue[] { + const paragraphs = this._data.body?.paragraphs ?? []; + + let start = 0; + return paragraphs.map((paragraph) => { + const sub = this.slice(start, paragraph.startIndex); + start = paragraph.startIndex; + return sub; + }); + } + + /** + * Gets the text runs of the current RichTextValue instance + * @returns {TextRunValue[]} The text runs of the current RichTextValue instance + */ + getTextRuns() { + return (this._data.body?.textRuns ?? []).map((t) => ({ + ...t, + ts: t.ts ? TextStyleValue.create(t.ts) : null, + })); + } + + /** + * Gets the links of the current RichTextValue instance + * @returns {LinkValue[]} The links of the current RichTextValue instance + */ + getLinks() { + return this._data.body?.customRanges?.filter((r) => r.rangeType === CustomRangeType.HYPERLINK) ?? []; + } + + /** + * Gets the data of the current RichTextValue instance + * @returns {IDocumentData} The data of the current RichTextValue instance + */ + getData(): IDocumentData { + return this._data; + } +} + +/** + * Represents a rich text builder + */ +export class RichTextBuilder extends RichTextValue { + public static newEmptyData(): IDocumentData { + return { + id: 'd', + documentStyle: {}, + drawings: {}, + drawingsOrder: [], + body: { + dataStream: '\r\n', + customBlocks: [], + customRanges: [], + paragraphs: [{ startIndex: 0 }], + textRuns: [], + tables: [], + sectionBreaks: [], + }, + }; + } + + /** + * Creates a new RichTextBuilder instance + * @param {IDocumentData} data The initial data for the rich text builder + * @returns {RichTextBuilder} A new RichTextBuilder instance + */ + public static override create(data?: IDocumentData): RichTextBuilder { + return new RichTextBuilder(data ?? RichTextBuilder.newEmptyData()); + } + + private _doc: DocumentDataModel; + + constructor(data: IDocumentData) { + super(data); + this._doc = new DocumentDataModel(data); + } + + /** + * Inserts text into the rich text builder at the specified start position + * @param start The start position of the text to insert + * @param text The text to insert + * @param style The style of the text to insert + * @returns {RichTextBuilder} The current RichTextBuilder instance + * @example + * ```ts + * const richText = RichTextValue.create({ body: { dataStream: 'Hello' } }); + * const newRichText = richText.insertText(0, 'World'); + * ``` + */ + insertText(start: string, style?: TextStyleBuilder | ITextStyle): RichTextBuilder; + /** + * Inserts text into the rich text builder at the specified start position + * @param start The start position of the text to insert + * @param text The text to insert + * @param style The style of the text to insert + * @returns {RichTextBuilder} The current RichTextBuilder instance + * @example + * ```ts + * const richText = RichTextValue.create({ body: { dataStream: 'Hello' } }); + * const newRichText = richText.insertText(5, 'World', { ff: 'Arial', fs: 12 }); + * ``` + */ + insertText(start: number, text: string, style?: TextStyleBuilder | ITextStyle): RichTextBuilder; + insertText(start: string | number, text?: string | TextStyleBuilder | ITextStyle, style?: TextStyleBuilder | ITextStyle): RichTextBuilder { + let startIndex = (this._data.body?.dataStream.length ?? 2) - 2; + let insertText; + let insertStyle; + if (typeof start === 'string') { + insertText = start; + } else { + startIndex = Math.min(start, startIndex); + insertText = text as string; + } + + if (typeof text === 'object') { + insertStyle = text instanceof TextStyleBuilder ? text.build() : text; + } else { + insertStyle = style instanceof TextStyleBuilder ? style.build() : style; + } + const newBody: IDocumentBody = { + dataStream: insertText, + textRuns: insertStyle + ? [ + { + ts: insertStyle, + st: startIndex, + ed: startIndex + insertText.length, + }, + ] + : [], + }; + + const textX = BuildTextUtils.selection.replace({ + doc: this._doc, + selection: { startOffset: startIndex, endOffset: startIndex, collapsed: true }, + body: newBody, + }); + + if (!textX) { + throw new Error('Insert text failed, please check.'); + } + + TextX.apply(this._doc.getBody()!, textX.serialize()); + return this; + } + + /** + * Inserts rich text into the rich text builder at the specified start position + * @param {RichTextValue} richText The rich text to insert + * @returns {RichTextValue | IDocumentData} The current RichTextBuilder instance + * @example + * ```ts + * const richText = RichTextValue.create({ body: { dataStream: 'Hello' } }); + * const newRichText = richText.insertRichText(RichTextValue.create({ body: { dataStream: 'World' } })); + * ``` + */ + insertRichText(richText: RichTextValue | IDocumentData): RichTextBuilder; + /** + * Inserts rich text into the rich text builder at the specified start position + * @param {number} start The start position of the text to insert + * @param { RichTextValue | IDocumentData} richText The rich text to insert + * @returns {RichTextValue | IDocumentData} The current RichTextBuilder instance + * @example + * ```ts + * const richText = RichTextValue.create({ body: { dataStream: 'Hello' } }); + * const newRichText = richText.insertRichText(5, RichTextValue.create({ body: { dataStream: 'World' } })); + * ``` + */ + insertRichText(start: number, richText: RichTextValue | IDocumentData): RichTextBuilder; + insertRichText(start: number | RichTextValue | IDocumentData, richText?: RichTextValue | IDocumentData): RichTextBuilder { + let startIndex = (this._data.body?.dataStream.length ?? 2) - 2; + let insertText: IDocumentData; + if (typeof start === 'object') { + insertText = start instanceof RichTextValue ? start.getData() : start; + } else { + startIndex = Math.min(start, startIndex); + insertText = richText instanceof RichTextValue ? richText.getData() : richText!; + } + + const textX = BuildTextUtils.selection.replace({ + doc: this._doc, + selection: { startOffset: startIndex, endOffset: startIndex, collapsed: true }, + body: insertText.body!, + }); + + if (!textX) { + throw new Error('Insert text failed, please check.'); + } + + TextX.apply(this._doc.getBody()!, textX.serialize()); + return this; + } + + /** + * Deletes text from the rich text builder from the end. + * @param {number} count The number of characters to delete (optional) + * @returns {RichTextBuilder} The current RichTextBuilder instance + * @example + * ```ts + * const richText = RichTextValue.create({ body: { dataStream: 'Hello World' } }); + * const newRichText = richText.delete(5); + * ``` + */ + delete(count: number): RichTextBuilder; + /** + * Deletes text from the rich text builder at the specified start position + * @param {number} start The start position of the text to delete + * @param {number} [count] The number of characters to delete (optional) + * @returns {RichTextBuilder} The current RichTextBuilder instance + * @example + * ```ts + * const richText = RichTextValue.create({ body: { dataStream: 'Hello World' } }); + * const newRichText = richText.delete(5, 5); + * ``` + */ + delete(start: number, count: number): RichTextBuilder; + delete(start: number, count?: number): RichTextBuilder { + // Implementation logic here + if (count !== undefined) { + if (!count) return this; + const actions = BuildTextUtils.selection.delete([{ startOffset: start, endOffset: start + count, collapsed: true }], this._data.body!); + TextX.apply(this._doc.getBody()!, actions); + } + return this; + } + + /** + * Sets the style of the text at the specified start and end positions + * @param {number} start The start position of the text to set the style + * @param {number} end The end position of the text to set the style + * @param {TextStyleBuilder | ITextStyle} style The style to set + * @returns {RichTextBuilder} The current RichTextBuilder instance + * @example + * ```ts + * const richText = RichTextValue.create({ body: { dataStream: 'Hello World' } }); + * const newRichText = richText.setStyle(5, 10, { ff: 'Arial', fs: 12 }); + * ``` + */ + setStyle(start: number, end: number, style: TextStyleBuilder | ITextStyle): RichTextBuilder { + const newBody: IDocumentBody = { + dataStream: '', + textRuns: [{ + ts: style instanceof TextStyleBuilder ? style.build() : style, + st: 0, + ed: end - start, + }], + }; + const actions = BuildTextUtils.selection.retain([{ startOffset: start, endOffset: end, collapsed: true }], newBody); + TextX.apply(this._doc.getBody()!, actions); + return this; + } + + /** + * Sets the link of the text at the specified start and end positions + * @param {number} start The start position of the text to set the link + * @param {number} end The end position of the text to set the link + * @param {string} link The link to set + * @returns {RichTextBuilder} The current RichTextBuilder instance + * @example + * ```ts + * const richText = RichTextValue.create({ body: { dataStream: 'Hello World' } }); + * const newRichText = richText.setLink(5, 10, 'https://www.example.com'); + * ``` + */ + setLink(start: number, end: number, link: string): RichTextBuilder { + const textX = BuildTextUtils.customRange.add({ + rangeType: CustomRangeType.HYPERLINK, + rangeId: generateRandomId(), + properties: { + url: link, + }, + ranges: [{ startOffset: start, endOffset: end, collapsed: false }], + body: this._data.body!, + }); + if (!textX) { + throw new Error('Insert text failed, please check.'); + } + TextX.apply(this._doc.getBody()!, textX.serialize()); + return this; + } + + /** + * Cancels the link of the text at the specified start and end positions + * @param {string} linkId The id of the link to cancel + * @returns {RichTextBuilder} The current RichTextBuilder instance + * @example + * ```ts + * const richText = RichTextValue.create({ + * body: { + * dataStream: 'Hello World', + * customRanges: [ + * { + * rangeType: CustomRangeType.HYPERLINK, + * rangeId: 'linkId', + * properties: { url: 'https://www.example.com' }, + * startIndex: 0, + * endIndex: 5 + * }] + * } + * }); + * const newRichText = richText.cancelLink('linkId'); + * ``` + */ + cancelLink(linkId: string): RichTextBuilder; + /** + * Cancels the link of the text at the specified start and end positions + * @param {number} start The start position of the text to cancel the link + * @param {number} end The end position of the text to cancel the link + * @returns {RichTextBuilder} The current RichTextBuilder instance + * @example + * ```ts + * const richText = RichTextValue.create({ + * body: { + * dataStream: 'Hello World', + * customRanges: [ + * { + * rangeType: CustomRangeType.HYPERLINK, + * rangeId: 'linkId', + * properties: { url: 'https://www.example.com' }, + * startIndex: 0, + * endIndex: 5 + * }] + * } + * }); + * const newRichText = richText.cancelLink(0, 10); + * ``` + */ + cancelLink(start: number, end: number): RichTextBuilder; + cancelLink(start: number | string, end?: number): RichTextBuilder { + if (typeof start === 'string') { + const textX = BuildTextUtils.customRange.delete({ + rangeId: start, + documentDataModel: this._doc, + }); + if (!textX) { + throw new Error('Insert text failed, please check.'); + } + TextX.apply(this._doc.getBody()!, textX.serialize()); + } else { + const slice = this.slice(start as number, end as number); + slice.getLinks().forEach((l) => { + const textX = BuildTextUtils.customRange.delete({ + rangeId: l.rangeId, + documentDataModel: this._doc, + }); + if (!textX) { + throw new Error('Insert text failed, please check.'); + } + TextX.apply(this._doc.getBody()!, textX.serialize()); + }); + } + + return this; + } + + updateLink(id: string, url: string): RichTextBuilder { + const current = this._data.body?.customRanges?.find((range) => range.rangeId === id); + if (!current) { + throw new Error('Link not found'); + } + + current.properties!.url = url; + return this; + } + + /** + * Inserts a new paragraph at the specified start position + * @param {number} start The start position of the paragraph to insert + * @param {ParagraphStyleBuilder} paragraphStyle The style of the paragraph to insert + * @returns {RichTextBuilder} The current RichTextBuilder instance + * @example + * ```ts + * const richText = RichTextValue.create({ body: { dataStream: 'Hello World' } }); + * const newRichText = richText.insertParagraph(); + * ``` + */ + insertParagraph(paragraphStyle?: ParagraphStyleBuilder): RichTextBuilder; + /** + * Inserts a new paragraph at the specified start position + * @param {number} start The start position of the paragraph to insert + * @param {ParagraphStyleBuilder} paragraphStyle The style of the paragraph to insert + * @returns {RichTextBuilder} The current RichTextBuilder instance + * @example + * ```ts + * const richText = RichTextValue.create({ body: { dataStream: 'Hello World' } }); + * const newRichText = richText.insertParagraph(5, { ff: 'Arial', fs: 12 }); + * ``` + */ + insertParagraph(start: number, paragraphStyle: ParagraphStyleBuilder): RichTextBuilder; + insertParagraph(start?: number | ParagraphStyleBuilder, paragraphStyle?: ParagraphStyleBuilder): RichTextBuilder { + let newBody: IDocumentBody; + let startIndex: number; + if (typeof start === 'object') { + newBody = { + dataStream: '\r', + paragraphs: [{ + startIndex: 0, + paragraphStyle: start.build(), + }], + }; + startIndex = (this._data.body?.dataStream.length ?? 2) - 2; + } else { + startIndex = start!; + newBody = { + dataStream: '\r', + paragraphs: [{ + startIndex: 0, + paragraphStyle: paragraphStyle?.build(), + }], + }; + } + + this.insertRichText(startIndex, RichTextValue.create({ body: newBody, id: 'd', documentStyle: {} })); + return this; + } + + /** + * Inserts a new link + * @param text + * @param url + * @returns + */ + insertLink(text: string, url: string): RichTextBuilder; + insertLink(start: number, text: string, url: string): RichTextBuilder; + insertLink(start: number | string, text: string, url?: string): RichTextBuilder { + let textStr = ''; + let textUrl = ''; + if (typeof start === 'string') { + textStr = start; + textUrl = text; + } else { + textStr = text; + textUrl = url!; + } + + const rich = RichTextBuilder.createByBody({ + dataStream: textStr, + customRanges: [{ + rangeType: CustomRangeType.HYPERLINK, + rangeId: generateRandomId(), + properties: { + url: textUrl, + }, + startIndex: 0, + endIndex: textStr.length - 1, + }], + }); + + return typeof start === 'number' ? this.insertRichText(start, rich) : this.insertRichText(rich); + } +} diff --git a/packages/core/src/docs/data-model/text-x/build-utils/index.ts b/packages/core/src/docs/data-model/text-x/build-utils/index.ts index 9ff2f59075a..fc707859310 100644 --- a/packages/core/src/docs/data-model/text-x/build-utils/index.ts +++ b/packages/core/src/docs/data-model/text-x/build-utils/index.ts @@ -20,7 +20,7 @@ import { addDrawing } from './drawings'; import { changeParagraphBulletNestLevel, setParagraphBullet, switchParagraphBullet, toggleChecklistParagraph } from './paragraph'; import { fromPlainText, getPlainText, isEmptyDocument } from './parse'; import { isSegmentIntersects, makeSelection, normalizeSelection } from './selection'; -import { addCustomRangeTextX, deleteCustomRangeTextX, deleteSelectionTextX, replaceSelectionTextX } from './text-x-utils'; +import { addCustomRangeTextX, deleteCustomRangeTextX, deleteSelectionTextX, replaceSelectionTextX, retainSelectionTextX } from './text-x-utils'; export class BuildTextUtils { static customRange = { @@ -41,6 +41,7 @@ export class BuildTextUtils { makeSelection, normalizeSelection, delete: deleteSelectionTextX, + retain: retainSelectionTextX, }; static range = { diff --git a/packages/core/src/docs/data-model/text-x/build-utils/text-x-utils.ts b/packages/core/src/docs/data-model/text-x/build-utils/text-x-utils.ts index f28ce073289..93a9d5988f0 100644 --- a/packages/core/src/docs/data-model/text-x/build-utils/text-x-utils.ts +++ b/packages/core/src/docs/data-model/text-x/build-utils/text-x-utils.ts @@ -14,13 +14,12 @@ * limitations under the License. */ -import type { IAccessor } from '@wendellhu/redi'; import type { ITextRange, ITextRangeParam } from '../../../../sheets/typedef'; import type { CustomRangeType, IDocumentBody } from '../../../../types/interfaces'; import type { DocumentDataModel } from '../../document-data-model'; import type { TextXAction } from '../action-types'; import type { TextXSelection } from '../text-x'; -import { type Nullable, UpdateDocsAttributeType } from '../../../../shared'; +import { type Nullable, Tools, UpdateDocsAttributeType } from '../../../../shared'; import { textDiff } from '../../../../shared/text-diff'; import { TextXActionType } from '../action-types'; import { TextX } from '../text-x'; @@ -34,7 +33,7 @@ export interface IDeleteCustomRangeParam { insert?: Nullable; } -export function deleteCustomRangeTextX(accessor: IAccessor, params: IDeleteCustomRangeParam) { +export function deleteCustomRangeTextX(params: IDeleteCustomRangeParam) { const { rangeId, segmentId, documentDataModel, insert } = params; const range = documentDataModel.getSelfOrHeaderFooterModel(segmentId).getBody()?.customRanges?.find((r) => r.rangeId === rangeId); if (!range) { @@ -234,6 +233,34 @@ export function deleteSelectionTextX( return dos; } +export function retainSelectionTextX(selections: ITextRange[], body: IDocumentBody, memoryCursor: number = 0) { + const dos: Array = []; + let cursor = memoryCursor; + selections.forEach((selection) => { + const { startOffset, endOffset } = selection; + if (startOffset > cursor) { + dos.push({ + t: TextXActionType.RETAIN, + len: startOffset - cursor, + }); + cursor = startOffset; + } + if (endOffset > cursor) { + dos.push({ + t: TextXActionType.RETAIN, + len: endOffset - cursor, + body: { + ...Tools.deepClone(body), + dataStream: '', + }, + }); + cursor = endOffset; + } + }); + + return dos; +} + export interface IReplaceSelectionTextXParams { /** * range to be replaced. diff --git a/packages/core/src/facade/f-doc.ts b/packages/core/src/facade/f-doc.ts new file mode 100644 index 00000000000..87ea21cf7d7 --- /dev/null +++ b/packages/core/src/facade/f-doc.ts @@ -0,0 +1,28 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { DocumentDataModel } from '../docs'; +import { Inject, Injector } from '../common/di'; +import { FBaseInitialable } from './f-base'; + +export class FDoc extends FBaseInitialable { + constructor( + protected doc: DocumentDataModel, + @Inject(Injector) _injector: Injector + ) { + super(_injector); + } +} diff --git a/packages/core/src/facade/f-enum.ts b/packages/core/src/facade/f-enum.ts index 7c2f55f0cad..ed5b2137dfd 100644 --- a/packages/core/src/facade/f-enum.ts +++ b/packages/core/src/facade/f-enum.ts @@ -15,7 +15,9 @@ */ import { UniverInstanceType } from '../common/unit'; +import { CommandType } from '../services/command/command.service'; import { LifecycleStages } from '../services/lifecycle/lifecycle'; +import { BaselineOffset, BooleanNumber, HorizontalAlign, TextDecoration, TextDirection, VerticalAlign } from '../types/enum'; import { DataValidationErrorStyle } from '../types/enum/data-validation-error-style'; import { DataValidationOperator } from '../types/enum/data-validation-operator'; import { DataValidationRenderMode } from '../types/enum/data-validation-render-mode'; @@ -24,6 +26,7 @@ import { DataValidationType } from '../types/enum/data-validation-type'; export class FEnum { static _instance: FEnum | null; + static get() { if (this._instance) { return this._instance; @@ -57,31 +60,101 @@ export class FEnum { } } + /** + * Defines different types of Univer instances + */ get UniverInstanceType() { return UniverInstanceType; } + /** + * Represents different stages in the lifecycle + */ get LifecycleStages() { return LifecycleStages; } + /** + * Different types of data validation + */ get DataValidationType() { return DataValidationType; } + /** + * Different error display styles + */ get DataValidationErrorStyle() { return DataValidationErrorStyle; } + /** + * Different validation rendering modes + */ get DataValidationRenderMode() { return DataValidationRenderMode; } + /** + * Different validation operators + */ get DataValidationOperator() { return DataValidationOperator; } + /** + * Different validation states + */ get DataValidationStatus() { return DataValidationStatus; } + + /** + * Different types of commands + */ + get CommandType() { + return CommandType; + } + + /** + * Different baseline offsets for text baseline positioning + */ + get BaselineOffset() { + return BaselineOffset; + } + + /** + * Boolean number representations + */ + get BooleanNumber() { + return BooleanNumber; + } + + /** + * Different horizontal text alignment options + */ + get HorizontalAlign() { + return HorizontalAlign; + } + + /** + * Different text decoration styles + */ + get TextDecoration() { + return TextDecoration; + } + + /** + * Different text direction options + */ + get TextDirection() { + return TextDirection; + } + + /** + * Different vertical text alignment options + */ + get VerticalAlign() { + return VerticalAlign; + } } diff --git a/packages/core/src/facade/f-event.ts b/packages/core/src/facade/f-event.ts index 550df6f4464..825977a03b0 100644 --- a/packages/core/src/facade/f-event.ts +++ b/packages/core/src/facade/f-event.ts @@ -15,31 +15,73 @@ */ import type { UniverInstanceType } from '../common/unit'; +import type { CommandType } from '../services/command/command.service'; import type { LifecycleStages } from '../services/lifecycle/lifecycle'; -import type { IWorkbookData } from '../sheets/typedef'; import type { IDocumentData } from '../types/interfaces'; +import type { FDoc } from './f-doc'; -export interface ISheetCreateParam { - unitId: string; - type: UniverInstanceType.UNIVER_SHEET; - data: IWorkbookData; +/** + * Base interface for all event parameters + * @interface IEventBase + */ +export interface IEventBase { + /** Flag to cancel the event if supported */ + cancel?: boolean; } -export interface IDocumentCreateParam { +/** + * Event interface triggered when a document is created + * @interface IDocCreatedParam + * @augments {IEventBase} + */ +export interface IDocCreatedParam extends IEventBase { + /** Unique identifier of the document unit */ unitId: string; + /** Type identifier for document instances */ type: UniverInstanceType.UNIVER_DOC; - data: IDocumentData; + /** The created document instance */ + doc: FDoc; + /** Reference to the document unit */ + unit: FDoc; } -export interface ILifeCycleChangedParam { - stage: LifecycleStages; +/** + * Event interface triggered when a document is disposed + * @interface IDocDisposedEvent + * @augments {IEventBase} + */ +export interface IDocDisposedEvent extends IEventBase { + /** Unique identifier of the disposed document unit */ + unitId: string; + /** Type identifier for document instances */ + unitType: UniverInstanceType.UNIVER_DOC; + /** Final state snapshot of the disposed document */ + snapshot: IDocumentData; } -export interface IEventBase { - cancel?: boolean; + +/** + * Event interface for lifecycle stage changes + * @interface ILifeCycleChangedEvent + * @augments {IEventBase} + */ +export interface ILifeCycleChangedEvent extends IEventBase { + /** Current stage of the lifecycle */ + stage: LifecycleStages; } -export type IUnitCreateEvent = IEventBase & (ISheetCreateParam | IDocumentCreateParam); -export type ILifeCycleChangedEvent = IEventBase & ILifeCycleChangedParam; +/** + * Event interface for command execution + * @interface ICommandEvent + * @augments {IEventBase} + */ +export interface ICommandEvent extends IEventBase { + /** Parameters passed to the command */ + params: any; + /** Unique identifier of the command */ + id: string; + /** Type of the command */ + type: CommandType; +} export class FEventName { static _instance: FEventName | null; @@ -76,16 +118,150 @@ export class FEventName { } } - get UnitCreated() { - return 'UnitCreated' as const; + /** + * Event fired when a document is created + * @see {@link IDocCreatedParam} + * @example + * ```ts + * univerAPI.addEvent(univerAPI.event.DocCreated, (params) => { + * const { unitId, type, doc, unit } = params; + * console.log('doc created', params); + * }); + * ``` + */ + get DocCreated() { + return 'DocCreated' as const; + } + + /** + * Event fired when a document is disposed + * @see {@link IDocDisposedEvent} + * @example + * ```ts + * univerAPI.addEvent(univerAPI.event.DocDisposed, (params) => { + * const { unitId, unitType, snapshot } = params; + * console.log('doc disposed', params); + * }); + * ``` + */ + get DocDisposed() { + return 'DocDisposed' as const; } + /** + * Event fired when life cycle is changed + * @see {@link ILifeCycleChangedEvent} + * @example + * ```ts + * univerAPI.addEvent(univerAPI.event.LifeCycleChanged, (params) => { + * const { stage } = params; + * console.log('life cycle changed', params); + * }); + * ``` + */ get LifeCycleChanged() { return 'LifeCycleChanged' as const; } + + /** + * Event fired when a redo command is executed + * @see {@link ICommandEvent} + * @example + * ```ts + * univerAPI.addEvent(univerAPI.event.Redo, (event) => { + * const { params, id, type } = event; + * console.log('command executed', event); + * }); + * ``` + */ + get Redo() { + return 'Redo' as const; + } + + /** + * Event fired when an undo command is executed + * @see {@link ICommandEvent} + * @example + * ```ts + * univerAPI.addEvent(univerAPI.event.Undo, (event) => { + * const { params, id, type } = event; + * console.log('command executed', event); + * }); + * ``` + */ + get Undo() { + return 'Undo' as const; + } + + /** + * Event fired before a redo command is executed + * @see {@link ICommandEvent} + * @example + * ```ts + * univerAPI.addEvent(univerAPI.event.BeforeRedo, (event) => { + * const { params, id, type } = event; + * console.log('command executed', event); + * }); + * ``` + */ + get BeforeRedo() { + return 'BeforeRedo' as const; + } + + /** + * Event fired before an undo command is executed + * @see {@link ICommandEvent} + * @example + * ```ts + * univerAPI.addEvent(univerAPI.event.BeforeUndo, (event) => { + * const { params, id, type } = event; + * console.log('command executed', event); + * }); + * ``` + */ + get BeforeUndo() { + return 'BeforeUndo' as const; + } + + /** + * Event fired when a command is executed + * @see {@link ICommandEvent} + * @example + * ```ts + * univerAPI.addEvent(univerAPI.event.CommandExecuted, (event) => { + * const { params, id, type } = event; + * console.log('command executed', event); + * }); + * ``` + */ + get CommandExecuted() { + return 'CommandExecuted' as const; + } + + /** + * Event fired before a command is executed + * @see {@link ICommandEvent} + * @example + * ```ts + * univerAPI.addEvent(univerAPI.event.BeforeCommandExecute, (event) => { + * const { params, id, type } = event; + * console.log('command executed', event); + * }); + * ``` + */ + get BeforeCommandExecute() { + return 'BeforeCommandExecute' as const; + } } export interface IEventParamConfig { - // UnitCreated: IUnitCreateEvent; LifeCycleChanged: ILifeCycleChangedEvent; + DocDisposed: IDocDisposedEvent; + DocCreated: IDocCreatedParam; + Redo: ICommandEvent; + Undo: ICommandEvent; + BeforeRedo: ICommandEvent; + BeforeUndo: ICommandEvent; + CommandExecuted: ICommandEvent; + BeforeCommandExecute: ICommandEvent; } diff --git a/packages/core/src/facade/f-univer.ts b/packages/core/src/facade/f-univer.ts index 91a7946f129..28c15b0accc 100644 --- a/packages/core/src/facade/f-univer.ts +++ b/packages/core/src/facade/f-univer.ts @@ -15,11 +15,16 @@ */ import type { IDisposable } from '../common/di'; +import type { DocumentDataModel } from '../docs'; import type { CommandListener, IExecutionOptions } from '../services/command/command.service'; import type { LifecycleStages } from '../services/lifecycle/lifecycle'; -import type { IEventParamConfig } from './f-event'; +import type { IDocumentData, IParagraphStyle, ITextDecoration, ITextStyle } from '../types/interfaces'; +import type { ICommandEvent, IEventParamConfig } from './f-event'; import { Inject, Injector } from '../common/di'; +import { CanceledError } from '../common/error'; import { Registry } from '../common/registry'; +import { UniverInstanceType } from '../common/unit'; +import { ParagraphStyleBuilder, ParagraphStyleValue, RichTextBuilder, RichTextValue, TextDecorationBuilder, TextStyleBuilder, TextStyleValue } from '../docs/data-model/rich-text-builder'; import { ICommandService } from '../services/command/command.service'; import { IUniverInstanceService } from '../services/instance/instance.service'; import { LifecycleService } from '../services/lifecycle/lifecycle.service'; @@ -28,10 +33,12 @@ import { ColorBuilder, toDisposable } from '../shared'; import { Univer } from '../univer'; import { FBaseInitialable } from './f-base'; import { FBlob } from './f-blob'; +import { FDoc } from './f-doc'; import { FEnum } from './f-enum'; import { FEventName } from './f-event'; import { FHooks } from './f-hooks'; import { FUserManager } from './f-usermanager'; +import { FUtil } from './f-util'; export class FUniver extends FBaseInitialable { /** @@ -45,9 +52,9 @@ export class FUniver extends FBaseInitialable { return injector.createInstance(FUniver); } - private _eventRegistry: Map void>> = new Map(); + protected _eventRegistry: Map void>> = new Map(); - private _ensureEventRegistry(event: string) { + protected _ensureEventRegistry(event: string) { if (!this._eventRegistry.has(event)) { this._eventRegistry.set(event, new Registry()); } @@ -69,11 +76,111 @@ export class FUniver extends FBaseInitialable { }) ); + this.disposeWithMe( + this._commandService.beforeCommandExecuted((commandInfo) => { + if ( + !this._eventRegistry.get(this.Event.BeforeRedo) && + !this._eventRegistry.get(this.Event.BeforeUndo) && + !this._eventRegistry.get(this.Event.BeforeCommandExecute) + ) { + return; + } + const { id, type: propType, params } = commandInfo; + const type = propType!; + const eventParams: ICommandEvent = { id, type, params }; + switch (commandInfo.id) { + case RedoCommand.id: + this.fireEvent(this.Event.BeforeRedo, eventParams); + break; + case UndoCommand.id: + this.fireEvent(this.Event.BeforeUndo, eventParams); + break; + default: + this.fireEvent(this.Event.BeforeCommandExecute, eventParams); + break; + } + + if (eventParams.cancel) { + throw new CanceledError(); + } + }) + ); + + this.disposeWithMe( + this._commandService.onCommandExecuted((commandInfo) => { + if ( + !this._eventRegistry.get(this.Event.Redo) && + !this._eventRegistry.get(this.Event.Undo) && + !this._eventRegistry.get(this.Event.CommandExecuted) + ) { + return; + } + const { id, type: propType, params } = commandInfo; + const type = propType!; + const eventParams: ICommandEvent = { id, type, params }; + switch (commandInfo.id) { + case RedoCommand.id: + this.fireEvent(this.Event.Redo, eventParams); + break; + case UndoCommand.id: + this.fireEvent(this.Event.Undo, eventParams); + break; + default: + this.fireEvent(this.Event.CommandExecuted, eventParams); + break; + } + }) + ); + + this._initUnitEvent(this._injector); this._injector.onDispose(() => { this.dispose(); }); } + private _initUnitEvent(injector: Injector): void { + const univerInstanceService = injector.get(IUniverInstanceService); + this.disposeWithMe( + univerInstanceService.unitDisposed$.subscribe((unit) => { + if (!this._eventRegistry.get(this.Event.DocDisposed)) return; + + if (unit.type === UniverInstanceType.UNIVER_DOC) { + this.fireEvent(this.Event.DocDisposed, + { + unitId: unit.getUnitId(), + unitType: unit.type, + snapshot: unit.getSnapshot() as IDocumentData, + + } + ); + } + }) + ); + + this.disposeWithMe( + univerInstanceService.unitAdded$.subscribe((unit) => { + if (!this._eventRegistry.get(this.Event.DocCreated)) return; + + if (unit.type === UniverInstanceType.UNIVER_DOC) { + const doc = unit as DocumentDataModel; + const docUnit = injector.createInstance(FDoc, doc); + this.fireEvent(this.Event.DocCreated, + { + unitId: unit.getUnitId(), + type: unit.type, + doc: docUnit, + unit: docUnit, + } + ); + } + }) + ); + } + + protected _eventListend(key: string) { + return this._eventRegistry.get(key); + } + /** * Dispose the UniverSheet by the `unitId`. The UniverSheet would be unload from the application. * @param unitId The unit id of the UniverSheet. @@ -171,22 +278,6 @@ export class FUniver extends FBaseInitialable { return this._injector.createInstance(FHooks); } - /** - * Create a new blob. - * @returns {FBlob} The new blob instance - * @example - * ```ts - * const blob = univerAPI.newBlob(); - * ``` - */ - newBlob(): FBlob { - return this._injector.createInstance(FBlob); - } - - newColor(): ColorBuilder { - return new ColorBuilder(); - } - get Enum() { return FEnum.get(); } @@ -195,6 +286,10 @@ export class FUniver extends FBaseInitialable { return FEventName.get(); } + get Util() { + return FUtil.get(); + } + /** * Add an event listener * @param event key of event @@ -232,6 +327,7 @@ export class FUniver extends FBaseInitialable { /** * Get the callback map corresponding to the event + * @param event * @returns {number} The number of callbacks */ protected hasEventCallback(event: keyof IEventParamConfig): boolean { @@ -242,4 +338,110 @@ export class FUniver extends FBaseInitialable { getUserManager(): FUserManager { return this._injector.createInstance(FUserManager); } + + /** + * Create a new blob. + * @returns {FBlob} The new blob instance + * @example + * ```ts + * const blob = univerApi.newBlob(); + * ``` + */ + newBlob(): FBlob { + return this._injector.createInstance(FBlob); + } + + /** + * Create a new color. + * @returns {ColorBuilder} The new color instance + * @example + * ```ts + * const color = univerApi.newColor(); + * ``` + */ + newColor(): ColorBuilder { + return new ColorBuilder(); + } + + /** + * Create a new rich text. + * @param data + * @returns {RichTextBuilder} The new rich text instance + * @example + * ```ts + * const richText = univerApi.newRichText(); + * ``` + */ + newRichText(data?: IDocumentData): RichTextBuilder { + return RichTextBuilder.create(data); + } + + /** + * Create a new rich text value. + * @param data - The rich text data + * @returns {RichTextValue} The new rich text value instance + * @example + * ```ts + * const richTextValue = univerApi.newRichTextValue(); + * ``` + */ + newRichTextValue(data: IDocumentData): RichTextValue { + return RichTextValue.create(data); + } + + /** + * Create a new paragraph style. + * @param style - The paragraph style + * @returns {ParagraphStyleBuilder} The new paragraph style instance + * @example + * ```ts + * const paragraphStyle = univerApi.newParagraphStyle(); + * ``` + */ + newParagraphStyle(style?: IParagraphStyle): ParagraphStyleBuilder { + return ParagraphStyleBuilder.create(style); + } + + /** + * Create a new paragraph style value. + * @param style - The paragraph style + * @returns {ParagraphStyleValue} The new paragraph style value instance + * @example + * ```ts + * const paragraphStyleValue = univerApi.newParagraphStyleValue(); + * ``` + */ + newParagraphStyleValue(style?: IParagraphStyle): ParagraphStyleValue { + return ParagraphStyleValue.create(style); + } + + /** + * Create a new text style. + * @param style - The text style + * @returns {TextStyleBuilder} The new text style instance + * @example + * ```ts + * const textStyle = univerApi.newTextStyle(); + * ``` + */ + newTextStyle(style?: ITextStyle): TextStyleBuilder { + return TextStyleBuilder.create(style); + } + + /** + * Create a new text style value. + * @param style - The text style + * @returns {TextStyleValue} The new text style value instance + * @example + * ```ts + * const textStyleValue = univerApi.newTextStyleValue(); + * ``` + */ + newTextStyleValue(style?: ITextStyle): TextStyleValue { + return TextStyleValue.create(style); + } + + newTextDecoration(decoration?: ITextDecoration): TextDecorationBuilder { + return new TextDecorationBuilder(decoration); + } } diff --git a/packages/core/src/facade/f-util.ts b/packages/core/src/facade/f-util.ts index f025dde7cbb..e39a8ff84a9 100644 --- a/packages/core/src/facade/f-util.ts +++ b/packages/core/src/facade/f-util.ts @@ -14,6 +14,54 @@ * limitations under the License. */ +import { numfmt, Rectangle, Tools } from '../shared'; + export class FUtil { - static Range = Range; + static _instance: FUtil | null; + static get() { + if (this._instance) { + return this._instance; + } + + const instance = new FUtil(); + this._instance = instance; + return instance; + } + + static extend(source: any): void { + Object.getOwnPropertyNames(source.prototype).forEach((name) => { + if (name !== 'constructor') { + // @ts-ignore + this.prototype[name] = source.prototype[name]; + } + }); + + Object.getOwnPropertyNames(source).forEach((name) => { + if (name !== 'prototype' && name !== 'name' && name !== 'length') { + // @ts-ignore + this[name] = source[name]; + } + }); + } + + /** + * Rectangle utils, including range operations likes merge, subtract, split + */ + get rectangle() { + return Rectangle; + } + + /** + * Number format utils, including parse and strigify about date, price, etc + */ + get numfmt() { + return numfmt; + } + + /** + * common tools + */ + get tools() { + return Tools; + } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 4ce572c3d07..3ad1a3bc704 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -33,7 +33,8 @@ export { } from './common/const'; export * from './common/di'; export { shallowEqual } from './common/equal'; -export { CustomCommandExecutionError } from './common/error'; +export { ParagraphStyleBuilder, ParagraphStyleValue, RichTextBuilder, RichTextValue, TextDecorationBuilder, TextStyleBuilder, TextStyleValue } from './docs/data-model/rich-text-builder'; +export { CanceledError, CustomCommandExecutionError } from './common/error'; export { throttle } from './common/function'; export type { IAsyncInterceptor, ICellInterceptor, IComposeInterceptors, IInterceptor, InterceptorHandler } from './common/interceptor'; export { AsyncInterceptorManager, composeInterceptors, createAsyncInterceptorKey, createInterceptorKey, InterceptorEffectEnum, InterceptorManager } from './common/interceptor'; @@ -46,6 +47,7 @@ export { FHooks } from './facade/f-hooks'; export { FBlob, type IFBlobSource } from './facade/f-blob'; export { FEventName, type IEventBase, type IEventParamConfig } from './facade/f-event'; export { FEnum } from './facade/f-enum'; +export { FUtil } from './facade/f-util'; export { isNumeric, isSafeNumeric } from './common/number'; export { Registry, RegistryAsMap } from './common/registry'; export { requestImmediateMacroTask } from './common/request-immediate-macro-task'; diff --git a/packages/core/src/shared/rectangle.ts b/packages/core/src/shared/rectangle.ts index a9450852d44..2ae8bf30226 100644 --- a/packages/core/src/shared/rectangle.ts +++ b/packages/core/src/shared/rectangle.ts @@ -19,9 +19,31 @@ import { AbsoluteRefType, type IRange, type IRectLTRB, RANGE_TYPE } from '../she import { mergeRanges, multiSubtractSingleRange, splitIntoGrid } from './range'; /** - * This class provides a set of methods to calculate `IRange`. + * This class provides a set of methods to calculate and manipulate rectangular ranges (IRange). + * A range represents a rectangular area in a grid, defined by start/end rows and columns. + * @example + * ```typescript + * // Example range representing cells from A1 to C3 + * const range: IRange = { + * startRow: 0, + * startColumn: 0, + * endRow: 2, + * endColumn: 2, + * rangeType: RANGE_TYPE.NORMAL + * }; + * ``` */ export class Rectangle { + /** + * Creates a deep copy of an IRange object + * @param src + * @example + * ```typescript + * const original = { startRow: 0, startColumn: 0, endRow: 1, endColumn: 1 }; + * const copy = Rectangle.clone(original); + * // copy = { startRow: 0, startColumn: 0, endRow: 1, endColumn: 1 } + * ``` + */ static clone(src: IRange): IRange { if (src.rangeType !== undefined) { return { @@ -41,6 +63,17 @@ export class Rectangle { }; } + /** + * Checks if two ranges are equal by comparing their properties + * @param src + * @param target + * @example + * ```typescript + * const range1 = { startRow: 0, startColumn: 0, endRow: 1, endColumn: 1 }; + * const range2 = { startRow: 0, startColumn: 0, endRow: 1, endColumn: 1 }; + * const areEqual = Rectangle.equals(range1, range2); // true + * ``` + */ static equals(src: IRange, target: IRange): boolean { if (src == null || target == null) { return false; @@ -57,11 +90,16 @@ export class Rectangle { } /** - * Check intersects of normal range(RANGE_TYPE.NORMAL) - * For other types of ranges, please consider using the intersects method. + * Quickly checks if two normal ranges intersect. For specialized range types, + * use the intersects() method instead. * @param rangeA * @param rangeB - * @returns boolean + * @example + * ```typescript + * const range1 = { startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }; + * const range2 = { startRow: 1, startColumn: 1, endRow: 3, endColumn: 3 }; + * const doIntersect = Rectangle.simpleRangesIntersect(range1, range2); // true + * ``` */ static simpleRangesIntersect(rangeA: IRange, rangeB: IRange): boolean { const { startRow: startRowA, endRow: endRowA, startColumn: startColumnA, endColumn: endColumnA } = rangeA; @@ -73,6 +111,25 @@ export class Rectangle { return rowsOverlap && columnsOverlap; } + /** + * Checks if two ranges intersect, handling special range types (ROW, COLUMN) + * @param src + * @param target + * @example + * ```typescript + * const rowRange = { + * startRow: 0, endRow: 2, + * startColumn: NaN, endColumn: NaN, + * rangeType: RANGE_TYPE.ROW + * }; + * const colRange = { + * startRow: NaN, endRow: NaN, + * startColumn: 0, endColumn: 2, + * rangeType: RANGE_TYPE.COLUMN + * }; + * const doIntersect = Rectangle.intersects(rowRange, colRange); // true + * ``` + */ static intersects(src: IRange, target: IRange): boolean { if (src.rangeType === RANGE_TYPE.ROW && target.rangeType === RANGE_TYPE.COLUMN) { return true; @@ -110,8 +167,17 @@ export class Rectangle { } /** - * - * @deprecated use `getIntersectRange` instead. This method does not handle NaN and does not return the correct rangeType + * Gets the intersection range between two ranges + * @param src + * @param target + * @deprecated use `getIntersectRange` instead + * @example + * ```typescript + * const range1 = { startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }; + * const range2 = { startRow: 1, startColumn: 1, endRow: 3, endColumn: 3 }; + * const intersection = Rectangle.getIntersects(range1, range2); + * // intersection = { startRow: 1, startColumn: 1, endRow: 2, endColumn: 2 } + * ``` */ static getIntersects(src: IRange, target: IRange): Nullable { const currentStartRow = src.startRow; @@ -177,19 +243,17 @@ export class Rectangle { }; } - // static subtract(src: IRange, target: IRange): Nullable { - // const intersected = Rectangle.getIntersects(src, target); - // if (!intersected) { - // return [src]; - // } - - // const result: IRange[] = []; - // const { startRow, endRow, startColumn, endColumn } = intersected; - // const { startRow: srcStartRow, endRow: srcEndRow, startColumn: srcStartColumn, endColumn: srcEndColumn } = src; - - // // subtract could result in eight pieces and these eight pieces and be merged to at most four pieces - // } - + /** + * Checks if one range completely contains another range + * @param src + * @param target + * @example + * ```typescript + * const outer = { startRow: 0, startColumn: 0, endRow: 3, endColumn: 3 }; + * const inner = { startRow: 1, startColumn: 1, endRow: 2, endColumn: 2 }; + * const contains = Rectangle.contains(outer, inner); // true + * ``` + */ static contains(src: IRange, target: IRange): boolean { return ( src.startRow <= target.startRow && @@ -199,6 +263,17 @@ export class Rectangle { ); } + /** + * Checks if one range strictly contains another range (not equal) + * @param src + * @param target + * @example + * ```typescript + * const outer = { startRow: 0, startColumn: 0, endRow: 3, endColumn: 3 }; + * const same = { startRow: 0, startColumn: 0, endRow: 3, endColumn: 3 }; + * const realContains = Rectangle.realContain(outer, same); // false + * ``` + */ static realContain(src: IRange, target: IRange): boolean { return ( Rectangle.contains(src, target) && @@ -209,6 +284,17 @@ export class Rectangle { ); } + /** + * Creates a union range that encompasses all input ranges + * @param {...any} ranges + * @example + * ```typescript + * const range1 = { startRow: 0, startColumn: 0, endRow: 1, endColumn: 1 }; + * const range2 = { startRow: 2, startColumn: 2, endRow: 3, endColumn: 3 }; + * const union = Rectangle.union(range1, range2); + * // union = { startRow: 0, startColumn: 0, endRow: 3, endColumn: 3 } + * ``` + */ static union(...ranges: IRange[]): IRange { // TODO: range type may not be accurate return ranges.reduce( @@ -223,6 +309,23 @@ export class Rectangle { ); } + /** + * Creates a union range considering special range types (ROW, COLUMN) + * @param {...any} ranges + * @example + * ```typescript + * const rowRange = { + * startRow: 0, endRow: 2, + * rangeType: RANGE_TYPE.ROW + * }; + * const normalRange = { + * startRow: 1, startColumn: 1, + * endRow: 3, endColumn: 3 + * }; + * const union = Rectangle.realUnion(rowRange, normalRange); + * // Result will have NaN for columns due to ROW type + * ``` + */ static realUnion(...ranges: IRange[]): IRange { const hasColRange = ranges.some((range) => range.rangeType === RANGE_TYPE.COLUMN); const hasRowRange = ranges.some((range) => range.rangeType === RANGE_TYPE.ROW); @@ -236,6 +339,18 @@ export class Rectangle { }; } + /** + * Converts an absolute range to a relative range based on an origin range + * @param range + * @param originRange + * @example + * ```typescript + * const range = { startRow: 5, startColumn: 5, endRow: 7, endColumn: 7 }; + * const origin = { startRow: 3, startColumn: 3, endRow: 8, endColumn: 8 }; + * const relative = Rectangle.getRelativeRange(range, origin); + * // relative = { startRow: 2, startColumn: 2, endRow: 2, endColumn: 2 } + * ``` + */ static getRelativeRange = (range: IRange, originRange: IRange) => ({ startRow: range.startRow - originRange.startRow, @@ -244,41 +359,98 @@ export class Rectangle { endColumn: range.endColumn - range.startColumn, }) as IRange; - static getPositionRange = (relativeRange: IRange, originRange: IRange, absoluteRange?: IRange) => { - return ({ + /** + * Converts a relative range back to an absolute range based on origin + * @param relativeRange + * @param originRange + * @param absoluteRange + * @example + * ```typescript + * const relative = { startRow: 2, startColumn: 2, endRow: 2, endColumn: 2 }; + * const origin = { startRow: 3, startColumn: 3, endRow: 8, endColumn: 8 }; + * const absolute = Rectangle.getPositionRange(relative, origin); + * // absolute = { startRow: 5, startColumn: 5, endRow: 7, endColumn: 7 } + * ``` + */ + static getPositionRange = (relativeRange: IRange, originRange: IRange, absoluteRange?: IRange) => + ({ ...(absoluteRange || {}), startRow: absoluteRange ? ([AbsoluteRefType.ROW, AbsoluteRefType.ALL].includes(absoluteRange.startAbsoluteRefType || 0) ? absoluteRange.startRow : relativeRange.startRow + originRange.startRow) : (relativeRange.startRow + originRange.startRow), endRow: absoluteRange ? ([AbsoluteRefType.ROW, AbsoluteRefType.ALL].includes(absoluteRange.endAbsoluteRefType || 0) ? absoluteRange.endRow : relativeRange.endRow + relativeRange.startRow + originRange.startRow) : (relativeRange.endRow + relativeRange.startRow + originRange.startRow), startColumn: absoluteRange ? ([AbsoluteRefType.COLUMN, AbsoluteRefType.ALL].includes(absoluteRange.startAbsoluteRefType || 0) ? absoluteRange.startColumn : relativeRange.startColumn + originRange.startColumn) : (relativeRange.startColumn + originRange.startColumn), endColumn: absoluteRange ? ([AbsoluteRefType.COLUMN, AbsoluteRefType.ALL].includes(absoluteRange.endAbsoluteRefType || 0) ? absoluteRange.endColumn : relativeRange.endColumn + relativeRange.startColumn + originRange.startColumn) : (relativeRange.endColumn + relativeRange.startColumn + originRange.startColumn), }) as IRange; - }; - - static moveHorizontal = (range: IRange, step: number = 0, length: number = 0): IRange => ({ - ...range, - startColumn: range.startColumn + step, - endColumn: range.endColumn + step + length, - }); - - static moveVertical = (range: IRange, step: number = 0, length: number = 0): IRange => ({ - ...range, - startRow: range.startRow + step, - endRow: range.endRow + step + length, - }); - - static moveOffset = (range: IRange, offsetX: number, offsetY: number): IRange => ({ - ...range, - startRow: range.startRow + offsetY, - endRow: range.endRow + offsetY, - startColumn: range.startColumn + offsetX, - endColumn: range.endColumn + offsetX, - }); /** - * Subtract range2 from range1, the result is is horizontal first then vertical - * @param {IRange} range1 The source range - * @param {IRange} range2 The range to be subtracted - * @returns {IRange[]} Returns the array of ranges, which are the result not intersected with range1 + * Moves a range horizontally by a specified step and optionally extends it + * @param range + * @param step + * @param length + * @example + * ```typescript + * const range = { startRow: 0, startColumn: 0, endRow: 1, endColumn: 1 }; + * const moved = Rectangle.moveHorizontal(range, 2, 1); + * // moved = { startRow: 0, startColumn: 2, endRow: 1, endColumn: 4 } + * ``` + */ + static moveHorizontal = (range: IRange, step: number = 0, length: number = 0): IRange => + ({ + ...range, + startColumn: range.startColumn + step, + endColumn: range.endColumn + step + length, + }); + + /** + * Moves a range vertically by a specified step and optionally extends it + * @param range + * @param step + * @param length + * @example + * ```typescript + * const range = { startRow: 0, startColumn: 0, endRow: 1, endColumn: 1 }; + * const moved = Rectangle.moveVertical(range, 2, 1); + * // moved = { startRow: 2, startColumn: 0, endRow: 4, endColumn: 1 } + * ``` + */ + static moveVertical = (range: IRange, step: number = 0, length: number = 0): IRange => + ({ + ...range, + startRow: range.startRow + step, + endRow: range.endRow + step + length, + }); + + /** + * Moves a range by specified offsets in both directions + * @param range + * @param offsetX + * @param offsetY + * @example + * ```typescript + * const range = { startRow: 0, startColumn: 0, endRow: 1, endColumn: 1 }; + * const moved = Rectangle.moveOffset(range, 2, 3); + * // moved = { startRow: 3, startColumn: 2, endRow: 4, endColumn: 3 } + * ``` + */ + static moveOffset = (range: IRange, offsetX: number, offsetY: number): IRange => + ({ + ...range, + startRow: range.startRow + offsetY, + endRow: range.endRow + offsetY, + startColumn: range.startColumn + offsetX, + endColumn: range.endColumn + offsetX, + }); + + /** + * Subtracts one range from another, returning the remaining areas as separate ranges + * @param range1 + * @param range2 + * @example + * ```typescript + * const range1 = { startRow: 0, startColumn: 0, endRow: 3, endColumn: 3 }; + * const range2 = { startRow: 1, startColumn: 1, endRow: 2, endColumn: 2 }; + * const result = Rectangle.subtract(range1, range2); + * // Results in up to 4 ranges representing the non-overlapping areas + * ``` */ static subtract(range1: IRange, range2: IRange): IRange[] { // 如果没有交集,则返回 range1 @@ -342,18 +514,54 @@ export class Rectangle { } /** - * Combine smaller rectangles into larger ones + * Merges overlapping or adjacent ranges into larger ranges * @param ranges - * @returns + * @example + * ```typescript + * const ranges = [ + * { startRow: 0, startColumn: 0, endRow: 1, endColumn: 1 }, + * { startRow: 1, startColumn: 1, endRow: 2, endColumn: 2 } + * ]; + * const merged = Rectangle.mergeRanges(ranges); + * // Combines overlapping ranges into larger ones + * ``` */ static mergeRanges(ranges: IRange[]): IRange[] { return mergeRanges(ranges); } + /** + * Splits overlapping ranges into a grid of non-overlapping ranges + * @param ranges + * @example + * ```typescript + * const ranges = [ + * { startRow: 0, startColumn: 0, endRow: 2, endColumn: 2 }, + * { startRow: 1, startColumn: 1, endRow: 3, endColumn: 3 } + * ]; + * const grid = Rectangle.splitIntoGrid(ranges); + * // Splits into non-overlapping grid sections + * ``` + */ static splitIntoGrid(ranges: IRange[]): IRange[] { return splitIntoGrid(ranges); } + /** + * Subtracts multiple ranges from multiple ranges + * @param ranges1 + * @param ranges2 + * @example + * ```typescript + * const ranges1 = [{ startRow: 0, startColumn: 0, endRow: 3, endColumn: 3 }]; + * const ranges2 = [ + * { startRow: 1, startColumn: 1, endRow: 2, endColumn: 2 }, + * { startRow: 2, startColumn: 2, endRow: 3, endColumn: 3 } + * ]; + * const result = Rectangle.subtractMulti(ranges1, ranges2); + * // Returns remaining non-overlapping areas + * ``` + */ static subtractMulti(ranges1: IRange[], ranges2: IRange[]): IRange[] { if (!ranges2.length) { return ranges1; @@ -367,6 +575,17 @@ export class Rectangle { return res; } + /** + * Checks if two rectangles defined by left, top, right, bottom coordinates intersect + * @param rect1 + * @param rect2 + * @example + * ```typescript + * const rect1 = { left: 0, top: 0, right: 10, bottom: 10 }; + * const rect2 = { left: 5, top: 5, right: 15, bottom: 15 }; + * const intersects = Rectangle.hasIntersectionBetweenTwoRect(rect1, rect2); // true + * ``` + */ static hasIntersectionBetweenTwoRect(rect1: IRectLTRB, rect2: IRectLTRB) { if ( rect1.left > rect2.right || // rect1 在 rect2 右侧 @@ -380,6 +599,18 @@ export class Rectangle { return true; } + /** + * Gets the intersection area between two rectangles defined by LTRB coordinates + * @param rect1 + * @param rect2 + * @example + * ```typescript + * const rect1 = { left: 0, top: 0, right: 10, bottom: 10 }; + * const rect2 = { left: 5, top: 5, right: 15, bottom: 15 }; + * const intersection = Rectangle.getIntersectionBetweenTwoRect(rect1, rect2); + * // Returns { left: 5, top: 5, right: 10, bottom: 10, width: 5, height: 5 } + * ``` + */ static getIntersectionBetweenTwoRect(rect1: IRectLTRB, rect2: IRectLTRB) { // 计算两个矩形的交集部分的坐标 const left = Math.max(rect1.left, rect2.left); @@ -403,6 +634,19 @@ export class Rectangle { } as Required; } + /** + * Sorts an array of ranges by startRow, then by startColumn + * @param ranges + * @example + * ```typescript + * const ranges = [ + * { startRow: 1, startColumn: 0, endRow: 2, endColumn: 1 }, + * { startRow: 0, startColumn: 0, endRow: 1, endColumn: 1 } + * ]; + * const sorted = Rectangle.sort(ranges); + * // Ranges will be sorted by startRow first, then startColumn + * ``` + */ static sort(ranges: IRange[]) { return ranges.sort((a, b) => a.startRow - b.startRow || a.startColumn - b.startColumn); } diff --git a/packages/core/src/sheets/column-manager.ts b/packages/core/src/sheets/column-manager.ts index 1bc85fb67b8..17e325b532f 100644 --- a/packages/core/src/sheets/column-manager.ts +++ b/packages/core/src/sheets/column-manager.ts @@ -17,9 +17,10 @@ import type { IObjectArrayPrimitiveType } from '../shared/object-matrix'; import type { Nullable } from '../shared/types'; import type { IStyleData } from '../types/interfaces'; +import type { CustomData, IColumnData, IRange, IWorksheetData } from './typedef'; import { getArrayLength } from '../shared/object-matrix'; import { BooleanNumber } from '../types/enum'; -import { type IColumnData, type IRange, type IWorksheetData, RANGE_TYPE } from './typedef'; +import { RANGE_TYPE } from './typedef'; /** * Manage configuration information of all columns, get column width, column length, set column width, etc. @@ -226,4 +227,15 @@ export class ColumnManager { this._columnData[columnPos] = create; return create; } + + setCustomMetadata(index: number, custom: CustomData | undefined) { + const row = this.getColumn(index); + if (row) { + row.custom = custom; + } + } + + getCustomMetadata(index: number): CustomData | undefined { + return this.getColumn(index)?.custom; + } } diff --git a/packages/core/src/sheets/row-manager.ts b/packages/core/src/sheets/row-manager.ts index ad93b33db40..ad47971e1a8 100644 --- a/packages/core/src/sheets/row-manager.ts +++ b/packages/core/src/sheets/row-manager.ts @@ -16,10 +16,11 @@ import type { Nullable } from '../shared/types'; import type { IStyleData } from '../types/interfaces'; +import type { CustomData, IRange, IRowData, IWorksheetData } from './typedef'; import type { SheetViewModel } from './view-model'; import { getArrayLength, type IObjectArrayPrimitiveType } from '../shared/object-matrix'; import { BooleanNumber } from '../types/enum'; -import { type IRange, type IRowData, type IWorksheetData, RANGE_TYPE } from './typedef'; +import { RANGE_TYPE } from './typedef'; /** * Manage configuration information of all rows, get row height, row length, set row height, etc. @@ -216,4 +217,15 @@ export class RowManager { getSize(): number { return getArrayLength(this._rowData); } + + setCustomMetadata(index: number, custom: CustomData | undefined) { + const row = this.getRow(index); + if (row) { + row.custom = custom; + } + } + + getCustomMetadata(index: number): CustomData | undefined { + return this.getRow(index)?.custom; + } } diff --git a/packages/core/src/sheets/typedef.ts b/packages/core/src/sheets/typedef.ts index 3bb17ba4e2f..6e00ff075e6 100644 --- a/packages/core/src/sheets/typedef.ts +++ b/packages/core/src/sheets/typedef.ts @@ -74,6 +74,11 @@ export interface IWorkbookData { * Resources of the Univer Sheet. It is used to store the data of other plugins. */ resources?: IResources; + + /** + * User stored custom fields + */ + custom?: CustomData; } /** @@ -143,6 +148,11 @@ export interface IWorksheetData { gridlinesColor?: string; rightToLeft: BooleanNumber; + + /** + * User stored custom fields + */ + custom?: CustomData; } export type CustomData = Nullable>; diff --git a/packages/core/src/sheets/workbook.ts b/packages/core/src/sheets/workbook.ts index 8a091f1107c..e2ed51c1a8f 100644 --- a/packages/core/src/sheets/workbook.ts +++ b/packages/core/src/sheets/workbook.ts @@ -17,7 +17,7 @@ import type { Observable } from 'rxjs'; import type { Nullable } from '../shared'; -import type { IRangeType, IWorkbookData, IWorksheetData } from './typedef'; +import type { CustomData, IRangeType, IWorkbookData, IWorksheetData } from './typedef'; import { BehaviorSubject, Subject } from 'rxjs'; import { UnitModel, UniverInstanceType } from '../common/unit'; import { ILogService } from '../services/log/log.service'; @@ -424,4 +424,20 @@ export class Workbook extends UnitModel { } +/** + * + * @param statusUpdate + */ function shouldStateUpdateTriggerResearch(statusUpdate: Partial): boolean { if (typeof statusUpdate.findString !== 'undefined') return true; if (typeof statusUpdate.inputtingFindString !== 'undefined') return true; @@ -500,6 +502,9 @@ export interface IFindReplaceState { findBy: FindBy; } +/** + * + */ export function createInitFindReplaceState(): IFindReplaceState { return { caseSensitive: false, diff --git a/packages/sheets-data-validation/src/facade/f-event.ts b/packages/sheets-data-validation/src/facade/f-event.ts new file mode 100644 index 00000000000..94ebfcf505a --- /dev/null +++ b/packages/sheets-data-validation/src/facade/f-event.ts @@ -0,0 +1,324 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { DataValidationStatus, IDataValidationRule, IDataValidationRuleBase, IDataValidationRuleOptions, IEventBase, IRange, ISheetDataValidationRule } from '@univerjs/core'; +import type { DataValidationChangeType, IRuleChange } from '@univerjs/data-validation'; +import type { FWorkbook, FWorksheet } from '@univerjs/sheets/facade'; +import type { FDataValidation } from './f-data-validation'; +import { FEventName } from '@univerjs/core'; + +/** + * Event interface triggered when a data validation rule is changed + * @interface ISheetDataValidationChangedEvent + * @augments {IEventBase} + */ +export interface ISheetDataValidationChangedEvent extends IEventBase { + /** The source of the rule change */ + origin: IRuleChange; + /** The worksheet containing the validation rule */ + worksheet: FWorksheet; + /** The workbook instance */ + workbook: FWorkbook; + /** Type of change made to the validation rule */ + changeType: DataValidationChangeType; + /** The previous validation rule, if it exists */ + oldRule?: IDataValidationRule; + /** The new or modified validation rule */ + rule: FDataValidation; +} + +/** + * Event interface triggered when a data validation status changes + * @interface ISheetDataValidatorStatusChangedEvent + * @augments {IEventBase} + */ +export interface ISheetDataValidatorStatusChangedEvent extends IEventBase { + /** The worksheet containing the validation */ + worksheet: FWorksheet; + /** The workbook instance */ + workbook: FWorkbook; + /** Row index of the validated cell */ + row: number; + /** Column index of the validated cell */ + column: number; + /** Current validation status */ + status: DataValidationStatus; + /** The validation rule that was checked */ + rule: FDataValidation; +} + +/** + * Event interface triggered before adding a new data validation rule + * @interface IBeforeSheetDataValidationAddEvent + * @augments {IEventBase} + */ +export interface IBeforeSheetDataValidationAddEvent extends IEventBase { + /** The worksheet to add the validation to */ + worksheet: FWorksheet; + /** The workbook instance */ + workbook: FWorkbook; + /** The validation rule to be added */ + rule: ISheetDataValidationRule; +} + +/** + * Event interface triggered before deleting a data validation rule + * @interface IBeforeSheetDataValidationDeleteEvent + * @augments {IEventBase} + */ +export interface IBeforeSheetDataValidationDeleteEvent extends IEventBase { + /** The worksheet containing the validation */ + worksheet: FWorksheet; + /** The workbook instance */ + workbook: FWorkbook; + /** Unique identifier of the rule to be deleted */ + ruleId: string; + /** The validation rule to be deleted */ + rule: FDataValidation; +} + +/** + * Event interface triggered before updating a data validation rule's criteria + * @interface IBeforeSheetDataValidationCriteriaUpdateEvent + * @augments {IEventBase} + */ +export interface IBeforeSheetDataValidationCriteriaUpdateEvent extends IEventBase { + /** The worksheet containing the validation */ + worksheet: FWorksheet; + /** The workbook instance */ + workbook: FWorkbook; + /** Unique identifier of the rule to be updated */ + ruleId: string; + /** The current validation rule */ + rule: FDataValidation; + /** The new criteria to be applied */ + newCriteria: IDataValidationRuleBase; +} + +/** + * Event interface triggered before updating a data validation rule's ranges + * @interface IBeforeSheetDataValidationRangeUpdateEvent + * @augments {IEventBase} + */ +export interface IBeforeSheetDataValidationRangeUpdateEvent extends IEventBase { + /** The worksheet containing the validation */ + worksheet: FWorksheet; + /** The workbook instance */ + workbook: FWorkbook; + /** Unique identifier of the rule to be updated */ + ruleId: string; + /** The current validation rule */ + rule: FDataValidation; + /** The new ranges to be applied */ + newRanges: IRange[]; +} + +/** + * Event interface triggered before updating a data validation rule's options + * @interface IBeforeSheetDataValidationOptionsUpdateEvent + * @augments {IEventBase} + */ +export interface IBeforeSheetDataValidationOptionsUpdateEvent extends IEventBase { + /** The worksheet containing the validation */ + worksheet: FWorksheet; + /** The workbook instance */ + workbook: FWorkbook; + /** Unique identifier of the rule to be updated */ + ruleId: string; + /** The current validation rule */ + rule: FDataValidation; + /** The new options to be applied */ + newOptions: IDataValidationRuleOptions; +} + +/** + * Event interface triggered before deleting all data validation rules + * @interface IBeforeSheetDataValidationDeleteAllEvent + * @augments {IEventBase} + */ +export interface IBeforeSheetDataValidationDeleteAllEvent extends IEventBase { + /** The worksheet containing the validations */ + worksheet: FWorksheet; + /** The workbook instance */ + workbook: FWorkbook; + /** Array of all validation rules to be deleted */ + rules: FDataValidation[]; +} + +export interface IDataValidationEventParamConfig { + SheetDataValidationChanged: ISheetDataValidationChangedEvent; + SheetDataValidatorStatusChanged: ISheetDataValidatorStatusChangedEvent; + BeforeSheetDataValidationAdd: IBeforeSheetDataValidationAddEvent; + BeforeSheetDataValidationDelete: IBeforeSheetDataValidationDeleteEvent; + BeforeSheetDataValidationDeleteAll: IBeforeSheetDataValidationDeleteAllEvent; + BeforeSheetDataValidationCriteriaUpdate: IBeforeSheetDataValidationCriteriaUpdateEvent; + BeforeSheetDataValidationRangeUpdate: IBeforeSheetDataValidationRangeUpdateEvent; + BeforeSheetDataValidationOptionsUpdate: IBeforeSheetDataValidationOptionsUpdateEvent; +} + +interface IDataValidationEvent { + /** + * Event fired when a rule is added, deleted, or modified + * @see {@link ISheetDataValidationChangedEvent} + * @example + * ```ts + * univerAPI.on(univerAPI.Event.SheetDataValidationChanged, (event) => { + * const { worksheet, workbook, changeType, oldRule, rule } = event; + * console.log(event); + * }); + * ``` + */ + readonly SheetDataValidationChanged: 'SheetDataValidationChanged'; + /** + * Event fired when a cell validator status is changed + * @see {@link ISheetDataValidatorStatusChangedEvent} + * @example + * ```ts + * univerAPI.on(univerAPI.Event.SheetDataValidatorStatusChanged, (event) => { + * const { worksheet, workbook, row, column, status, rule } = event; + * console.log(event); + * }); + * ``` + */ + readonly SheetDataValidatorStatusChanged: 'SheetDataValidatorStatusChanged'; + /** + * Event fired before a rule is added + * @see {@link IBeforeSheetDataValidationAddEvent} + * @example + * ```ts + * univerAPI.on(univerAPI.Event.BeforeSheetDataValidationAdd, (event) => { + * const { worksheet, workbook, rule } = event; + * console.log(event); + * }); + * ``` + */ + readonly BeforeSheetDataValidationAdd: 'BeforeSheetDataValidationAdd'; + /** + * Event fired before a rule is deleted + * @see {@link IBeforeSheetDataValidationDeleteEvent} + * @example + * ```ts + * univerAPI.on(univerAPI.Event.BeforeSheetDataValidationDelete, (event) => { + * const { worksheet, workbook, rule } = event; + * console.log(event); + * }); + * ``` + */ + readonly BeforeSheetDataValidationDelete: 'BeforeSheetDataValidationDelete'; + /** + * Event fired before all rules are deleted + * @see {@link IBeforeSheetDataValidationDeleteAllEvent} + * @example + * ```ts + * univerAPI.on(univerAPI.Event.BeforeSheetDataValidationDeleteAll, (event) => { + * const { worksheet, workbook, rules } = event; + * console.log(event); + * }); + * ``` + */ + readonly BeforeSheetDataValidationDeleteAll: 'BeforeSheetDataValidationDeleteAll'; + /** + * Event fired before the criteria of a rule are updated + * @see {@link IBeforeSheetDataValidationCriteriaUpdateEvent} + * @example + * ```ts + * univerAPI.on(univerAPI.Event.BeforeSheetDataValidationCriteriaUpdate, (event) => { + * const { worksheet, workbook, rule, newCriteria } = event; + * console.log(event); + * }); + * ``` + */ + readonly BeforeSheetDataValidationCriteriaUpdate: 'BeforeSheetDataValidationCriteriaUpdate'; + /** + * Event fired before the range of a rule is updated + * @see {@link IBeforeSheetDataValidationRangeUpdateEvent} + * @example + * ```ts + * univerAPI.on(univerAPI.Event.BeforeSheetDataValidationRangeUpdate, (event) => { + * const { worksheet, workbook, rule, newRanges } = event; + * console.log(event); + * }); + * ``` + */ + readonly BeforeSheetDataValidationRangeUpdate: 'BeforeSheetDataValidationRangeUpdate'; + /** + * Event fired before the options of a rule are updated + * @see {@link IBeforeSheetDataValidationOptionsUpdateEvent} + * @example + * ```ts + * univerAPI.on(univerAPI.Event.BeforeSheetDataValidationOptionsUpdate, (event) => { + * const { worksheet, workbook, rule, newOptions } = event; + * console.log(event); + * }); + * ``` + */ + readonly BeforeSheetDataValidationOptionsUpdate: 'BeforeSheetDataValidationOptionsUpdate'; +} + +export class FDataValidationEvent implements IDataValidationEvent { + get SheetDataValidationChanged(): 'SheetDataValidationChanged' { + return 'SheetDataValidationChanged'; + } + + get SheetDataValidatorStatusChanged(): 'SheetDataValidatorStatusChanged' { + return 'SheetDataValidatorStatusChanged'; + } + + get BeforeSheetDataValidationAdd(): 'BeforeSheetDataValidationAdd' { + return 'BeforeSheetDataValidationAdd'; + } + + get BeforeSheetDataValidationDelete(): 'BeforeSheetDataValidationDelete' { + return 'BeforeSheetDataValidationDelete'; + } + + get BeforeSheetDataValidationDeleteAll(): 'BeforeSheetDataValidationDeleteAll' { + return 'BeforeSheetDataValidationDeleteAll'; + } + + get BeforeSheetDataValidationCriteriaUpdate(): 'BeforeSheetDataValidationCriteriaUpdate' { + return 'BeforeSheetDataValidationCriteriaUpdate'; + } + + get BeforeSheetDataValidationRangeUpdate(): 'BeforeSheetDataValidationRangeUpdate' { + return 'BeforeSheetDataValidationRangeUpdate'; + } + + get BeforeSheetDataValidationOptionsUpdate(): 'BeforeSheetDataValidationOptionsUpdate' { + return 'BeforeSheetDataValidationOptionsUpdate'; + } +} + +export interface IDataValidationEventConfig { + SheetDataValidationChanged: ISheetDataValidationChangedEvent; + SheetDataValidatorStatusChanged: ISheetDataValidatorStatusChangedEvent; + BeforeSheetDataValidationAdd: IBeforeSheetDataValidationAddEvent; + BeforeSheetDataValidationDelete: IBeforeSheetDataValidationDeleteEvent; + BeforeSheetDataValidationDeleteAll: IBeforeSheetDataValidationDeleteAllEvent; + BeforeSheetDataValidationCriteriaUpdate: IBeforeSheetDataValidationCriteriaUpdateEvent; + BeforeSheetDataValidationRangeUpdate: IBeforeSheetDataValidationRangeUpdateEvent; + BeforeSheetDataValidationOptionsUpdate: IBeforeSheetDataValidationOptionsUpdateEvent; +} + +FEventName.extend(FDataValidationEvent); +declare module '@univerjs/core' { + // eslint-disable-next-line ts/naming-convention + interface FEventName extends IDataValidationEvent { + } + + interface IEventParamConfig extends IDataValidationEventParamConfig { + } +} diff --git a/packages/sheets-data-validation/src/facade/f-univer.ts b/packages/sheets-data-validation/src/facade/f-univer.ts index a765dc2aab6..e89abfb3b9d 100644 --- a/packages/sheets-data-validation/src/facade/f-univer.ts +++ b/packages/sheets-data-validation/src/facade/f-univer.ts @@ -14,20 +14,218 @@ * limitations under the License. */ -import { FUniver } from '@univerjs/core'; +import type { Injector } from '@univerjs/core'; +import type { IAddSheetDataValidationCommandParams, IRemoveSheetAllDataValidationCommandParams, IRemoveSheetDataValidationCommandParams, IUpdateSheetDataValidationOptionsCommandParams, IUpdateSheetDataValidationRangeCommandParams, IUpdateSheetDataValidationSettingCommandParams } from '@univerjs/sheets-data-validation'; +import type { IBeforeSheetDataValidationAddEvent, IBeforeSheetDataValidationCriteriaUpdateEvent, IBeforeSheetDataValidationDeleteAllEvent, IBeforeSheetDataValidationDeleteEvent, IBeforeSheetDataValidationOptionsUpdateEvent, IBeforeSheetDataValidationRangeUpdateEvent } from './f-event'; +import { CanceledError, FUniver, ICommandService } from '@univerjs/core'; +import { AddSheetDataValidationCommand, RemoveSheetAllDataValidationCommand, RemoveSheetDataValidationCommand, SheetDataValidationModel, UpdateSheetDataValidationOptionsCommand, UpdateSheetDataValidationRangeCommand, UpdateSheetDataValidationSettingCommand } from '@univerjs/sheets-data-validation'; +import { FDataValidation } from './f-data-validation'; import { FDataValidationBuilder } from './f-data-validation-builder'; -export class FUnvierDataValidationMixin { +export class FUnvierDataValidationMixin extends FUniver { + /** /** * @deparecated use `univerAPI.newDataValidation()` as instead. */ - static newDataValidation(): FDataValidationBuilder { + static override newDataValidation(): FDataValidationBuilder { return new FDataValidationBuilder(); } newDataValidation(): FDataValidationBuilder { return new FDataValidationBuilder(); } + + // eslint-disable-next-line max-lines-per-function + override _initialize(injector: Injector): void { + if (!injector.has(SheetDataValidationModel)) return; + const sheetDataValidationModel = injector.get(SheetDataValidationModel); + const commadnService = injector.get(ICommandService); + + this.disposeWithMe(sheetDataValidationModel.ruleChange$.subscribe((ruleChange) => { + const { unitId, subUnitId, rule, oldRule, type } = ruleChange; + const target = this.getSheetTarget(unitId, subUnitId); + if (!target) { + return; + } + const { workbook, worksheet } = target; + + const fRule = new FDataValidation(rule, worksheet.getSheet(), this._injector); + this.fireEvent(this.Event.SheetDataValidationChanged, { + origin: ruleChange, + worksheet, + workbook, + changeType: type, + oldRule, + rule: fRule, + }); + })); + + this.disposeWithMe(sheetDataValidationModel.validStatusChange$.subscribe((statusChange) => { + const { unitId, subUnitId, ruleId, status, row, col } = statusChange; + const target = this.getSheetTarget(unitId, subUnitId); + if (!target) { + return; + } + const { workbook, worksheet } = target; + const rule = worksheet.getDataValidation(ruleId); + if (!rule) { + return; + } + this.fireEvent(this.Event.SheetDataValidatorStatusChanged, { + workbook, + worksheet, + row, + column: col, + rule, + status, + }); + })); + + // eslint-disable-next-line max-lines-per-function, complexity + this.disposeWithMe(commadnService.beforeCommandExecuted((commandInfo) => { + switch (commandInfo.id) { + case AddSheetDataValidationCommand.id: { + const params = commandInfo.params as IAddSheetDataValidationCommandParams; + const target = this.getSheetTarget(params.unitId, params.subUnitId); + if (!target) { + return; + } + const { workbook, worksheet } = target; + const eventParams: IBeforeSheetDataValidationAddEvent = { + worksheet, + workbook, + rule: params.rule, + }; + this.fireEvent(this.Event.BeforeSheetDataValidationAdd, eventParams); + if (eventParams.cancel) { + throw new CanceledError(); + } + break; + } + + case UpdateSheetDataValidationSettingCommand.id: { + const params = commandInfo.params as IUpdateSheetDataValidationSettingCommandParams; + const target = this.getSheetTarget(params.unitId, params.subUnitId); + if (!target) { + return; + } + const { workbook, worksheet } = target; + const rule = worksheet.getDataValidation(params.ruleId); + if (!rule) { + return; + } + const eventParams: IBeforeSheetDataValidationCriteriaUpdateEvent = { + worksheet, + workbook, + rule, + ruleId: params.ruleId, + newCriteria: params.setting, + }; + this.fireEvent(this.Event.BeforeSheetDataValidationCriteriaUpdate, eventParams); + if (eventParams.cancel) { + throw new CanceledError(); + } + break; + } + + case UpdateSheetDataValidationRangeCommand.id: { + const params = commandInfo.params as IUpdateSheetDataValidationRangeCommandParams; + const target = this.getSheetTarget(params.unitId, params.subUnitId); + if (!target) { + return; + } + const { workbook, worksheet } = target; + const rule = worksheet.getDataValidation(params.ruleId); + if (!rule) { + return; + } + const eventParams: IBeforeSheetDataValidationRangeUpdateEvent = { + worksheet, + workbook, + rule, + ruleId: params.ruleId, + newRanges: params.ranges, + }; + this.fireEvent(this.Event.BeforeSheetDataValidationRangeUpdate, eventParams); + if (eventParams.cancel) { + throw new CanceledError(); + } + break; + } + + case UpdateSheetDataValidationOptionsCommand.id: { + const params = commandInfo.params as IUpdateSheetDataValidationOptionsCommandParams; + const target = this.getSheetTarget(params.unitId, params.subUnitId); + if (!target) { + return; + } + const { workbook, worksheet } = target; + const rule = worksheet.getDataValidation(params.ruleId); + if (!rule) { + return; + } + const eventParams: IBeforeSheetDataValidationOptionsUpdateEvent = { + worksheet, + workbook, + rule, + ruleId: params.ruleId, + newOptions: params.options, + }; + this.fireEvent(this.Event.BeforeSheetDataValidationOptionsUpdate, eventParams); + if (eventParams.cancel) { + throw new CanceledError(); + } + break; + } + + case RemoveSheetDataValidationCommand.id: { + const params = commandInfo.params as IRemoveSheetDataValidationCommandParams; + const target = this.getSheetTarget(params.unitId, params.subUnitId); + if (!target) { + return; + } + const { workbook, worksheet } = target; + const rule = worksheet.getDataValidation(params.ruleId); + if (!rule) { + return; + } + const eventParams: IBeforeSheetDataValidationDeleteEvent = { + worksheet, + workbook, + rule, + ruleId: params.ruleId, + }; + this.fireEvent(this.Event.BeforeSheetDataValidationDelete, eventParams); + if (eventParams.cancel) { + throw new CanceledError(); + } + break; + } + + case RemoveSheetAllDataValidationCommand.id: { + const params = commandInfo.params as IRemoveSheetAllDataValidationCommandParams; + const target = this.getSheetTarget(params.unitId, params.subUnitId); + if (!target) { + return; + } + const { workbook, worksheet } = target; + const eventParams: IBeforeSheetDataValidationDeleteAllEvent = { + worksheet, + workbook, + rules: worksheet.getDataValidations(), + }; + this.fireEvent(this.Event.BeforeSheetDataValidationDeleteAll, eventParams); + if (eventParams.cancel) { + throw new CanceledError(); + } + break; + } + + default: + break; + } + })); + } } FUniver.extend(FUnvierDataValidationMixin); diff --git a/packages/sheets-data-validation/src/facade/f-worksheet.ts b/packages/sheets-data-validation/src/facade/f-worksheet.ts index 2075e25d051..47ff94fc076 100644 --- a/packages/sheets-data-validation/src/facade/f-worksheet.ts +++ b/packages/sheets-data-validation/src/facade/f-worksheet.ts @@ -24,13 +24,36 @@ export interface IFWorksheetDataValidationMixin { /** * Get all data validation rules in current sheet. * @returns all data validation rules + * ```ts + * const workbook = univerAPI.getActiveUniverSheet(); + * const worksheet = workbook.getWorksheet('sheet1'); + * const dataValidations = worksheet.getDataValidations(); + * ``` */ getDataValidations(): FDataValidation[]; /** * Get data validation validator status for current sheet. * @returns matrix of validator status + * ```ts + * const workbook = univerAPI.getActiveUniverSheet(); + * const worksheet = workbook.getWorksheet('sheet1'); + * const validatorStatus = worksheet.getValidatorStatus(); + * ``` */ getValidatorStatus(): Promise>>; + + /** + * get data validation rule by rule id + * @param ruleId - the rule id + * @returns data validation rule + * ```ts + * const workbook = univerAPI.getActiveUniverSheet(); + * const worksheet = workbook.getWorksheet('sheet1'); + * const dataValidation = worksheet.getDataValidation('ruleId'); + * ``` + */ + getDataValidation(ruleId: string): Nullable; + } export class FWorksheetDataValidationMixin extends FWorksheet implements IFWorksheetDataValidationMixin { @@ -46,6 +69,15 @@ export class FWorksheetDataValidationMixin extends FWorksheet implements IFWorks this._worksheet.getSheetId() ); } + + override getDataValidation(ruleId: string): Nullable { + const dataValidationModel = this._injector.get(DataValidationModel); + const rule = dataValidationModel.getRuleById(this._workbook.getUnitId(), this._worksheet.getSheetId(), ruleId); + if (rule) { + return new FDataValidation(rule, this._worksheet, this._injector); + } + return null; + } } FWorksheet.extend(FWorksheetDataValidationMixin); diff --git a/packages/sheets-data-validation/src/facade/index.ts b/packages/sheets-data-validation/src/facade/index.ts index bee8e3e0f0a..9c4af92e3f2 100644 --- a/packages/sheets-data-validation/src/facade/index.ts +++ b/packages/sheets-data-validation/src/facade/index.ts @@ -18,10 +18,12 @@ import './f-range'; import './f-univer'; import './f-workbook'; import './f-worksheet'; +import './f-event'; export { FDataValidation } from './f-data-validation'; export { FDataValidationBuilder } from './f-data-validation-builder'; +export type * from './f-event'; export type * from './f-range'; export type * from './f-univer'; export type * from './f-workbook'; diff --git a/packages/sheets-find-replace/src/facade/f-text-finder.ts b/packages/sheets-find-replace/src/facade/f-text-finder.ts index 6a5f5a1e6c8..d37c890f7c6 100644 --- a/packages/sheets-find-replace/src/facade/f-text-finder.ts +++ b/packages/sheets-find-replace/src/facade/f-text-finder.ts @@ -25,7 +25,6 @@ export interface IFTextFinder { * get all the matched range in the univer * @returns all the matched range * @throws if the find operation is not completed - * * @example * ```typescript * const textFinder = await univerAPI.createTextFinderAsync('hello'); @@ -41,7 +40,6 @@ export interface IFTextFinder { * @returns the next matched range * @throws if the find operation is not completed * @returns null if no more match - * * @example * ```typescript * const textFinder = await univerAPI.createTextFinderAsync('hello'); @@ -57,7 +55,6 @@ export interface IFTextFinder { * @returns the previous matched range * @throws if the find operation is not completed * @returns null if no more match - * * @example * ```typescript * const textFinder = await univerAPI.createTextFinderAsync('hello'); @@ -72,7 +69,6 @@ export interface IFTextFinder { * get the current matched range in the univer * @returns the current matched range * @throws if the find operation is not completed - * * @example * ```typescript * const textFinder = await univerAPI.createTextFinderAsync('hello'); @@ -88,7 +84,6 @@ export interface IFTextFinder { * set the match case option * @param {boolean} matchCase whether to match case * @returns text-finder instance - * * @example * ```typescript * const textFinder = await univerAPI.createTextFinderAsync('hello'); @@ -100,7 +95,6 @@ export interface IFTextFinder { * set the match entire cell option * @param {boolean} matchEntireCell whether to match entire cell * @returns text-finder instance - * * @example * ```typescript * const textFinder = await univerAPI.createTextFinderAsync('hello'); @@ -112,7 +106,6 @@ export interface IFTextFinder { * set the match formula text option * @param {boolean} matchFormulaText whether to match formula text * @returns text-finder instance - * * @example * ```typescript * const textFinder = await univerAPI.createTextFinderAsync('hello'); @@ -125,7 +118,6 @@ export interface IFTextFinder { * @param {string} replaceText the text to replace * @returns the number of replaced text * @throws if the find operation is not completed - * * @example * ```typescript * const textFinder = await univerAPI.createTextFinderAsync('hello'); @@ -139,7 +131,6 @@ export interface IFTextFinder { * @param {string} replaceText the text to replace * @returns whether the replace is successful * @throws if the find operation is not completed - * * @example * ```typescript * const textFinder = await univerAPI.createTextFinderAsync('hello'); @@ -151,7 +142,6 @@ export interface IFTextFinder { /** * ensure the find operation is completed * @returns the find complete result - * * @example * ```typescript * const textFinder = await univerAPI.createTextFinderAsync('hello'); diff --git a/packages/sheets-find-replace/src/facade/f-univer.ts b/packages/sheets-find-replace/src/facade/f-univer.ts index 99aa55569c3..1205131ba4f 100644 --- a/packages/sheets-find-replace/src/facade/f-univer.ts +++ b/packages/sheets-find-replace/src/facade/f-univer.ts @@ -21,11 +21,8 @@ import { FTextFinder } from './f-text-finder'; export interface IFUniverFindReplaceMixin { /** * Create a text-finder for the current univer. - * * @param {string} text - The text to find. - * * @returns {Promise} A promise that resolves to the text-finder instance. - * * @example * ```typescript * const textFinder = await univerAPI.createTextFinderAsync('Hello'); diff --git a/packages/sheets-hyper-link/src/commands/commands/remove-hyper-link.command.ts b/packages/sheets-hyper-link/src/commands/commands/remove-hyper-link.command.ts index 30a11e09d07..127d1052313 100644 --- a/packages/sheets-hyper-link/src/commands/commands/remove-hyper-link.command.ts +++ b/packages/sheets-hyper-link/src/commands/commands/remove-hyper-link.command.ts @@ -63,7 +63,7 @@ export const CancelHyperLinkCommand: ICommand = { const range = snapshot.body?.customRanges?.find((range) => `${range.rangeId}` === id); if (!range) return false; - const textX = BuildTextUtils.customRange.delete(accessor, { documentDataModel: doc.documentModel, rangeId: range.rangeId }); + const textX = BuildTextUtils.customRange.delete({ documentDataModel: doc.documentModel, rangeId: range.rangeId }); if (!textX) return false; const newBody = TextX.apply(snapshot.body!, textX.serialize()); diff --git a/packages/sheets-hyper-link/src/facade/f-range.ts b/packages/sheets-hyper-link/src/facade/f-range.ts index 9b6ca40e4fd..cf5df3728c8 100644 --- a/packages/sheets-hyper-link/src/facade/f-range.ts +++ b/packages/sheets-hyper-link/src/facade/f-range.ts @@ -16,7 +16,7 @@ import type { IAddHyperLinkCommandParams, ICancelHyperLinkCommandParams, IUpdateHyperLinkCommandParams } from '@univerjs/sheets-hyper-link'; import { CustomRangeType, DataStreamTreeTokenType, generateRandomId } from '@univerjs/core'; -import { AddHyperLinkCommand, CancelHyperLinkCommand, UpdateHyperLinkCommand } from '@univerjs/sheets-hyper-link'; +import { AddHyperLinkCommand, CancelHyperLinkCommand, SheetsHyperLinkParserService, UpdateHyperLinkCommand } from '@univerjs/sheets-hyper-link'; import { FRange } from '@univerjs/sheets/facade'; export interface ICellHyperLink { @@ -29,39 +29,34 @@ export interface ICellHyperLink { export interface IFRangeHyperlinkMixin { /** - * Set hyperlink in the cell in the range. - * [!important] This method is async. - * @param url url - * @param label optional, label of the url - * @returns success or not + * @deprecated use `range.setRichTextValueForCell(univerAPI.newRichText().insertLink(label, url))` instead */ setHyperLink(url: string, label?: string): Promise; /** - * Get all hyperlinks in the cell in the range. - * @returns hyperlinks + * @deprecated use `range.setRichTextValueForCell(range.getRichTextValue().getLinks())` instead */ getHyperLinks(): ICellHyperLink[]; /** - * Update hyperlink in the cell in the range. - * [!important] This method is async. - * @param id id of the hyperlink - * @param url url - * @param label optional, label of the url - * @returns success or not + * @deprecated use `range.setRichTextValueForCell(range.getRichTextValue().copy().updateLink(id, url))` instead */ updateHyperLink(id: string, url: string, label?: string): Promise; /** - * Cancel hyperlink in the cell in the range. - * [!important] This method is async. - * @param id id of the hyperlink - * @returns success or not + * @deprecated use `range.setRichTextValueForCell(range.getRichTextValue().copy().cancelLink(id))` instead */ cancelHyperLink(id: string): boolean; + + /** + * Get the url of this range. + */ + getUrl(): string; } export class FRangeHyperlinkMixin extends FRange implements IFRangeHyperlinkMixin { // #region hyperlink + /** + * @deprecated + */ override setHyperLink(url: string, label?: string): Promise { const params: IAddHyperLinkCommandParams = { unitId: this.getUnitId(), @@ -79,8 +74,7 @@ export class FRangeHyperlinkMixin extends FRange implements IFRangeHyperlinkMixi } /** - * Get all hyperlinks in the cell in the range. - * @returns hyperlinks + * @deprecated */ override getHyperLinks(): ICellHyperLink[] { const cellValue = this._worksheet.getCellRaw(this._range.startRow, this._range.startColumn); @@ -100,12 +94,7 @@ export class FRangeHyperlinkMixin extends FRange implements IFRangeHyperlinkMixi } /** - * Update hyperlink in the cell in the range. - * [!important] This method is async. - * @param id id of the hyperlink - * @param url url - * @param label optional, label of the url - * @returns success or not + * @deprecated */ override updateHyperLink(id: string, url: string, label?: string): Promise { const params: IUpdateHyperLinkCommandParams = { @@ -124,9 +113,7 @@ export class FRangeHyperlinkMixin extends FRange implements IFRangeHyperlinkMixi } /** - * Cancel hyperlink in the cell in the range. - * @param id id of the hyperlink - * @returns success or not + * @deprecated */ override cancelHyperLink(id: string): boolean { const params: ICancelHyperLinkCommandParams = { @@ -140,6 +127,11 @@ export class FRangeHyperlinkMixin extends FRange implements IFRangeHyperlinkMixi return this._commandService.syncExecuteCommand(CancelHyperLinkCommand.id, params); } + override getUrl(): string { + const parserService = this._injector.get(SheetsHyperLinkParserService); + return parserService.buildHyperLink(this.getUnitId(), this.getSheetId(), this.getRange()); + } + // #endregion } diff --git a/packages/sheets-hyper-link/src/facade/f-workbook.ts b/packages/sheets-hyper-link/src/facade/f-workbook.ts index d4ffb2d1a4c..b28e6b88ff8 100644 --- a/packages/sheets-hyper-link/src/facade/f-workbook.ts +++ b/packages/sheets-hyper-link/src/facade/f-workbook.ts @@ -14,23 +14,27 @@ * limitations under the License. */ -import type { IRange } from '@univerjs/core'; import type { ISheetHyperLinkInfo } from '@univerjs/sheets-hyper-link'; +import type { FRange } from '@univerjs/sheets/facade'; +import { Inject, type IRange } from '@univerjs/core'; import { SheetsHyperLinkParserService } from '@univerjs/sheets-hyper-link'; import { FWorkbook } from '@univerjs/sheets/facade'; +export class SheetHyperLinkBuilder { + constructor( + private _workbook: FWorkbook, + @Inject(SheetsHyperLinkParserService) private readonly _parserService: SheetsHyperLinkParserService + ) {} + + getRangeUrl(range: FRange): this { + this._parserService.buildHyperLink(this._workbook.getId(), range.getSheetId(), range.getRange()); + return this; + } +} + export interface IFWorkbookHyperlinkMixin { /** - * Create a hyperlink for the sheet. - * @param sheetId the sheet id to link - * @param range the range to link, or define-name id - * @returns the hyperlink string - * @example - * ```ts - * let range = univerAPI.getActiveWorkbook().getActiveSheet().getRange(5, 5); - * // return something like '#gid=sheet_Id&range=F6' - * univerAPI.getActiveWorkbook().createSheetHyperlink('sheet_Id', r.getRange()); - * ``` + * @deprecated use `getUrl` method in `FRange` or `FWorksheet` instead. */ createSheetHyperlink(this: FWorkbook, sheetId: string, range?: string | IRange): string; diff --git a/packages/sheets-hyper-link/src/facade/f-worksheet.ts b/packages/sheets-hyper-link/src/facade/f-worksheet.ts new file mode 100644 index 00000000000..925afeda64e --- /dev/null +++ b/packages/sheets-hyper-link/src/facade/f-worksheet.ts @@ -0,0 +1,39 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { SheetsHyperLinkParserService } from '@univerjs/sheets-hyper-link'; +import { FWorksheet } from '@univerjs/sheets/facade'; + +export interface IFWorksheetHyperlinkMixin { + /** + * Get the url of this sheet + */ + getUrl(): string; +} + +export class FWorksheetHyperlinkMixin extends FWorksheet implements IFWorksheetHyperlinkMixin { + override getUrl(): string { + const parserService = this._injector.get(SheetsHyperLinkParserService); + return parserService.buildHyperLink(this._workbook.getUnitId(), this._worksheet.getSheetId()); + } +} + +FWorksheet.extend(FWorksheetHyperlinkMixin); + +declare module '@univerjs/sheets/facade' { + // eslint-disable-next-line ts/naming-convention + interface FWorksheet extends IFWorksheetHyperlinkMixin {} +} diff --git a/packages/sheets-hyper-link/src/facade/index.ts b/packages/sheets-hyper-link/src/facade/index.ts index d391aae9b2e..4cee2076dfa 100644 --- a/packages/sheets-hyper-link/src/facade/index.ts +++ b/packages/sheets-hyper-link/src/facade/index.ts @@ -15,6 +15,7 @@ */ import './f-workbook'; +import './f-worksheet'; import './f-range'; export { FWorkbookHyperLinkMixin } from './f-workbook'; @@ -22,3 +23,4 @@ export { FWorkbookHyperLinkMixin } from './f-workbook'; // eslint-disable-next-line perfectionist/sort-exports export type * from './f-range'; export type * from './f-workbook'; +export type * from './f-worksheet'; diff --git a/packages/sheets-thread-comment/src/facade/f-event.ts b/packages/sheets-thread-comment/src/facade/f-event.ts new file mode 100644 index 00000000000..ace3da73a6b --- /dev/null +++ b/packages/sheets-thread-comment/src/facade/f-event.ts @@ -0,0 +1,324 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { IEventBase, RichTextValue } from '@univerjs/core'; +import type { FWorkbook, FWorksheet } from '@univerjs/sheets/facade'; +import type { FTheadCommentItem, FThreadComment } from './f-thread-comment'; +import { FEventName } from '@univerjs/core'; + +interface ICommentEventMixin { + /** + * Event fired after comment added + * @see {@link ISheetCommentAddEvent} + * @example + * ```ts + * univerAPI.addEventListener(CommentEvent.CommentAdded, (event) => { + * const { comment, workbook, worksheet, row, col } = event; + * console.log(event); + * }); + * ``` + */ + readonly CommentAdded: 'CommentAdded'; + + /** + * Event fired before comment added + * @see {@link IBeforeSheetCommentAddEvent} + * @example + * ```ts + * univerAPI.addEventListener(CommentEvent.BeforeCommentAdd, (event) => { + * const { comment, workbook, worksheet, row, col } = event; + * console.log(event); + * }); + * ``` + */ + readonly BeforeCommentAdd: 'BeforeCommentAdd'; + + /** + * Event fired after comment updated + * @see {@link ISheetCommentUpdateEvent} + * @example + * ```ts + * univerAPI.addEventListener(CommentEvent.CommentUpdated, (event) => { + * const { comment, workbook, worksheet, row, col } = event; + * console.log(event); + * }); + * ``` + */ + readonly CommentUpdated: 'CommentUpdated'; + + /** + * Event fired before comment update + * @see {@link IBeforeSheetCommentUpdateEvent} + * @example + * ```ts + * univerAPI.addEventListener(CommentEvent.BeforeCommentUpdate, (event) => { + * const { comment, workbook, worksheet, row, col, newContent } = event; + * console.log(event); + * }); + * ``` + */ + readonly BeforeCommentUpdate: 'BeforeCommentUpdate'; + + /** + * Event fired after comment deleted + * @see {@link ISheetCommentDeleteEvent} + * @example + * ```ts + * univerAPI.addEventListener(CommentEvent.CommentDeleted, (event) => { + * const { commentId, workbook, worksheet } = event; + * console.log(event); + * }); + * ``` + */ + readonly CommentDeleted: 'CommentDeleted'; + + /** + * Event fired before comment delete + * @see {@link IBeforeSheetCommentDeleteEvent} + * @example + * ```ts + * univerAPI.addEventListener(CommentEvent.BeforeCommentDeleted, (event) => { + * const { commentId, workbook, worksheet } = event; + * console.log(event); + * }); + * ``` + */ + readonly BeforeCommentDeleted: 'BeforeCommentDeleted'; + + /** + * Event fired after comment resolve + * @see {@link ISheetCommentResolveEvent} + * @example + * ```ts + * univerAPI.addEventListener(CommentEvent.CommentResolved, (event) => { + * const { comment, row, col, resolved, workbook, worksheet } = event; + * console.log(event); + * }); + * ``` + */ + readonly CommentResolved: 'CommentResolved'; + + /** + * Event fired before comment resolve + * @see {@link ISheetCommentResolveEvent} + * @example + * ```ts + * univerAPI.addEventListener(CommentEvent.BeforeCommentResolve, (event) => { + * const { comment, row, col, resolved, workbook, worksheet } = event; + * console.log(event); + * }); + * ``` + */ + readonly BeforeCommentResolve: 'BeforeCommentResolve'; +} + +const CommentEvent: ICommentEventMixin = { + CommentAdded: 'CommentAdded', + BeforeCommentAdd: 'BeforeCommentAdd', + + CommentUpdated: 'CommentUpdated', + BeforeCommentUpdate: 'BeforeCommentUpdate', + + CommentDeleted: 'CommentDeleted', + BeforeCommentDeleted: 'BeforeCommentDeleted', + + CommentResolved: 'CommentResolved', + BeforeCommentResolve: 'BeforeCommentResolve', +} as const; + +export class FCommentEvent extends FEventName { + override get CommentAdded(): 'CommentAdded' { + return CommentEvent.CommentAdded; + } + + override get BeforeCommentAdd(): 'BeforeCommentAdd' { + return CommentEvent.BeforeCommentAdd; + } + + override get CommentUpdated(): 'CommentUpdated' { + return CommentEvent.CommentUpdated; + } + + override get BeforeCommentUpdate(): 'BeforeCommentUpdate' { + return CommentEvent.BeforeCommentUpdate; + } + + override get CommentDeleted(): 'CommentDeleted' { + return CommentEvent.CommentDeleted; + } + + override get BeforeCommentDeleted(): 'BeforeCommentDeleted' { + return CommentEvent.BeforeCommentDeleted; + } + + override get CommentResolved(): 'CommentResolved' { + return CommentEvent.CommentResolved; + } + + override get BeforeCommentResolve(): 'BeforeCommentResolve' { + return CommentEvent.BeforeCommentResolve; + } +} + +/** + * Event interface triggered after a comment is added to a sheet + * @interface ISheetCommentAddEvent + * @augments {IEventBase} + */ +export interface ISheetCommentAddEvent extends IEventBase { + /** The workbook instance */ + workbook: FWorkbook; + /** The worksheet where the comment is added */ + worksheet: FWorksheet; + /** Row index of the comment */ + row: number; + /** Column index of the comment */ + col: number; + /** The added comment object */ + comment: FThreadComment; +} + +/** + * Event interface triggered before a comment is added to a sheet + * @interface IBeforeSheetCommentAddEvent + * @augments {IEventBase} + */ +export interface IBeforeSheetCommentAddEvent extends IEventBase { + /** The workbook instance */ + workbook: FWorkbook; + /** The worksheet where the comment will be added */ + worksheet: FWorksheet; + /** Row index for the new comment */ + row: number; + /** Column index for the new comment */ + col: number; + /** The comment item to be added */ + comment: FTheadCommentItem; +} + +/** + * Event interface triggered after a comment is updated in a sheet + * @interface ISheetCommentUpdateEvent + * @augments {IEventBase} + */ +export interface ISheetCommentUpdateEvent extends IEventBase { + /** The workbook instance */ + workbook: FWorkbook; + /** The worksheet containing the updated comment */ + worksheet: FWorksheet; + /** Row index of the comment */ + row: number; + /** Column index of the comment */ + col: number; + /** The updated comment object */ + comment: FThreadComment; +} + +/** + * Event interface triggered before a comment is updated in a sheet + * @interface IBeforeSheetCommentUpdateEvent + * @augments {IEventBase} + */ +export interface IBeforeSheetCommentUpdateEvent extends IEventBase { + /** The workbook instance */ + workbook: FWorkbook; + /** The worksheet containing the comment */ + worksheet: FWorksheet; + /** Row index of the comment */ + row: number; + /** Column index of the comment */ + col: number; + /** The current comment object */ + comment: FThreadComment; + /** The new content to replace the existing comment */ + newContent: RichTextValue; +} + +/** + * Event interface triggered before a comment is deleted from a sheet + * @interface IBeforeSheetCommentDeleteEvent + * @augments {IEventBase} + */ +export interface IBeforeSheetCommentDeleteEvent extends IEventBase { + /** The workbook instance */ + workbook: FWorkbook; + /** The worksheet containing the comment */ + worksheet: FWorksheet; + /** Row index of the comment */ + row: number; + /** Column index of the comment */ + col: number; + /** The comment to be deleted */ + comment: FThreadComment; +} + +/** + * Event interface triggered after a comment is deleted from a sheet + * @interface ISheetCommentDeleteEvent + * @augments {IEventBase} + */ +export interface ISheetCommentDeleteEvent extends IEventBase { + /** The workbook instance */ + workbook: FWorkbook; + /** The worksheet that contained the comment */ + worksheet: FWorksheet; + /** The ID of the deleted comment */ + commentId: string; +} + +/** + * Event interface triggered when a comment's resolve status changes + * @interface ISheetCommentResolveEvent + * @augments {IEventBase} + */ +export interface ISheetCommentResolveEvent extends IEventBase { + /** The workbook instance */ + workbook: FWorkbook; + /** The worksheet containing the comment */ + worksheet: FWorksheet; + /** Row index of the comment */ + row: number; + /** Column index of the comment */ + col: number; + /** The comment object */ + comment: FThreadComment; + /** The new resolve status */ + resolved: boolean; +} + +FEventName.extend(FCommentEvent); +export interface ISheetCommentEventConfig { + BeforeCommentAdd: IBeforeSheetCommentAddEvent; + CommentAdded: ISheetCommentAddEvent; + + BeforeCommentUpdate: IBeforeSheetCommentUpdateEvent; + CommentUpdated: ISheetCommentUpdateEvent; + + BeforeCommentDeleted: IBeforeSheetCommentDeleteEvent; + CommentDeleted: ISheetCommentDeleteEvent; + + BeforeCommentResolve: ISheetCommentResolveEvent; + CommentResolved: ISheetCommentResolveEvent; +} + +declare module '@univerjs/core' { + // eslint-disable-next-line ts/naming-convention + interface FEventName extends ICommentEventMixin { + } + + interface IEventParamConfig extends ISheetCommentEventConfig { + } +} diff --git a/packages/sheets-thread-comment/src/facade/f-range.ts b/packages/sheets-thread-comment/src/facade/f-range.ts index fa740e1b097..d4b78267bcf 100644 --- a/packages/sheets-thread-comment/src/facade/f-range.ts +++ b/packages/sheets-thread-comment/src/facade/f-range.ts @@ -14,11 +14,12 @@ * limitations under the License. */ -import { ICommandService, type IDocumentBody, type Nullable, Tools, UserManagerService } from '@univerjs/core'; +import type { IThreadComment } from '@univerjs/thread-comment'; +import { generateRandomId, ICommandService, type IDocumentBody, type Nullable, Range, Tools, UserManagerService } from '@univerjs/core'; import { SheetsThreadCommentModel } from '@univerjs/sheets-thread-comment'; import { FRange } from '@univerjs/sheets/facade'; import { AddCommentCommand, DeleteCommentTreeCommand, getDT } from '@univerjs/thread-comment'; -import { FThreadComment } from './f-thread-comment'; +import { FTheadCommentBuilder, FThreadComment } from './f-thread-comment'; export interface IFRangeCommentMixin { /** @@ -26,17 +27,30 @@ export interface IFRangeCommentMixin { * @returns The comment of the start cell in the current range. If the cell does not have a comment, return `null`. */ getComment(): Nullable; + + /** + * Get the comments in the current range. + * @returns {FThreadComment[]} The comments in the current range. + */ + getComments(): FThreadComment[]; + /** * Add a comment to the start cell in the current range. * @param content The content of the comment. * @returns Whether the comment is added successfully. */ - addComment(this: FRange, content: IDocumentBody): Promise; + addComment(content: IDocumentBody | FTheadCommentBuilder): Promise; /** * Clear the comment of the start cell in the current range. * @returns Whether the comment is cleared successfully. */ clearComment(): Promise; + + /** + * Clear all of the comments in the current range. + * @returns Whether the comments are cleared successfully. + */ + clearComments(): Promise; } export class FRangeCommentMixin extends FRange implements IFRangeCommentMixin { @@ -58,7 +72,26 @@ export class FRangeCommentMixin extends FRange implements IFRangeCommentMixin { return null; } - override addComment(content: IDocumentBody): Promise { + override getComments(): FThreadComment[] { + const injector = this._injector; + const sheetsTheadCommentModel = injector.get(SheetsThreadCommentModel); + const unitId = this._workbook.getUnitId(); + const sheetId = this._worksheet.getSheetId(); + const comments: FThreadComment[] = []; + Range.foreach(this._range, (row, col) => { + const commentId = sheetsTheadCommentModel.getByLocation(unitId, sheetId, row, col); + if (commentId) { + const comment = sheetsTheadCommentModel.getComment(unitId, sheetId, commentId); + if (comment) { + comments.push(this._injector.createInstance(FThreadComment, comment)); + } + } + }); + + return comments; + } + + override addComment(content: IDocumentBody | FTheadCommentBuilder): Promise { const injector = this._injector; const currentComment = this.getComment()?.getCommentData(); const commentService = injector.get(ICommandService); @@ -67,21 +100,22 @@ export class FRangeCommentMixin extends FRange implements IFRangeCommentMixin { const sheetId = this._worksheet.getSheetId(); const refStr = `${Tools.chatAtABC(this._range.startColumn)}${this._range.startRow + 1}`; const currentUser = userService.getCurrentUser(); + const commentData: Partial = content instanceof FTheadCommentBuilder ? content.build() : { text: content }; return commentService.executeCommand(AddCommentCommand.id, { unitId, subUnitId: sheetId, comment: { - text: content, + text: commentData.text, + dT: commentData.dT || getDT(), attachments: [], - dT: getDT(), - id: Tools.generateRandomId(), - ref: refStr!, - personId: currentUser.userID, + id: commentData.id || generateRandomId(), + ref: refStr, + personId: commentData.personId || currentUser.userID, parentId: currentComment?.id, unitId, subUnitId: sheetId, - threadId: currentComment?.threadId, + threadId: currentComment?.threadId || generateRandomId(), }, }); } @@ -104,6 +138,13 @@ export class FRangeCommentMixin extends FRange implements IFRangeCommentMixin { return Promise.resolve(true); } + + override clearComments(): Promise { + const comments = this.getComments(); + const promises = comments.map((comment) => comment.deleteAsync()); + + return Promise.all(promises).then(() => true); + } } FRange.extend(FRangeCommentMixin); diff --git a/packages/sheets-thread-comment/src/facade/f-thread-comment.ts b/packages/sheets-thread-comment/src/facade/f-thread-comment.ts index 78dc48857e5..e80c67c9626 100644 --- a/packages/sheets-thread-comment/src/facade/f-thread-comment.ts +++ b/packages/sheets-thread-comment/src/facade/f-thread-comment.ts @@ -15,12 +15,234 @@ */ import type { IDocumentBody, IRange, Workbook } from '@univerjs/core'; -import type { IBaseComment, IDeleteCommentCommandParams, IResolveCommentCommandParams, IThreadComment, IUpdateCommentCommandParams } from '@univerjs/thread-comment'; -import { ICommandService, Inject, Injector, IUniverInstanceService, UniverInstanceType } from '@univerjs/core'; +import type { IAddCommentCommandParams, IBaseComment, IDeleteCommentCommandParams, IResolveCommentCommandParams, IThreadComment, IUpdateCommentCommandParams } from '@univerjs/thread-comment'; +import { generateRandomId, ICommandService, Inject, Injector, IUniverInstanceService, RichTextBuilder, RichTextValue, Tools, UniverInstanceType, UserManagerService } from '@univerjs/core'; import { deserializeRangeWithSheet } from '@univerjs/engine-formula'; -import { FRange } from '@univerjs/sheets/facade'; import { SheetsThreadCommentModel } from '@univerjs/sheets-thread-comment'; -import { DeleteCommentCommand, DeleteCommentTreeCommand, getDT, ResolveCommentCommand, UpdateCommentCommand } from '@univerjs/thread-comment'; +import { FRange } from '@univerjs/sheets/facade'; +import { AddCommentCommand, DeleteCommentCommand, DeleteCommentTreeCommand, getDT, ResolveCommentCommand, UpdateCommentCommand } from '@univerjs/thread-comment'; + +export class FTheadCommentItem { + protected _comment: IThreadComment = { + id: generateRandomId(), + ref: '', + threadId: '', + dT: '', + personId: '', + text: RichTextBuilder.newEmptyData().body!, + attachments: [], + unitId: '', + subUnitId: '', + }; + + /** + * Create a new FTheadCommentItem + * @param {IThreadComment|undefined} comment The comment + * @returns {FTheadCommentItem} A new instance of FTheadCommentItem + * @example + * ```ts + * const comment = univerAPI.newTheadComment(); + * ``` + */ + static create(comment?: IThreadComment): FTheadCommentItem { + return new FTheadCommentItem(comment); + } + + constructor(comment?: IThreadComment) { + if (comment) { + this._comment = comment; + } + } + + /** + * Get the person id of the comment + * @returns {string} The person id of the comment + * @example + * ```ts + * const comment = univerAPI.getActiveUniverSheet() + * .getSheetById(sheetId) + * .getCommentById(commentId); + * const personId = comment.personId; + * ``` + */ + get personId(): string { + return this._comment.personId; + } + + /** + * Get the date time of the comment + * @returns {string} The date time of the comment + * @example + * ```ts + * const comment = univerAPI.getActiveUniverSheet() + * .getSheetById(sheetId) + * .getCommentById(commentId); + * const dateTime = comment.dateTime; + * ``` + */ + get dateTime(): string { + return this._comment.dT; + } + + /** + * Get the content of the comment + * @returns {RichTextValue} The content of the comment + * @example + * ```ts + * const comment = univerAPI.getActiveUniverSheet() + * .getSheetById(sheetId) + * .getCommentById(commentId); + * const content = comment.content; + * ``` + */ + get content(): RichTextValue { + return RichTextValue.createByBody(this._comment.text); + } + + /** + * Get the id of the comment + * @returns {string} The id of the comment + * @example + * ```ts + * const comment = univerAPI.getActiveUniverSheet() + * .getSheetById(sheetId) + * .getCommentById(commentId); + * const id = comment.id; + * ``` + */ + get id(): string { + return this._comment.id; + } + + /** + * Get the thread id of the comment + * @returns {string} The thread id of the comment + * @example + * ```ts + * const comment = univerAPI.getActiveUniverSheet() + * .getSheetById(sheetId) + * .getCommentById(commentId); + * const threadId = comment.threadId; + * ``` + */ + get threadId(): string { + return this._comment.threadId; + } + + /** + * Copy the comment + * @returns {FTheadCommentBuilder} The comment builder + * @example + * ```ts + * const comment = univerAPI.getActiveUniverSheet() + * .getSheetById(sheetId) + * .getCommentById(commentId); + * const newComment = comment.copy(); + * ``` + */ + copy(): FTheadCommentBuilder { + return FTheadCommentBuilder.create(Tools.deepClone(this._comment)); + } +} + +export class FTheadCommentBuilder extends FTheadCommentItem { + static override create(comment?: IThreadComment): FTheadCommentBuilder { + return new FTheadCommentBuilder(comment); + } + + /** + * Set the content of the comment + * @param {IDocumentBody | RichTextValue} content The content of the comment + * @returns {FTheadCommentBuilder} The comment builder + * @example + * ```ts + * const comment = univerAPI.newTheadComment() + * .setContent(univerAPI.newRichText().insertText('hello zhangsan')); + * ``` + */ + setContent(content: IDocumentBody | RichTextValue): FTheadCommentBuilder { + if (content instanceof RichTextValue) { + this._comment.text = content.getData().body!; + } else { + this._comment.text = content; + } + return this; + } + + /** + * Set the person id of the comment + * @param {string} userId The person id of the comment + * @returns {FTheadCommentBuilder} The comment builder + * @example + * ```ts + * const comment = univerAPI.newTheadComment() + * .setPersonId('123'); + * ``` + */ + setPersonId(userId: string): FTheadCommentBuilder { + this._comment.personId = userId; + return this; + } + + /** + * Set the date time of the comment + * @param {Date} date The date time of the comment + * @returns {FTheadCommentBuilder} The comment builder + * @example + * ```ts + * const comment = univerAPI.newTheadComment() + * .setDateTime(new Date()); + * ``` + */ + setDateTime(date: Date): FTheadCommentBuilder { + this._comment.dT = getDT(date); + return this; + } + + /** + * Set the id of the comment + * @param {string} id The id of the comment + * @returns {FTheadCommentBuilder} The comment builder + * @example + * ```ts + * const comment = univerAPI.newTheadComment() + * .setId('123'); + * ``` + */ + setId(id: string): FTheadCommentBuilder { + this._comment.id = id; + return this; + } + + /** + * Set the thread id of the comment + * @param {string} threadId The thread id of the comment + * @returns {FTheadCommentBuilder} The comment builder + * @example + * ```ts + * const comment = univerAPI.newTheadComment() + * .setThreadId('123'); + * ``` + */ + setThreadId(threadId: string): FTheadCommentBuilder { + this._comment.threadId = threadId; + return this; + } + + /** + * Build the comment + * @returns {IThreadComment} The comment + * @example + * ```ts + * const comment = univerAPI.newTheadComment() + * .setContent(univerAPI.newRichText().insertText('hello zhangsan')) + * .build(); + * ``` + */ + build(): IThreadComment { + return this._comment; + } +} export class FThreadComment { constructor( @@ -29,7 +251,8 @@ export class FThreadComment { @Inject(Injector) private readonly _injector: Injector, @ICommandService private readonly _commandService: ICommandService, @IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService, - @Inject(SheetsThreadCommentModel) private readonly _threadCommentModel: SheetsThreadCommentModel + @Inject(SheetsThreadCommentModel) private readonly _threadCommentModel: SheetsThreadCommentModel, + @Inject(UserManagerService) private readonly _userManagerService: UserManagerService ) { } @@ -43,6 +266,13 @@ export class FThreadComment { /** * Whether the comment is a root comment * @returns Whether the comment is a root comment + * @example + * ```ts + * const comment = univerAPI.getActiveUniverSheet() + * .getSheetById(sheetId) + * .getCommentById(commentId); + * const isRoot = comment.getIsRoot(); + * ``` */ getIsRoot(): boolean { return !this._parent; @@ -51,6 +281,13 @@ export class FThreadComment { /** * Get the comment data * @returns The comment data + * @example + * ```ts + * const comment = univerAPI.getActiveUniverSheet() + * .getSheetById(sheetId) + * .getCommentById(commentId); + * const commentData = comment.getCommentData(); + * ``` */ getCommentData(): IBaseComment { const { children, ...comment } = this._thread; @@ -60,6 +297,13 @@ export class FThreadComment { /** * Get the replies of the comment * @returns the replies of the comment + * @example + * ```ts + * const comment = univerAPI.getActiveUniverSheet() + * .getSheetById(sheetId) + * .getCommentById(commentId); + * const replies = comment.getReplies(); + * ``` */ getReplies(): FThreadComment[] | undefined { const range = this._getRef(); @@ -71,6 +315,13 @@ export class FThreadComment { /** * Get the range of the comment * @returns The range of the comment + * @example + * ```ts + * const comment = univerAPI.getActiveUniverSheet() + * .getSheetById(sheetId) + * .getCommentById(commentId); + * const range = comment.getRange(); + * ``` */ getRange(): FRange | null { const workbook = this._univerInstanceService.getUnit(this._thread.unitId, UniverInstanceType.UNIVER_SHEET); @@ -86,18 +337,40 @@ export class FThreadComment { } /** - * Get the content of the comment - * @returns The content of the comment + * @deprecated use `getRichText` as instead */ getContent(): IDocumentBody { return this._thread.text; } + /** + * Get the rich text of the comment + * @returns {RichTextValue} The rich text of the comment + * @example + * ```ts + * const comment = univerAPI.getActiveUniverSheet() + * .getSheetById(sheetId) + * .getCommentById(commentId); + * const richText = comment.getRichText(); + * ``` + */ + getRichText(): RichTextValue { + const body = this._thread.text; + return RichTextValue.create({ body, documentStyle: {}, id: 'd' }); + } + /** * Delete the comment and it's replies - * @returns success or not + * @returns {Promise} success or not + * @example + * ```ts + * const comment = univerAPI.getActiveUniverSheet() + * .getSheetById(sheetId) + * .getCommentById(commentId); + * const success = await comment.deleteAsync(); + * ``` */ - delete(): Promise { + deleteAsync(): Promise { return this._commandService.executeCommand( this.getIsRoot() ? DeleteCommentTreeCommand.id : DeleteCommentCommand.id, { @@ -108,12 +381,35 @@ export class FThreadComment { ); } + /** + * @deprecated use `deleteAsync` as instead. + */ + delete(): Promise { + return this.deleteAsync(); + } + + /** + * @param content + * @deprecated use `updateAsync` as instead + */ + async update(content: IDocumentBody): Promise { + return this.updateAsync(content); + } + /** * Update the comment content * @param content The new content of the comment * @returns success or not + * @example + * ```ts + * const comment = univerAPI.getActiveUniverSheet() + * .getSheetById(sheetId) + * .getCommentById(commentId); + * const success = await comment.updateAsync(univerAPI.newRichText().insertText('hello zhangsan')); + * ``` */ - async update(content: IDocumentBody): Promise { + async updateAsync(content: IDocumentBody | RichTextValue): Promise { + const body = content instanceof RichTextValue ? content.getData().body : content; const dt = getDT(); const res = await this._commandService.executeCommand( UpdateCommentCommand.id, @@ -122,7 +418,7 @@ export class FThreadComment { subUnitId: this._thread.subUnitId, payload: { commentId: this._thread.id, - text: content, + text: body, updated: true, updateT: dt, }, @@ -133,11 +429,26 @@ export class FThreadComment { } /** - * Resolve the comment - * @param resolved Whether the comment is resolved - * @returns success or not + * @param resolved + * @deprecated use `resolveAsync` as instead */ resolve(resolved?: boolean): Promise { + return this.resolveAsync(resolved); + } + + /** + * Resolve the comment + * @param resolved Whether the comment is resolved + * @returns success or not + * @example + * ```ts + * const comment = univerAPI.getActiveUniverSheet() + * .getSheetById(sheetId) + * .getCommentById(commentId); + * const success = await comment.resolveAsync(true); + * ``` + */ + resolveAsync(resolved?: boolean): Promise { return this._commandService.executeCommand( ResolveCommentCommand.id, { @@ -148,4 +459,44 @@ export class FThreadComment { } as IResolveCommentCommandParams ); } + + /** + * Reply to the comment + * @param comment The comment to reply to + * @returns success or not + * @example + * ```ts + * const comment = univerAPI.getActiveUniverSheet() + * .getSheetById(sheetId) + * .getCommentById(commentId); + * + * const reply = univerAPI.newTheadComment() + * .setContent(univerAPI.newRichText().insertText('hello zhangsan')); + * + * const success = await comment.replyAsync(reply); + * ``` + */ + async replyAsync(comment: FTheadCommentBuilder): Promise { + const commentData = comment.build(); + return this._commandService.executeCommand( + AddCommentCommand.id, + { + unitId: this._thread.unitId, + subUnitId: this._thread.subUnitId, + comment: { + + id: generateRandomId(), + parentId: this._thread.id, + threadId: this._thread.threadId, + ref: this._parent?.ref, + unitId: this._thread.unitId, + subUnitId: this._thread.subUnitId, + text: commentData.text, + attachments: commentData.attachments, + dT: commentData.dT || getDT(), + personId: commentData.personId || this._userManagerService.getCurrentUser().userID, + }, + } as IAddCommentCommandParams + ); + } } diff --git a/packages/sheets-thread-comment/src/facade/f-univer.ts b/packages/sheets-thread-comment/src/facade/f-univer.ts new file mode 100644 index 00000000000..be6b08c19c4 --- /dev/null +++ b/packages/sheets-thread-comment/src/facade/f-univer.ts @@ -0,0 +1,260 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ICommandInfo, IDisposable, Injector } from '@univerjs/core'; +import type { IAddCommentCommandParams, IDeleteCommentCommandParams, IResolveCommentCommandParams, IThreadComment, IUpdateCommentCommandParams } from '@univerjs/thread-comment'; +import type { IBeforeSheetCommentAddEvent, IBeforeSheetCommentDeleteEvent, IBeforeSheetCommentUpdateEvent, ISheetCommentAddEvent, ISheetCommentDeleteEvent, ISheetCommentResolveEvent, ISheetCommentUpdateEvent } from './f-event'; +import { CanceledError, FUniver, ICommandService, RichTextValue } from '@univerjs/core'; +import { AddCommentCommand, DeleteCommentCommand, ResolveCommentCommand, UpdateCommentCommand } from '@univerjs/thread-comment'; +import { FTheadCommentBuilder, FTheadCommentItem } from './f-thread-comment'; + +export interface IFUniverCommentMixin { + /** + * @deprecated use `univerAPI.addEvent(univerAPI.event.CommentAdded, () => {})` as instead + */ + onCommentAdded(callback: (event: ISheetCommentAddEvent) => void): IDisposable; + + /** + * @deprecated use `univerAPI.addEvent(univerAPI.event.CommentUpdated, () => {})` as instead + */ + onCommentUpdated(callback: (event: ISheetCommentUpdateEvent) => void): IDisposable; + + /** + * @deprecated use `univerAPI.addEvent(univerAPI.event.CommentDeleted, () => {})` as instead + */ + onCommentDeleted(callback: (event: ISheetCommentDeleteEvent) => void): IDisposable; + + /** + * @deprecated use `univerAPI.addEvent(univerAPI.event.CommentResolved, () => {})` as instead + */ + onCommentResolved(callback: (event: ISheetCommentResolveEvent) => void): IDisposable; + + /** + * create a new thread comment + * @return {FTheadCommentBuilder} thead comment builder + * @example + * ```ts + * const comment = univerAPI.newTheadComment().setContent(univerAPI.newRichText().insertText('hello zhangsan')); + * ``` + */ + newTheadComment(): FTheadCommentBuilder; +} + +export class FUniverCommentMixin extends FUniver implements IFUniverCommentMixin { + // eslint-disable-next-line complexity + private _handleCommentCommand(commandInfo: ICommandInfo): void { + const params = commandInfo.params as { unitId: string; subUnitId: string; sheetId: string }; + if (!params) return; + const workbook = params.unitId ? this.getUniverSheet(params.unitId) : this.getActiveWorkbook?.(); + if (!workbook) { + return; + } + + const worksheet = workbook.getSheetBySheetId(params.subUnitId || params.sheetId) || workbook.getActiveSheet(); + if (!worksheet) { + return; + } + + switch (commandInfo.id) { + case AddCommentCommand.id: { + if (!this._eventListend(this.Event.CommentAdded)) return; + const addParams = commandInfo.params as IAddCommentCommandParams; + const { comment } = addParams; + const threadComment = worksheet.getRange(comment.ref).getComment(); + if (threadComment) { + this.fireEvent(this.Event.CommentAdded, { + workbook, + worksheet, + row: threadComment.getRange()?.getRow() ?? 0, + col: threadComment.getRange()?.getColumn() ?? 0, + comment: threadComment, + }); + } + break; + } + case UpdateCommentCommand.id: { + if (!this._eventListend(this.Event.CommentUpdated)) return; + const updateParams = commandInfo.params as IUpdateCommentCommandParams; + const { commentId } = updateParams.payload; + const threadComment = worksheet.getCommentById(commentId); + if (threadComment) { + this.fireEvent(this.Event.CommentUpdated, { + workbook, + worksheet, + row: threadComment.getRange()?.getRow() ?? 0, + col: threadComment.getRange()?.getColumn() ?? 0, + comment: threadComment, + }); + } + break; + } + case DeleteCommentCommand.id: { + if (!this._eventListend(this.Event.CommentDeleted)) return; + const deleteParams = commandInfo.params as IDeleteCommentCommandParams; + const { commentId } = deleteParams; + this.fireEvent(this.Event.CommentDeleted, { + workbook, + worksheet, + commentId, + }); + break; + } + case ResolveCommentCommand.id: { + if (!this._eventListend(this.Event.CommentResolved)) return; + const resolveParams = commandInfo.params as IResolveCommentCommandParams; + const { commentId, resolved } = resolveParams; + const threadComment = worksheet.getComments().find((c) => c.getCommentData().id === commentId); + if (threadComment) { + this.fireEvent(this.Event.CommentResolved, { + workbook, + worksheet, + row: threadComment.getRange()!.getRow() ?? 0, + col: threadComment.getRange()!.getColumn() ?? 0, + comment: threadComment, + resolved, + }); + } + break; + } + } + } + + // eslint-disable-next-line complexity, max-lines-per-function + private _handleBeforeCommentCommand(commandInfo: ICommandInfo): void { + const params = commandInfo.params as { unitId: string; subUnitId: string; sheetId: string }; + if (!params) return; + const workbook = params.unitId ? this.getUniverSheet(params.unitId) : this.getActiveWorkbook?.(); + if (!workbook) { + return; + } + + const worksheet = workbook.getSheetBySheetId(params.subUnitId || params.sheetId) || workbook.getActiveSheet(); + if (!worksheet) { + return; + } + + switch (commandInfo.id) { + case AddCommentCommand.id: { + if (!this._eventListend(this.Event.BeforeCommentAdd)) return; + const addParams = commandInfo.params as IAddCommentCommandParams; + const { comment } = addParams; + const activeRange = worksheet.getActiveRange(); + if (!activeRange) return; + const eventParams: IBeforeSheetCommentAddEvent = { + workbook, + worksheet, + row: activeRange.getRow() ?? 0, + col: activeRange.getColumn() ?? 0, + comment: FTheadCommentItem.create(comment), + }; + + this.fireEvent(this.Event.BeforeCommentAdd, eventParams); + if (eventParams.cancel) { + throw new CanceledError(); + }; + break; + } + case UpdateCommentCommand.id: { + if (!this._eventListend(this.Event.BeforeCommentUpdate)) return; + const updateParams = commandInfo.params as IUpdateCommentCommandParams; + const { commentId, text } = updateParams.payload; + const threadComment = worksheet.getCommentById(commentId); + if (threadComment) { + const eventParams: IBeforeSheetCommentUpdateEvent = { + workbook, + worksheet, + row: threadComment.getRange()?.getRow() ?? 0, + col: threadComment.getRange()?.getColumn() ?? 0, + comment: threadComment, + newContent: RichTextValue.createByBody(text), + }; + this.fireEvent(this.Event.BeforeCommentUpdate, eventParams); + if (eventParams.cancel) { + throw new CanceledError(); + }; + } + break; + } + case DeleteCommentCommand.id: { + if (!this._eventListend(this.Event.BeforeCommentDeleted)) return; + const deleteParams = commandInfo.params as IDeleteCommentCommandParams; + const { commentId } = deleteParams; + const threadComment = worksheet.getCommentById(commentId); + if (threadComment) { + const eventParams: IBeforeSheetCommentDeleteEvent = { + workbook, + worksheet, + row: threadComment.getRange()?.getRow() ?? 0, + col: threadComment.getRange()?.getColumn() ?? 0, + comment: threadComment, + }; + this.fireEvent(this.Event.BeforeCommentDeleted, eventParams); + if (eventParams.cancel) { + throw new CanceledError(); + }; + } + break; + } + case ResolveCommentCommand.id: { + if (!this._eventListend(this.Event.BeforeCommentResolve)) return; + const resolveParams = commandInfo.params as IResolveCommentCommandParams; + const { commentId, resolved } = resolveParams; + const threadComment = worksheet.getComments().find((c) => c.getCommentData().id === commentId); + if (threadComment) { + const eventParams: ISheetCommentResolveEvent = { + workbook, + worksheet, + row: threadComment.getRange()!.getRow() ?? 0, + col: threadComment.getRange()!.getColumn() ?? 0, + comment: threadComment, + resolved, + }; + this.fireEvent(this.Event.BeforeCommentResolve, eventParams); + if (eventParams.cancel) { + throw new CanceledError(); + } + } + break; + } + } + } + + override _initialize(injector: Injector): void { + const commandService = injector.get(ICommandService); + this.disposeWithMe( + commandService.onCommandExecuted((commandInfo) => { + this._handleCommentCommand(commandInfo); + }) + ); + + this.disposeWithMe( + commandService.beforeCommandExecuted((commandInfo) => { + this._handleBeforeCommentCommand(commandInfo); + }) + ); + } + + override newTheadComment(comment?: IThreadComment): FTheadCommentBuilder { + return new FTheadCommentBuilder(comment); + } +} + +FUniver.extend(FUniverCommentMixin); + +declare module '@univerjs/core' { + // eslint-disable-next-line ts/naming-convention + interface FUniver extends IFUniverCommentMixin {} +} diff --git a/packages/sheets-thread-comment/src/facade/f-workbook.ts b/packages/sheets-thread-comment/src/facade/f-workbook.ts index 347cc8e1d22..582a85d2b08 100644 --- a/packages/sheets-thread-comment/src/facade/f-workbook.ts +++ b/packages/sheets-thread-comment/src/facade/f-workbook.ts @@ -20,33 +20,38 @@ import { toDisposable } from '@univerjs/core'; import { FWorkbook } from '@univerjs/sheets/facade'; import { AddCommentCommand, DeleteCommentCommand, DeleteCommentTreeCommand, ThreadCommentModel, UpdateCommentCommand } from '@univerjs/thread-comment'; import { filter } from 'rxjs'; +import { FThreadComment } from './f-thread-comment'; -// FIXME@weird94: this plugin should not rely on docs-ui // eslint-disable-next-line ts/no-explicit-any type IUpdateCommandParams = any; export interface IFWorkbookThreadCommentMixin { /** - * The onThreadCommentChange event is fired when the thread comment of this sheet is changed. - * @param callback Callback function that will be called when the event is fired - * @returns A disposable object that can be used to unsubscribe from the event + * Get all comments in the current sheet + * @returns all comments in the current sheet */ - onThreadCommentChange(callback: (commentUpdate: CommentUpdate) => void | false): IDisposable; + getComments(): FThreadComment[]; /** - * The onThreadCommentChange event is fired when the thread comment of this sheet is changed. - * @param callback Callback function that will be called when the event is fired - * @returns A disposable object that can be used to unsubscribe from the event + * Clear all comments in the current sheet */ + clearComments(): Promise; + + /** + * @deprecated use `univerAPI.addEvent(univerAPI.event.CommentUpdated, () => {})` as instead + */ + onThreadCommentChange(callback: (commentUpdate: CommentUpdate) => void | false): IDisposable; + + /** + * @deprecated use `univerAPI.addEvent(univerAPI.event.BeforeCommentAdd, () => {})` as instead + */ onBeforeAddThreadComment( this: FWorkbook, callback: (params: IAddCommentCommandParams, options: IExecutionOptions | undefined) => void | false ): IDisposable; /** - * The onBeforeUpdateThreadComment event is fired before the thread comment is updated. - * @param callback Callback function that will be called when the event is fired - * @returns A disposable object that can be used to unsubscribe from the event + * @deprecated use `univerAPI.addEvent(univerAPI.event.BeforeCommentUpdate, () => {})` as instead */ onBeforeUpdateThreadComment( this: FWorkbook, @@ -54,9 +59,7 @@ export interface IFWorkbookThreadCommentMixin { ): IDisposable; /** - * The onBeforeDeleteThreadComment event is fired before the thread comment is deleted. - * @param callback Callback function that will be called when the event is fired - * @returns A disposable object that can be used to unsubscribe from the event + * @deprecated use `univerAPI.addEvent(univerAPI.event.BeforeCommentDelete, () => {})` as instead */ onBeforeDeleteThreadComment( this: FWorkbook, @@ -75,12 +78,29 @@ export class FWorkbookThreadCommentMixin extends FWorkbook implements IFWorkbook }); } + override getComments(): FThreadComment[] { + return this._threadCommentModel.getUnit(this._workbook.getUnitId()).map((i) => this._injector.createInstance(FThreadComment, i.root)); + } + + override clearComments(): Promise { + const comments = this.getComments(); + const promises = comments.map((comment) => comment.deleteAsync()); + + return Promise.all(promises).then(() => true); + } + + /** + * @deprecated + */ override onThreadCommentChange(callback: (commentUpdate: CommentUpdate) => void | false): IDisposable { return toDisposable(this._threadCommentModel.commentUpdate$ .pipe(filter((change) => change.unitId === this._workbook.getUnitId())) .subscribe(callback)); } + /** + * @deprecated + */ override onBeforeAddThreadComment(callback: (params: IAddCommentCommandParams, options: IExecutionOptions | undefined) => void | false): IDisposable { return toDisposable(this._commandService.beforeCommandExecuted((commandInfo, options) => { const params = commandInfo.params as IAddCommentCommandParams; @@ -95,6 +115,9 @@ export class FWorkbookThreadCommentMixin extends FWorkbook implements IFWorkbook })); } + /** + * @deprecated + */ override onBeforeUpdateThreadComment(callback: (params: IUpdateCommandParams, options: IExecutionOptions | undefined) => void | false): IDisposable { return toDisposable(this._commandService.beforeCommandExecuted((commandInfo, options) => { const params = commandInfo.params as IUpdateCommandParams; @@ -109,6 +132,9 @@ export class FWorkbookThreadCommentMixin extends FWorkbook implements IFWorkbook })); } + /** + * @deprecated + */ override onBeforeDeleteThreadComment(callback: (params: IDeleteCommentCommandParams, options: IExecutionOptions | undefined) => void | false): IDisposable { return toDisposable(this._commandService.beforeCommandExecuted((commandInfo, options) => { const params = commandInfo.params as IDeleteCommentCommandParams; diff --git a/packages/sheets-thread-comment/src/facade/f-worksheet.ts b/packages/sheets-thread-comment/src/facade/f-worksheet.ts index 682027bf63e..41e4d1df7ca 100644 --- a/packages/sheets-thread-comment/src/facade/f-worksheet.ts +++ b/packages/sheets-thread-comment/src/facade/f-worksheet.ts @@ -26,18 +26,34 @@ export interface IFWorksheetCommentMixin { /** * Get all comments in the current sheet * @returns all comments in the current sheet + * ```ts + * const workbook = univerAPI.getActiveUniverSheet(); + * const worksheet = workbook.getSheetById(sheetId); + * const comments = worksheet.getComments(); + * ``` */ getComments(): FThreadComment[]; /** - * Subscribe to comment events. - * @param callback (cellPos: Nullable) => void Callback function, param contains comment info and target cell. - * @example + * Clear all comments in the current sheet * ```ts - * univerAPI.getActiveWorkbook().getActiveSheet().onCommented((params) => {...}) + * const workbook = univerAPI.getActiveUniverSheet(); + * const worksheet = workbook.getSheetById(sheetId); + * await worksheet.clearComments(); * ``` */ - onCommented(callback: (params: IAddCommentCommandParams) => void): IDisposable; + clearComments(): Promise; + + /** + * get comment by comment id + * @param {string} commentId comment id + * ```ts + * const workbook = univerAPI.getActiveUniverSheet(); + * const worksheet = workbook.getSheetById(sheetId); + * const comment = worksheet.getCommentById(commentId); + * ``` + */ + getCommentById(commentId: string): FThreadComment | undefined; } export class FWorksheetCommentMixin extends FWorksheet implements IFWorksheetCommentMixin { @@ -47,7 +63,18 @@ export class FWorksheetCommentMixin extends FWorksheet implements IFWorksheetCom return comments.map((comment) => this._injector.createInstance(FThreadComment, comment)); } - override onCommented(callback: (params: IAddCommentCommandParams) => void): IDisposable { + override clearComments(): Promise { + const comments = this.getComments(); + const promises = comments.map((comment) => comment.deleteAsync()); + + return Promise.all(promises).then(() => true); + } + + /** + * Subscribe to comment events. + * @param callback Callback function, param contains comment info and target cell. + */ + onCommented(callback: (params: IAddCommentCommandParams) => void): IDisposable { const commandService = this._injector.get(ICommandService); return commandService.onCommandExecuted((command) => { if (command.id === AddCommentCommand.id) { @@ -56,6 +83,14 @@ export class FWorksheetCommentMixin extends FWorksheet implements IFWorksheetCom } }); } + + override getCommentById(commentId: string): FThreadComment | undefined { + const sheetsTheadCommentModel = this._injector.get(SheetsThreadCommentModel); + const comment = sheetsTheadCommentModel.getComment(this._workbook.getUnitId(), this._worksheet.getSheetId(), commentId); + if (comment) { + return this._injector.createInstance(FThreadComment, comment); + } + } } FWorksheet.extend(FWorksheetCommentMixin); diff --git a/packages/sheets-thread-comment/src/facade/index.ts b/packages/sheets-thread-comment/src/facade/index.ts index 2fffbdfe559..74b295303fa 100644 --- a/packages/sheets-thread-comment/src/facade/index.ts +++ b/packages/sheets-thread-comment/src/facade/index.ts @@ -17,10 +17,11 @@ import './f-range'; import './f-workbook'; import './f-worksheet'; +import './f-event'; -export { FThreadComment } from './f-thread-comment'; - -// eslint-disable-next-line perfectionist/sort-exports +export type * from './f-event'; export type * from './f-range'; +export type * from './f-thread-comment'; +export { FThreadComment } from './f-thread-comment'; export type * from './f-workbook'; export type * from './f-worksheet'; diff --git a/packages/sheets-ui/src/controllers/clipboard/utils.ts b/packages/sheets-ui/src/controllers/clipboard/utils.ts index 2bcb82eceae..6f3c677acb6 100644 --- a/packages/sheets-ui/src/controllers/clipboard/utils.ts +++ b/packages/sheets-ui/src/controllers/clipboard/utils.ts @@ -47,6 +47,14 @@ import { isRichText } from '../editor/editing.render-controller'; import { discreteRangeToRange, type IDiscreteRange, virtualizeDiscreteRanges } from '../utils/range-tools'; // if special paste need append mutations instead of replace the default, it can use this function to generate default mutations. +/** + * + * @param pasteFrom + * @param pasteTo + * @param data + * @param payload + * @param accessor + */ export function getDefaultOnPasteCellMutations( pasteFrom: ISheetDiscreteRangeLocation, pasteTo: ISheetDiscreteRangeLocation, @@ -101,6 +109,18 @@ export function getDefaultOnPasteCellMutations( }; } +/** + * + * @param from + * @param from.unitId + * @param from.subUnitId + * @param from.range + * @param to + * @param to.unitId + * @param to.subUnitId + * @param to.range + * @param accessor + */ export function getMoveRangeMutations( from: { unitId: string; @@ -292,6 +312,13 @@ export function getMoveRangeMutations( }; } +/** + * + * @param pasteTo + * @param pasteFrom + * @param matrix + * @param accessor + */ export function getSetCellValueMutations( pasteTo: ISheetDiscreteRangeLocation, pasteFrom: Nullable, @@ -350,6 +377,13 @@ export function getSetCellValueMutations( }; } +/** + * + * @param pasteTo + * @param pasteFrom + * @param matrix + * @param accessor + */ export function getSetCellCustomMutations( pasteTo: ISheetDiscreteRangeLocation, pasteFrom: Nullable, @@ -397,6 +431,13 @@ export function getSetCellCustomMutations( }; } +/** + * + * @param pasteTo + * @param matrix + * @param accessor + * @param withRichFormat + */ export function getSetCellStyleMutations( pasteTo: ISheetDiscreteRangeLocation, matrix: ObjectMatrix, @@ -480,6 +521,12 @@ export function getSetCellStyleMutations( }; } +/** + * + * @param pasteTo + * @param matrix + * @param accessor + */ export function getClearCellStyleMutations( pasteTo: ISheetDiscreteRangeLocation, matrix: ObjectMatrix, @@ -525,6 +572,12 @@ export function getClearCellStyleMutations( return { undos: undoMutationsInfo, redos: redoMutationsInfo }; } +/** + * + * @param pasteTo + * @param matrix + * @param accessor + */ export function getClearCellValueMutations( pasteTo: ISheetDiscreteRangeLocation, matrix: ObjectMatrix, @@ -566,6 +619,12 @@ export function getClearCellValueMutations( return { undos: undoMutationsInfo, redos: redoMutationsInfo }; } +/** + * + * @param pasteTo + * @param matrix + * @param accessor + */ export function getClearAndSetMergeMutations( pasteTo: ISheetDiscreteRangeLocation, matrix: ObjectMatrix, @@ -662,6 +721,10 @@ export function getClearAndSetMergeMutations( return { undos: undoMutationsInfo, redos: redoMutationsInfo }; } +/** + * + * @param text + */ export function generateBody(text: string): IDocumentBody { if (!text.includes('\r') && Tools.isLegalUrl(text)) { const id = generateRandomId(); diff --git a/packages/sheets-ui/src/facade/f-event.ts b/packages/sheets-ui/src/facade/f-event.ts index 989de2fe755..6e2e13cd206 100644 --- a/packages/sheets-ui/src/facade/f-event.ts +++ b/packages/sheets-ui/src/facade/f-event.ts @@ -14,9 +14,124 @@ * limitations under the License. */ -import type { IEventBase } from '@univerjs/core'; +import type { DeviceInputEventType } from '@univerjs/engine-render'; import type { FRange, FWorkbook, FWorksheet } from '@univerjs/sheets/facade'; -import { FEventName } from '@univerjs/core'; +import type { KeyCode } from '@univerjs/ui'; +import { FEventName, type IEventBase, type RichTextValue } from '@univerjs/core'; + +/** + * Event interface triggered when cell editing starts + * @interface ISheetEditStartedEventParams + * @augments {IEventBase} + */ +export interface ISheetEditStartedEventParams extends IEventBase { + /** The workbook instance */ + workbook: FWorkbook; + /** The worksheet being edited */ + worksheet: FWorksheet; + /** Row index of the editing cell */ + row: number; + /** Column index of the editing cell */ + column: number; + /** Type of input device event that triggered the edit */ + eventType: DeviceInputEventType; + /** Optional keycode that triggered the edit */ + keycode?: KeyCode; + /** Whether the edit is happening in zen editor mode */ + isZenEditor: boolean; +} + +/** + * Event interface triggered when cell editing ends + * @interface ISheetEditEndedEventParams + * @augments {IEventBase} + */ +export interface ISheetEditEndedEventParams extends IEventBase { + /** The workbook instance */ + workbook: FWorkbook; + /** The worksheet being edited */ + worksheet: FWorksheet; + /** Row index of the edited cell */ + row: number; + /** Column index of the edited cell */ + column: number; + /** Type of input device event that triggered the edit end */ + eventType: DeviceInputEventType; + /** Optional keycode that triggered the edit end */ + keycode?: KeyCode; + /** Whether the edit happened in zen editor mode */ + isZenEditor: boolean; + /** Whether the edit was confirmed or cancelled */ + isConfirm: boolean; +} + +/** + * Event interface triggered while cell content is being changed + * @interface ISheetEditChangingEventParams + * @augments {IEventBase} + */ +export interface ISheetEditChangingEventParams extends IEventBase { + /** The workbook instance */ + workbook: FWorkbook; + /** The worksheet being edited */ + worksheet: FWorksheet; + /** Row index of the editing cell */ + row: number; + /** Column index of the editing cell */ + column: number; + /** Current value being edited */ + value: RichTextValue; + /** Whether the edit is happening in zen editor mode */ + isZenEditor: boolean; +} + +/** + * Event interface triggered before cell editing starts + * @interface IBeforeSheetEditStartEventParams + * @augments {IEventBase} + */ +export interface IBeforeSheetEditStartEventParams extends IEventBase { + /** The workbook instance */ + workbook: FWorkbook; + /** The worksheet to be edited */ + worksheet: FWorksheet; + /** Row index of the cell to be edited */ + row: number; + /** Column index of the cell to be edited */ + column: number; + /** Type of input device event triggering the edit */ + eventType: DeviceInputEventType; + /** Optional keycode triggering the edit */ + keycode?: KeyCode; + /** Whether the edit will happen in zen editor mode */ + isZenEditor: boolean; +} + +/** + * Event interface triggered before cell editing ends + * @interface IBeforeSheetEditEndEventParams + * @augments {IEventBase} + */ +export interface IBeforeSheetEditEndEventParams extends IEventBase { + /** The workbook instance */ + workbook: FWorkbook; + /** The worksheet being edited */ + worksheet: FWorksheet; + /** Row index of the editing cell */ + row: number; + /** Column index of the editing cell */ + column: number; + /** Current value being edited */ + value: RichTextValue; + /** Type of input device event triggering the edit end */ + eventType: DeviceInputEventType; + /** Optional keycode triggering the edit end */ + keycode?: KeyCode; + /** Whether the edit is happening in zen editor mode */ + isZenEditor: boolean; + /** Whether the edit will be confirmed or cancelled */ + isConfirm: boolean; +} interface IFSheetsUIEventNameMixin { /** @@ -72,6 +187,62 @@ interface IFSheetsUIEventNameMixin { * ``` */ readonly ClipboardPasted: 'ClipboardPasted'; + + /** + * Event fired before a cell is edited + * @see {@link IBeforeSheetEditStartEventParams} + * @example + * ```ts + * univerAPI.addEvent(univerAPI.Event.BeforeSheetEditStart, (params) => { + * const { worksheet, workbook, row, column, eventType, keycode, isZenEditor } = params; + * }); + * ``` + */ + readonly BeforeSheetEditStart: 'BeforeSheetEditStart'; + /** + * Event fired after a cell is edited + * @see {@link ISheetEditEndedEventParams} + * @example + * ```ts + * univerAPI.addEvent(univerAPI.Event.SheetEditStarted, (params) => { + * const { worksheet, workbook, row, column, eventType, keycode, isZenEditor } = params; + * }); + * ``` + */ + readonly SheetEditStarted: 'SheetEditStarted'; + /** + * Event fired when a cell is being edited + * @see {@link ISheetEditChangingEventParams} + * @example + * ```ts + * univerAPI.addEvent(univerAPI.Event.SheetEditChanging, (params) => { + * const { worksheet, workbook, row, column, value, isZenEditor } = params; + * }); + * ``` + */ + readonly SheetEditChanging: 'SheetEditChanging'; + /** + * Event fired before a cell edit ends + * @see {@link IBeforeSheetEditEndEventParams} + * @example + * ```ts + * univerAPI.addEvent(univerAPI.Event.BeforeSheetEditEnd, (params) => { + * const { worksheet, workbook, row, column, value, eventType, keycode, isZenEditor } = params; + * }); + * ``` + */ + readonly BeforeSheetEditEnd: 'BeforeSheetEditEnd'; + /** + * Event fired after a cell edit ends + * @see {@link ISheetEditEndedEventParams} + * @example + * ```ts + * univerAPI.addEvent(univerAPI.Event.SheetEditEnded, (params) => { + * const { worksheet, workbook, row, column, eventType, keycode, isZenEditor } = params; + * }); + * ``` + */ + readonly SheetEditEnded: 'SheetEditEnded'; } export class FSheetsUIEventName extends FEventName implements IFSheetsUIEventNameMixin { @@ -90,6 +261,26 @@ export class FSheetsUIEventName extends FEventName implements IFSheetsUIEventNam override get ClipboardPasted(): 'ClipboardPasted' { return 'ClipboardPasted' as const; } + + override get BeforeSheetEditStart(): 'BeforeSheetEditStart' { + return 'BeforeSheetEditStart'; + } + + override get SheetEditStarted(): 'SheetEditStarted' { + return 'SheetEditStarted'; + } + + override get SheetEditChanging(): 'SheetEditChanging' { + return 'SheetEditChanging'; + } + + override get BeforeSheetEditEnd(): 'BeforeSheetEditEnd' { + return 'BeforeSheetEditEnd'; + } + + override get SheetEditEnded(): 'SheetEditEnded' { + return 'SheetEditEnded'; + } } export interface IBeforeClipboardChangeParam extends IEventBase { @@ -147,6 +338,12 @@ interface IFSheetsUIEventParamConfig { ClipboardChanged: IClipboardChangedParam; BeforeClipboardPaste: IBeforeClipboardPasteParam; ClipboardPasted: IClipboardPastedParam; + + BeforeSheetEditStart: IBeforeSheetEditStartEventParams; + SheetEditStarted: ISheetEditStartedEventParams; + SheetEditChanging: ISheetEditChangingEventParams; + BeforeSheetEditEnd: IBeforeSheetEditEndEventParams; + SheetEditEnded: ISheetEditEndedEventParams; } FEventName.extend(FSheetsUIEventName); @@ -155,4 +352,3 @@ declare module '@univerjs/core' { interface FEventName extends IFSheetsUIEventNameMixin { } interface IEventParamConfig extends IFSheetsUIEventParamConfig { } } - diff --git a/packages/sheets-ui/src/facade/f-univer.ts b/packages/sheets-ui/src/facade/f-univer.ts index a18cace2b53..8074dbae136 100644 --- a/packages/sheets-ui/src/facade/f-univer.ts +++ b/packages/sheets-ui/src/facade/f-univer.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import type { IDisposable, Injector, Nullable } from '@univerjs/core'; +import type { DocumentDataModel, IDisposable, Injector, Nullable } from '@univerjs/core'; +import type { IRichTextEditingMutationParams } from '@univerjs/docs'; import type { IColumnsHeaderCfgParam, IRowsHeaderCfgParam, @@ -24,13 +25,14 @@ import type { SpreadsheetColumnHeader, SpreadsheetRowHeader, } from '@univerjs/engine-render'; -import type { ISheetPasteByShortKeyParams } from '@univerjs/sheets-ui'; -import type { IBeforeClipboardChangeParam, IBeforeClipboardPasteParam } from './f-event'; -import { FUniver, ICommandService, ILogService, toDisposable } from '@univerjs/core'; +import type { IEditorBridgeServiceVisibleParam, ISheetPasteByShortKeyParams } from '@univerjs/sheets-ui'; +import type { IBeforeClipboardChangeParam, IBeforeClipboardPasteParam, IBeforeSheetEditEndEventParams, IBeforeSheetEditStartEventParams, ISheetEditChangingEventParams, ISheetEditEndedEventParams, ISheetEditStartedEventParams } from './f-event'; +import { CanceledError, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, FUniver, ICommandService, ILogService, IUniverInstanceService, RichTextValue, toDisposable } from '@univerjs/core'; +import { RichTextEditingMutation } from '@univerjs/docs'; import { IRenderManagerService } from '@univerjs/engine-render'; -import { ISheetClipboardService, SHEET_VIEW_KEY, SheetPasteShortKeyCommand } from '@univerjs/sheets-ui'; +import { IEditorBridgeService, ISheetClipboardService, SetCellEditVisibleOperation, SHEET_VIEW_KEY, SheetPasteShortKeyCommand } from '@univerjs/sheets-ui'; import { FSheetHooks } from '@univerjs/sheets/facade'; -import { CopyCommand, CutCommand, HTML_CLIPBOARD_MIME_TYPE, IClipboardInterfaceService, PasteCommand, PLAIN_TEXT_CLIPBOARD_MIME_TYPE, supportClipboardAPI } from '@univerjs/ui'; +import { CopyCommand, CutCommand, HTML_CLIPBOARD_MIME_TYPE, IClipboardInterfaceService, KeyCode, PasteCommand, PLAIN_TEXT_CLIPBOARD_MIME_TYPE, supportClipboardAPI } from '@univerjs/ui'; export interface IFUniverSheetsUIMixin { /** @@ -74,18 +76,137 @@ export interface IFUniverSheetsUIMixin { registerSheetMainExtension(unitId: string, ...extensions: SheetExtension[]): IDisposable; /** - * Get sheet hooks. - * @returns {FSheetHooks} FSheetHooks instance. - * @example - * ``` ts - * univerAPI.getSheetHooks(); - * ``` + * @deprecated use `univerAPI.addEvent` as instead. */ getSheetHooks(): FSheetHooks; } export class FUniverSheetsUIMixin extends FUniver implements IFUniverSheetsUIMixin { + // eslint-disable-next-line max-lines-per-function + private _initSheetUIEvent(injector: Injector): void { + const commandService = injector.get(ICommandService); + this.disposeWithMe(commandService.beforeCommandExecuted((commandInfo) => { + if (commandInfo.id === SetCellEditVisibleOperation.id) { + if (!this._eventListend(this.Event.BeforeSheetEditStart) && !this._eventListend(this.Event.BeforeSheetEditEnd)) { + return; + } + const target = this.getCommandSheetTarget(commandInfo); + if (!target) { + return; + } + const { workbook, worksheet } = target; + const editorBridgeService = injector.get(IEditorBridgeService); + const univerInstanceService = injector.get(IUniverInstanceService); + const params = commandInfo.params as IEditorBridgeServiceVisibleParam; + const { visible, keycode, eventType } = params; + const loc = editorBridgeService.getEditLocation()!; + if (visible) { + const eventParams: IBeforeSheetEditStartEventParams = { + row: loc.row, + column: loc.column, + eventType, + keycode, + workbook, + worksheet, + isZenEditor: false, + }; + this.fireEvent(this.Event.BeforeSheetEditStart, eventParams); + if (eventParams.cancel) { + throw new CanceledError(); + } + } else { + const eventParams: IBeforeSheetEditEndEventParams = { + row: loc.row, + column: loc.column, + eventType, + keycode, + workbook, + worksheet, + isZenEditor: false, + value: RichTextValue.create(univerInstanceService.getUnit(DOCS_NORMAL_EDITOR_UNIT_ID_KEY)!.getSnapshot()), + isConfirm: keycode !== KeyCode.ESC, + }; + this.fireEvent(this.Event.BeforeSheetEditEnd, eventParams); + if (eventParams.cancel) { + throw new CanceledError(); + } + } + } + })); + + this.disposeWithMe(commandService.onCommandExecuted((commandInfo) => { + if (commandInfo.id === SetCellEditVisibleOperation.id) { + if (!this._eventListend(this.Event.SheetEditStarted) && !this._eventListend(this.Event.SheetEditEnded)) { + return; + } + const target = this.getCommandSheetTarget(commandInfo); + if (!target) { + return; + } + const { workbook, worksheet } = target; + + const editorBridgeService = injector.get(IEditorBridgeService); + const params = commandInfo.params as IEditorBridgeServiceVisibleParam; + const { visible, keycode, eventType } = params; + const loc = editorBridgeService.getEditLocation()!; + if (visible) { + const eventParams: ISheetEditStartedEventParams = { + row: loc.row, + column: loc.column, + eventType, + keycode, + workbook, + worksheet, + isZenEditor: false, + }; + this.fireEvent(this.Event.SheetEditStarted, eventParams); + } else { + const eventParams: ISheetEditEndedEventParams = { + row: loc.row, + column: loc.column, + eventType, + keycode, + workbook, + worksheet, + isZenEditor: false, + isConfirm: keycode !== KeyCode.ESC, + }; + this.fireEvent(this.Event.SheetEditEnded, eventParams); + } + } + + if (commandInfo.id === RichTextEditingMutation.id) { + if (!this._eventListend(this.Event.SheetEditChanging)) { + return; + } + const target = this.getCommandSheetTarget(commandInfo); + if (!target) { + return; + } + const { workbook, worksheet } = target; + const editorBridgeService = injector.get(IEditorBridgeService); + const univerInstanceService = injector.get(IUniverInstanceService); + const params = commandInfo.params as IRichTextEditingMutationParams; + if (!editorBridgeService.isVisible().visible) return; + const { unitId } = params; + if (unitId === DOCS_NORMAL_EDITOR_UNIT_ID_KEY) { + const { row, column } = editorBridgeService.getEditLocation()!; + const eventParams: ISheetEditChangingEventParams = { + workbook, + worksheet, + row, + column, + value: RichTextValue.create(univerInstanceService.getUnit(DOCS_NORMAL_EDITOR_UNIT_ID_KEY)!.getSnapshot()), + isZenEditor: false, + }; + this.fireEvent(this.Event.SheetEditChanging, eventParams); + } + } + })); + } + override _initialize(injector: Injector): void { + this._initSheetUIEvent(injector); const commandService = injector.get(ICommandService); this.disposeWithMe(commandService.beforeCommandExecuted((commandInfo) => { switch (commandInfo.id) { diff --git a/packages/sheets-ui/src/facade/index.ts b/packages/sheets-ui/src/facade/index.ts index 04356ee417c..1ccb63b0f86 100644 --- a/packages/sheets-ui/src/facade/index.ts +++ b/packages/sheets-ui/src/facade/index.ts @@ -21,10 +21,10 @@ import './f-permission'; import './f-sheet-hooks'; import './f-event'; -export { type IFComponentKey, transformComponentKey } from './f-range'; +export type * from './f-event'; -// eslint-disable-next-line perfectionist/sort-exports export type * from './f-permission'; +export { type IFComponentKey, transformComponentKey } from './f-range'; export type * from './f-sheet-hooks'; export type * from './f-univer'; export type * from './f-workbook'; diff --git a/packages/sheets-ui/src/views/cell-alert/CellAlertPopup.tsx b/packages/sheets-ui/src/views/cell-alert/CellAlertPopup.tsx index 0719a553fd2..bee91d4237a 100644 --- a/packages/sheets-ui/src/views/cell-alert/CellAlertPopup.tsx +++ b/packages/sheets-ui/src/views/cell-alert/CellAlertPopup.tsx @@ -22,6 +22,11 @@ import React from 'react'; import { CellAlertType } from '../../services/cell-alert-manager.service'; import styles from './index.module.less'; +/** + * + * @param root0 + * @param root0.popup + */ export function CellAlert({ popup }: { popup: ICanvasPopup }) { const alert = popup.extraProps?.alert; diff --git a/packages/sheets-zen-editor/package.json b/packages/sheets-zen-editor/package.json index be25d85a598..88d3217b2e6 100644 --- a/packages/sheets-zen-editor/package.json +++ b/packages/sheets-zen-editor/package.json @@ -23,7 +23,8 @@ "exports": { ".": "./src/index.ts", "./*": "./src/*", - "./locale/*": "./src/locale/*.ts" + "./locale/*": "./src/locale/*.ts", + "./facade": "./src/facade/index.ts" }, "main": "./src/index.ts", "types": "./lib/types/index.d.ts", @@ -47,6 +48,11 @@ "require": "./lib/cjs/locale/*.js", "types": "./lib/types/locale/*.d.ts" }, + "./facade": { + "import": "./lib/es/facade.js", + "require": "./lib/cjs/facade.js", + "types": "./lib/types/facade/index.d.ts" + }, "./lib/*": "./lib/*" } }, @@ -69,6 +75,7 @@ }, "dependencies": { "@univerjs/core": "workspace:*", + "@univerjs/docs": "workspace:*", "@univerjs/docs-ui": "workspace:*", "@univerjs/engine-render": "workspace:*", "@univerjs/icons": "^0.2.10", diff --git a/packages/sheets-zen-editor/src/facade/f-univer.ts b/packages/sheets-zen-editor/src/facade/f-univer.ts new file mode 100644 index 00000000000..9bd97989b36 --- /dev/null +++ b/packages/sheets-zen-editor/src/facade/f-univer.ts @@ -0,0 +1,172 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { DocumentDataModel, Injector } from '@univerjs/core'; +import type { IRichTextEditingMutationParams } from '@univerjs/docs'; +import type { IEditorBridgeServiceVisibleParam } from '@univerjs/sheets-ui'; +import type { IBeforeSheetEditEndEventParams, IBeforeSheetEditStartEventParams, ISheetEditChangingEventParams, ISheetEditEndedEventParams, ISheetEditStartedEventParams } from '@univerjs/sheets-ui/facade'; +import { CanceledError, DOCS_ZEN_EDITOR_UNIT_ID_KEY, FUniver, ICommandService, IUniverInstanceService, RichTextValue } from '@univerjs/core'; +import { RichTextEditingMutation } from '@univerjs/docs'; + +import { IEditorBridgeService } from '@univerjs/sheets-ui'; +import { CancelZenEditCommand, ConfirmZenEditCommand, OpenZenEditorCommand } from '@univerjs/sheets-zen-editor'; + +export interface IFUniverSheetsZenEditorMixin {} + +export class FUniverSheetsZenEditorMixin extends FUniver implements IFUniverSheetsZenEditorMixin { + // eslint-disable-next-line max-lines-per-function + private _initSheetZenEditorEvent(injector: Injector): void { + const commandService = injector.get(ICommandService); + this.disposeWithMe(commandService.beforeCommandExecuted((commandInfo) => { + if ( + commandInfo.id === OpenZenEditorCommand.id || + commandInfo.id === CancelZenEditCommand.id || + commandInfo.id === ConfirmZenEditCommand.id + ) { + if (!this._eventListend(this.Event.BeforeSheetEditStart) && !this._eventListend(this.Event.BeforeSheetEditEnd)) { + return; + } + const target = this.getCommandSheetTarget(commandInfo); + if (!target) { + return; + } + const { workbook, worksheet } = target; + const editorBridgeService = injector.get(IEditorBridgeService); + const univerInstanceService = injector.get(IUniverInstanceService); + const params = commandInfo.params as IEditorBridgeServiceVisibleParam; + const { keycode, eventType } = params; + const loc = editorBridgeService.getEditLocation()!; + + if (commandInfo.id === OpenZenEditorCommand.id) { + const eventParams: IBeforeSheetEditStartEventParams = { + row: loc.row, + column: loc.column, + eventType, + keycode, + workbook, + worksheet, + isZenEditor: true, + }; + this.fireEvent(this.Event.BeforeSheetEditStart, eventParams); + if (eventParams.cancel) { + throw new CanceledError(); + } + } else { + const eventParams: IBeforeSheetEditEndEventParams = { + row: loc.row, + column: loc.column, + eventType, + keycode, + workbook, + worksheet, + isZenEditor: true, + value: RichTextValue.create(univerInstanceService.getUnit(DOCS_ZEN_EDITOR_UNIT_ID_KEY)!.getSnapshot()), + isConfirm: commandInfo.id === ConfirmZenEditCommand.id, + }; + this.fireEvent(this.Event.BeforeSheetEditEnd, eventParams); + if (eventParams.cancel) { + throw new CanceledError(); + } + } + } + })); + + this.disposeWithMe(commandService.onCommandExecuted((commandInfo) => { + if ( + commandInfo.id === OpenZenEditorCommand.id || + commandInfo.id === CancelZenEditCommand.id || + commandInfo.id === ConfirmZenEditCommand.id + ) { + if (!this._eventListend(this.Event.SheetEditStarted) && !this._eventListend(this.Event.SheetEditEnded)) { + return; + } + const target = this.getCommandSheetTarget(commandInfo); + if (!target) { + return; + } + const { workbook, worksheet } = target; + + const editorBridgeService = injector.get(IEditorBridgeService); + const params = commandInfo.params as IEditorBridgeServiceVisibleParam; + const { keycode, eventType } = params; + const loc = editorBridgeService.getEditLocation()!; + if (commandInfo.id === OpenZenEditorCommand.id) { + const eventParams: ISheetEditStartedEventParams = { + row: loc.row, + column: loc.column, + eventType, + keycode, + workbook, + worksheet, + isZenEditor: true, + }; + this.fireEvent(this.Event.SheetEditStarted, eventParams); + } else { + const eventParams: ISheetEditEndedEventParams = { + row: loc.row, + column: loc.column, + eventType, + keycode, + workbook, + worksheet, + isZenEditor: true, + isConfirm: commandInfo.id === ConfirmZenEditCommand.id, + }; + this.fireEvent(this.Event.SheetEditEnded, eventParams); + } + } + + if (commandInfo.id === RichTextEditingMutation.id) { + if (!this._eventListend(this.Event.SheetEditChanging)) { + return; + } + const target = this.getCommandSheetTarget(commandInfo); + if (!target) { + return; + } + const { workbook, worksheet } = target; + const editorBridgeService = injector.get(IEditorBridgeService); + const univerInstanceService = injector.get(IUniverInstanceService); + const params = commandInfo.params as IRichTextEditingMutationParams; + if (!editorBridgeService.isVisible().visible) return; + const { unitId } = params; + if (unitId === DOCS_ZEN_EDITOR_UNIT_ID_KEY) { + const { row, column } = editorBridgeService.getEditLocation()!; + const eventParams: ISheetEditChangingEventParams = { + workbook, + worksheet, + row, + column, + value: RichTextValue.create(univerInstanceService.getUnit(DOCS_ZEN_EDITOR_UNIT_ID_KEY)!.getSnapshot()), + isZenEditor: true, + }; + this.fireEvent(this.Event.SheetEditChanging, eventParams); + } + } + })); + } + + override _initialize(injector: Injector): void { + this._initSheetZenEditorEvent(injector); + } +} + +FUniver.extend(FUniverSheetsZenEditorMixin); + +declare module '@univerjs/core' { + // eslint-disable-next-line ts/naming-convention + interface FUniver extends IFUniverSheetsZenEditorMixin { } +} diff --git a/packages/sheets-zen-editor/src/facade/f-workbook.ts b/packages/sheets-zen-editor/src/facade/f-workbook.ts new file mode 100644 index 00000000000..1dcb08aa585 --- /dev/null +++ b/packages/sheets-zen-editor/src/facade/f-workbook.ts @@ -0,0 +1,65 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ICommandService } from '@univerjs/core'; +import { CancelZenEditCommand, ConfirmZenEditCommand, OpenZenEditorCommand } from '@univerjs/sheets-zen-editor'; +import { FWorkbook } from '@univerjs/sheets/facade'; + +export interface IFWorkbookSheetsZenEditorMixin { + /** + * Start the zen editing process + * @returns A promise that resolves to a boolean indicating whether the zen editing process was started successfully. + * @example + * ```ts + * univerAPI.getActiveWorkbook().startZenEditingAsync(); + * ``` + */ + startZenEditingAsync(): Promise; + + /** + * End the zen editing process + * @async + * @param {boolean} save - Whether to save the changes, default is true + * @returns A promise that resolves to a boolean indicating whether the zen editing process was ended successfully. + * @example + * ```ts + * univerAPI.getActiveWorkbook().endZenEditingAsync(false); + * ``` + */ + endZenEditingAsync(save?: boolean): Promise; +} + +export class FWorkbookSheetsZenEditorMixin extends FWorkbook implements IFWorkbookSheetsZenEditorMixin { + override startZenEditingAsync(): Promise { + const commandService = this._injector.get(ICommandService); + + return commandService.executeCommand(OpenZenEditorCommand.id); + } + + override endZenEditingAsync(save = true): Promise { + const commandService = this._injector.get(ICommandService); + + return save + ? commandService.executeCommand(ConfirmZenEditCommand.id) + : commandService.executeCommand(CancelZenEditCommand.id); + } +} + +FWorkbook.extend(FWorkbookSheetsZenEditorMixin); +declare module '@univerjs/sheets/facade' { + // eslint-disable-next-line ts/naming-convention + interface FWorkbook extends IFWorkbookSheetsZenEditorMixin {} +} diff --git a/packages/sheets-zen-editor/src/facade/index.ts b/packages/sheets-zen-editor/src/facade/index.ts new file mode 100644 index 00000000000..24580fa1c16 --- /dev/null +++ b/packages/sheets-zen-editor/src/facade/index.ts @@ -0,0 +1,21 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import './f-univer'; +import './f-workbook'; + +export type * from './f-univer'; +export type * from './f-workbook'; diff --git a/packages/sheets/src/basics/cell-style.ts b/packages/sheets/src/basics/cell-style.ts index 5f0bd584313..91863044324 100644 --- a/packages/sheets/src/basics/cell-style.ts +++ b/packages/sheets/src/basics/cell-style.ts @@ -17,6 +17,12 @@ import type { IBorderData, ICellData, IDocumentData, IKeyValue, IParagraph, IStyleData, ITextRun, ITextStyle, Nullable, Styles } from '@univerjs/core'; import { normalizeTextRuns, Tools } from '@univerjs/core'; +/** + * + * @param styles + * @param oldVal + * @param newVal + */ export function handleStyle(styles: Styles, oldVal: ICellData, newVal: ICellData) { // use null to clear style const oldStyle = styles.getStyleByCell(oldVal); @@ -65,6 +71,8 @@ export function handleStyle(styles: Styles, oldVal: ICellData, newVal: ICellData /** * Convert old style data for storage * @param style + * @param oldStyle + * @param newStyle */ export function transformStyle(oldStyle: Nullable, newStyle: Nullable): Nullable { // If there is no newly set style, directly store the historical style @@ -89,6 +97,8 @@ export function transformStyle(oldStyle: Nullable, newStyle: Nullabl /** * Convert old style border for storage * @param style + * @param oldBorders + * @param newBorders */ function transformBorders(oldBorders: IBorderData, newBorders: Nullable): IBorderData { // If there is no newly set border, directly store the historical border @@ -111,6 +121,7 @@ function transformBorders(oldBorders: IBorderData, newBorders: Nullable, @@ -154,6 +165,11 @@ function mergeStyle( return backupStyle; } +/** + * + * @param paragraphs + * @param offset + */ function skipParagraphs(paragraphs: IParagraph[], offset: number): number { if (paragraphs.some((p) => p.startIndex === offset)) { return skipParagraphs(paragraphs, offset + 1); diff --git a/packages/sheets/src/facade/f-event.ts b/packages/sheets/src/facade/f-event.ts index f97ea29b0e7..d182a0fef07 100644 --- a/packages/sheets/src/facade/f-event.ts +++ b/packages/sheets/src/facade/f-event.ts @@ -14,35 +14,132 @@ * limitations under the License. */ -import type { IEventBase, IWorksheetData } from '@univerjs/core'; +import type { IEventBase, IWorkbookData, IWorksheetData, UniverInstanceType } from '@univerjs/core'; import type { FWorkbook } from './f-workbook'; import type { FWorksheet } from './f-worksheet'; import { FEventName } from '@univerjs/core'; export interface IFSheetEventMixin { + /** + * Event fired after a sheet is created + * @see {@link ISheetCreatedEventParams} + * @example + * ```ts + * univerAPI.addEvent(univerAPI.event.SheetCreated, (params) => { + * const { workbook, worksheet } = params; + * console.log('unit created', params); + * }); + * ``` + */ get SheetCreated(): 'SheetCreated' ; + /** + * Event fired before a sheet is created + * @see {@link IBeforeSheetCreateEventParams} + * @example + * ```ts + * univerAPI.addEvent(univerAPI.event.BeforeSheetCreate, (params) => { + * const { workbook, index, sheet } = params; + * console.log('unit created', params); + * }); + * ``` + */ get BeforeSheetCreate(): 'BeforeSheetCreate'; + /** + * Event fired after a workbook is created + * @see {@link IWorkbookCreateParam} + * @example + * ```ts + * univerAPI.addEvent(univerAPI.event.WorkbookCreated, (params) => { + * const { unitId, type, workbook, unit } = params; + * console.log('unit created', params); + * }); + * ``` + */ + get WorkbookCreated(): 'WorkbookCreated'; + /** + * Event fired after a workbook is disposed + * @see {@link IWorkbookDisposedEvent} + * @example + * ```ts + * univerAPI.addEvent(univerAPI.event.WorkbookDisposed, (params) => { + * const { unitId, unitType, snapshot } = params; + * console.log('unit disposed', params); + * }); + * ``` + */ + get WorkbookDisposed(): 'WorkbookDisposed'; +} + +export interface IWorkbookCreateParam extends IEventBase { + unitId: string; + type: UniverInstanceType.UNIVER_SHEET; + workbook: FWorkbook; + unit: FWorkbook; +} + +export interface IWorkbookDisposedEvent extends IEventBase { + unitId: string; + unitType: UniverInstanceType.UNIVER_SHEET; + snapshot: IWorkbookData; } export class FSheetEventName extends FEventName implements IFSheetEventMixin { - override get SheetCreated(): 'SheetCreated' { return 'SheetCreated' as const; } - override get BeforeSheetCreate(): 'BeforeSheetCreate' { return 'BeforeSheetCreate' as const; } + override get SheetCreated(): 'SheetCreated' { + return 'SheetCreated' as const; + } + + override get BeforeSheetCreate(): 'BeforeSheetCreate' { + return 'BeforeSheetCreate' as const; + } + + override get WorkbookCreated(): 'WorkbookCreated' { + return 'WorkbookCreated' as const; + } + + override get WorkbookDisposed(): 'WorkbookDisposed' { + return 'WorkbookDisposed' as const; + } } +/** + * Event interface triggered before creating a new worksheet + * @interface IBeforeSheetCreateEventParams + * @augments {IEventBase} + */ export interface IBeforeSheetCreateEventParams extends IEventBase { + /** The workbook instance */ workbook: FWorkbook; + /** Optional index where the new sheet will be inserted */ index?: number; + /** Optional initial worksheet data */ sheet?: IWorksheetData; } +/** + * Event interface triggered after a worksheet is created + * @interface ISheetCreatedEventParams + * @augments {IEventBase} + */ export interface ISheetCreatedEventParams extends IEventBase { + /** The workbook instance */ workbook: FWorkbook; + /** The newly created worksheet */ worksheet: FWorksheet; } +/** + * Configuration interface for sheet-related events + * @interface ISheetEventParamConfig + */ export interface ISheetEventParamConfig { + /** Event fired after a worksheet is created */ SheetCreated: ISheetCreatedEventParams; + /** Event fired before creating a worksheet */ BeforeSheetCreate: IBeforeSheetCreateEventParams; + /** Event fired after a workbook is created */ + WorkbookCreated: IWorkbookCreateParam; + /** Event fired when a workbook is disposed */ + WorkbookDisposed: IWorkbookDisposedEvent; } FEventName.extend(FSheetEventName); @@ -51,4 +148,3 @@ declare module '@univerjs/core' { interface FEventName extends IFSheetEventMixin { } interface IEventParamConfig extends ISheetEventParamConfig { } } - diff --git a/packages/sheets/src/facade/f-range.ts b/packages/sheets/src/facade/f-range.ts index 37cdebb062a..88ccff11dbd 100644 --- a/packages/sheets/src/facade/f-range.ts +++ b/packages/sheets/src/facade/f-range.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import type { CellValue, ICellData, IColorStyle, IObjectMatrixPrimitiveType, IRange, IStyleData, ITextDecoration, Nullable, Workbook, Worksheet } from '@univerjs/core'; -import type { ISetHorizontalTextAlignCommandParams, ISetStyleCommandParams, ISetTextWrapCommandParams, ISetVerticalTextAlignCommandParams, IStyleTypeValue, SplitDelimiterEnum } from '@univerjs/sheets'; +import type { CellValue, CustomData, ICellData, IColorStyle, IDocumentData, IObjectMatrixPrimitiveType, IRange, IStyleData, ITextDecoration, Nullable, Workbook, Worksheet } from '@univerjs/core'; +import type { ISetHorizontalTextAlignCommandParams, ISetRangeValuesCommandParams, ISetStyleCommandParams, ISetTextWrapCommandParams, ISetVerticalTextAlignCommandParams, IStyleTypeValue, SplitDelimiterEnum } from '@univerjs/sheets'; import type { FHorizontalAlignment, FVerticalAlignment } from './utils'; -import { BooleanNumber, Dimension, FBaseInitialable, ICommandService, Inject, Injector, Rectangle, WrapStrategy } from '@univerjs/core'; +import { BooleanNumber, Dimension, FBaseInitialable, ICommandService, Inject, Injector, Rectangle, RichTextValue, WrapStrategy } from '@univerjs/core'; import { FormulaDataModel, serializeRange, serializeRangeWithSheet } from '@univerjs/engine-formula'; import { addMergeCellsUtil, DeleteWorksheetRangeThemeStyleCommand, getAddMergeMutationRangeByType, RemoveWorksheetMergeCommand, SetHorizontalTextAlignCommand, SetRangeValuesCommand, SetStyleCommand, SetTextWrapCommand, SetVerticalTextAlignCommand, SetWorksheetRangeThemeStyleCommand, SheetRangeThemeService, SplitTextToColumnsCommand } from '@univerjs/sheets'; import { FWorkbook } from './f-workbook'; @@ -55,6 +55,14 @@ export class FRange extends FBaseInitialable { return this._worksheet.getName(); } + /** + * Gets the ID of the worksheet + * @returns The ID of the worksheet + */ + getSheetId(): string { + return this._worksheet.getSheetId(); + } + /** * Gets the area where the statement is applied * @returns The area where the statement is applied @@ -95,14 +103,6 @@ export class FRange extends FBaseInitialable { return this._range.endRow - this._range.startRow + 1; } - /** - * Return first cell model data in this range - * @returns The cell model data - */ - getCellData(): ICellData | null { - return this._worksheet.getCell(this._range.startRow, this._range.startColumn) ?? null; - } - /** * Return range whether this range is merged * @returns if true is merged @@ -115,7 +115,7 @@ export class FRange extends FBaseInitialable { /** * Return first cell style data in this range - * @returns The cell style data + * @returns {IStyleData | null} The cell style data */ getCellStyleData(): IStyleData | null { const cell = this.getCellData(); @@ -129,7 +129,7 @@ export class FRange extends FBaseInitialable { /** * Returns the value of the cell at the start of this range. - * @returns The value of the cell. + * @returns {CellValue | null} The value of the cell. */ getValue(): CellValue | null { return this._worksheet.getCell(this._range.startRow, this._range.startColumn)?.v ?? null; @@ -138,7 +138,7 @@ export class FRange extends FBaseInitialable { /** * Returns the rectangular grid of values for this range. * Returns a two-dimensional array of values, indexed by row, then by column. - * @returns A two-dimensional array of values. + * @returns {Nullable[][]} A two-dimensional array of values. */ getValues(): Nullable[][] { const { startRow, endRow, startColumn, endColumn } = this._range; @@ -156,11 +156,33 @@ export class FRange extends FBaseInitialable { return range; } + /** + * Return first cell model data in this range + * @returns {ICellData | null} The cell model data + * @example + * ``` + * univerAPI.getActiveWorkbook() + * .getActiveSheet() + * .getActiveRange() + * .getCellData() + * ``` + */ + getCellData(): ICellData | null { + return this._worksheet.getCell(this._range.startRow, this._range.startColumn) ?? null; + } + /** * Returns the cell data for the cells in the range. - * @returns A two-dimensional array of cell data. + * @returns {Nullable[][]} A two-dimensional array of cell data. + * @example + * ``` + * univerAPI.getActiveWorkbook() + * .getActiveSheet() + * .getActiveRange() + * .getCellDatas() + * ``` */ - getCellDataGrid(): Nullable[][] { + getCellDatas(): Nullable[][] { const { startRow, endRow, startColumn, endColumn } = this._range; const range: Nullable[][] = []; @@ -174,9 +196,90 @@ export class FRange extends FBaseInitialable { return range; } + /** + * @deprecated use `getCellDatas` instead. + */ + getCellDataGrid(): Nullable[][] { + return this.getCellDatas(); + } + + /** + * Returns the rich text value for the cell at the start of this range. + * @returns {Nullable} The rich text value + * @example + * ``` + * univerAPI.getActiveWorkbook() + * .getActiveSheet() + * .getActiveRange() + * .getRichTextValue() + * ``` + */ + getRichTextValue(): Nullable { + const data = this.getCellData(); + if (data?.p) { + return new RichTextValue(data.p); + } + return null; + } + + /** + * Returns the rich text value for the cells in the range. + * @returns {Nullable[][]} A two-dimensional array of RichTextValue objects. + * @example + * ``` + * univerAPI.getActiveWorkbook() + * .getActiveSheet() + * .getActiveRange() + * .getRichTextValues() + * ``` + */ + getRichTextValues(): Nullable[][] { + const dataGrid = this.getCellDataGrid(); + return dataGrid.map((row) => row.map((data) => data?.p ? new RichTextValue(data.p) : null)); + } + + /** + * Returns the value and rich text value for the cell at the start of this range. + * @returns {Nullable} The value and rich text value + * @example + * ``` + * univerAPI.getActiveWorkbook() + * .getActiveSheet() + * .getActiveRange() + * .getValueAndRichTextValue() + * ``` + */ + getValueAndRichTextValue(): Nullable { + const cell = this.getCellData(); + return cell?.p ? new RichTextValue(cell.p) : cell?.v; + } + + /** + * Returns the value and rich text value for the cells in the range. + * @returns {Nullable[][]} A two-dimensional array of value and rich text value + * @example + * ``` + * univerAPI.getActiveWorkbook() + * .getActiveSheet() + * .getActiveRange() + * .getValueAndRichTextValues() + * ``` + */ + getValueAndRichTextValues(): Nullable[][] { + const dataGrid = this.getCellDatas(); + return dataGrid.map((row) => row.map((data) => data?.p ? new RichTextValue(data.p) : data?.v)); + } + /** * Returns the formulas (A1 notation) for the cells in the range. Entries in the 2D array are empty strings for cells with no formula. - * @returns A two-dimensional array of formulas in string format. + * @returns {string[][]} A two-dimensional array of formulas in string format. + * @example + * ```ts + * univerAPI.getActiveWorkbook() + * .getActiveSheet() + * .getActiveRange() + * .getFormulas() + * ``` */ getFormulas(): string[][] { const formulas: string[][] = []; @@ -215,14 +318,81 @@ export class FRange extends FBaseInitialable { return transformCoreVerticalAlignment(this._worksheet.getRange(this._range).getVerticalAlignment()); } + /** + * Set custom meta data for first cell in current range. + * @param {CustomData} data The custom meta data + * @returns {FRange} This range, for chaining + * ```ts + * univerAPI.getActiveWorkbook() + * .getActiveSheet() + * .getActiveRange() + * .setCustomMetaData({ key: 'value' }); + * ``` + */ + setCustomMetaData(data: CustomData): FRange { + return this.setValue({ + custom: data, + }); + } + + /** + * Set custom meta data for current range. + * @param {CustomData[][]} datas The custom meta data + * @returns {FRange} This range, for chaining + * ```ts + * univerAPI.getActiveWorkbook() + * .getActiveSheet() + * .getActiveRange() + * .setCustomMetaDatas([[{ key: 'value' }]]); + * ``` + */ + setCustomMetaDatas(datas: CustomData[][]): FRange { + return this.setValues(datas.map((row) => row.map((data) => ({ custom: data })))); + } + + /** + * Returns the custom meta data for the cell at the start of this range. + * @returns {CustomData | null} The custom meta data + * @example + * ``` + * univerAPI.getActiveWorkbook() + * .getActiveSheet() + * .getActiveRange() + * .getCustomMetaData() + * ``` + */ + getCustomMetaData(): CustomData | null { + const cell = this.getCellData(); + return cell?.custom ?? null; + } + + /** + * Returns the custom meta data for the cells in the range. + * @returns {CustomData[][]} A two-dimensional array of custom meta data + * @example + * ``` + * univerAPI.getActiveWorkbook() + * .getActiveSheet() + * .getActiveRange() + * .getCustomMetaDatas() + * ``` + */ + getCustomMetaDatas(): Nullable[][] { + const dataGrid = this.getCellDataGrid(); + return dataGrid.map((row) => row.map((data) => data?.custom ?? null)); + } + // #region editing /** * Set background color for current range. * @param color {string} * @example - * ```typescript - * univerAPI.getActiveWorkbook().getActiveSheet().getActiveRange().setBackgroundColor('red') + * ``` + * univerAPI.getActiveWorkbook() + * .getActiveSheet() + * .getActiveRange() + * .setBackgroundColor('red') * ``` */ setBackgroundColor(color: string): FRange { @@ -254,8 +424,8 @@ export class FRange extends FBaseInitialable { } /** - * The value can be a number, string, boolean, or standard cell format. If it begins with `=`, it is interpreted as a formula. The value is tiled to all cells in the range. - * @param value + * Set new value for current cell, first cell in this range. + * @param {CellValue | ICellData} value The value can be a number, string, boolean, or standard cell format. If it begins with `=`, it is interpreted as a formula. The value is tiled to all cells in the range. */ setValue(value: CellValue | ICellData): FRange { const realValue = covertCellValue(value); @@ -274,6 +444,94 @@ export class FRange extends FBaseInitialable { return this; } + /** + * Set new value for current cell, first cell in this range. + * @param {CellValue | ICellData} value The value can be a number, string, boolean, or standard cell format. If it begins with `=`, it is interpreted as a formula. The value is tiled to all cells in the range. + * ```ts + * univerAPI.getActiveWorkbook() + * .getActiveSheet() + * .getActiveRange() + * .setValueForCell(1); + * ``` + */ + setValueForCell(value: CellValue | ICellData): FRange { + const realValue = covertCellValue(value); + + if (!realValue) { + throw new Error('Invalid value'); + } + + this._commandService.syncExecuteCommand(SetRangeValuesCommand.id, { + unitId: this._workbook.getUnitId(), + subUnitId: this._worksheet.getSheetId(), + range: { + startColumn: this._range.startColumn, + startRow: this._range.startRow, + endColumn: this._range.endColumn, + endRow: this._range.endRow, + }, + value: realValue, + }); + + return this; + } + + /** + * Set the rich text value for the cell at the start of this range. + * @param {RichTextValue | IDocumentData} value The rich text value + * @returns {FRange} The range + * @example + * ``` + * univerAPI.getActiveWorkbook() + * .getActiveSheet() + * .getActiveRange() + * .setRichTextValueForCell(new RichTextValue().insertText('Hello')); + * ``` + */ + setRichTextValueForCell(value: RichTextValue | IDocumentData): FRange { + const p = value instanceof RichTextValue ? value.getData() : value; + const params: ISetRangeValuesCommandParams = { + unitId: this._workbook.getUnitId(), + subUnitId: this._worksheet.getSheetId(), + range: { + startColumn: this._range.startColumn, + startRow: this._range.startRow, + endColumn: this._range.endColumn, + endRow: this._range.endRow, + }, + value: { p }, + }; + this._commandService.syncExecuteCommand(SetRangeValuesCommand.id, params); + return this; + } + + /** + * Set the rich text value for the cells in the range. + * @param {RichTextValue[][]} values The rich text value + * @returns {FRange} The range + * @example + * ```ts + * univerAPI + * .getActiveWorkbook() + * .getActiveSheet() + * .getActiveRange() + * .setRichTextValues([[new RichTextValue().insertText('Hello')]]); + * ``` + */ + setRichTextValues(values: (RichTextValue | IDocumentData)[][]): FRange { + const cellDatas = values.map((row) => row.map((item) => item && { p: item instanceof RichTextValue ? item.getData() : item })); + const realValue = covertCellValues(cellDatas, this._range); + + const params: ISetRangeValuesCommandParams = { + unitId: this._workbook.getUnitId(), + subUnitId: this._worksheet.getSheetId(), + range: this._range, + value: realValue, + }; + this._commandService.syncExecuteCommand(SetRangeValuesCommand.id, params); + return this; + } + /** * Set the cell wrap of the given range. * Cells with wrap enabled (the default) resize to display their full content. Cells with wrap disabled display as much as possible in the cell without resizing or running to multiple lines. diff --git a/packages/sheets/src/facade/f-univer.ts b/packages/sheets/src/facade/f-univer.ts index 49777bffe84..59b7eb2ebbb 100644 --- a/packages/sheets/src/facade/f-univer.ts +++ b/packages/sheets/src/facade/f-univer.ts @@ -14,10 +14,11 @@ * limitations under the License. */ -import type { IDisposable, Injector, IWorkbookData, Workbook } from '@univerjs/core'; +import type { ICommandInfo, IDisposable, Injector, IWorkbookData, Nullable, Workbook } from '@univerjs/core'; import type { IInsertSheetCommandParams } from '@univerjs/sheets'; import type { IBeforeSheetCreateEventParams, ISheetCreatedEventParams } from './f-event'; -import { FUniver, ICommandService, IUniverInstanceService, toDisposable, UniverInstanceType } from '@univerjs/core'; +import type { FWorksheet } from './f-worksheet'; +import { CanceledError, FUniver, ICommandService, IUniverInstanceService, toDisposable, UniverInstanceType } from '@univerjs/core'; import { InsertSheetCommand } from '@univerjs/sheets'; import { FDefinedNameBuilder } from './f-defined-name'; import { FPermission } from './f-permission'; @@ -76,9 +77,105 @@ export interface IFUniverSheetsMixin { * ``` */ newDefinedName(): FDefinedNameBuilder; + + /** + * Get the target of the sheet. + * @param {string} unitId - The unitId of the sheet. + * @param {string} subUnitId - The subUnitId of the sheet. + * @returns {Nullable<{ workbook: FWorkbook; worksheet: FWorksheet }>} - The target of the sheet. + * @example + * ```ts + * univerAPI.getSheetTarget('unitId', 'subUnitId'); + * ``` + */ + getSheetTarget(unitId: string, subUnitId: string): Nullable<{ workbook: FWorkbook; worksheet: FWorksheet }>; + + /** + * Get the target of the sheet. + * @param {ICommandInfo} commandInfo - The commandInfo of the command. + * @returns {Nullable<{ workbook: FWorkbook; worksheet: FWorksheet }>} - The target of the sheet. + * @example + * ```ts + * univerAPI.addEvent(univerAPI.event.CommandExecuted, (commandInfo) => { + * const target = univerAPI.getCommandSheetTarget(commandInfo); + * if (!target) return; + * const { workbook, worksheet } = target; + * }); + * ``` + */ + getCommandSheetTarget(commandInfo: ICommandInfo): Nullable<{ workbook: FWorkbook; worksheet: FWorksheet }>; } export class FUniverSheetsMixin extends FUniver implements IFUniverSheetsMixin { + override getCommandSheetTarget(commandInfo: ICommandInfo): Nullable<{ workbook: FWorkbook; worksheet: FWorksheet }> { + const params = commandInfo.params as { unitId: string; subUnitId: string; sheetId: string }; + if (!params) return; + const workbook = params.unitId ? this.getUniverSheet(params.unitId) : this.getActiveWorkbook?.(); + if (!workbook) { + return; + } + + const worksheet = workbook.getSheetBySheetId(params.subUnitId || params.sheetId) || workbook.getActiveSheet(); + if (!worksheet) { + return; + } + + return { workbook, worksheet }; + } + + override getSheetTarget(unitId: string, subUnitId: string): Nullable<{ workbook: FWorkbook; worksheet: FWorksheet }> { + const workbook = this.getUniverSheet(unitId); + if (!workbook) { + return; + } + + const worksheet = workbook.getSheetBySheetId(subUnitId); + if (!worksheet) { + return; + } + + return { workbook, worksheet }; + } + + private _initWorkbookEvent(injector: Injector): void { + const univerInstanceService = injector.get(IUniverInstanceService); + this.disposeWithMe( + univerInstanceService.unitDisposed$.subscribe((unit) => { + if (!this._eventRegistry.get(this.Event.WorkbookDisposed)) return; + + if (unit.type === UniverInstanceType.UNIVER_SHEET) { + this.fireEvent(this.Event.WorkbookDisposed, + { + unitId: unit.getUnitId(), + unitType: unit.type, + snapshot: unit.getSnapshot() as IWorkbookData, + + } + ); + } + }) + ); + + this.disposeWithMe( + univerInstanceService.unitAdded$.subscribe((unit) => { + if (!this._eventRegistry.get(this.Event.WorkbookCreated)) return; + + if (unit.type === UniverInstanceType.UNIVER_SHEET) { + const workbook = unit as Workbook; + const workbookUnit = injector.createInstance(FWorkbook, workbook); + this.fireEvent(this.Event.WorkbookCreated, + { + unitId: unit.getUnitId(), + type: unit.type, + workbook: workbookUnit, + unit: workbookUnit, + } + ); + } + }) + ); + } + override _initialize(injector: Injector): void { const commandService = injector.get(ICommandService); this.disposeWithMe( @@ -102,7 +199,7 @@ export class FUniverSheetsMixin extends FUniver implements IFUniverSheetsMixin { ); // cancel this command if (eventParams.cancel) { - throw new Error('Sheet create canceled by facade api.'); + throw new CanceledError(); } break; } @@ -143,6 +240,8 @@ export class FUniverSheetsMixin extends FUniver implements IFUniverSheetsMixin { } }) ); + + this._initWorkbookEvent(injector); } override createUniverSheet(data: Partial): FWorkbook { diff --git a/packages/sheets/src/facade/f-workbook.ts b/packages/sheets/src/facade/f-workbook.ts index 278ee27b3aa..4ce04d79c4a 100644 --- a/packages/sheets/src/facade/f-workbook.ts +++ b/packages/sheets/src/facade/f-workbook.ts @@ -14,11 +14,10 @@ * limitations under the License. */ -import type { CommandListener, ICommandInfo, IDisposable, IRange, IWorkbookData, LocaleType, Workbook } from '@univerjs/core'; +import type { CommandListener, CustomData, ICommandInfo, IDisposable, IRange, IWorkbookData, LocaleType, Workbook } from '@univerjs/core'; import type { ISetDefinedNameMutationParam } from '@univerjs/engine-formula'; import type { ISetSelectionsOperationParams, ISheetCommandSharedParams, RangeThemeStyle } from '@univerjs/sheets'; import { FBaseInitialable, ICommandService, ILogService, Inject, Injector, IPermissionService, IResourceLoaderService, IUniverInstanceService, LocaleService, mergeWorksheetSnapshotWithDefault, RedoCommand, toDisposable, UndoCommand, UniverInstanceType } from '@univerjs/core'; - import { IDefinedNamesService } from '@univerjs/engine-formula'; import { CopySheetCommand, getPrimaryForRange, InsertSheetCommand, RegisterWorksheetRangeThemeStyleCommand, RemoveSheetCommand, SCOPE_WORKBOOK_VALUE_DEFINED_NAME, SetDefinedNameCommand, SetSelectionsOperation, SetWorksheetActiveOperation, SetWorksheetOrderCommand, SheetRangeThemeService, SheetsSelectionsService, UnregisterWorksheetRangeThemeStyleCommand, WorkbookEditablePermission } from '@univerjs/sheets'; import { FDefinedName, FDefinedNameBuilder } from './f-defined-name'; @@ -46,6 +45,10 @@ export class FWorkbook extends FBaseInitialable { this.id = this._workbook.getUnitId(); } + getWorkbook(): Workbook { + return this._workbook; + } + /** * Get the id of the workbook. * @returns {string} The id of the workbook. @@ -821,4 +824,31 @@ export class FWorkbook extends FBaseInitialable { themeName, }); } + + /** + * Set custom metadata of workbook + * @param {CustomData | undefined} custom custom metadata + * @example + * ```ts + * const fWorkbook = univerAPI.getActiveWorkbook(); + * fWorkbook.setCustomMetadata({ key: 'value' }); + * ``` + */ + setCustomMetadata(custom: CustomData | undefined): FWorkbook { + this._workbook.setCustomMetadata(custom); + return this; + } + + /** + * Get custom metadata of workbook + * @returns {CustomData | undefined} custom metadata + * @example + * ```ts + * const fWorkbook = univerAPI.getActiveWorkbook(); + * const custom = fWorkbook.getCustomMetadata(); + * ``` + */ + getCustomMetadata(): CustomData | undefined { + return this._workbook.getCustomMetadata(); + } } diff --git a/packages/sheets/src/facade/f-worksheet.ts b/packages/sheets/src/facade/f-worksheet.ts index 932d8900a83..dfaa1b04355 100644 --- a/packages/sheets/src/facade/f-worksheet.ts +++ b/packages/sheets/src/facade/f-worksheet.ts @@ -47,6 +47,10 @@ export class FWorksheet extends FBaseInitialable { super(_injector); } + getSheet(): Worksheet { + return this._worksheet; + } + /** * Returns the injector * @returns The injector @@ -1722,6 +1726,83 @@ export class FWorksheet extends FBaseInitialable { return names.filter((name) => name.getLocalSheetId() === this.getSheetId()); } + /** + * Set custom metadata of worksheet + * @param {CustomData | undefined} custom custom metadata + * @example + * ```ts + * const fWorkbook = univerAPI.getActiveWorkbook(); + * const fWorkSheet = fWorkbook.getActiveSheet(); + * fWorkSheet.setCustomMetadata({ key: 'value' }); + * ``` + */ + setCustomMetadata(custom: CustomData | undefined): FWorksheet { + this._worksheet.setCustomMetadata(custom); + return this; + } + + /** + * Set custom metadata of row + * @param {number} index row index + * @param {CustomData | undefined} custom custom metadata + * @example + * ```ts + * const fWorkbook = univerAPI.getActiveWorkbook(); + * const fWorkSheet = fWorkbook.getActiveSheet(); + * fWorkSheet.setRowCustomMetadata(0, { key: 'value' }); + * ``` + */ + setRowCustomMetadata(index: number, custom: CustomData | undefined): FWorksheet { + this._worksheet.getRowManager().setCustomMetadata(index, custom); + return this; + } + + /** + * Set custom metadata of column + * @param {number} index column index + * @param {CustomData | undefined} custom custom metadata + * @example + * ```ts + * const fWorkbook = univerAPI.getActiveWorkbook(); + * const fWorkSheet = fWorkbook.getActiveSheet(); + * fWorkSheet.setColumnCustomMetadata(0, { key: 'value' }); + * ``` + */ + setColumnCustomMetadata(index: number, custom: CustomData | undefined): FWorksheet { + this._worksheet.getColumnManager().setCustomMetadata(index, custom); + return this; + } + + /** + * Get custom metadata of row + * @param {number} index row index + * @returns {CustomData | undefined} custom metadata + * @example + * ```ts + * const fWorkbook = univerAPI.getActiveWorkbook(); + * const fWorkSheet = fWorkbook.getActiveSheet(); + * const custom = fWorkSheet.getRowCustomMetadata(0); + * ``` + */ + getRowCustomMetadata(index: number): CustomData | undefined { + return this._worksheet.getRowManager().getCustomMetadata(index); + } + + /** + * Get custom metadata of column + * @param {number} index column index + * @returns {CustomData | undefined} custom metadata + * @example + * ```ts + * const fWorkbook = univerAPI.getActiveWorkbook(); + * const fWorkSheet = fWorkbook.getActiveSheet(); + * const custom = fWorkSheet.getColumnCustomMetadata(0); + * ``` + */ + getColumnCustomMetadata(index: number): CustomData | undefined { + return this._worksheet.getColumnManager().getCustomMetadata(index); + } + /** * Get all merged cells in the current worksheet * @returns {FRange[]} All the merged cells in the worksheet diff --git a/packages/thread-comment/src/common/utils.ts b/packages/thread-comment/src/common/utils.ts index 2c01ff8cb7e..440f60290dc 100644 --- a/packages/thread-comment/src/common/utils.ts +++ b/packages/thread-comment/src/common/utils.ts @@ -16,6 +16,6 @@ import { dayjs } from '@univerjs/core'; -export function getDT() { - return dayjs().format('YYYY/MM/DD HH:mm'); +export function getDT(date?: Date) { + return dayjs(date).format('YYYY/MM/DD HH:mm'); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 913868a46f3..093b92b6eff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1789,6 +1789,9 @@ importers: '@univerjs/sheets-ui': specifier: workspace:* version: link:../sheets-ui + '@univerjs/sheets-zen-editor': + specifier: workspace:* + version: link:../sheets-zen-editor '@univerjs/ui': specifier: workspace:* version: link:../ui @@ -3008,6 +3011,9 @@ importers: '@univerjs/core': specifier: workspace:* version: link:../core + '@univerjs/docs': + specifier: workspace:* + version: link:../docs '@univerjs/docs-ui': specifier: workspace:* version: link:../docs-ui