From e071ef5231eeb25d89e367b652233acba8bcd453 Mon Sep 17 00:00:00 2001 From: jocs Date: Sat, 11 Nov 2023 18:50:25 +0800 Subject: [PATCH 01/12] feat: support copy cut and paste in doc --- .../commands/commands/clipboard.command.ts | 33 ++++++++++++++++--- .../src/controllers/clipboard.controller.ts | 4 +-- .../services/clipboard/clipboard.service.ts | 19 +++++++++-- .../commands/commands/clipboard.command.ts | 1 + .../services/clipboard/clipboard.service.ts | 2 ++ 5 files changed, 50 insertions(+), 9 deletions(-) diff --git a/packages/ui-plugin-docs/src/commands/commands/clipboard.command.ts b/packages/ui-plugin-docs/src/commands/commands/clipboard.command.ts index adda4f043da..81f68d3cfb8 100644 --- a/packages/ui-plugin-docs/src/commands/commands/clipboard.command.ts +++ b/packages/ui-plugin-docs/src/commands/commands/clipboard.command.ts @@ -1,5 +1,5 @@ -import { CopyCommand, CutCommand, PasteCommand } from '@univerjs/base-ui'; -import { CommandType, FOCUSING_DOC, IMultiCommand } from '@univerjs/core'; +import { CopyCommand, CutCommand, IClipboardInterfaceService, PasteCommand } from '@univerjs/base-ui'; +import { CommandType, FOCUSING_DOC, ILogService, IMultiCommand } from '@univerjs/core'; import { IDocClipboardService } from '../../services/clipboard/clipboard.service'; @@ -37,7 +37,32 @@ export const DocPasteCommand: IMultiCommand = { priority: 1100, preconditions: (contextService) => contextService.getContextValue(FOCUSING_DOC), handler: async (accessor, params) => { - const docClipboardService = accessor.get(IDocClipboardService); - return docClipboardService.paste(); + const logService = accessor.get(ILogService); + + // use cell editor to get ClipboardData first + // if that doesn't work, use the browser's clipboard API + // this clipboard API would ask user for permission, so we may need to notify user (and retry perhaps) + logService.log('[DocPasteCommand]', 'the focusing element is', document.activeElement); + + const result = document.execCommand('paste'); + + if (!result) { + logService.log( + '[DocPasteCommand]', + 'failed to execute paste command on the activeElement, trying to use clipboard API.' + ); + + const clipboardInterfaceService = accessor.get(IClipboardInterfaceService); + const clipboardItems = await clipboardInterfaceService.read(); + const sheetClipboardService = accessor.get(IDocClipboardService); + + if (clipboardItems.length !== 0) { + return sheetClipboardService.paste(clipboardItems[0]); + } + + return false; + } + + return false; }, }; diff --git a/packages/ui-plugin-docs/src/controllers/clipboard.controller.ts b/packages/ui-plugin-docs/src/controllers/clipboard.controller.ts index 38f9325f8e4..40c5da34f07 100644 --- a/packages/ui-plugin-docs/src/controllers/clipboard.controller.ts +++ b/packages/ui-plugin-docs/src/controllers/clipboard.controller.ts @@ -1,6 +1,6 @@ import { Disposable, ICommandService, IUniverInstanceService } from '@univerjs/core'; -import { DocCopyCommand } from '../commands/commands/clipboard.command'; +import { DocCopyCommand, DocCutCommand, DocPasteCommand } from '../commands/commands/clipboard.command'; import { IDocClipboardService } from '../services/clipboard/clipboard.service'; export class DocClipboardController extends Disposable { @@ -13,7 +13,7 @@ export class DocClipboardController extends Disposable { } initialize() { - [DocCopyCommand, DocCopyCommand, DocCopyCommand].forEach((command) => + [DocCopyCommand, DocCutCommand, DocPasteCommand].forEach((command) => this.disposeWithMe(this._commandService.registerAsMultipleCommand(command)) ); } diff --git a/packages/ui-plugin-docs/src/services/clipboard/clipboard.service.ts b/packages/ui-plugin-docs/src/services/clipboard/clipboard.service.ts index 6fb0d4c64a4..7f98fd776aa 100644 --- a/packages/ui-plugin-docs/src/services/clipboard/clipboard.service.ts +++ b/packages/ui-plugin-docs/src/services/clipboard/clipboard.service.ts @@ -1,4 +1,8 @@ -import { IClipboardInterfaceService } from '@univerjs/base-ui'; +import { + HTML_CLIPBOARD_MIME_TYPE, + IClipboardInterfaceService, + PLAIN_TEXT_CLIPBOARD_MIME_TYPE, +} from '@univerjs/base-ui'; import { Disposable, IUniverInstanceService, toDisposable } from '@univerjs/core'; import { createIdentifier, IDisposable } from '@wendellhu/redi'; @@ -12,7 +16,7 @@ export interface IDocClipboardHook { export interface IDocClipboardService { copy(): Promise; cut(): Promise; - paste(): Promise; + paste(item: ClipboardItem): Promise; addClipboardHook(hook: IDocClipboardHook): IDisposable; } @@ -37,14 +41,23 @@ export class DocClipboardService extends Disposable implements IDocClipboardServ throw new Error('Method not implemented.'); } - paste(): Promise { + async paste(item: ClipboardItem): Promise { + const text = await item.getType(PLAIN_TEXT_CLIPBOARD_MIME_TYPE).then((blob) => blob && blob.text()); + const html = await item.getType(HTML_CLIPBOARD_MIME_TYPE).then((blob) => blob && blob.text()); + + console.log(text); + console.log(html); + + // this._logService.error('[SheetClipboardService]', 'No valid data on clipboard'); throw new Error('Method not implemented.'); } addClipboardHook(hook: IDocClipboardHook): IDisposable { this._clipboardHooks.push(hook); + return toDisposable(() => { const index = this._clipboardHooks.indexOf(hook); + if (index > -1) { this._clipboardHooks.splice(index, 1); } diff --git a/packages/ui-plugin-sheets/src/commands/commands/clipboard.command.ts b/packages/ui-plugin-sheets/src/commands/commands/clipboard.command.ts index e4d8baf1cf0..2a6609bb999 100644 --- a/packages/ui-plugin-sheets/src/commands/commands/clipboard.command.ts +++ b/packages/ui-plugin-sheets/src/commands/commands/clipboard.command.ts @@ -46,6 +46,7 @@ export const SheetPasteCommand: IMultiCommand = { logService.log('[SheetPasteCommand]', 'the focusing element is', document.activeElement); const result = document.execCommand('paste'); + if (!result) { logService.log( '[SheetPasteCommand]', diff --git a/packages/ui-plugin-sheets/src/services/clipboard/clipboard.service.ts b/packages/ui-plugin-sheets/src/services/clipboard/clipboard.service.ts index f55e3a71eef..27c9f5a5a28 100644 --- a/packages/ui-plugin-sheets/src/services/clipboard/clipboard.service.ts +++ b/packages/ui-plugin-sheets/src/services/clipboard/clipboard.service.ts @@ -185,6 +185,7 @@ export class SheetClipboardService extends Disposable implements ISheetClipboard const colStyles = getColStyle(getArrayFromTo(startColumn, endColumn), hooks); // row styles and table contents const rowContents: string[] = []; + matrix.forRow((row, cols) => { // TODO: cols here should filtered out those in span cells rowContents.push(getRowContent(row, cols, hooks, matrix)); @@ -736,6 +737,7 @@ function getTDContent( const mergedProperties = mergeProperties(properties); const str = zipClipboardPropertyItemToString(mergedProperties); const content = hooks.reduce((acc, hook) => acc || hook.onCopyCellContent?.(row, col) || '', ''); + return `${content}`; } From 75d0575807542fc1a4bed917fd824fa3cf490c98 Mon Sep 17 00:00:00 2001 From: jocs Date: Mon, 13 Nov 2023 21:01:17 +0800 Subject: [PATCH 02/12] feat: paste content from word --- .../base-docs/src/basics/memory-cursor.ts | 4 +- .../commands/commands/core-editing.command.ts | 9 +- .../commands/inline-format.command.ts | 2 +- .../mutations/core-editing.mutation.ts | 4 +- packages/base-docs/src/index.ts | 2 + .../commands/commands/clipboard.command.ts | 123 +++++++++++++----- .../src/controllers/clipboard.controller.ts | 49 ++++++- .../services/clipboard/clipboard.service.ts | 38 +++--- .../clipboard/html-to-udm/converter.ts | 69 ++++++++++ .../clipboard/html-to-udm/parse-node-style.ts | 102 +++++++++++++++ .../clipboard/html-to-udm/parse-to-dom.ts | 7 + 11 files changed, 350 insertions(+), 59 deletions(-) create mode 100644 packages/ui-plugin-docs/src/services/clipboard/html-to-udm/converter.ts create mode 100644 packages/ui-plugin-docs/src/services/clipboard/html-to-udm/parse-node-style.ts create mode 100644 packages/ui-plugin-docs/src/services/clipboard/html-to-udm/parse-to-dom.ts diff --git a/packages/base-docs/src/basics/memory-cursor.ts b/packages/base-docs/src/basics/memory-cursor.ts index 0387ac23869..5f737a62dce 100644 --- a/packages/base-docs/src/basics/memory-cursor.ts +++ b/packages/base-docs/src/basics/memory-cursor.ts @@ -1,4 +1,4 @@ -class MemoryCursor { +export class MemoryCursor { cursor: number = 0; reset() { @@ -10,5 +10,3 @@ class MemoryCursor { this.cursor += pos; } } - -export default MemoryCursor; diff --git a/packages/base-docs/src/commands/commands/core-editing.command.ts b/packages/base-docs/src/commands/commands/core-editing.command.ts index 0592a595147..09911f1e2dd 100644 --- a/packages/base-docs/src/commands/commands/core-editing.command.ts +++ b/packages/base-docs/src/commands/commands/core-editing.command.ts @@ -301,15 +301,16 @@ export const CoverCommand: ICommand = { }, }; -function getRetainAndDeleteFromReplace( +export function getRetainAndDeleteFromReplace( range: ITextRange, - segmentId?: string + segmentId: string = '', + memoryCursor: number = 0 ): Array { const { startOffset, endOffset, collapsed } = range; const dos: Array = []; - const textStart = startOffset + (collapsed ? -1 : 0); - const textEnd = endOffset - 1; + const textStart = startOffset + (collapsed ? -1 : 0) - memoryCursor; + const textEnd = endOffset - 1 - memoryCursor; if (textStart > 0) { dos.push({ diff --git a/packages/base-docs/src/commands/commands/inline-format.command.ts b/packages/base-docs/src/commands/commands/inline-format.command.ts index 024ae3d3779..5480d8a81fa 100644 --- a/packages/base-docs/src/commands/commands/inline-format.command.ts +++ b/packages/base-docs/src/commands/commands/inline-format.command.ts @@ -13,7 +13,7 @@ import { IUniverInstanceService, } from '@univerjs/core'; -import MemoryCursor from '../../basics/memory-cursor'; +import { MemoryCursor } from '../../basics/memory-cursor'; import { TextSelectionManagerService } from '../../services/text-selection-manager.service'; import { IRichTextEditingMutationParams, RichTextEditingMutation } from '../mutations/core-editing.mutation'; diff --git a/packages/base-docs/src/commands/mutations/core-editing.mutation.ts b/packages/base-docs/src/commands/mutations/core-editing.mutation.ts index 9521eb3eee5..892f5466907 100644 --- a/packages/base-docs/src/commands/mutations/core-editing.mutation.ts +++ b/packages/base-docs/src/commands/mutations/core-editing.mutation.ts @@ -7,7 +7,7 @@ import { UpdateDocsAttributeType, } from '@univerjs/core'; -import MemoryCursor from '../../basics/memory-cursor'; +import { MemoryCursor } from '../../basics/memory-cursor'; import { DeleteApply } from './functions/delete-apply'; import { InsertApply } from './functions/insert-apply'; import { UpdateAttributeApply } from './functions/update-apply'; @@ -71,6 +71,8 @@ export const RichTextEditingMutation: IMutation { // FIXME: @jocs Since UpdateAttributeApply modifies the mutation(used in undo/redo), // so make a deep copy here, does UpdateAttributeApply need to diff --git a/packages/base-docs/src/index.ts b/packages/base-docs/src/index.ts index d92e2600ab2..39d716087bd 100644 --- a/packages/base-docs/src/index.ts +++ b/packages/base-docs/src/index.ts @@ -1,8 +1,10 @@ export * from './basics/component-tools'; export * from './basics/docs-view-key'; +export { MemoryCursor } from './basics/memory-cursor'; export { CoverCommand, DeleteCommand, + getRetainAndDeleteFromReplace, type ICoverCommandParams, type IDeleteCommandParams, type IIMEInputCommandParams, diff --git a/packages/ui-plugin-docs/src/commands/commands/clipboard.command.ts b/packages/ui-plugin-docs/src/commands/commands/clipboard.command.ts index 81f68d3cfb8..287dbb96b91 100644 --- a/packages/ui-plugin-docs/src/commands/commands/clipboard.command.ts +++ b/packages/ui-plugin-docs/src/commands/commands/clipboard.command.ts @@ -1,7 +1,27 @@ -import { CopyCommand, CutCommand, IClipboardInterfaceService, PasteCommand } from '@univerjs/base-ui'; -import { CommandType, FOCUSING_DOC, ILogService, IMultiCommand } from '@univerjs/core'; +import { + getRetainAndDeleteFromReplace, + IRichTextEditingMutationParams, + MemoryCursor, + RichTextEditingMutation, + TextSelectionManagerService, +} from '@univerjs/base-docs'; +import { CopyCommand, CutCommand, PasteCommand } from '@univerjs/base-ui'; +import { + CommandType, + FOCUSING_DOC, + ICommand, + ICommandInfo, + ICommandService, + IDocumentBody, + IMultiCommand, + IUndoRedoService, + IUniverInstanceService, +} from '@univerjs/core'; -import { IDocClipboardService } from '../../services/clipboard/clipboard.service'; +interface IInnerPasteCommandParams { + segmentId: string; + body: IDocumentBody; +} export const DocCopyCommand: IMultiCommand = { id: CopyCommand.id, @@ -10,10 +30,7 @@ export const DocCopyCommand: IMultiCommand = { multi: true, priority: 1100, preconditions: (contextService) => contextService.getContextValue(FOCUSING_DOC), - handler: async (accessor, params) => { - const docClipboardService = accessor.get(IDocClipboardService); - return docClipboardService.copy(); - }, + handler: async () => true, }; export const DocCutCommand: IMultiCommand = { @@ -23,10 +40,7 @@ export const DocCutCommand: IMultiCommand = { multi: true, priority: 1100, preconditions: (contextService) => contextService.getContextValue(FOCUSING_DOC), - handler: async (accessor, params) => { - const docClipboardService = accessor.get(IDocClipboardService); - return docClipboardService.cut(); - }, + handler: async () => true, }; export const DocPasteCommand: IMultiCommand = { @@ -36,31 +50,82 @@ export const DocPasteCommand: IMultiCommand = { multi: true, priority: 1100, preconditions: (contextService) => contextService.getContextValue(FOCUSING_DOC), - handler: async (accessor, params) => { - const logService = accessor.get(ILogService); + handler: async () => true, +}; + +export const InnerPasteCommand: ICommand = { + id: 'doc.command.inner-paste', + type: CommandType.COMMAND, + handler: async (accessor, params: IInnerPasteCommandParams) => { + const { segmentId, body } = params; + const undoRedoService = accessor.get(IUndoRedoService); + const commandService = accessor.get(ICommandService); + const textSelectionManagerService = accessor.get(TextSelectionManagerService); + const currentUniverService = accessor.get(IUniverInstanceService); + + const selections = textSelectionManagerService.getSelections(); + + if (!Array.isArray(selections) || selections.length === 0) { + return false; + } - // use cell editor to get ClipboardData first - // if that doesn't work, use the browser's clipboard API - // this clipboard API would ask user for permission, so we may need to notify user (and retry perhaps) - logService.log('[DocPasteCommand]', 'the focusing element is', document.activeElement); + const docsModel = currentUniverService.getCurrentUniverDocInstance(); + const unitId = docsModel.getUnitId(); - const result = document.execCommand('paste'); + const doMutation: ICommandInfo = { + id: RichTextEditingMutation.id, + params: { + unitId, + mutations: [], + }, + }; - if (!result) { - logService.log( - '[DocPasteCommand]', - 'failed to execute paste command on the activeElement, trying to use clipboard API.' - ); + const memoryCursor = new MemoryCursor(); - const clipboardInterfaceService = accessor.get(IClipboardInterfaceService); - const clipboardItems = await clipboardInterfaceService.read(); - const sheetClipboardService = accessor.get(IDocClipboardService); + memoryCursor.reset(); - if (clipboardItems.length !== 0) { - return sheetClipboardService.paste(clipboardItems[0]); + for (const selection of selections) { + const { startOffset, endOffset, collapsed } = selection; + + const len = startOffset - memoryCursor.cursor; + + if (collapsed) { + doMutation.params!.mutations.push({ + t: 'r', + len, + segmentId, + }); + } else { + doMutation.params!.mutations.push( + ...getRetainAndDeleteFromReplace(selection, segmentId, memoryCursor.cursor) + ); } - return false; + doMutation.params!.mutations.push({ + t: 'i', + body, + len: body.dataStream.length, + line: 0, + segmentId, + }); + + memoryCursor.reset(); + memoryCursor.moveCursor(endOffset); + } + + const result = commandService.syncExecuteCommand< + IRichTextEditingMutationParams, + IRichTextEditingMutationParams + >(doMutation.id, doMutation.params); + + if (result) { + undoRedoService.pushUndoRedo({ + unitID: unitId, + undoMutations: [{ id: RichTextEditingMutation.id, params: result }], + redoMutations: [{ id: RichTextEditingMutation.id, params: doMutation.params }], + }); + + return true; } return false; diff --git a/packages/ui-plugin-docs/src/controllers/clipboard.controller.ts b/packages/ui-plugin-docs/src/controllers/clipboard.controller.ts index 40c5da34f07..c0f2275abb3 100644 --- a/packages/ui-plugin-docs/src/controllers/clipboard.controller.ts +++ b/packages/ui-plugin-docs/src/controllers/clipboard.controller.ts @@ -1,20 +1,63 @@ -import { Disposable, ICommandService, IUniverInstanceService } from '@univerjs/core'; +import { ITextSelectionRenderManager } from '@univerjs/base-render'; +import { Disposable, ICommandInfo, ICommandService, ILogService, IUniverInstanceService } from '@univerjs/core'; -import { DocCopyCommand, DocCutCommand, DocPasteCommand } from '../commands/commands/clipboard.command'; +import { + DocCopyCommand, + DocCutCommand, + DocPasteCommand, + InnerPasteCommand, +} from '../commands/commands/clipboard.command'; import { IDocClipboardService } from '../services/clipboard/clipboard.service'; export class DocClipboardController extends Disposable { constructor( + @ILogService private readonly _logService: ILogService, @ICommandService private readonly _commandService: ICommandService, @IUniverInstanceService private readonly _currentUniverService: IUniverInstanceService, - @IDocClipboardService private readonly _docClipboardService: IDocClipboardService + @IDocClipboardService private readonly _docClipboardService: IDocClipboardService, + @ITextSelectionRenderManager private _textSelectionRenderManager: ITextSelectionRenderManager ) { super(); + this.commandExecutedListener(); } initialize() { [DocCopyCommand, DocCutCommand, DocPasteCommand].forEach((command) => this.disposeWithMe(this._commandService.registerAsMultipleCommand(command)) ); + [InnerPasteCommand].forEach((command) => this.disposeWithMe(this._commandService.registerCommand(command))); + } + + private commandExecutedListener() { + const updateCommandList = [DocPasteCommand.id]; + + this.disposeWithMe( + this._commandService.onCommandExecuted((command: ICommandInfo) => { + if (!updateCommandList.includes(command.id)) { + return; + } + + this.handlePaste(); + }) + ); + } + + private async handlePaste() { + const { _docClipboardService: docClipboardService } = this; + const { segmentId } = this._textSelectionRenderManager.getActiveRange() ?? {}; + + if (!segmentId) { + this._logService.error('[DocClipboardController] segmentId is not existed'); + } + + try { + const body = await docClipboardService.queryClipboardData(); + + this._commandService.executeCommand(InnerPasteCommand.id, { body, segmentId }); + + // TODO: @jocs, reset selections. + } catch (_e) { + this._logService.error('[DocClipboardController] clipboard is empty'); + } } } diff --git a/packages/ui-plugin-docs/src/services/clipboard/clipboard.service.ts b/packages/ui-plugin-docs/src/services/clipboard/clipboard.service.ts index 7f98fd776aa..031e08a8c51 100644 --- a/packages/ui-plugin-docs/src/services/clipboard/clipboard.service.ts +++ b/packages/ui-plugin-docs/src/services/clipboard/clipboard.service.ts @@ -3,9 +3,11 @@ import { IClipboardInterfaceService, PLAIN_TEXT_CLIPBOARD_MIME_TYPE, } from '@univerjs/base-ui'; -import { Disposable, IUniverInstanceService, toDisposable } from '@univerjs/core'; +import { Disposable, IDocumentBody, IUniverInstanceService, toDisposable } from '@univerjs/core'; import { createIdentifier, IDisposable } from '@wendellhu/redi'; +import HtmlToUDMService from './html-to-udm/converter'; + export interface IClipboardPropertyItem {} export interface IDocClipboardHook { @@ -14,9 +16,7 @@ export interface IDocClipboardHook { } export interface IDocClipboardService { - copy(): Promise; - cut(): Promise; - paste(item: ClipboardItem): Promise; + queryClipboardData(): Promise; addClipboardHook(hook: IDocClipboardHook): IDisposable; } @@ -25,6 +25,7 @@ export const IDocClipboardService = createIdentifier('doc. export class DocClipboardService extends Disposable implements IDocClipboardService { private _clipboardHooks: IDocClipboardHook[] = []; + private htmlToUDM = new HtmlToUDMService(); constructor( @IUniverInstanceService private readonly _currentUniverService: IUniverInstanceService, @@ -33,23 +34,24 @@ export class DocClipboardService extends Disposable implements IDocClipboardServ super(); } - copy(): Promise { - throw new Error('Method not implemented.'); - } - - cut(): Promise { - throw new Error('Method not implemented.'); - } + async queryClipboardData(): Promise { + const clipboardItems = await this._clipboardInterfaceService.read(); - async paste(item: ClipboardItem): Promise { - const text = await item.getType(PLAIN_TEXT_CLIPBOARD_MIME_TYPE).then((blob) => blob && blob.text()); - const html = await item.getType(HTML_CLIPBOARD_MIME_TYPE).then((blob) => blob && blob.text()); + if (clipboardItems.length === 0) { + return Promise.reject(); + } + const clipboardItem = clipboardItems[0]; + const text = await clipboardItem.getType(PLAIN_TEXT_CLIPBOARD_MIME_TYPE).then((blob) => blob && blob.text()); + const html = await clipboardItem.getType(HTML_CLIPBOARD_MIME_TYPE).then((blob) => blob && blob.text()); - console.log(text); - console.log(html); + if (!html) { + // TODO: @JOCS, Parsing paragraphs and sections + return { + dataStream: text, + }; + } - // this._logService.error('[SheetClipboardService]', 'No valid data on clipboard'); - throw new Error('Method not implemented.'); + return this.htmlToUDM.convert(html); } addClipboardHook(hook: IDocClipboardHook): IDisposable { diff --git a/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/converter.ts b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/converter.ts new file mode 100644 index 00000000000..0b9595037e7 --- /dev/null +++ b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/converter.ts @@ -0,0 +1,69 @@ +import { IDocumentBody, ITextStyle, Nullable } from '@univerjs/core'; + +import extractNodeStyle from './parse-node-style'; +import parseToDom from './parse-to-dom'; + +/** + * Convert html strings into data structures in univer, IDocumentBody. + * Support plug-in, add custom rules, + */ +class HtmlToUDMService { + private styleCache: Map = new Map(); + + convert(html: string): IDocumentBody { + const dom = parseToDom(html); + const newDocBody: IDocumentBody = { + dataStream: '', + textRuns: [], + paragraphs: [], + }; + this.styleCache.clear(); + this.process(null, dom?.childNodes!, newDocBody); + this.styleCache.clear(); + + return newDocBody; + } + + private process(parent: Nullable, nodes: NodeListOf, doc: IDocumentBody) { + for (const node of nodes) { + if (node.nodeType === Node.TEXT_NODE) { + // TODO: @JOCS, More characters need to be replaced, like `\b` + const text = node.nodeValue?.replace(/\r\n/g, ''); + let style; + + if (parent && this.styleCache.has(parent)) { + style = this.styleCache.get(parent); + } + + doc.dataStream += text; + + if (style) { + doc.textRuns!.push({ + st: doc.dataStream.length - text!.length, + ed: doc.dataStream.length, + ts: style, + }); + } + } else if (node.nodeType === Node.ELEMENT_NODE) { + const parentStyles = parent ? this.styleCache.get(parent) : {}; + const nodeStyles = extractNodeStyle(node as HTMLElement); + + this.styleCache.set(node, { ...parentStyles, ...nodeStyles }); + + const { childNodes } = node; + + this.process(node, childNodes, doc); + + // TODO: @JOCS, Use plugin + if (node.nodeName.toLocaleLowerCase() === 'p') { + doc.paragraphs?.push({ + startIndex: doc.dataStream.length, + }); + doc.dataStream += '\r'; + } + } + } + } +} + +export default HtmlToUDMService; diff --git a/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/parse-node-style.ts b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/parse-node-style.ts new file mode 100644 index 00000000000..22ad1780411 --- /dev/null +++ b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/parse-node-style.ts @@ -0,0 +1,102 @@ +import { ptToPx } from '@univerjs/base-render'; +import { BooleanNumber, ITextStyle } from '@univerjs/core'; + +export default function extractNodeStyle(node: HTMLElement): ITextStyle { + const styles = node.style; + const docStyles: ITextStyle = {}; + const tagName = node.tagName; + + switch (tagName.toLocaleLowerCase()) { + case 'b': + case 'em': + case 'strong': { + docStyles.bl = BooleanNumber.TRUE; + break; + } + + case 'u': { + docStyles.ul = { + s: BooleanNumber.TRUE, + }; + break; + } + + case 'i': { + docStyles.it = BooleanNumber.TRUE; + break; + } + } + + for (let i = 0; i < styles.length; i++) { + const cssRule = styles[i]; + const cssValue = styles.getPropertyValue(cssRule); + + switch (cssRule) { + case 'font-family': { + docStyles.ff = cssValue; + + break; + } + + case 'font-size': { + const fontSize = parseInt(cssValue); + // TODO: @JOCS, hand other CSS value unit, rem, em, pt, % + docStyles.fs = /pt$/.test(cssValue) ? ptToPx(fontSize) : fontSize; + + break; + } + + case 'font-style': { + if (cssValue === 'italic') { + docStyles.it = BooleanNumber.TRUE; + } + + break; + } + + case 'font-weight': { + const MIDDLE_FONT_WEIGHT = 400; + + if (Number(cssValue) > MIDDLE_FONT_WEIGHT) { + docStyles.bl = BooleanNumber.TRUE; + } + + break; + } + + case 'text-decoration': { + // TODO: @JOCS, Parse CSS values like: underline dotted; + if (/underline/.test(cssValue)) { + docStyles.ul = { + s: BooleanNumber.TRUE, + }; + } else if (/overline/.test(cssValue)) { + docStyles.ol = { + s: BooleanNumber.TRUE, + }; + } else if (/line-through/.test(cssValue)) { + docStyles.st = { + s: BooleanNumber.TRUE, + }; + } + + break; + } + + case 'color': { + docStyles.cl = { + rgb: cssValue, + }; + + break; + } + + default: { + // console.log(`Unhandled css rule ${cssRule}`); + break; + } + } + } + + return docStyles; +} diff --git a/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/parse-to-dom.ts b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/parse-to-dom.ts new file mode 100644 index 00000000000..efdba9bc0dd --- /dev/null +++ b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/parse-to-dom.ts @@ -0,0 +1,7 @@ +export default function parseToDom(rawHtml: string) { + const parser = new DOMParser(); + const html = `${rawHtml}`; + const doc = parser.parseFromString(html, 'text/html'); + + return doc.querySelector('#univer-root'); +} From 705967367683b4e6d508577458817b07fc54f135 Mon Sep 17 00:00:00 2001 From: jocs Date: Mon, 13 Nov 2023 21:30:42 +0800 Subject: [PATCH 03/12] feat: support background color --- .../src/services/clipboard/clipboard.service.ts | 2 +- .../services/clipboard/html-to-udm/parse-node-style.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/ui-plugin-docs/src/services/clipboard/clipboard.service.ts b/packages/ui-plugin-docs/src/services/clipboard/clipboard.service.ts index 031e08a8c51..ff890849271 100644 --- a/packages/ui-plugin-docs/src/services/clipboard/clipboard.service.ts +++ b/packages/ui-plugin-docs/src/services/clipboard/clipboard.service.ts @@ -43,7 +43,7 @@ export class DocClipboardService extends Disposable implements IDocClipboardServ const clipboardItem = clipboardItems[0]; const text = await clipboardItem.getType(PLAIN_TEXT_CLIPBOARD_MIME_TYPE).then((blob) => blob && blob.text()); const html = await clipboardItem.getType(HTML_CLIPBOARD_MIME_TYPE).then((blob) => blob && blob.text()); - + console.log(html); if (!html) { // TODO: @JOCS, Parsing paragraphs and sections return { diff --git a/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/parse-node-style.ts b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/parse-node-style.ts index 22ad1780411..910f355e0b4 100644 --- a/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/parse-node-style.ts +++ b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/parse-node-style.ts @@ -91,6 +91,14 @@ export default function extractNodeStyle(node: HTMLElement): ITextStyle { break; } + case 'background-color': { + docStyles.bg = { + rgb: cssValue, + }; + + break; + } + default: { // console.log(`Unhandled css rule ${cssRule}`); break; From 32df9da6e6f27197c4f36a564e7c6958ff9cb2d1 Mon Sep 17 00:00:00 2001 From: jocs Date: Tue, 14 Nov 2023 10:53:29 +0800 Subject: [PATCH 04/12] refactor: add Px to Pt ratio --- .../src/services/clipboard/html-to-udm/parse-node-style.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/parse-node-style.ts b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/parse-node-style.ts index 910f355e0b4..23a8d2ad333 100644 --- a/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/parse-node-style.ts +++ b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/parse-node-style.ts @@ -1,4 +1,3 @@ -import { ptToPx } from '@univerjs/base-render'; import { BooleanNumber, ITextStyle } from '@univerjs/core'; export default function extractNodeStyle(node: HTMLElement): ITextStyle { @@ -41,7 +40,9 @@ export default function extractNodeStyle(node: HTMLElement): ITextStyle { case 'font-size': { const fontSize = parseInt(cssValue); // TODO: @JOCS, hand other CSS value unit, rem, em, pt, % - docStyles.fs = /pt$/.test(cssValue) ? ptToPx(fontSize) : fontSize; + // 1 pixel * 0.75 = 1 pt + const PX_TO_PT_RATIO = 0.75; + docStyles.fs = /pt$/.test(cssValue) ? fontSize / PX_TO_PT_RATIO : fontSize; break; } From 634339da64860013db4f4e18b0d5c77f7f2c4fdb Mon Sep 17 00:00:00 2001 From: jocs Date: Tue, 14 Nov 2023 19:05:05 +0800 Subject: [PATCH 05/12] feat: paste action support plugins --- .../mutations/functions/insert-apply.ts | 3 +- packages/base-docs/src/doc-plugin.ts | 4 +- .../services/doc-skeleton-manager.service.ts | 2 + .../base-docs/src/views/doc-canvas-view.ts | 4 +- packages/base-render/src/basics/font-cache.ts | 1 + .../base-render/src/components/component.ts | 1 + .../src/components/docs/doc-skeleton.ts | 1 + .../src/components/docs/document.ts | 3 + .../base-render/src/render-manager.service.ts | 2 + .../src/docs/default-document-data-en.ts | 3 + .../src/docs/domain/data-stream-tree-node.ts | 6 +- .../src/docs/domain/document-body-model.ts | 7 ++- .../core/src/docs/domain/document-model.ts | 1 + packages/core/src/shared/common.ts | 1 + packages/core/src/shared/doc-tool.ts | 4 -- .../services/clipboard/clipboard.service.ts | 5 ++ .../clipboard/html-to-udm/converter.ts | 60 ++++++++++++++++--- .../clipboard/html-to-udm/parse-node-style.ts | 19 +++++- .../paste-plugins/plugin-feishu.ts | 46 ++++++++++++++ .../html-to-udm/paste-plugins/plugin-word.ts | 56 +++++++++++++++++ .../html-to-udm/paste-plugins/type.ts | 18 ++++++ 21 files changed, 223 insertions(+), 24 deletions(-) create mode 100644 packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/plugin-feishu.ts create mode 100644 packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/plugin-word.ts create mode 100644 packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/type.ts diff --git a/packages/base-docs/src/commands/mutations/functions/insert-apply.ts b/packages/base-docs/src/commands/mutations/functions/insert-apply.ts index cad28b8d5c6..b4cc1f9d717 100644 --- a/packages/base-docs/src/commands/mutations/functions/insert-apply.ts +++ b/packages/base-docs/src/commands/mutations/functions/insert-apply.ts @@ -30,10 +30,11 @@ export function InsertApply( throw new Error('no body has changed'); } - bodyModel.insert(insertBody, currentIndex); + // bodyModel.insert(insertBody, currentIndex); updateAttributeByInsert(body, insertBody, textLength, currentIndex); + bodyModel.reset(body); console.log('插入的model打印', bodyModel, textLength, currentIndex); } diff --git a/packages/base-docs/src/doc-plugin.ts b/packages/base-docs/src/doc-plugin.ts index e27d63d8c04..2112519bb91 100644 --- a/packages/base-docs/src/doc-plugin.ts +++ b/packages/base-docs/src/doc-plugin.ts @@ -106,7 +106,7 @@ export class DocPlugin extends Plugin { // this._markDocAsFocused(); } - initializeCommands(): void { + private initializeCommands(): void { ( [ MoveCursorOperation, @@ -151,7 +151,7 @@ export class DocPlugin extends Plugin { }); } - initialConfig(config: IDocPluginConfig) { + private initialConfig(config: IDocPluginConfig) { this._currentUniverService.docAdded$.subscribe((documentModel) => { if (documentModel == null) { throw new Error('documentModel is null'); diff --git a/packages/base-docs/src/services/doc-skeleton-manager.service.ts b/packages/base-docs/src/services/doc-skeleton-manager.service.ts index 9dc0a580e91..a67638f8a13 100644 --- a/packages/base-docs/src/services/doc-skeleton-manager.service.ts +++ b/packages/base-docs/src/services/doc-skeleton-manager.service.ts @@ -85,11 +85,13 @@ export class DocSkeletonManagerService implements IDisposable { setCurrent(searchParm: IDocSkeletonManagerSearch): Nullable { const param = this._getCurrentBySearch(searchParm); + if (param != null) { if (param.dirty) { param.skeleton.makeDirty(true); param.dirty = false; } + param.skeleton.calculate(); } else { const { unitId } = searchParm; diff --git a/packages/base-docs/src/views/doc-canvas-view.ts b/packages/base-docs/src/views/doc-canvas-view.ts index 11b54f390b3..4a2daa147bc 100644 --- a/packages/base-docs/src/views/doc-canvas-view.ts +++ b/packages/base-docs/src/views/doc-canvas-view.ts @@ -43,7 +43,6 @@ export class DocCanvasView { @IConfigService private readonly _configService: IConfigService, @IUniverInstanceService private readonly _currentUniverService: IUniverInstanceService, @Inject(DocSkeletonManagerService) private readonly _docSkeletonManagerService: DocSkeletonManagerService, - // @IRenderingEngine private readonly _engine: Engine, @Inject(Injector) private readonly _injector: Injector ) { this._currentUniverService.currentDoc$.subscribe((documentModel) => { @@ -52,6 +51,7 @@ export class DocCanvasView { } const unitId = documentModel.getUnitId(); + if (!this._loadedMap.has(unitId)) { this._currentDocumentModel = documentModel; this._addNewRender(); @@ -103,6 +103,7 @@ export class DocCanvasView { scene.on(EVENT_TYPE.wheel, (evt: unknown, state: EventState) => { const e = evt as IWheelEvent; + if (e.ctrlKey) { const deltaFactor = Math.abs(e.deltaX); let scrollNum = deltaFactor < 40 ? 0.2 : deltaFactor < 80 ? 0.4 : 0.2; @@ -141,6 +142,7 @@ export class DocCanvasView { this._addComponent(currentRender); const should = this._currentDocumentModel.getShouldRenderLoopImmediately(); + if (should) { engine.runRenderLoop(() => { scene.render(); diff --git a/packages/base-render/src/basics/font-cache.ts b/packages/base-render/src/basics/font-cache.ts index 12a778bcfff..8c77063384b 100644 --- a/packages/base-render/src/basics/font-cache.ts +++ b/packages/base-render/src/basics/font-cache.ts @@ -230,6 +230,7 @@ export class FontCache { } this.setFontMeasureCache(fontString, content, cache); + return cache; } diff --git a/packages/base-render/src/components/component.ts b/packages/base-render/src/components/component.ts index 724b37b0b64..ee35c2ec286 100644 --- a/packages/base-render/src/components/component.ts +++ b/packages/base-render/src/components/component.ts @@ -27,6 +27,7 @@ export class RenderComponent extends BaseObject { getExtensionsByOrder() { const extensionArray = Array.from(this._extensions.values()); extensionArray.sort(sortRules); + return extensionArray; } diff --git a/packages/base-render/src/components/docs/doc-skeleton.ts b/packages/base-render/src/components/docs/doc-skeleton.ts index 05010bcd3d4..c755d25ad45 100644 --- a/packages/base-render/src/components/docs/doc-skeleton.ts +++ b/packages/base-render/src/components/docs/doc-skeleton.ts @@ -501,6 +501,7 @@ export class DocumentSkeleton extends Skeleton { wrapStrategy: WrapStrategy.UNSPECIFIED, }, } = documentStyle; + const skeleton = this.__getNullSke(); const docsConfig: IDocsConfig = { diff --git a/packages/base-render/src/components/docs/document.ts b/packages/base-render/src/components/docs/document.ts index 318c7ac6469..67bcb96643a 100644 --- a/packages/base-render/src/components/docs/document.ts +++ b/packages/base-render/src/components/docs/document.ts @@ -175,6 +175,7 @@ export class Documents extends DocComponent { override draw(ctx: CanvasRenderingContext2D, bounds?: IBoundRect) { const documentSkeleton = this.getSkeleton(); + if (!documentSkeleton) { return; } @@ -195,6 +196,7 @@ export class Documents extends DocComponent { const parentScale = this.getParentScale(); const extensions = this.getExtensionsByOrder(); const scale = getScale(parentScale); + for (const extension of extensions) { extension.clearCache(); } @@ -267,6 +269,7 @@ export class Documents extends DocComponent { let alignOffset; let rotateTranslateXListApply = null; + if (vertexAngle !== 0) { const { rotateTranslateXList, diff --git a/packages/base-render/src/render-manager.service.ts b/packages/base-render/src/render-manager.service.ts index 4005fd10541..024dcae7f30 100644 --- a/packages/base-render/src/render-manager.service.ts +++ b/packages/base-render/src/render-manager.service.ts @@ -93,12 +93,14 @@ export class RenderManagerService implements IRenderManagerService { private _createRender(unitId: string, engine: Engine, isMainScene: boolean = true): IRender { const existItem = this.getRenderById(unitId); let shouldDestroyEngine = true; + if (existItem != null) { const existEngine = existItem.engine; if (existEngine === engine) { shouldDestroyEngine = false; } } + this._disposeItem(existItem, shouldDestroyEngine); const { width, height } = DEFAULT_SCENE_SIZE; diff --git a/packages/common-plugin-data/src/docs/default-document-data-en.ts b/packages/common-plugin-data/src/docs/default-document-data-en.ts index adf4c9ddb3d..69390312812 100644 --- a/packages/common-plugin-data/src/docs/default-document-data-en.ts +++ b/packages/common-plugin-data/src/docs/default-document-data-en.ts @@ -254,6 +254,9 @@ export const DEFAULT_DOCUMENT_DATA_EN: IDocumentData = { nestingLevel: 0, textStyle: { fs: 20, + cl: { + rgb: 'rgb(0, 255, 0)', + }, }, }, paragraphStyle: { diff --git a/packages/core/src/docs/domain/data-stream-tree-node.ts b/packages/core/src/docs/domain/data-stream-tree-node.ts index b92dd471aa9..e797229e4b1 100644 --- a/packages/core/src/docs/domain/data-stream-tree-node.ts +++ b/packages/core/src/docs/domain/data-stream-tree-node.ts @@ -84,7 +84,7 @@ export class DataStreamTreeNode { } split(index: number) { - const { children, parent, startIndex, endIndex, nodeType, content = '' } = this.getProps(); + const { children, parent, startIndex, nodeType, content = '' } = this.getProps(); if (this.exclude(index)) { return; @@ -152,14 +152,17 @@ export class DataStreamTreeNode { if (index == null) { return -1; } + return index; } remove() { this.children = []; + if (this.parent == null) { return; } + this.parent.children.splice(this.getPositionInParent(), 1); this.parent = null; } @@ -220,6 +223,7 @@ export class DataStreamTreeNode { for (let i = 0, len = this.content.length; i < len; i++) { const char = this.content[i]; + if (char === DataStreamTreeTokenType.CUSTOM_BLOCK) { this.blocks.push(this.startIndex + i); } diff --git a/packages/core/src/docs/domain/document-body-model.ts b/packages/core/src/docs/domain/document-body-model.ts index 7af096612de..b4a52b5936b 100644 --- a/packages/core/src/docs/domain/document-body-model.ts +++ b/packages/core/src/docs/domain/document-body-model.ts @@ -240,7 +240,7 @@ export class DocumentBodyModel extends DocumentBodyModelSimple { insert(insertBody: IDocumentBody, insertIndex = 0) { const dataStream = insertBody.dataStream; let dataStreamLen = dataStream.length; - const insertedNode = this.getParagraphByTree(this.children, insertIndex); + const insertedNode = this.getParagraphByIndex(this.children, insertIndex); if (insertedNode == null) { return; @@ -279,6 +279,7 @@ export class DocumentBodyModel extends DocumentBodyModelSimple { currentNode.selfPlus(dataStreamLen, currentNode.getPositionInParent()); const children = currentNode.children; let isStartFix = false; + for (const node of children) { if (node === insertedLastNode) { isStartFix = true; @@ -328,7 +329,7 @@ export class DocumentBodyModel extends DocumentBodyModelSimple { this.deleteTree(nodes, currentIndex, textLength); } - private getParagraphByTree(nodes: DataStreamTreeNode[], insertIndex: number): Nullable { + private getParagraphByIndex(nodes: DataStreamTreeNode[], insertIndex: number): Nullable { for (const node of nodes) { const { children } = node; @@ -340,7 +341,7 @@ export class DocumentBodyModel extends DocumentBodyModelSimple { return node; } - return this.getParagraphByTree(children, insertIndex); + return this.getParagraphByIndex(children, insertIndex); } return null; diff --git a/packages/core/src/docs/domain/document-model.ts b/packages/core/src/docs/domain/document-model.ts index 2b892dfa793..fe2bcd89cf1 100644 --- a/packages/core/src/docs/domain/document-model.ts +++ b/packages/core/src/docs/domain/document-model.ts @@ -230,6 +230,7 @@ export class DocumentModel extends DocumentModelSimple { override updateDocumentId(unitId: string) { super.updateDocumentId(unitId); + this._unitId = unitId; } diff --git a/packages/core/src/shared/common.ts b/packages/core/src/shared/common.ts index f1d47dbb32b..46f219b9d67 100644 --- a/packages/core/src/shared/common.ts +++ b/packages/core/src/shared/common.ts @@ -502,6 +502,7 @@ export function getDocsUpdateBody(model: IDocumentData, segmentId?: string) { if (segmentId) { const { headers, footers } = model; + if (headers?.[segmentId]) { body = headers[segmentId].body; } else if (footers?.[segmentId]) { diff --git a/packages/core/src/shared/doc-tool.ts b/packages/core/src/shared/doc-tool.ts index 40dd7213b12..0bca8f0f20c 100644 --- a/packages/core/src/shared/doc-tool.ts +++ b/packages/core/src/shared/doc-tool.ts @@ -81,9 +81,5 @@ export function deleteContent(content: string, start: number, end: number) { return content; } - // if (start === end) { - // start -= 1; - // } - return content.slice(0, start) + content.slice(end); } diff --git a/packages/ui-plugin-docs/src/services/clipboard/clipboard.service.ts b/packages/ui-plugin-docs/src/services/clipboard/clipboard.service.ts index ff890849271..f6df295fbfa 100644 --- a/packages/ui-plugin-docs/src/services/clipboard/clipboard.service.ts +++ b/packages/ui-plugin-docs/src/services/clipboard/clipboard.service.ts @@ -7,6 +7,11 @@ import { Disposable, IDocumentBody, IUniverInstanceService, toDisposable } from import { createIdentifier, IDisposable } from '@wendellhu/redi'; import HtmlToUDMService from './html-to-udm/converter'; +import PastePluginFeishu from './html-to-udm/paste-plugins/plugin-feishu'; +import PastePluginWord from './html-to-udm/paste-plugins/plugin-word'; + +HtmlToUDMService.use(PastePluginWord); +HtmlToUDMService.use(PastePluginFeishu); export interface IClipboardPropertyItem {} diff --git a/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/converter.ts b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/converter.ts index 0b9595037e7..4ae88491a2b 100644 --- a/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/converter.ts +++ b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/converter.ts @@ -2,24 +2,63 @@ import { IDocumentBody, ITextStyle, Nullable } from '@univerjs/core'; import extractNodeStyle from './parse-node-style'; import parseToDom from './parse-to-dom'; +import { IAfterProcessRule, IPastePlugin, IStyleRule } from './paste-plugins/type'; + +function matchFilter(node: HTMLElement, filter: IStyleRule['filter']) { + const tagName = node.tagName.toLowerCase(); + + if (typeof filter === 'string') { + return tagName === filter; + } + + if (Array.isArray(filter)) { + return filter.some((name) => name === tagName); + } + + return filter(node); +} /** * Convert html strings into data structures in univer, IDocumentBody. * Support plug-in, add custom rules, */ class HtmlToUDMService { + private static pluginList: IPastePlugin[] = []; + + static use(plugin: IPastePlugin) { + if (this.pluginList.includes(plugin)) { + throw new Error(`Univer paste plugin ${plugin.name} already added`); + } + + this.pluginList.push(plugin); + } + private styleCache: Map = new Map(); + private styleRules: IStyleRule[] = []; + + private afterProcessRules: IAfterProcessRule[] = []; + convert(html: string): IDocumentBody { + const pastePlugin = HtmlToUDMService.pluginList.find((plugin) => plugin.checkPasteType(html)); + const dom = parseToDom(html); + const newDocBody: IDocumentBody = { dataStream: '', textRuns: [], - paragraphs: [], }; + + if (pastePlugin) { + this.styleRules = [...pastePlugin.stylesRules]; + this.afterProcessRules = [...pastePlugin.afterProcessRules]; + } + this.styleCache.clear(); this.process(null, dom?.childNodes!, newDocBody); this.styleCache.clear(); + this.styleRules = []; + this.afterProcessRules = []; return newDocBody; } @@ -37,7 +76,7 @@ class HtmlToUDMService { doc.dataStream += text; - if (style) { + if (style && Object.getOwnPropertyNames(style).length) { doc.textRuns!.push({ st: doc.dataStream.length - text!.length, ed: doc.dataStream.length, @@ -46,7 +85,10 @@ class HtmlToUDMService { } } else if (node.nodeType === Node.ELEMENT_NODE) { const parentStyles = parent ? this.styleCache.get(parent) : {}; - const nodeStyles = extractNodeStyle(node as HTMLElement); + const styleRule = this.styleRules.find(({ filter }) => matchFilter(node as HTMLElement, filter)); + const nodeStyles = styleRule + ? styleRule.getStyle(node as HTMLElement, extractNodeStyle) + : extractNodeStyle(node as HTMLElement); this.styleCache.set(node, { ...parentStyles, ...nodeStyles }); @@ -54,12 +96,12 @@ class HtmlToUDMService { this.process(node, childNodes, doc); - // TODO: @JOCS, Use plugin - if (node.nodeName.toLocaleLowerCase() === 'p') { - doc.paragraphs?.push({ - startIndex: doc.dataStream.length, - }); - doc.dataStream += '\r'; + const afterProcessRule = this.afterProcessRules.find(({ filter }) => + matchFilter(node as HTMLElement, filter) + ); + + if (afterProcessRule) { + afterProcessRule.handler(doc); } } } diff --git a/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/parse-node-style.ts b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/parse-node-style.ts index 23a8d2ad333..39f1243e1fb 100644 --- a/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/parse-node-style.ts +++ b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/parse-node-style.ts @@ -1,11 +1,11 @@ -import { BooleanNumber, ITextStyle } from '@univerjs/core'; +import { BaselineOffset, BooleanNumber, ITextStyle } from '@univerjs/core'; export default function extractNodeStyle(node: HTMLElement): ITextStyle { const styles = node.style; const docStyles: ITextStyle = {}; - const tagName = node.tagName; + const tagName = node.tagName.toLowerCase(); - switch (tagName.toLocaleLowerCase()) { + switch (tagName) { case 'b': case 'em': case 'strong': { @@ -13,6 +13,13 @@ export default function extractNodeStyle(node: HTMLElement): ITextStyle { break; } + case 's': { + docStyles.st = { + s: BooleanNumber.TRUE, + }; + break; + } + case 'u': { docStyles.ul = { s: BooleanNumber.TRUE, @@ -24,6 +31,12 @@ export default function extractNodeStyle(node: HTMLElement): ITextStyle { docStyles.it = BooleanNumber.TRUE; break; } + + case 'sub': + case 'sup': { + docStyles.va = tagName === 'sup' ? BaselineOffset.SUPERSCRIPT : BaselineOffset.SUBSCRIPT; + break; + } } for (let i = 0; i < styles.length; i++) { diff --git a/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/plugin-feishu.ts b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/plugin-feishu.ts new file mode 100644 index 00000000000..05c6e8b98f7 --- /dev/null +++ b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/plugin-feishu.ts @@ -0,0 +1,46 @@ +import { BooleanNumber } from '@univerjs/core'; + +import { IPastePlugin } from './type'; + +const wordPastePlugin: IPastePlugin = { + name: 'univer-doc-paste-plugin-feishu', + checkPasteType(html: string) { + return /lark-record-clipboard/i.test(html); + }, + // TODO: @JOCS, support inline code copy from feishu. + stylesRules: [ + { + filter: ['s'], + getStyle(node, getInlineStyle) { + const inlineStyle = getInlineStyle(node); + + return { + st: { + s: BooleanNumber.TRUE, + }, + ...inlineStyle, + }; + }, + }, + ], + + afterProcessRules: [ + { + filter(el: HTMLElement) { + return el.tagName === 'DIV' && /ace-line/i.test(el.className); + }, + handler(doc) { + if (doc.paragraphs == null) { + doc.paragraphs = []; + } + + doc.paragraphs.push({ + startIndex: doc.dataStream.length, + }); + doc.dataStream += '\r'; + }, + }, + ], +}; + +export default wordPastePlugin; diff --git a/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/plugin-word.ts b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/plugin-word.ts new file mode 100644 index 00000000000..a5a0d36b5e2 --- /dev/null +++ b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/plugin-word.ts @@ -0,0 +1,56 @@ +import { BooleanNumber } from '@univerjs/core'; + +import { IPastePlugin } from './type'; + +const wordPastePlugin: IPastePlugin = { + name: 'univer-doc-paste-plugin-word', + checkPasteType(html: string) { + return /word|mso/i.test(html); + }, + + stylesRules: [ + { + filter: ['b'], + getStyle(node, getInlineStyle) { + const inlineStyle = getInlineStyle(node); + + return { bl: BooleanNumber.TRUE, ...inlineStyle }; + }, + }, + ], + + afterProcessRules: [ + { + filter(el: HTMLElement) { + return el.tagName === 'P' && /mso/i.test(el.className); + }, + handler(doc) { + if (doc.paragraphs == null) { + doc.paragraphs = []; + } + + doc.paragraphs.push({ + startIndex: doc.dataStream.length, + }); + doc.dataStream += '\r'; + }, + }, + { + filter(el: HTMLElement) { + return el.tagName === 'DIV' && /word/i.test(el.className); + }, + handler(doc) { + if (doc.sectionBreaks == null) { + doc.sectionBreaks = []; + } + + doc.sectionBreaks.push({ + startIndex: doc.dataStream.length, + }); + doc.dataStream += '\n'; + }, + }, + ], +}; + +export default wordPastePlugin; diff --git a/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/type.ts b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/type.ts new file mode 100644 index 00000000000..d09425aada8 --- /dev/null +++ b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/type.ts @@ -0,0 +1,18 @@ +import { IDocumentBody, ITextStyle } from '@univerjs/core'; + +export interface IStyleRule { + filter: string | string[] | ((node: HTMLElement) => boolean); + getStyle(node: HTMLElement, getStyleFromProperty: (n: HTMLElement) => ITextStyle): ITextStyle; +} + +export interface IAfterProcessRule { + filter: string | string[] | ((node: HTMLElement) => boolean); + handler(doc: IDocumentBody): void; +} + +export interface IPastePlugin { + name: string; + checkPasteType(html: string): boolean; + stylesRules: IStyleRule[]; + afterProcessRules: IAfterProcessRule[]; +} From e191c1bca23069a249685476cd5ffa8b58f6d519 Mon Sep 17 00:00:00 2001 From: jocs Date: Tue, 14 Nov 2023 20:12:48 +0800 Subject: [PATCH 06/12] feat: reset cursor after pasted --- .../mutations/functions/insert-apply.ts | 12 +++-- .../src/controllers/clipboard.controller.ts | 46 ++++++++++++++++--- .../clipboard/html-to-udm/converter.ts | 4 +- .../clipboard/html-to-udm/parse-node-style.ts | 6 +-- .../paste-plugins/plugin-feishu.ts | 7 +-- .../html-to-udm/paste-plugins/plugin-word.ts | 20 ++++++-- .../html-to-udm/paste-plugins/type.ts | 4 +- .../services/clipboard/html-to-udm/utils.ts | 42 +++++++++++++++++ 8 files changed, 117 insertions(+), 24 deletions(-) create mode 100644 packages/ui-plugin-docs/src/services/clipboard/html-to-udm/utils.ts diff --git a/packages/base-docs/src/commands/mutations/functions/insert-apply.ts b/packages/base-docs/src/commands/mutations/functions/insert-apply.ts index b4cc1f9d717..6d5e7d88ed5 100644 --- a/packages/base-docs/src/commands/mutations/functions/insert-apply.ts +++ b/packages/base-docs/src/commands/mutations/functions/insert-apply.ts @@ -30,11 +30,17 @@ export function InsertApply( throw new Error('no body has changed'); } - // bodyModel.insert(insertBody, currentIndex); - updateAttributeByInsert(body, insertBody, textLength, currentIndex); - bodyModel.reset(body); + if (insertBody.dataStream.length > 1 && /\r/.test(insertBody.dataStream)) { + // TODO: @JOCS, The DocumentModel needs to be rewritten to better support the + // large area of updates that are brought about by the paste, abstract the + // methods associated with the DocumentModel insertion, and support atomic operations + bodyModel.reset(body); + } else { + bodyModel.insert(insertBody, currentIndex); + } + console.log('插入的model打印', bodyModel, textLength, currentIndex); } diff --git a/packages/ui-plugin-docs/src/controllers/clipboard.controller.ts b/packages/ui-plugin-docs/src/controllers/clipboard.controller.ts index c0f2275abb3..d8b243856ca 100644 --- a/packages/ui-plugin-docs/src/controllers/clipboard.controller.ts +++ b/packages/ui-plugin-docs/src/controllers/clipboard.controller.ts @@ -1,5 +1,7 @@ +import { TextSelectionManagerService } from '@univerjs/base-docs'; import { ITextSelectionRenderManager } from '@univerjs/base-render'; import { Disposable, ICommandInfo, ICommandService, ILogService, IUniverInstanceService } from '@univerjs/core'; +import { Inject } from '@wendellhu/redi'; import { DocCopyCommand, @@ -15,7 +17,8 @@ export class DocClipboardController extends Disposable { @ICommandService private readonly _commandService: ICommandService, @IUniverInstanceService private readonly _currentUniverService: IUniverInstanceService, @IDocClipboardService private readonly _docClipboardService: IDocClipboardService, - @ITextSelectionRenderManager private _textSelectionRenderManager: ITextSelectionRenderManager + @ITextSelectionRenderManager private _textSelectionRenderManager: ITextSelectionRenderManager, + @Inject(TextSelectionManagerService) private _textSelectionManagerService: TextSelectionManagerService ) { super(); this.commandExecutedListener(); @@ -43,19 +46,50 @@ export class DocClipboardController extends Disposable { } private async handlePaste() { - const { _docClipboardService: docClipboardService } = this; - const { segmentId } = this._textSelectionRenderManager.getActiveRange() ?? {}; + const { _docClipboardService: clipboard } = this; + const { + segmentId, + endOffset: activeEndOffset, + style, + } = this._textSelectionRenderManager.getActiveRange() ?? {}; + const ranges = this._textSelectionRenderManager.getAllTextRanges(); - if (!segmentId) { + if (segmentId == null) { this._logService.error('[DocClipboardController] segmentId is not existed'); } + if (activeEndOffset == null) { + return; + } + try { - const body = await docClipboardService.queryClipboardData(); + const body = await clipboard.queryClipboardData(); this._commandService.executeCommand(InnerPasteCommand.id, { body, segmentId }); - // TODO: @jocs, reset selections. + // When doc has multiple selections, the cursor moves to the last pasted content's end. + let cursor = activeEndOffset; + for (const range of ranges) { + const { startOffset, endOffset } = range; + + if (startOffset == null || endOffset == null) { + continue; + } + + if (endOffset <= activeEndOffset) { + cursor += body.dataStream.length - (endOffset - startOffset); + } + } + + // move selection + this._textSelectionManagerService.replace([ + { + startOffset: cursor, + endOffset: cursor, + collapsed: true, + style, + }, + ]); } catch (_e) { this._logService.error('[DocClipboardController] clipboard is empty'); } diff --git a/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/converter.ts b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/converter.ts index 4ae88491a2b..1f22bdc0565 100644 --- a/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/converter.ts +++ b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/converter.ts @@ -87,7 +87,7 @@ class HtmlToUDMService { const parentStyles = parent ? this.styleCache.get(parent) : {}; const styleRule = this.styleRules.find(({ filter }) => matchFilter(node as HTMLElement, filter)); const nodeStyles = styleRule - ? styleRule.getStyle(node as HTMLElement, extractNodeStyle) + ? styleRule.getStyle(node as HTMLElement) : extractNodeStyle(node as HTMLElement); this.styleCache.set(node, { ...parentStyles, ...nodeStyles }); @@ -101,7 +101,7 @@ class HtmlToUDMService { ); if (afterProcessRule) { - afterProcessRule.handler(doc); + afterProcessRule.handler(doc, node as HTMLElement); } } } diff --git a/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/parse-node-style.ts b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/parse-node-style.ts index 39f1243e1fb..458d5f8013d 100644 --- a/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/parse-node-style.ts +++ b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/parse-node-style.ts @@ -1,5 +1,7 @@ import { BaselineOffset, BooleanNumber, ITextStyle } from '@univerjs/core'; +import { ptToPixel } from './utils'; + export default function extractNodeStyle(node: HTMLElement): ITextStyle { const styles = node.style; const docStyles: ITextStyle = {}; @@ -53,9 +55,7 @@ export default function extractNodeStyle(node: HTMLElement): ITextStyle { case 'font-size': { const fontSize = parseInt(cssValue); // TODO: @JOCS, hand other CSS value unit, rem, em, pt, % - // 1 pixel * 0.75 = 1 pt - const PX_TO_PT_RATIO = 0.75; - docStyles.fs = /pt$/.test(cssValue) ? fontSize / PX_TO_PT_RATIO : fontSize; + docStyles.fs = /pt$/.test(cssValue) ? ptToPixel(fontSize) : fontSize; break; } diff --git a/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/plugin-feishu.ts b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/plugin-feishu.ts index 05c6e8b98f7..2f2fcaa36d7 100644 --- a/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/plugin-feishu.ts +++ b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/plugin-feishu.ts @@ -1,17 +1,18 @@ import { BooleanNumber } from '@univerjs/core'; +import getInlineStyle from '../parse-node-style'; import { IPastePlugin } from './type'; const wordPastePlugin: IPastePlugin = { - name: 'univer-doc-paste-plugin-feishu', + name: 'univer-doc-paste-plugin-lark', checkPasteType(html: string) { return /lark-record-clipboard/i.test(html); }, - // TODO: @JOCS, support inline code copy from feishu. + // TODO: @JOCS, support inline code copy from lark. stylesRules: [ { filter: ['s'], - getStyle(node, getInlineStyle) { + getStyle(node) { const inlineStyle = getInlineStyle(node); return { diff --git a/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/plugin-word.ts b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/plugin-word.ts index a5a0d36b5e2..413210aeec4 100644 --- a/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/plugin-word.ts +++ b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/plugin-word.ts @@ -1,5 +1,7 @@ -import { BooleanNumber } from '@univerjs/core'; +import { BooleanNumber, IParagraph } from '@univerjs/core'; +import getInlineStyle from '../parse-node-style'; +import { getParagraphStyle } from '../utils'; import { IPastePlugin } from './type'; const wordPastePlugin: IPastePlugin = { @@ -11,7 +13,7 @@ const wordPastePlugin: IPastePlugin = { stylesRules: [ { filter: ['b'], - getStyle(node, getInlineStyle) { + getStyle(node) { const inlineStyle = getInlineStyle(node); return { bl: BooleanNumber.TRUE, ...inlineStyle }; @@ -24,14 +26,22 @@ const wordPastePlugin: IPastePlugin = { filter(el: HTMLElement) { return el.tagName === 'P' && /mso/i.test(el.className); }, - handler(doc) { + handler(doc, el) { if (doc.paragraphs == null) { doc.paragraphs = []; } - doc.paragraphs.push({ + const paragraph: IParagraph = { startIndex: doc.dataStream.length, - }); + }; + + const paragraphStyle = getParagraphStyle(el); + + if (paragraphStyle) { + paragraph.paragraphStyle = paragraphStyle; + } + + doc.paragraphs.push(paragraph); doc.dataStream += '\r'; }, }, diff --git a/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/type.ts b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/type.ts index d09425aada8..ac6934c1f6e 100644 --- a/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/type.ts +++ b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/type.ts @@ -2,12 +2,12 @@ import { IDocumentBody, ITextStyle } from '@univerjs/core'; export interface IStyleRule { filter: string | string[] | ((node: HTMLElement) => boolean); - getStyle(node: HTMLElement, getStyleFromProperty: (n: HTMLElement) => ITextStyle): ITextStyle; + getStyle(node: HTMLElement): ITextStyle; } export interface IAfterProcessRule { filter: string | string[] | ((node: HTMLElement) => boolean); - handler(doc: IDocumentBody): void; + handler(doc: IDocumentBody, node: HTMLElement): void; } export interface IPastePlugin { diff --git a/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/utils.ts b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/utils.ts new file mode 100644 index 00000000000..b4337af90b0 --- /dev/null +++ b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/utils.ts @@ -0,0 +1,42 @@ +import { IParagraphStyle, Nullable } from '@univerjs/core'; + +// TODO: @JOCS, Complete other missing attributes that exist in IParagraphStyle +export function getParagraphStyle(el: HTMLElement): Nullable { + const styles = el.style; + + const paragraphStyle: IParagraphStyle = {}; + + for (let i = 0; i < styles.length; i++) { + const cssRule = styles[i]; + const cssValue = styles.getPropertyValue(cssRule); + + switch (cssRule) { + case 'margin-top': { + const marginTopValue = parseInt(cssValue); + paragraphStyle.spaceAbove = /pt/.test(cssValue) ? ptToPixel(marginTopValue) : marginTopValue; + break; + } + + case 'margin-bottom': { + const marginBottomValue = parseInt(cssValue); + paragraphStyle.spaceBelow = /pt/.test(cssValue) ? ptToPixel(marginBottomValue) : marginBottomValue; + + break; + } + + default: { + // console.log(`Unhandled css rule ${cssRule} in getParagraphStyle`); + break; + } + } + } + + return Object.getOwnPropertyNames(paragraphStyle).length ? paragraphStyle : null; +} + +export function ptToPixel(pt: number) { + // 1 pixel * 0.75 = 1 pt + const PX_TO_PT_RATIO = 0.75; + + return pt / PX_TO_PT_RATIO; +} From cfa179f27ccab989902f897315e902fb9876f06b Mon Sep 17 00:00:00 2001 From: jocs Date: Tue, 14 Nov 2023 21:46:24 +0800 Subject: [PATCH 07/12] feat: get copied documentBody from ranges --- .../commands/commands/core-editing.command.ts | 3 +- packages/core/src/shared/tools.ts | 9 ++ .../src/controllers/clipboard.controller.ts | 134 +++++++++++++++++- .../services/clipboard/clipboard.service.ts | 6 +- .../{plugin-feishu.ts => plugin-lark.ts} | 0 5 files changed, 144 insertions(+), 8 deletions(-) rename packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/{plugin-feishu.ts => plugin-lark.ts} (100%) diff --git a/packages/base-docs/src/commands/commands/core-editing.command.ts b/packages/base-docs/src/commands/commands/core-editing.command.ts index 09911f1e2dd..461d585f779 100644 --- a/packages/base-docs/src/commands/commands/core-editing.command.ts +++ b/packages/base-docs/src/commands/commands/core-editing.command.ts @@ -103,7 +103,7 @@ export interface IDeleteCommandParams { } /** - * The command to delete text. + * The command to delete text. mainly used in BACKSPACE. */ export const DeleteCommand: ICommand = { id: 'doc.command.delete-text', @@ -131,6 +131,7 @@ export const DeleteCommand: ICommand = { IRichTextEditingMutationParams, IRichTextEditingMutationParams >(doMutation.id, doMutation.params); + if (result) { undoRedoService.pushUndoRedo({ unitID: unitId, diff --git a/packages/core/src/shared/tools.ts b/packages/core/src/shared/tools.ts index c88c2cd81d8..fcc93ee0577 100644 --- a/packages/core/src/shared/tools.ts +++ b/packages/core/src/shared/tools.ts @@ -633,4 +633,13 @@ export class Tools { } return new ObjectArray(array); } + + static hasIntersectionBetweenTwoRanges( + range1Start: number, + range1End: number, + range2Start: number, + range2End: number + ) { + return range1End >= range2Start && range2End >= range1Start; + } } diff --git a/packages/ui-plugin-docs/src/controllers/clipboard.controller.ts b/packages/ui-plugin-docs/src/controllers/clipboard.controller.ts index d8b243856ca..5f2e8559e29 100644 --- a/packages/ui-plugin-docs/src/controllers/clipboard.controller.ts +++ b/packages/ui-plugin-docs/src/controllers/clipboard.controller.ts @@ -1,6 +1,16 @@ -import { TextSelectionManagerService } from '@univerjs/base-docs'; +import { DocSkeletonManagerService, TextSelectionManagerService } from '@univerjs/base-docs'; import { ITextSelectionRenderManager } from '@univerjs/base-render'; -import { Disposable, ICommandInfo, ICommandService, ILogService, IUniverInstanceService } from '@univerjs/core'; +import { + Disposable, + ICommandInfo, + ICommandService, + IDocumentBody, + ILogService, + IParagraph, + ITextRun, + IUniverInstanceService, + Tools, +} from '@univerjs/core'; import { Inject } from '@wendellhu/redi'; import { @@ -18,7 +28,8 @@ export class DocClipboardController extends Disposable { @IUniverInstanceService private readonly _currentUniverService: IUniverInstanceService, @IDocClipboardService private readonly _docClipboardService: IDocClipboardService, @ITextSelectionRenderManager private _textSelectionRenderManager: ITextSelectionRenderManager, - @Inject(TextSelectionManagerService) private _textSelectionManagerService: TextSelectionManagerService + @Inject(TextSelectionManagerService) private _textSelectionManagerService: TextSelectionManagerService, + @Inject(DocSkeletonManagerService) private _docSkeletonManagerService: DocSkeletonManagerService ) { super(); this.commandExecutedListener(); @@ -32,7 +43,7 @@ export class DocClipboardController extends Disposable { } private commandExecutedListener() { - const updateCommandList = [DocPasteCommand.id]; + const updateCommandList = [DocCutCommand.id, DocCopyCommand.id, DocPasteCommand.id]; this.disposeWithMe( this._commandService.onCommandExecuted((command: ICommandInfo) => { @@ -40,7 +51,25 @@ export class DocClipboardController extends Disposable { return; } - this.handlePaste(); + switch (command.id) { + case DocPasteCommand.id: { + this.handlePaste(); + break; + } + + case DocCopyCommand.id: { + this.handleCopy(); + break; + } + + case DocCutCommand.id: { + this.handleCut(); + break; + } + + default: + throw new Error(`Unhandled command ${command.id}`); + } }) ); } @@ -94,4 +123,99 @@ export class DocClipboardController extends Disposable { this._logService.error('[DocClipboardController] clipboard is empty'); } } + + private getDocumentBodyInRanges(): IDocumentBody[] { + const ranges = this._textSelectionRenderManager.getAllTextRanges(); + const skeletonObject = this._docSkeletonManagerService.getCurrent(); + + if (skeletonObject == null) { + return []; + } + + const { skeleton } = skeletonObject; + const { dataStream, textRuns = [], paragraphs = [] } = skeleton.getModel().getBodyModel().getBody(); + + const results: IDocumentBody[] = []; + + for (const range of ranges) { + const { startOffset, endOffset, collapsed } = range; + + if (collapsed) { + continue; + } + + if (startOffset == null || endOffset == null) { + continue; + } + + const docBody: IDocumentBody = { + dataStream: dataStream.slice(startOffset, endOffset), + }; + + const newTextRuns: ITextRun[] = []; + + for (const textRun of textRuns) { + const clonedTextRun = Tools.deepClone(textRun); + const { st, ed } = clonedTextRun; + if (Tools.hasIntersectionBetweenTwoRanges(st, ed, startOffset, endOffset)) { + if (startOffset >= st && startOffset <= ed) { + newTextRuns.push({ + ...clonedTextRun, + st: startOffset, + ed: Math.min(endOffset, ed), + }); + } else if (endOffset >= st && endOffset <= ed) { + newTextRuns.push({ + ...clonedTextRun, + st: Math.max(startOffset, st), + ed: endOffset, + }); + } else { + newTextRuns.push(clonedTextRun); + } + } + } + + if (newTextRuns.length) { + docBody.textRuns = newTextRuns.map((tr) => { + const { st, ed } = tr; + return { + ...tr, + st: st - startOffset, + ed: ed - startOffset, + }; + }); + } + + const newParagraphs: IParagraph[] = []; + + for (const paragraph of paragraphs) { + const { startIndex } = paragraph; + if (startIndex >= startOffset && startIndex <= endOffset) { + newParagraphs.push(Tools.deepClone(paragraph)); + } + } + + if (newParagraphs.length) { + docBody.paragraphs = newParagraphs.map((p) => ({ + ...p, + startIndex: p.startIndex - startOffset, + })); + } + + results.push(docBody); + } + + return results; + } + + private async handleCopy() { + console.log('copy'); + const bodys = this.getDocumentBodyInRanges(); + console.log(bodys); + } + + private async handleCut() { + console.log('cut'); + } } diff --git a/packages/ui-plugin-docs/src/services/clipboard/clipboard.service.ts b/packages/ui-plugin-docs/src/services/clipboard/clipboard.service.ts index f6df295fbfa..c58d118107e 100644 --- a/packages/ui-plugin-docs/src/services/clipboard/clipboard.service.ts +++ b/packages/ui-plugin-docs/src/services/clipboard/clipboard.service.ts @@ -7,11 +7,11 @@ import { Disposable, IDocumentBody, IUniverInstanceService, toDisposable } from import { createIdentifier, IDisposable } from '@wendellhu/redi'; import HtmlToUDMService from './html-to-udm/converter'; -import PastePluginFeishu from './html-to-udm/paste-plugins/plugin-feishu'; +import PastePluginLark from './html-to-udm/paste-plugins/plugin-lark'; import PastePluginWord from './html-to-udm/paste-plugins/plugin-word'; HtmlToUDMService.use(PastePluginWord); -HtmlToUDMService.use(PastePluginFeishu); +HtmlToUDMService.use(PastePluginLark); export interface IClipboardPropertyItem {} @@ -48,6 +48,8 @@ export class DocClipboardService extends Disposable implements IDocClipboardServ const clipboardItem = clipboardItems[0]; const text = await clipboardItem.getType(PLAIN_TEXT_CLIPBOARD_MIME_TYPE).then((blob) => blob && blob.text()); const html = await clipboardItem.getType(HTML_CLIPBOARD_MIME_TYPE).then((blob) => blob && blob.text()); + + console.log(text); console.log(html); if (!html) { // TODO: @JOCS, Parsing paragraphs and sections diff --git a/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/plugin-feishu.ts b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/plugin-lark.ts similarity index 100% rename from packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/plugin-feishu.ts rename to packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/plugin-lark.ts From 3db0f89b1f0b0c411a4752580a05cb90ed108e5d Mon Sep 17 00:00:00 2001 From: jocs Date: Tue, 14 Nov 2023 21:49:08 +0800 Subject: [PATCH 08/12] refactor: remove debug codes --- .../base-docs/src/commands/mutations/core-editing.mutation.ts | 2 -- .../src/services/clipboard/clipboard.service.ts | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/base-docs/src/commands/mutations/core-editing.mutation.ts b/packages/base-docs/src/commands/mutations/core-editing.mutation.ts index 892f5466907..032d734787c 100644 --- a/packages/base-docs/src/commands/mutations/core-editing.mutation.ts +++ b/packages/base-docs/src/commands/mutations/core-editing.mutation.ts @@ -71,8 +71,6 @@ export const RichTextEditingMutation: IMutation { // FIXME: @jocs Since UpdateAttributeApply modifies the mutation(used in undo/redo), // so make a deep copy here, does UpdateAttributeApply need to diff --git a/packages/ui-plugin-docs/src/services/clipboard/clipboard.service.ts b/packages/ui-plugin-docs/src/services/clipboard/clipboard.service.ts index c58d118107e..1a67a65a5fe 100644 --- a/packages/ui-plugin-docs/src/services/clipboard/clipboard.service.ts +++ b/packages/ui-plugin-docs/src/services/clipboard/clipboard.service.ts @@ -49,8 +49,8 @@ export class DocClipboardService extends Disposable implements IDocClipboardServ const text = await clipboardItem.getType(PLAIN_TEXT_CLIPBOARD_MIME_TYPE).then((blob) => blob && blob.text()); const html = await clipboardItem.getType(HTML_CLIPBOARD_MIME_TYPE).then((blob) => blob && blob.text()); - console.log(text); - console.log(html); + // console.log(text); + // console.log(html); if (!html) { // TODO: @JOCS, Parsing paragraphs and sections return { From f50d99dc1e9f9f18d13e395210449fa1f6aa9dac Mon Sep 17 00:00:00 2001 From: jocs Date: Wed, 15 Nov 2023 14:00:23 +0800 Subject: [PATCH 09/12] fix: some minor bugs --- .../src/commands/commands/core-editing.command.ts | 2 +- packages/base-docs/src/doc-plugin.ts | 4 ++-- .../src/services/clipboard/clipboard.service.ts | 2 +- .../services/clipboard/html-to-udm/converter.ts | 4 +--- .../html-to-udm/paste-plugins/plugin-word.ts | 15 --------------- 5 files changed, 5 insertions(+), 22 deletions(-) diff --git a/packages/base-docs/src/commands/commands/core-editing.command.ts b/packages/base-docs/src/commands/commands/core-editing.command.ts index 461d585f779..291f37e8a63 100644 --- a/packages/base-docs/src/commands/commands/core-editing.command.ts +++ b/packages/base-docs/src/commands/commands/core-editing.command.ts @@ -103,7 +103,7 @@ export interface IDeleteCommandParams { } /** - * The command to delete text. mainly used in BACKSPACE. + * The command to delete text, mainly used in BACKSPACE. */ export const DeleteCommand: ICommand = { id: 'doc.command.delete-text', diff --git a/packages/base-docs/src/doc-plugin.ts b/packages/base-docs/src/doc-plugin.ts index 2112519bb91..c9a3f9b2cb4 100644 --- a/packages/base-docs/src/doc-plugin.ts +++ b/packages/base-docs/src/doc-plugin.ts @@ -91,7 +91,7 @@ export class DocPlugin extends Plugin { this._initializeDependencies(_injector); - this.initializeCommands(); + this._initializeCommands(); } initialize(): void { @@ -106,7 +106,7 @@ export class DocPlugin extends Plugin { // this._markDocAsFocused(); } - private initializeCommands(): void { + private _initializeCommands(): void { ( [ MoveCursorOperation, diff --git a/packages/ui-plugin-docs/src/services/clipboard/clipboard.service.ts b/packages/ui-plugin-docs/src/services/clipboard/clipboard.service.ts index 1a67a65a5fe..bed2403ad6d 100644 --- a/packages/ui-plugin-docs/src/services/clipboard/clipboard.service.ts +++ b/packages/ui-plugin-docs/src/services/clipboard/clipboard.service.ts @@ -6,7 +6,7 @@ import { import { Disposable, IDocumentBody, IUniverInstanceService, toDisposable } from '@univerjs/core'; import { createIdentifier, IDisposable } from '@wendellhu/redi'; -import HtmlToUDMService from './html-to-udm/converter'; +import { HtmlToUDMService } from './html-to-udm/converter'; import PastePluginLark from './html-to-udm/paste-plugins/plugin-lark'; import PastePluginWord from './html-to-udm/paste-plugins/plugin-word'; diff --git a/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/converter.ts b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/converter.ts index 1f22bdc0565..55f615d5381 100644 --- a/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/converter.ts +++ b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/converter.ts @@ -22,7 +22,7 @@ function matchFilter(node: HTMLElement, filter: IStyleRule['filter']) { * Convert html strings into data structures in univer, IDocumentBody. * Support plug-in, add custom rules, */ -class HtmlToUDMService { +export class HtmlToUDMService { private static pluginList: IPastePlugin[] = []; static use(plugin: IPastePlugin) { @@ -107,5 +107,3 @@ class HtmlToUDMService { } } } - -export default HtmlToUDMService; diff --git a/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/plugin-word.ts b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/plugin-word.ts index 413210aeec4..fdcf90c38de 100644 --- a/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/plugin-word.ts +++ b/packages/ui-plugin-docs/src/services/clipboard/html-to-udm/paste-plugins/plugin-word.ts @@ -45,21 +45,6 @@ const wordPastePlugin: IPastePlugin = { doc.dataStream += '\r'; }, }, - { - filter(el: HTMLElement) { - return el.tagName === 'DIV' && /word/i.test(el.className); - }, - handler(doc) { - if (doc.sectionBreaks == null) { - doc.sectionBreaks = []; - } - - doc.sectionBreaks.push({ - startIndex: doc.dataStream.length, - }); - doc.dataStream += '\n'; - }, - }, ], }; From 54ad797b4834bfa6a868dfc710a548ee0284bd7c Mon Sep 17 00:00:00 2001 From: jocs Date: Wed, 15 Nov 2023 14:04:07 +0800 Subject: [PATCH 10/12] fix: some minor issues --- .../src/controllers/clipboard.controller.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/ui-plugin-docs/src/controllers/clipboard.controller.ts b/packages/ui-plugin-docs/src/controllers/clipboard.controller.ts index 5f2e8559e29..18e0ade4597 100644 --- a/packages/ui-plugin-docs/src/controllers/clipboard.controller.ts +++ b/packages/ui-plugin-docs/src/controllers/clipboard.controller.ts @@ -32,7 +32,7 @@ export class DocClipboardController extends Disposable { @Inject(DocSkeletonManagerService) private _docSkeletonManagerService: DocSkeletonManagerService ) { super(); - this.commandExecutedListener(); + this._commandExecutedListener(); } initialize() { @@ -42,7 +42,7 @@ export class DocClipboardController extends Disposable { [InnerPasteCommand].forEach((command) => this.disposeWithMe(this._commandService.registerCommand(command))); } - private commandExecutedListener() { + private _commandExecutedListener() { const updateCommandList = [DocCutCommand.id, DocCopyCommand.id, DocPasteCommand.id]; this.disposeWithMe( @@ -53,17 +53,17 @@ export class DocClipboardController extends Disposable { switch (command.id) { case DocPasteCommand.id: { - this.handlePaste(); + this._handlePaste(); break; } case DocCopyCommand.id: { - this.handleCopy(); + this._handleCopy(); break; } case DocCutCommand.id: { - this.handleCut(); + this._handleCut(); break; } @@ -74,7 +74,7 @@ export class DocClipboardController extends Disposable { ); } - private async handlePaste() { + private async _handlePaste() { const { _docClipboardService: clipboard } = this; const { segmentId, @@ -124,7 +124,7 @@ export class DocClipboardController extends Disposable { } } - private getDocumentBodyInRanges(): IDocumentBody[] { + private _getDocumentBodyInRanges(): IDocumentBody[] { const ranges = this._textSelectionRenderManager.getAllTextRanges(); const skeletonObject = this._docSkeletonManagerService.getCurrent(); @@ -209,13 +209,13 @@ export class DocClipboardController extends Disposable { return results; } - private async handleCopy() { + private async _handleCopy() { console.log('copy'); - const bodys = this.getDocumentBodyInRanges(); + const bodys = this._getDocumentBodyInRanges(); console.log(bodys); } - private async handleCut() { + private async _handleCut() { console.log('cut'); } } From c5c3ceb7218282491d8e34bdf4952e47953cc965 Mon Sep 17 00:00:00 2001 From: jocs Date: Wed, 15 Nov 2023 15:21:53 +0800 Subject: [PATCH 11/12] test: add clipboard test --- packages/base-docs/src/index.ts | 2 +- packages/ui-plugin-docs/package.json | 1 + .../__test__/clipboard.command.spec.ts | 109 ++++++++++++++++++ .../__test__/create-command-test-bed.ts | 63 ++++++++++ .../commands/commands/clipboard.command.ts | 103 +---------------- .../commands/inner.clipboard.command.ts | 100 ++++++++++++++++ .../src/controllers/clipboard.controller.ts | 8 +- packages/ui-plugin-docs/vitest.config.ts | 1 + pnpm-lock.yaml | 3 + 9 files changed, 281 insertions(+), 109 deletions(-) create mode 100644 packages/ui-plugin-docs/src/commands/commands/__test__/clipboard.command.spec.ts create mode 100644 packages/ui-plugin-docs/src/commands/commands/__test__/create-command-test-bed.ts create mode 100644 packages/ui-plugin-docs/src/commands/commands/inner.clipboard.command.ts diff --git a/packages/base-docs/src/index.ts b/packages/base-docs/src/index.ts index 39d716087bd..2457e2bd7ab 100644 --- a/packages/base-docs/src/index.ts +++ b/packages/base-docs/src/index.ts @@ -30,6 +30,6 @@ export { RichTextEditingMutation, } from './commands/mutations/core-editing.mutation'; export { MoveCursorOperation, MoveSelectionOperation } from './commands/operations/cursor.operation'; -export * from './doc-plugin'; +export { DocPlugin, type IDocPluginConfig } from './doc-plugin'; export { DocSkeletonManagerService } from './services/doc-skeleton-manager.service'; export { TextSelectionManagerService } from './services/text-selection-manager.service'; diff --git a/packages/ui-plugin-docs/package.json b/packages/ui-plugin-docs/package.json index 3c5ccd4c3d5..4ac54cea54d 100644 --- a/packages/ui-plugin-docs/package.json +++ b/packages/ui-plugin-docs/package.json @@ -54,6 +54,7 @@ "@types/react": "^18.2.37", "@types/react-dom": "^18.2.15", "@vitest/coverage-istanbul": "^0.34.6", + "happy-dom": "^12.10.3", "less": "^4.2.0", "ts-node": "^10.9.1", "vitest": "^0.34.6" diff --git a/packages/ui-plugin-docs/src/commands/commands/__test__/clipboard.command.spec.ts b/packages/ui-plugin-docs/src/commands/commands/__test__/clipboard.command.spec.ts new file mode 100644 index 00000000000..aa7952652b1 --- /dev/null +++ b/packages/ui-plugin-docs/src/commands/commands/__test__/clipboard.command.spec.ts @@ -0,0 +1,109 @@ +/* eslint-disable no-magic-numbers */ + +import { + NORMAL_TEXT_SELECTION_PLUGIN_NAME, + RichTextEditingMutation, + TextSelectionManagerService, +} from '@univerjs/base-docs'; +import { BooleanNumber, ICommand, ICommandService, IStyleBase, IUniverInstanceService, Univer } from '@univerjs/core'; +import { Injector } from '@wendellhu/redi'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { IInnerPasteCommandParams, InnerPasteCommand } from '../inner.clipboard.command'; +import { createCommandTestBed } from './create-command-test-bed'; + +describe('test cases in clipboard', () => { + let univer: Univer; + let get: Injector['get']; + let commandService: ICommandService; + + function getFormatValueAt(key: keyof IStyleBase, pos: number) { + const univerInstanceService = get(IUniverInstanceService); + const docsModel = univerInstanceService.getUniverDocInstance('test-doc'); + + if (docsModel?.body?.textRuns == null) { + return; + } + + for (const textRun of docsModel.body?.textRuns) { + const { st, ed, ts = {} } = textRun; + + if (st <= pos && ed >= pos) { + return ts[key]; + } + } + } + + function getTextByPosition(start: number, end: number) { + const univerInstanceService = get(IUniverInstanceService); + const docsModel = univerInstanceService.getUniverDocInstance('test-doc'); + + return docsModel?.body?.dataStream.slice(start, end); + } + + beforeEach(() => { + const testBed = createCommandTestBed(); + univer = testBed.univer; + get = testBed.get; + + commandService = get(ICommandService); + commandService.registerCommand(InnerPasteCommand); + commandService.registerCommand(RichTextEditingMutation as unknown as ICommand); + + const selectionManager = get(TextSelectionManagerService); + + selectionManager.setCurrentSelection({ + pluginName: NORMAL_TEXT_SELECTION_PLUGIN_NAME, + unitId: 'test-doc', + }); + + selectionManager.add([ + { + startOffset: 0, + endOffset: 5, + collapsed: false, + segmentId: '', + }, + ]); + + selectionManager.add([ + { + startOffset: 10, + endOffset: 15, + collapsed: false, + segmentId: '', + }, + ]); + }); + + afterEach(() => univer.dispose()); + + describe('Test paste in multiple ranges', () => { + it('Should paste content to each selection ranges', async () => { + expect(getTextByPosition(0, 5)).toBe(`What’`); + expect(getFormatValueAt('bl', 0)).toBe(BooleanNumber.FALSE); + + const commandParams: IInnerPasteCommandParams = { + segmentId: '', + body: { + dataStream: 'univer', + textRuns: [ + { + st: 0, + ed: 6, + ts: { + bl: BooleanNumber.TRUE, + }, + }, + ], + }, + }; + + await commandService.executeCommand(InnerPasteCommand.id, commandParams); + + expect(getTextByPosition(0, 6)).toBe(`univer`); + expect(getTextByPosition(11, 17)).toBe('univer'); + expect(getFormatValueAt('bl', 0)).toBe(BooleanNumber.TRUE); + }); + }); +}); diff --git a/packages/ui-plugin-docs/src/commands/commands/__test__/create-command-test-bed.ts b/packages/ui-plugin-docs/src/commands/commands/__test__/create-command-test-bed.ts new file mode 100644 index 00000000000..4453722cb05 --- /dev/null +++ b/packages/ui-plugin-docs/src/commands/commands/__test__/create-command-test-bed.ts @@ -0,0 +1,63 @@ +import { createCommandTestBed as createTestBed } from '@univerjs/base-docs/commands/commands/__tests__/create-command-test-bed.js'; +import { BooleanNumber, IDocumentData } from '@univerjs/core'; + +const TEST_DOCUMENT_DATA_EN: IDocumentData = { + id: 'test-doc', + body: { + dataStream: 'What’s New in the 2022\r Gartner Hype Cycle for Emerging Technologies\r\n', + textRuns: [ + { + st: 0, + ed: 22, + ts: { + bl: BooleanNumber.FALSE, + fs: 24, + cl: { + rgb: 'rgb(0, 40, 86)', + }, + }, + }, + { + st: 23, + ed: 68, + ts: { + bl: BooleanNumber.TRUE, + fs: 24, + cl: { + rgb: 'rgb(0, 40, 86)', + }, + }, + }, + ], + paragraphs: [ + { + startIndex: 22, + }, + { + startIndex: 68, + paragraphStyle: { + spaceAbove: 20, + indentFirstLine: 20, + }, + }, + ], + sectionBreaks: [], + customBlocks: [], + }, + documentStyle: { + pageSize: { + width: 594.3, + height: 840.51, + }, + marginTop: 72, + marginBottom: 72, + marginRight: 90, + marginLeft: 90, + }, +}; + +export function createCommandTestBed() { + const { univer, get, doc } = createTestBed(TEST_DOCUMENT_DATA_EN); + + return { univer, get, doc }; +} diff --git a/packages/ui-plugin-docs/src/commands/commands/clipboard.command.ts b/packages/ui-plugin-docs/src/commands/commands/clipboard.command.ts index 287dbb96b91..6c46f167978 100644 --- a/packages/ui-plugin-docs/src/commands/commands/clipboard.command.ts +++ b/packages/ui-plugin-docs/src/commands/commands/clipboard.command.ts @@ -1,27 +1,5 @@ -import { - getRetainAndDeleteFromReplace, - IRichTextEditingMutationParams, - MemoryCursor, - RichTextEditingMutation, - TextSelectionManagerService, -} from '@univerjs/base-docs'; import { CopyCommand, CutCommand, PasteCommand } from '@univerjs/base-ui'; -import { - CommandType, - FOCUSING_DOC, - ICommand, - ICommandInfo, - ICommandService, - IDocumentBody, - IMultiCommand, - IUndoRedoService, - IUniverInstanceService, -} from '@univerjs/core'; - -interface IInnerPasteCommandParams { - segmentId: string; - body: IDocumentBody; -} +import { CommandType, FOCUSING_DOC, IMultiCommand } from '@univerjs/core'; export const DocCopyCommand: IMultiCommand = { id: CopyCommand.id, @@ -52,82 +30,3 @@ export const DocPasteCommand: IMultiCommand = { preconditions: (contextService) => contextService.getContextValue(FOCUSING_DOC), handler: async () => true, }; - -export const InnerPasteCommand: ICommand = { - id: 'doc.command.inner-paste', - type: CommandType.COMMAND, - handler: async (accessor, params: IInnerPasteCommandParams) => { - const { segmentId, body } = params; - const undoRedoService = accessor.get(IUndoRedoService); - const commandService = accessor.get(ICommandService); - const textSelectionManagerService = accessor.get(TextSelectionManagerService); - const currentUniverService = accessor.get(IUniverInstanceService); - - const selections = textSelectionManagerService.getSelections(); - - if (!Array.isArray(selections) || selections.length === 0) { - return false; - } - - const docsModel = currentUniverService.getCurrentUniverDocInstance(); - const unitId = docsModel.getUnitId(); - - const doMutation: ICommandInfo = { - id: RichTextEditingMutation.id, - params: { - unitId, - mutations: [], - }, - }; - - const memoryCursor = new MemoryCursor(); - - memoryCursor.reset(); - - for (const selection of selections) { - const { startOffset, endOffset, collapsed } = selection; - - const len = startOffset - memoryCursor.cursor; - - if (collapsed) { - doMutation.params!.mutations.push({ - t: 'r', - len, - segmentId, - }); - } else { - doMutation.params!.mutations.push( - ...getRetainAndDeleteFromReplace(selection, segmentId, memoryCursor.cursor) - ); - } - - doMutation.params!.mutations.push({ - t: 'i', - body, - len: body.dataStream.length, - line: 0, - segmentId, - }); - - memoryCursor.reset(); - memoryCursor.moveCursor(endOffset); - } - - const result = commandService.syncExecuteCommand< - IRichTextEditingMutationParams, - IRichTextEditingMutationParams - >(doMutation.id, doMutation.params); - - if (result) { - undoRedoService.pushUndoRedo({ - unitID: unitId, - undoMutations: [{ id: RichTextEditingMutation.id, params: result }], - redoMutations: [{ id: RichTextEditingMutation.id, params: doMutation.params }], - }); - - return true; - } - - return false; - }, -}; diff --git a/packages/ui-plugin-docs/src/commands/commands/inner.clipboard.command.ts b/packages/ui-plugin-docs/src/commands/commands/inner.clipboard.command.ts new file mode 100644 index 00000000000..03477f3f3f0 --- /dev/null +++ b/packages/ui-plugin-docs/src/commands/commands/inner.clipboard.command.ts @@ -0,0 +1,100 @@ +import { + getRetainAndDeleteFromReplace, + IRichTextEditingMutationParams, + MemoryCursor, + RichTextEditingMutation, + TextSelectionManagerService, +} from '@univerjs/base-docs'; +import { + CommandType, + ICommand, + ICommandInfo, + ICommandService, + IDocumentBody, + IUndoRedoService, + IUniverInstanceService, +} from '@univerjs/core'; + +export interface IInnerPasteCommandParams { + segmentId: string; + body: IDocumentBody; +} + +export const InnerPasteCommand: ICommand = { + id: 'doc.command.inner-paste', + type: CommandType.COMMAND, + handler: async (accessor, params: IInnerPasteCommandParams) => { + const { segmentId, body } = params; + const undoRedoService = accessor.get(IUndoRedoService); + const commandService = accessor.get(ICommandService); + const textSelectionManagerService = accessor.get(TextSelectionManagerService); + const currentUniverService = accessor.get(IUniverInstanceService); + + const selections = textSelectionManagerService.getSelections(); + + if (!Array.isArray(selections) || selections.length === 0) { + return false; + } + + const docsModel = currentUniverService.getCurrentUniverDocInstance(); + const unitId = docsModel.getUnitId(); + + const doMutation: ICommandInfo = { + id: RichTextEditingMutation.id, + params: { + unitId, + mutations: [], + }, + }; + + const memoryCursor = new MemoryCursor(); + + memoryCursor.reset(); + + for (const selection of selections) { + const { startOffset, endOffset, collapsed } = selection; + + const len = startOffset - memoryCursor.cursor; + + if (collapsed) { + doMutation.params!.mutations.push({ + t: 'r', + len, + segmentId, + }); + } else { + doMutation.params!.mutations.push( + ...getRetainAndDeleteFromReplace(selection, segmentId, memoryCursor.cursor) + ); + } + + doMutation.params!.mutations.push({ + t: 'i', + body, + len: body.dataStream.length, + line: 0, + segmentId, + }); + + memoryCursor.reset(); + memoryCursor.moveCursor(endOffset); + } + + const result = commandService.syncExecuteCommand< + IRichTextEditingMutationParams, + IRichTextEditingMutationParams + >(doMutation.id, doMutation.params); + + if (result) { + undoRedoService.pushUndoRedo({ + unitID: unitId, + undoMutations: [{ id: RichTextEditingMutation.id, params: result }], + redoMutations: [{ id: RichTextEditingMutation.id, params: doMutation.params }], + }); + + return true; + } + + return false; + }, +}; diff --git a/packages/ui-plugin-docs/src/controllers/clipboard.controller.ts b/packages/ui-plugin-docs/src/controllers/clipboard.controller.ts index 18e0ade4597..1ccaf9de52e 100644 --- a/packages/ui-plugin-docs/src/controllers/clipboard.controller.ts +++ b/packages/ui-plugin-docs/src/controllers/clipboard.controller.ts @@ -13,12 +13,8 @@ import { } from '@univerjs/core'; import { Inject } from '@wendellhu/redi'; -import { - DocCopyCommand, - DocCutCommand, - DocPasteCommand, - InnerPasteCommand, -} from '../commands/commands/clipboard.command'; +import { DocCopyCommand, DocCutCommand, DocPasteCommand } from '../commands/commands/clipboard.command'; +import { InnerPasteCommand } from '../commands/commands/inner.clipboard.command'; import { IDocClipboardService } from '../services/clipboard/clipboard.service'; export class DocClipboardController extends Disposable { diff --git a/packages/ui-plugin-docs/vitest.config.ts b/packages/ui-plugin-docs/vitest.config.ts index 3c74cf802f6..e840ca961ae 100644 --- a/packages/ui-plugin-docs/vitest.config.ts +++ b/packages/ui-plugin-docs/vitest.config.ts @@ -2,6 +2,7 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { + environment: 'happy-dom', coverage: { provider: 'istanbul', }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a50eed0c12f..7a18357077e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1171,6 +1171,9 @@ importers: '@vitest/coverage-istanbul': specifier: ^0.34.6 version: 0.34.6(vitest@0.34.6) + happy-dom: + specifier: ^12.10.3 + version: 12.10.3 less: specifier: ^4.2.0 version: 4.2.0 From d6704240ec921a6cb992c798d3bbd63e7d8a2959 Mon Sep 17 00:00:00 2001 From: jocs Date: Wed, 15 Nov 2023 15:24:10 +0800 Subject: [PATCH 12/12] refactor: move inner.clipboard to clipboard --- .../__test__/clipboard.command.spec.ts | 2 +- .../commands/commands/clipboard.command.ts | 103 +++++++++++++++++- .../commands/inner.clipboard.command.ts | 100 ----------------- .../src/controllers/clipboard.controller.ts | 8 +- 4 files changed, 109 insertions(+), 104 deletions(-) delete mode 100644 packages/ui-plugin-docs/src/commands/commands/inner.clipboard.command.ts diff --git a/packages/ui-plugin-docs/src/commands/commands/__test__/clipboard.command.spec.ts b/packages/ui-plugin-docs/src/commands/commands/__test__/clipboard.command.spec.ts index aa7952652b1..6c5b3d4864e 100644 --- a/packages/ui-plugin-docs/src/commands/commands/__test__/clipboard.command.spec.ts +++ b/packages/ui-plugin-docs/src/commands/commands/__test__/clipboard.command.spec.ts @@ -9,7 +9,7 @@ import { BooleanNumber, ICommand, ICommandService, IStyleBase, IUniverInstanceSe import { Injector } from '@wendellhu/redi'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { IInnerPasteCommandParams, InnerPasteCommand } from '../inner.clipboard.command'; +import { IInnerPasteCommandParams, InnerPasteCommand } from '../clipboard.command'; import { createCommandTestBed } from './create-command-test-bed'; describe('test cases in clipboard', () => { diff --git a/packages/ui-plugin-docs/src/commands/commands/clipboard.command.ts b/packages/ui-plugin-docs/src/commands/commands/clipboard.command.ts index 6c46f167978..c9cd321a82e 100644 --- a/packages/ui-plugin-docs/src/commands/commands/clipboard.command.ts +++ b/packages/ui-plugin-docs/src/commands/commands/clipboard.command.ts @@ -1,5 +1,22 @@ +import { + getRetainAndDeleteFromReplace, + IRichTextEditingMutationParams, + MemoryCursor, + RichTextEditingMutation, + TextSelectionManagerService, +} from '@univerjs/base-docs'; import { CopyCommand, CutCommand, PasteCommand } from '@univerjs/base-ui'; -import { CommandType, FOCUSING_DOC, IMultiCommand } from '@univerjs/core'; +import { + CommandType, + FOCUSING_DOC, + ICommand, + ICommandInfo, + ICommandService, + IDocumentBody, + IMultiCommand, + IUndoRedoService, + IUniverInstanceService, +} from '@univerjs/core'; export const DocCopyCommand: IMultiCommand = { id: CopyCommand.id, @@ -30,3 +47,87 @@ export const DocPasteCommand: IMultiCommand = { preconditions: (contextService) => contextService.getContextValue(FOCUSING_DOC), handler: async () => true, }; + +export interface IInnerPasteCommandParams { + segmentId: string; + body: IDocumentBody; +} + +export const InnerPasteCommand: ICommand = { + id: 'doc.command.inner-paste', + type: CommandType.COMMAND, + handler: async (accessor, params: IInnerPasteCommandParams) => { + const { segmentId, body } = params; + const undoRedoService = accessor.get(IUndoRedoService); + const commandService = accessor.get(ICommandService); + const textSelectionManagerService = accessor.get(TextSelectionManagerService); + const currentUniverService = accessor.get(IUniverInstanceService); + + const selections = textSelectionManagerService.getSelections(); + + if (!Array.isArray(selections) || selections.length === 0) { + return false; + } + + const docsModel = currentUniverService.getCurrentUniverDocInstance(); + const unitId = docsModel.getUnitId(); + + const doMutation: ICommandInfo = { + id: RichTextEditingMutation.id, + params: { + unitId, + mutations: [], + }, + }; + + const memoryCursor = new MemoryCursor(); + + memoryCursor.reset(); + + for (const selection of selections) { + const { startOffset, endOffset, collapsed } = selection; + + const len = startOffset - memoryCursor.cursor; + + if (collapsed) { + doMutation.params!.mutations.push({ + t: 'r', + len, + segmentId, + }); + } else { + doMutation.params!.mutations.push( + ...getRetainAndDeleteFromReplace(selection, segmentId, memoryCursor.cursor) + ); + } + + doMutation.params!.mutations.push({ + t: 'i', + body, + len: body.dataStream.length, + line: 0, + segmentId, + }); + + memoryCursor.reset(); + memoryCursor.moveCursor(endOffset); + } + + const result = commandService.syncExecuteCommand< + IRichTextEditingMutationParams, + IRichTextEditingMutationParams + >(doMutation.id, doMutation.params); + + if (result) { + undoRedoService.pushUndoRedo({ + unitID: unitId, + undoMutations: [{ id: RichTextEditingMutation.id, params: result }], + redoMutations: [{ id: RichTextEditingMutation.id, params: doMutation.params }], + }); + + return true; + } + + return false; + }, +}; diff --git a/packages/ui-plugin-docs/src/commands/commands/inner.clipboard.command.ts b/packages/ui-plugin-docs/src/commands/commands/inner.clipboard.command.ts deleted file mode 100644 index 03477f3f3f0..00000000000 --- a/packages/ui-plugin-docs/src/commands/commands/inner.clipboard.command.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { - getRetainAndDeleteFromReplace, - IRichTextEditingMutationParams, - MemoryCursor, - RichTextEditingMutation, - TextSelectionManagerService, -} from '@univerjs/base-docs'; -import { - CommandType, - ICommand, - ICommandInfo, - ICommandService, - IDocumentBody, - IUndoRedoService, - IUniverInstanceService, -} from '@univerjs/core'; - -export interface IInnerPasteCommandParams { - segmentId: string; - body: IDocumentBody; -} - -export const InnerPasteCommand: ICommand = { - id: 'doc.command.inner-paste', - type: CommandType.COMMAND, - handler: async (accessor, params: IInnerPasteCommandParams) => { - const { segmentId, body } = params; - const undoRedoService = accessor.get(IUndoRedoService); - const commandService = accessor.get(ICommandService); - const textSelectionManagerService = accessor.get(TextSelectionManagerService); - const currentUniverService = accessor.get(IUniverInstanceService); - - const selections = textSelectionManagerService.getSelections(); - - if (!Array.isArray(selections) || selections.length === 0) { - return false; - } - - const docsModel = currentUniverService.getCurrentUniverDocInstance(); - const unitId = docsModel.getUnitId(); - - const doMutation: ICommandInfo = { - id: RichTextEditingMutation.id, - params: { - unitId, - mutations: [], - }, - }; - - const memoryCursor = new MemoryCursor(); - - memoryCursor.reset(); - - for (const selection of selections) { - const { startOffset, endOffset, collapsed } = selection; - - const len = startOffset - memoryCursor.cursor; - - if (collapsed) { - doMutation.params!.mutations.push({ - t: 'r', - len, - segmentId, - }); - } else { - doMutation.params!.mutations.push( - ...getRetainAndDeleteFromReplace(selection, segmentId, memoryCursor.cursor) - ); - } - - doMutation.params!.mutations.push({ - t: 'i', - body, - len: body.dataStream.length, - line: 0, - segmentId, - }); - - memoryCursor.reset(); - memoryCursor.moveCursor(endOffset); - } - - const result = commandService.syncExecuteCommand< - IRichTextEditingMutationParams, - IRichTextEditingMutationParams - >(doMutation.id, doMutation.params); - - if (result) { - undoRedoService.pushUndoRedo({ - unitID: unitId, - undoMutations: [{ id: RichTextEditingMutation.id, params: result }], - redoMutations: [{ id: RichTextEditingMutation.id, params: doMutation.params }], - }); - - return true; - } - - return false; - }, -}; diff --git a/packages/ui-plugin-docs/src/controllers/clipboard.controller.ts b/packages/ui-plugin-docs/src/controllers/clipboard.controller.ts index 1ccaf9de52e..18e0ade4597 100644 --- a/packages/ui-plugin-docs/src/controllers/clipboard.controller.ts +++ b/packages/ui-plugin-docs/src/controllers/clipboard.controller.ts @@ -13,8 +13,12 @@ import { } from '@univerjs/core'; import { Inject } from '@wendellhu/redi'; -import { DocCopyCommand, DocCutCommand, DocPasteCommand } from '../commands/commands/clipboard.command'; -import { InnerPasteCommand } from '../commands/commands/inner.clipboard.command'; +import { + DocCopyCommand, + DocCutCommand, + DocPasteCommand, + InnerPasteCommand, +} from '../commands/commands/clipboard.command'; import { IDocClipboardService } from '../services/clipboard/clipboard.service'; export class DocClipboardController extends Disposable {