diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 78f3a43303..218828196d 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -1411,6 +1411,12 @@ importers: '@tiptap/core': specifier: ^2.11.7 version: 2.11.7(@tiptap/pm@2.11.7) + '@tiptap/extension-blockquote': + specifier: ^2.11.7 + version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) + '@tiptap/extension-bold': + specifier: ^2.11.7 + version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) '@tiptap/extension-bubble-menu': specifier: ^2.11.7 version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) @@ -1432,6 +1438,12 @@ importers: '@tiptap/extension-collaboration-cursor': specifier: ^2.11.7 version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(y-prosemirror@1.3.6(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23)) + '@tiptap/extension-document': + specifier: ^2.11.7 + version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) + '@tiptap/extension-dropcursor': + specifier: ^2.11.7 + version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) '@tiptap/extension-gapcursor': specifier: ^2.11.7 version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) @@ -1447,6 +1459,12 @@ importers: '@tiptap/extension-history': specifier: ^2.11.7 version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) + '@tiptap/extension-horizontal-rule': + specifier: ^2.11.7 + version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) + '@tiptap/extension-italic': + specifier: ^2.11.7 + version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) '@tiptap/extension-link': specifier: ^2.11.7 version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) @@ -1462,9 +1480,15 @@ importers: '@tiptap/extension-ordered-list': specifier: ^2.11.7 version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) + '@tiptap/extension-paragraph': + specifier: ^2.11.7 + version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) '@tiptap/extension-placeholder': specifier: ^2.11.7 version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) + '@tiptap/extension-strike': + specifier: ^2.11.7 + version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) '@tiptap/extension-table': specifier: ^2.11.7 version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) @@ -1483,6 +1507,9 @@ importers: '@tiptap/extension-task-list': specifier: ^2.11.7 version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) + '@tiptap/extension-text': + specifier: ^2.11.7 + version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) '@tiptap/extension-text-align': specifier: ~2.11.0 version: 2.11.0(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) @@ -4891,7 +4918,7 @@ packages: version: 0.0.0 '@rush-temp/pod-media@file:projects/pod-media.tgz': - resolution: {integrity: sha512-Hs0uxssWRvXJjxSjpg8HA+i412/QDzOjq29YLmGnkOuSRlxZTPH6hYelFIjEH2SSzbgZf2I8T4vBuBZToHySZA==, tarball: file:projects/pod-media.tgz} + resolution: {integrity: sha512-JQpemCeuwf6zJoUbXQtS8RR7pZT5oCCR3BQ5nZsZeWrBARAgaLplX29mBLTXmE56nEG+cq6SQJEuJENSB8lavw==, tarball: file:projects/pod-media.tgz} version: 0.0.0 '@rush-temp/pod-msg2file@file:projects/pod-msg2file.tgz': @@ -5503,7 +5530,7 @@ packages: version: 0.0.0 '@rush-temp/text-editor-resources@file:projects/text-editor-resources.tgz': - resolution: {integrity: sha512-I36q2YtMBV6V9y6S69o3cUWU82/s9Ekb1yunAWdMdqitMITc5GsCc7LfSSk+HUbTvycphD/X272w86g5Xh4++Q==, tarball: file:projects/text-editor-resources.tgz} + resolution: {integrity: sha512-0FuTY6/95g1xMR1QGgnj3l+pDR0F1GAwZxosX6WwUunaGTVqIpZHz+F4OgKLsejWFI2HvvUpd0a9nwvSR7AE1A==, tarball: file:projects/text-editor-resources.tgz} version: 0.0.0 '@rush-temp/text-editor@file:projects/text-editor.tgz': @@ -5523,7 +5550,7 @@ packages: version: 0.0.0 '@rush-temp/text@file:projects/text.tgz': - resolution: {integrity: sha512-JMas/sauTPfzLbOTYbRaP3w/sEF5hcXeihmWxtgrjq8wUXSBspdu7mhKvr3pBqQ+NfJXV9phwvuHxCxBF8j/lg==, tarball: file:projects/text.tgz} + resolution: {integrity: sha512-I83OF82U5cZMNyZAACqQcPjIxepw9L6xyMElZPpjl0o4oV61zDB774W7zwspdLgMGUDSItfdyavAtLqVpsGMTQ==, tarball: file:projects/text.tgz} version: 0.0.0 '@rush-temp/theme@file:projects/theme.tgz': @@ -27410,23 +27437,33 @@ snapshots: '@rush-temp/text@file:projects/text.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.8.3))': dependencies: '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) + '@tiptap/extension-blockquote': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) + '@tiptap/extension-bold': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) '@tiptap/extension-bullet-list': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) '@tiptap/extension-code': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) '@tiptap/extension-code-block': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) + '@tiptap/extension-document': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) + '@tiptap/extension-dropcursor': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) '@tiptap/extension-gapcursor': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) + '@tiptap/extension-hard-break': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) '@tiptap/extension-heading': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) '@tiptap/extension-highlight': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) '@tiptap/extension-history': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) + '@tiptap/extension-horizontal-rule': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) + '@tiptap/extension-italic': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) '@tiptap/extension-link': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) '@tiptap/extension-list-item': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) '@tiptap/extension-mention': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(@tiptap/suggestion@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)) '@tiptap/extension-ordered-list': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) + '@tiptap/extension-paragraph': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) + '@tiptap/extension-strike': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) '@tiptap/extension-table': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) '@tiptap/extension-table-cell': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) '@tiptap/extension-table-header': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) '@tiptap/extension-table-row': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) '@tiptap/extension-task-item': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) '@tiptap/extension-task-list': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) + '@tiptap/extension-text': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) '@tiptap/extension-text-align': 2.11.0(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) '@tiptap/extension-text-style': 2.11.0(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) '@tiptap/extension-typography': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) diff --git a/models/contact/src/plugin.ts b/models/contact/src/plugin.ts index 9f1d6a4b11..73122a5076 100644 --- a/models/contact/src/plugin.ts +++ b/models/contact/src/plugin.ts @@ -40,7 +40,6 @@ export default mergeIds(contactId, contact, { Contacts: '' as AnyComponent, ContactsTabs: '' as AnyComponent, OrganizationEditor: '' as AnyComponent, - EmployeePresenter: '' as AnyComponent, EmployeeRefPresenter: '' as AnyComponent, PersonRefPresenter: '' as AnyComponent, PersonEditor: '' as AnyComponent, diff --git a/models/time/src/index.ts b/models/time/src/index.ts index 0922b08212..ed545f67c0 100644 --- a/models/time/src/index.ts +++ b/models/time/src/index.ts @@ -18,60 +18,59 @@ import board from '@hcengineering/board' import calendarPlugin, { type Visibility } from '@hcengineering/calendar' import contactPlugin, { type Employee } from '@hcengineering/contact' import { + AccountRole, DOMAIN_MODEL, + DateRangeMode, + IndexKind, type Class, + type ClassCollaborators, type Domain, type Markup, type Ref, type Space, type Timestamp, - type Type, - DateRangeMode, - IndexKind, - AccountRole, - type ClassCollaborators + type Type } from '@hcengineering/core' import lead from '@hcengineering/lead' import { Collection, + Hidden, + Index, Mixin, Model, Prop, + TypeDate, TypeRef, TypeString, UX, type Builder, - TypeDate, - Hidden, - Index, TypeMarkup } from '@hcengineering/model' -import textEditor from '@hcengineering/text-editor' import { TEvent } from '@hcengineering/model-calendar' import core, { TAttachedDoc, TClass, TDoc, TType } from '@hcengineering/model-core' -import tracker from '@hcengineering/model-tracker' import document from '@hcengineering/model-document' +import tracker from '@hcengineering/model-tracker' import view, { createAction } from '@hcengineering/model-view' import workbench from '@hcengineering/model-workbench' import notification from '@hcengineering/notification' import recruit from '@hcengineering/recruit' import tags from '@hcengineering/tags' -import { type AnyComponent } from '@hcengineering/ui/src/types' import { - type TodoDoneTester, timeId, type ItemPresenter, type ProjectToDo, type ToDo, type ToDoPriority, type TodoAutomationHelper, + type TodoDoneTester, type WorkSlot } from '@hcengineering/time' +import { type AnyComponent } from '@hcengineering/ui/src/types' import type { Resource } from '@hcengineering/platform' import type { Rank } from '@hcengineering/task' -import time from './plugin' import task from '@hcengineering/task' +import time from './plugin' export { timeId } from '@hcengineering/time' export { default } from './plugin' @@ -416,16 +415,6 @@ export function createModel (builder: Builder): void { { modifiedOn: 1 } ] }) - - builder.createDoc(textEditor.class.TextEditorExtensionFactory, core.space.Model, { - index: 510, - create: time.function.CreateTodoItemExtension - }) - - builder.createDoc(textEditor.class.TextEditorExtensionFactory, core.space.Model, { - index: 520, - create: time.function.CreateTodoListExtension - }) } export * from './migration' diff --git a/models/time/src/plugin.ts b/models/time/src/plugin.ts index 5e799286fc..5ef69c4a54 100644 --- a/models/time/src/plugin.ts +++ b/models/time/src/plugin.ts @@ -15,13 +15,12 @@ import { type Client, type Doc, type Ref } from '@hcengineering/core' import { type Application } from '@hcengineering/model-workbench' +import { type NotificationGroup, type NotificationType } from '@hcengineering/notification' import { type IntlString, mergeIds, type Resource } from '@hcengineering/platform' -import type { AnyComponent } from '@hcengineering/ui/src/types' -import { type Action, type ActionCategory } from '@hcengineering/view' import { timeId } from '@hcengineering/time' import time from '@hcengineering/time-resources/src/plugin' -import { type NotificationGroup, type NotificationType } from '@hcengineering/notification' -import { type ExtensionCreator } from '@hcengineering/text-editor' +import type { AnyComponent } from '@hcengineering/ui/src/types' +import { type Action, type ActionCategory } from '@hcengineering/view' export default mergeIds(timeId, time, { action: { @@ -61,8 +60,6 @@ export default mergeIds(timeId, time, { ToDoCreated: '' as Ref }, function: { - ToDoTitleProvider: '' as Resource<(client: Client, ref: Ref, doc?: Doc) => Promise>, - CreateTodoItemExtension: '' as Resource, - CreateTodoListExtension: '' as Resource + ToDoTitleProvider: '' as Resource<(client: Client, ref: Ref, doc?: Doc) => Promise> } }) diff --git a/packages/text/package.json b/packages/text/package.json index 23294daad1..e2e0a0fdc0 100644 --- a/packages/text/package.json +++ b/packages/text/package.json @@ -56,9 +56,19 @@ "@tiptap/extension-table-row": "^2.11.7", "@tiptap/extension-task-item": "^2.11.7", "@tiptap/extension-task-list": "^2.11.7", + "@tiptap/extension-bold": "^2.11.7", + "@tiptap/extension-blockquote": "^2.11.7", + "@tiptap/extension-text": "^2.11.7", + "@tiptap/extension-document": "^2.11.7", "@tiptap/extension-ordered-list": "^2.11.7", "@tiptap/extension-bullet-list": "^2.11.7", "@tiptap/extension-list-item": "^2.11.7", + "@tiptap/extension-dropcursor": "^2.11.7", + "@tiptap/extension-hard-break": "^2.11.7", + "@tiptap/extension-horizontal-rule": "^2.11.7", + "@tiptap/extension-italic": "^2.11.7", + "@tiptap/extension-paragraph": "^2.11.7", + "@tiptap/extension-strike": "^2.11.7", "@tiptap/extension-typography": "^2.11.7", "@tiptap/extension-code-block": "^2.11.7", "@tiptap/extension-code": "^2.11.7", diff --git a/packages/text/src/index.ts b/packages/text/src/index.ts index 2a69c881d7..ae3ab5ea13 100644 --- a/packages/text/src/index.ts +++ b/packages/text/src/index.ts @@ -22,7 +22,8 @@ export * from './marks/colors' export * from './marks/noteBase' export * from './marks/inlineComment' -export * from './kits/default-kit' +export * from './kits/common-kit' export * from './kits/server-kit' +export * from './kit' -export { TextStyle, type TextStyleOptions } from '@tiptap/extension-text-style' +export * from './tiptapExtensions' diff --git a/packages/text/src/kit.ts b/packages/text/src/kit.ts new file mode 100644 index 0000000000..7600dbf98f --- /dev/null +++ b/packages/text/src/kit.ts @@ -0,0 +1,89 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { AnyExtension, Extension } from '@tiptap/core' + +export type ExtensionFactory = ( + extension: T, + options?: Partial | boolean +) => ExtensionSpec + +export interface ExtensionSpec { + extension: T + options: Partial | boolean +} + +export type ExtensionSpecOptions = { + [K in keyof T]: T[K] extends ExtensionSpec ? Partial | boolean : never +} + +export function extensionKit ( + name: string, + fn: (e: ExtensionFactory, o: O) => K +): Extension> { + return Extension.create({ + name, + addExtensions () { + const e: ExtensionFactory = (extension, options) => { + // ExtensionFactory -> ExtensionSpec is mostly intented as a wrapper to provide a comfortable typing + return { extension, options: options ?? true } + } + + const extensions: AnyExtension[] = [] + const entries: object = fn(e, this.options) as any + + for (const [key, _data] of Object.entries(entries)) { + const data = _data as ExtensionSpec + if (data?.extension === undefined) continue + + let options = mergeKitOptions(data.options, (this.options as any)[key]) ?? false + + // "false" is indication that the extension should not be loaded at all + if (options === false) continue + + // "true" is indication that the extension should be loaded with whatever options some parent loader provided + if (options === true) { + if (typeof data.options === 'object') { + options = data.options + } else { + options = {} + } + } + + extensions.push(data.extension.configure(options)) + } + + return extensions + } + }) +} + +export function mergeKitOptions> (target: T, source: T): T { + if (typeof target === 'object' && typeof source === 'object') { + const output = { ...target } + Object.keys(source).forEach((key: keyof T) => { + const a = target[key] + const b = source[key] + if (typeof a === 'object' && typeof b === 'object') { + output[key] = mergeKitOptions(a, b) + } else { + output[key] = b ?? a + } + }) + return output + } + + return source ?? target +} diff --git a/packages/text/src/kits/common-kit.ts b/packages/text/src/kits/common-kit.ts new file mode 100644 index 0000000000..4e4639a43e --- /dev/null +++ b/packages/text/src/kits/common-kit.ts @@ -0,0 +1,135 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { + Blockquote, + Bold, + BulletList, + Document, + Dropcursor, + Gapcursor, + Heading, + History, + HorizontalRule, + Italic, + Link, + ListItem, + OrderedList, + Paragraph, + Strike, + Table, + TableCell, + TableHeader, + TableRow, + Text, + TextAlign, + TextStyle, + Typography, + Underline +} from '../tiptapExtensions' + +import { ExtensionFactory, extensionKit } from '../kit' +import { BackgroundColor, TextColor } from '../marks/colors' +import { InlineCommentMark } from '../marks/inlineComment' +import { CodeBlockExtension, codeBlockOptions, CommentNode, MarkdownNode, TodoItemNode, TodoListNode } from '../nodes' + +import { CodeExtension, codeOptions } from '../marks/code' +import { MermaidExtension, mermaidOptions } from '../nodes/mermaid' + +export const CommonKitFactory = (e: ExtensionFactory) => + ({ + text: e(Text), + document: e(Document), + paragraph: e(Paragraph), + + bold: e(Bold), + italic: e(Italic), + strike: e(Strike), + + comment: e(CommentNode), + markdown: e(MarkdownNode, { HTMLAttributes: { class: 'proseCodeBlock' } }), + + inlineComment: e(InlineCommentMark), + + horizontalRule: e(HorizontalRule), + heading: e(Heading), + underline: e(Underline), + blockquote: e(Blockquote, { HTMLAttributes: { class: 'proseBlockQuote' } }), + link: e(Link.extend({ inclusive: false }), { + openOnClick: false, + HTMLAttributes: { class: 'cursor-pointer', rel: 'noopener noreferrer', target: '_blank' } + }), + textAlign: e(TextAlign, { + types: ['heading', 'paragraph'], + alignments: ['left', 'center', 'right'], + defaultAlignment: null + }), + + typography: e(Typography), + + dropcursor: e(Dropcursor), + gapcursor: e(Gapcursor), + history: e(History) + }) as const + +export const CommonKit = extensionKit('common-kit', CommonKitFactory) + +export const TextColorStylingKit = extensionKit( + 'text-color-styling', + (e) => + ({ + textStyle: e(TextStyle), + testColor: e(TextColor), + backgroundColor: e(BackgroundColor, { types: ['tableCell'] }) + }) as const +) + +export const TableKit = extensionKit( + 'table-kit', + (e) => + ({ + table: e(Table, { resizable: false, HTMLAttributes: { class: 'proseTable' } }), + tableRow: e(TableRow), + tableHeader: e(TableHeader), + tableCell: e(TableCell) + }) as const +) + +export const CodeSnippetsKit = extensionKit( + 'code-snippet-kit', + (e) => + ({ + codeBlock: e(CodeBlockExtension, codeBlockOptions), + codeBlockMermaid: e(MermaidExtension, mermaidOptions), + codeInline: e(CodeExtension, codeOptions) + }) as const +) + +export const CommonListKitFactory = (e: ExtensionFactory) => + ({ + listItem: e(ListItem.extend({ group: 'listItems' })), + bulletList: e(BulletList.extend({ content: 'listItems+' })), + orderedList: e(OrderedList.extend({ content: 'listItems+' })) + }) as const + +export const ListKit = extensionKit( + 'list-kit', + (e) => + ({ + ...CommonListKitFactory(e), + todoItem: e(TodoItemNode), + todoList: e(TodoListNode) + }) as const +) diff --git a/packages/text/src/kits/default-kit.ts b/packages/text/src/kits/default-kit.ts deleted file mode 100644 index 35da7f5de2..0000000000 --- a/packages/text/src/kits/default-kit.ts +++ /dev/null @@ -1,73 +0,0 @@ -// -// Copyright © 2023 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { Extension } from '@tiptap/core' -import BulletList from '@tiptap/extension-bullet-list' -import type { CodeOptions } from '@tiptap/extension-code' -import type { CodeBlockOptions } from '@tiptap/extension-code-block' -import { Level } from '@tiptap/extension-heading' -import Highlight from '@tiptap/extension-highlight' -import Link from '@tiptap/extension-link' -import ListItem from '@tiptap/extension-list-item' -import OrderedList from '@tiptap/extension-ordered-list' -import Typography from '@tiptap/extension-typography' -import Underline from '@tiptap/extension-underline' -import StarterKit from '@tiptap/starter-kit' -import { codeOptions } from '../marks/code' -import { codeBlockOptions } from '../nodes' - -export interface DefaultKitOptions { - codeBlock?: Partial | false - code?: Partial | false - heading?: { - levels?: Level[] - } - history?: false -} - -export const DefaultKit = Extension.create({ - name: 'defaultKit', - - addExtensions () { - return [ - StarterKit.configure({ - blockquote: { - HTMLAttributes: { - class: 'proseBlockQuote' - } - }, - code: this.options.code ?? codeOptions, - codeBlock: this.options.codeBlock ?? codeBlockOptions, - heading: this.options.heading, - history: this.options.history, - bulletList: false, - orderedList: false, - listItem: false - }), - ListItem.extend({ group: 'listItems' }), - BulletList.extend({ content: 'listItems+' }), - OrderedList.extend({ content: 'listItems+' }), - Underline, - Highlight.configure({ - multicolor: false - }), - Typography.configure({}), - Link.extend({ inclusive: false }).configure({ - openOnClick: false, - HTMLAttributes: { class: 'cursor-pointer', rel: 'noopener noreferrer', target: '_blank' } - }) - ] - } -}) diff --git a/packages/text/src/kits/server-kit.ts b/packages/text/src/kits/server-kit.ts index 16fe35eb97..3778c22fd0 100644 --- a/packages/text/src/kits/server-kit.ts +++ b/packages/text/src/kits/server-kit.ts @@ -1,5 +1,5 @@ // -// Copyright © 2023 Hardcore Engineering Inc. +// Copyright © 2025 Hardcore Engineering Inc. // // Licensed under the Eclipse Public License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. You may @@ -13,107 +13,43 @@ // limitations under the License. // -import { Extension } from '@tiptap/core' -import { Level } from '@tiptap/extension-heading' -import Table from '@tiptap/extension-table' -import TableCell from '@tiptap/extension-table-cell' -import TableHeader from '@tiptap/extension-table-header' -import TableRow from '@tiptap/extension-table-row' -import TaskItem from '@tiptap/extension-task-item' -import TaskList from '@tiptap/extension-task-list' -import TextAlign from '@tiptap/extension-text-align' -import TextStyle from '@tiptap/extension-text-style' - -import { CodeExtension, codeOptions } from '../marks/code' -import { BackgroundColor, TextColor } from '../marks/colors' -import { InlineCommentMark } from '../marks/inlineComment' import { NoteBaseExtension } from '../marks/noteBase' -import { NodeUuid } from '../marks/nodeUuid' +import { QMSInlineCommentMark } from '../marks/qmsInlineCommentMark' -import { CodeBlockExtension, codeBlockOptions } from '../nodes' -import { CommentNode } from '../nodes/comment' -import { FileNode, FileOptions } from '../nodes/file' -import { ImageNode, ImageOptions } from '../nodes/image' -import { MarkdownNode } from '../nodes/markdown' -import { MermaidExtension, mermaidOptions } from '../nodes/mermaid' -import { ReferenceNode } from '../nodes/reference' import { EmojiNode } from '../nodes/emoji' -import { TodoItemNode, TodoListNode } from '../nodes/todo' +import { FileNode } from '../nodes/file' +import { ImageNode } from '../nodes/image' +import { ReferenceNode } from '../nodes/reference' -import { DefaultKit, DefaultKitOptions } from './default-kit' import { EmbedNode } from '../nodes/embed' -const headingLevels: Level[] = [1, 2, 3, 4, 5, 6] +import { ExtensionFactory, extensionKit } from '../kit' +import { HardBreak } from '../tiptapExtensions' +import { CodeSnippetsKit, CommonKitFactory, ListKit, TableKit, TextColorStylingKit } from './common-kit' -const tableExtensions = [ - Table.configure({ - resizable: false, - HTMLAttributes: { - class: 'proseTable' - } - }), - TableRow.configure({}), - TableHeader.configure({}), - TableCell.configure({}) -] +export const ServerKitFactory = (e: ExtensionFactory) => + ({ + ...CommonKitFactory(e), -const taskListExtensions = [ - TaskList, - TaskItem.configure({ - nested: true, - HTMLAttributes: { - class: 'flex flex-grow gap-1 checkbox_style' - } - }) -] + // ========================================================================================== + // Extensions and kits with separate / extended implementations in the client-side editor kit + // See file://./../../../../plugins/text-editor-resources/src/kits/editor-kit.ts + // ============================================================================= -export interface ServerKitOptions extends DefaultKitOptions { - file: Partial | false - image: Partial | false -} + lists: e(ListKit), + codeSnippets: e(CodeSnippetsKit), + tables: e(TableKit), + textColorStyling: e(TextColorStylingKit), -export const ServerKit = Extension.create({ - name: 'serverKit', + hardBreak: e(HardBreak), + reference: e(ReferenceNode), + file: e(FileNode), + image: e(ImageNode), + embed: e(EmbedNode), + emoji: e(EmojiNode), - addExtensions () { - const fileExtensions = this.options.file !== false ? [FileNode.configure(this.options.file)] : [] + inlineNote: e(NoteBaseExtension), // Semi-deprecated, should be removed in the future + qmsInlineCommentMark: e(QMSInlineCommentMark) // Semi-deprecated, should be removed in the future + }) as const - const imageExtensions = this.options.image !== false ? [ImageNode.configure(this.options.image)] : [] - - return [ - DefaultKit.configure({ - ...this.options, - codeBlock: false, - code: false, - heading: { - levels: headingLevels - } - }), - InlineCommentMark.configure({}), - CodeBlockExtension.configure(codeBlockOptions), - CodeExtension.configure(codeOptions), - MermaidExtension.configure(mermaidOptions), - ...tableExtensions, - ...taskListExtensions, - ...fileExtensions, - ...imageExtensions, - TextAlign.configure({ - types: ['heading', 'paragraph'], - alignments: ['left', 'center', 'right'], - defaultAlignment: null - }), - TodoItemNode, - TodoListNode, - ReferenceNode, - EmojiNode, - CommentNode, - MarkdownNode, - NodeUuid, - NoteBaseExtension, - TextStyle.configure({}), - TextColor.configure({}), - BackgroundColor.configure({ types: ['tableCell'] }), - EmbedNode.configure({}) - ] - } -}) +export const ServerKit = extensionKit('server-kit', ServerKitFactory) diff --git a/packages/text/src/marks/nodeUuid.ts b/packages/text/src/marks/qmsInlineCommentMark.ts similarity index 84% rename from packages/text/src/marks/nodeUuid.ts rename to packages/text/src/marks/qmsInlineCommentMark.ts index 126fc84090..72391e10dc 100644 --- a/packages/text/src/marks/nodeUuid.ts +++ b/packages/text/src/marks/qmsInlineCommentMark.ts @@ -5,7 +5,7 @@ const NAME = 'node-uuid' /** * @public */ -export const NodeUuid = Mark.create({ +export const QMSInlineCommentMark = Mark.create({ name: NAME, inline: true, diff --git a/packages/text/src/tiptapExtensions.ts b/packages/text/src/tiptapExtensions.ts new file mode 100644 index 0000000000..ff889ad401 --- /dev/null +++ b/packages/text/src/tiptapExtensions.ts @@ -0,0 +1,43 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export { TextStyle, type TextStyleOptions } from '@tiptap/extension-text-style' +export { Blockquote, type BlockquoteOptions } from '@tiptap/extension-blockquote' +export { Bold, type BoldOptions } from '@tiptap/extension-bold' +export { Document } from '@tiptap/extension-document' +export { Dropcursor, type DropcursorOptions } from '@tiptap/extension-dropcursor' +export { Gapcursor } from '@tiptap/extension-gapcursor' +export { HardBreak, type HardBreakOptions } from '@tiptap/extension-hard-break' +export { Heading, type HeadingOptions } from '@tiptap/extension-heading' +export { History, type HistoryOptions } from '@tiptap/extension-history' +export { HorizontalRule, type HorizontalRuleOptions } from '@tiptap/extension-horizontal-rule' +export { Italic, type ItalicOptions } from '@tiptap/extension-italic' +export { Paragraph, type ParagraphOptions } from '@tiptap/extension-paragraph' +export { Strike, type StrikeOptions } from '@tiptap/extension-strike' +export { Text } from '@tiptap/extension-text' +export { Link, type LinkOptions } from '@tiptap/extension-link' +export { Typography, type TypographyOptions } from '@tiptap/extension-typography' +export { Underline, type UnderlineOptions } from '@tiptap/extension-underline' +export { BulletList, type BulletListOptions } from '@tiptap/extension-bullet-list' +export { ListItem, type ListItemOptions } from '@tiptap/extension-list-item' +export { OrderedList, type OrderedListOptions } from '@tiptap/extension-ordered-list' +export { TextAlign, type TextAlignOptions } from '@tiptap/extension-text-align' +export { TaskList, type TaskListOptions } from '@tiptap/extension-task-list' + +export { Table, type TableOptions } from '@tiptap/extension-table' +export { TableCell, type TableCellOptions } from '@tiptap/extension-table-cell' +export { TableHeader, type TableHeaderOptions } from '@tiptap/extension-table-header' +export { TableRow, type TableRowOptions } from '@tiptap/extension-table-row' +export { TaskItem, type TaskItemOptions } from '@tiptap/extension-task-item' diff --git a/plugins/attachment-resources/src/components/AccordionEditor.svelte b/plugins/attachment-resources/src/components/AccordionEditor.svelte index 753ea63129..8d16fd6b0b 100644 --- a/plugins/attachment-resources/src/components/AccordionEditor.svelte +++ b/plugins/attachment-resources/src/components/AccordionEditor.svelte @@ -13,11 +13,11 @@ // limitations under the License. --> {#key object?._id} { - save(object, description) - const doc = await client.findOne(event.detail._class, { _id: event.detail._id }) - if (doc != null) { - await openDoc(client.getHierarchy(), doc) - } - }} /> {/key} diff --git a/plugins/attachment-resources/src/components/AttachmentStyledBox.svelte b/plugins/attachment-resources/src/components/AttachmentStyledBox.svelte index c909dcc19e..5480d75e2f 100644 --- a/plugins/attachment-resources/src/components/AttachmentStyledBox.svelte +++ b/plugins/attachment-resources/src/components/AttachmentStyledBox.svelte @@ -15,17 +15,17 @@
@@ -149,9 +137,6 @@ bind:this={editor} {autofocus} {boundary} - canEmbedFiles={false} - canEmbedImages={false} - dropcursor={false} {placeholder} {placeholderParams} {onPaste} @@ -171,11 +156,17 @@ updateFocus() dispatch('focus') }} - extensions={[ - completionPlugin, - EmojiExtension.configure(), - IsEmptyContentExtension.configure({ onChange: (value) => (isEmpty = value) }) - ]} + kitOptions={{ + dropcursor: false, + file: false, + image: false, + emoji: true, + hooks: { + emptyContent: { + onChange: (a) => (isEmpty = a) + } + } + }} on:update />
diff --git a/plugins/contact-resources/src/components/CreateOrganization.svelte b/plugins/contact-resources/src/components/CreateOrganization.svelte index 4818633fb9..b4c70cf046 100644 --- a/plugins/contact-resources/src/components/CreateOrganization.svelte +++ b/plugins/contact-resources/src/components/CreateOrganization.svelte @@ -139,7 +139,7 @@ placeholder={core.string.Description} kind="indented" isScrollable={false} - enableBackReferences={true} + kitOptions={{ reference: true }} enableAttachments={false} on:attachments={(ev) => { if (ev.detail.size > 0) attachments = ev.detail.values diff --git a/plugins/contact-resources/src/index.ts b/plugins/contact-resources/src/index.ts index a5ee4bca7b..3258a29182 100644 --- a/plugins/contact-resources/src/index.ts +++ b/plugins/contact-resources/src/index.ts @@ -396,7 +396,8 @@ export default async (): Promise => ({ ChannelIcon, SpaceMembersEditor, ContactNamePresenter, - PersonIdFilter + PersonIdFilter, + AssigneePopup }, completion: { EmployeeQuery: async ( diff --git a/plugins/contact/src/index.ts b/plugins/contact/src/index.ts index f282f6a783..4b0efaae88 100644 --- a/plugins/contact/src/index.ts +++ b/plugins/contact/src/index.ts @@ -252,7 +252,9 @@ export const contactPlugin = plugin(contactId, { SpaceMembersEditor: '' as AnyComponent, ContactNamePresenter: '' as AnyComponent, PersonFilterValuePresenter: '' as AnyComponent, - PersonIdFilter: '' as AnyComponent + PersonIdFilter: '' as AnyComponent, + AssigneePopup: '' as AnyComponent, + EmployeePresenter: '' as AnyComponent }, channelProvider: { Email: '' as Ref, diff --git a/plugins/controlled-documents-resources/src/components/document/EditDocContent.svelte b/plugins/controlled-documents-resources/src/components/document/EditDocContent.svelte index 11566f7c7e..b59243e9ba 100644 --- a/plugins/controlled-documents-resources/src/components/document/EditDocContent.svelte +++ b/plugins/controlled-documents-resources/src/components/document/EditDocContent.svelte @@ -21,10 +21,6 @@ import { Editor, Heading } from '@hcengineering/text-editor' import { CollaboratorEditor, - FocusExtension, - HeadingsExtension, - IsEmptyContentExtension, - NodeHighlightExtension, NodeHighlightType, TableOfContents, TableOfContentsContent, @@ -32,13 +28,12 @@ highlightUpdateCommand } from '@hcengineering/text-editor-resources' import { EditBox, Label, Scroller } from '@hcengineering/ui' - import { getCollaborationUser, openDoc } from '@hcengineering/view-resources' + import { getCollaborationUser } from '@hcengineering/view-resources' import { merge } from 'effector' import { createEventDispatcher, onDestroy, tick } from 'svelte' import plugin from '../../plugin' import { - $areDocumentCommentPopupsOpened as areDocumentCommentPopupsOpened, $areDocumentCommentPopupsOpened as arePopupsOpened, $canAddDocumentComments as canAddDocumentComments, $canViewDocumentComments as canViewDocumentComments, @@ -66,7 +61,6 @@ let textEditor: CollaboratorEditor let selectedNodeId: string | null | undefined = undefined let isFocused = false - let isEmpty = true let editor: Editor let title = $controlledDocument?.title ?? '' @@ -193,47 +187,6 @@ } } - function handleExtensions () { - return [ - FocusExtension.configure({ - onFocus (focused) { - isFocused = focused - } - }), - IsEmptyContentExtension.configure({ - onChange (empty) { - isEmpty = empty - } - }), - HeadingsExtension.configure({ - onChange: (h) => { - headings = h - } - }), - NodeHighlightExtension.configure({ - isHighlightModeOn: () => $canViewDocumentComments || $canAddDocumentComments, - getNodeHighlight: handleNodeHighlight, - onNodeSelected: (uuid: string | null) => { - if (selectedNodeId !== uuid) { - selectedNodeId = uuid - } - if (isFocused) { - documentCommentsHighlightUpdated(selectedNodeId !== null ? { nodeId: selectedNodeId } : null) - } - }, - onNodeClicked: (uuid: string) => { - if (selectedNodeId !== uuid) { - selectedNodeId = uuid - } - - if (!$arePopupsOpened && $canViewDocumentComments && selectedNodeId) { - handleShowDocumentComments(selectedNodeId) - } - } - }) - ] - } - $: attribute = { key: 'content', attr: client.getHierarchy().getAttribute(documents.class.ControlledDocument, 'content') @@ -285,21 +238,51 @@ readonly={!$isEditable} editorAttributes={{ style: 'padding: 0 2em; margin: 0 -2em;' }} overflow="none" - canShowPopups={!$areDocumentCommentPopupsOpened} - enableInlineComments={false} - onExtensions={handleExtensions} kitOptions={{ - note: { + inlineNote: { readonly: !isTemplate + }, + qms: { + qmsInlineComment: { + isHighlightModeOn: () => $canViewDocumentComments || $canAddDocumentComments, + getNodeHighlight: handleNodeHighlight, + onNodeSelected: (uuid) => { + if (selectedNodeId !== uuid) { + selectedNodeId = uuid + } + if (isFocused) { + documentCommentsHighlightUpdated(selectedNodeId !== null ? { nodeId: selectedNodeId } : null) + } + }, + onNodeClicked: (uuid) => { + if (selectedNodeId !== uuid) { + selectedNodeId = uuid + } + + if (!$arePopupsOpened && $canViewDocumentComments && selectedNodeId) { + handleShowDocumentComments(selectedNodeId) + } + } + } + }, + hooks: { + focus: { + onFocus: (focused) => { + isFocused = focused + } + } + }, + toc: { + onChange: (h) => { + headings = h + dispatch('headings', h) + } + }, + collaboration: { + inlineComments: false } }} on:editor={(e) => (editor = e.detail)} - on:open-document={async (event) => { - const doc = await client.findOne(event.detail._class, { _id: event.detail._id }) - if (doc != null) { - await openDoc(client.getHierarchy(), doc) - } - }} attachFile={async (file) => { return await createEmbedding(file) }} diff --git a/plugins/controlled-documents-resources/src/text.ts b/plugins/controlled-documents-resources/src/text.ts index ad8beb3eb9..1b370ce60b 100644 --- a/plugins/controlled-documents-resources/src/text.ts +++ b/plugins/controlled-documents-resources/src/text.ts @@ -127,13 +127,13 @@ async function canAddDocumentComments (doc: ControlledDocument, mode: EditorMode return false } -function markNodeWithUuid (editor: Editor): string | undefined { +function setQMSInlineCommentMark (editor: Editor): string | undefined { if (editor === undefined) { return } const nodeId = generateId() - editor.commands.setNodeUuid(nodeId) + editor.commands.setQMSInlineCommentMark(nodeId) return nodeId } @@ -147,7 +147,7 @@ export async function comment (editor: Editor, event: MouseEvent, ctx: ActionCon let selectedNodeId = editor.extensionStorage[nodeUuidName].activeNodeUuid if (selectedNodeId == null) { - selectedNodeId = markNodeWithUuid(editor) + selectedNodeId = setQMSInlineCommentMark(editor) } if (selectedNodeId == null) { diff --git a/plugins/document-resources/src/components/DocumentEditor.svelte b/plugins/document-resources/src/components/DocumentEditor.svelte index 6e6d29f923..52a3d00cfa 100644 --- a/plugins/document-resources/src/components/DocumentEditor.svelte +++ b/plugins/document-resources/src/components/DocumentEditor.svelte @@ -19,10 +19,10 @@ import document, { Document } from '@hcengineering/document' import { getResource } from '@hcengineering/platform' import { getClient } from '@hcengineering/presentation' - import { CollaboratorEditor, HeadingsExtension, ImageUploadOptions } from '@hcengineering/text-editor-resources' + import { CollaboratorEditor, ImageUploadOptions } from '@hcengineering/text-editor-resources' import { AnySvelteComponent } from '@hcengineering/ui' import { getCollaborationUser } from '@hcengineering/view-resources' - import { Extensions, FocusPosition } from '@tiptap/core' + import { FocusPosition } from '@tiptap/core' import { createEventDispatcher } from 'svelte' export let object: Document @@ -35,6 +35,7 @@ export let requestSideSpace: ((width: number) => void) | undefined = undefined const client = getClient() + const dispatch = createEventDispatcher() const user = getCollaborationUser() let userComponent: AnySvelteComponent | undefined @@ -48,16 +49,6 @@ collabEditor.focus(position) } - const dispatch = createEventDispatcher() - - const handleExtensions = (): Extensions => [ - HeadingsExtension.configure({ - onChange: (headings) => { - dispatch('headings', headings) - } - }) - ] - $: attribute = { key: 'content', attr: client.getHierarchy().getAttribute(document.class.Document, 'content') @@ -76,9 +67,14 @@ {overflow} {editorAttributes} {requestSideSpace} - onExtensions={handleExtensions} on:update - on:open-document on:loaded + kitOptions={{ + toc: { + onChange: (h) => { + dispatch('headings', h) + } + } + }} bind:this={collabEditor} /> diff --git a/plugins/document-resources/src/components/EditDoc.svelte b/plugins/document-resources/src/components/EditDoc.svelte index f054bf4ab7..94db8f2c7e 100644 --- a/plugins/document-resources/src/components/EditDoc.svelte +++ b/plugins/document-resources/src/components/EditDoc.svelte @@ -45,7 +45,6 @@ IconPicker, ParentsNavigator, RelationsEditor, - openDoc, restrictionStore, showMenu } from '@hcengineering/view-resources' @@ -396,12 +395,6 @@ on:headings={(evt) => { headings = evt.detail }} - on:open-document={async (event) => { - const doc = await client.findOne(event.detail._class, { _id: event.detail._id }) - if (doc != null) { - await openDoc(client.getHierarchy(), doc) - } - }} on:loaded={() => { loadedDocumentContent = true }} diff --git a/plugins/products-resources/src/components/product/CreateProduct.svelte b/plugins/products-resources/src/components/product/CreateProduct.svelte index 3abee361a5..423b580e1f 100644 --- a/plugins/products-resources/src/components/product/CreateProduct.svelte +++ b/plugins/products-resources/src/components/product/CreateProduct.svelte @@ -288,7 +288,7 @@ showButtons={false} kind={'indented'} maxHeight="limited" - enableBackReferences={true} + kitOptions={{ reference: true }} enableAttachments={false} bind:content={object.fullDescription} placeholder={core.string.Description} diff --git a/plugins/test-management-resources/src/components/test-case/CreateTestCase.svelte b/plugins/test-management-resources/src/components/test-case/CreateTestCase.svelte index 2a250b33fb..fce254f756 100644 --- a/plugins/test-management-resources/src/components/test-case/CreateTestCase.svelte +++ b/plugins/test-management-resources/src/components/test-case/CreateTestCase.svelte @@ -154,7 +154,7 @@ placeholder={core.string.Description} kind="indented" isScrollable={false} - enableBackReferences={true} + kitOptions={{ reference: true }} enableAttachments={false} on:attachments={(ev) => { if (ev.detail.size > 0) attachments = ev.detail.values diff --git a/plugins/test-management-resources/src/components/test-plan/NewTestPlanPanel.svelte b/plugins/test-management-resources/src/components/test-plan/NewTestPlanPanel.svelte index c62b15bbd1..4979eb1f2b 100644 --- a/plugins/test-management-resources/src/components/test-plan/NewTestPlanPanel.svelte +++ b/plugins/test-management-resources/src/components/test-plan/NewTestPlanPanel.svelte @@ -141,7 +141,7 @@ placeholder={core.string.Description} kind="indented" isScrollable={false} - enableBackReferences={true} + kitOptions={{ reference: true }} enableAttachments={false} on:attachments={(ev) => { if (ev.detail.size > 0) attachments = ev.detail.values diff --git a/plugins/test-management-resources/src/components/test-run/NewTestRunPanel.svelte b/plugins/test-management-resources/src/components/test-run/NewTestRunPanel.svelte index 1ace20304d..030e1414f4 100644 --- a/plugins/test-management-resources/src/components/test-run/NewTestRunPanel.svelte +++ b/plugins/test-management-resources/src/components/test-run/NewTestRunPanel.svelte @@ -178,7 +178,7 @@ placeholder={core.string.Description} kind="indented" isScrollable={false} - enableBackReferences={true} + kitOptions={{ reference: true }} enableAttachments={false} on:attachments={(ev) => { if (ev.detail.size > 0) attachments = ev.detail.values diff --git a/plugins/text-editor-resources/package.json b/plugins/text-editor-resources/package.json index 2f39c735d6..6382f16cd1 100644 --- a/plugins/text-editor-resources/package.json +++ b/plugins/text-editor-resources/package.json @@ -99,6 +99,8 @@ "@hcengineering/chunter": "^0.6.20", "@tiptap/extension-text-align": "~2.11.0", "@hcengineering/workbench": "^0.6.16", - "@hcengineering/drive": "^0.6.0" + "@hcengineering/drive": "^0.6.0", + "@hcengineering/time": "^0.6.0", + "@hcengineering/rank": "^0.6.4" } } diff --git a/plugins/text-editor-resources/src/components/CollaborationDiffViewer.svelte b/plugins/text-editor-resources/src/components/CollaborationDiffViewer.svelte index bbd3c373b7..d6183a1bd6 100644 --- a/plugins/text-editor-resources/src/components/CollaborationDiffViewer.svelte +++ b/plugins/text-editor-resources/src/components/CollaborationDiffViewer.svelte @@ -21,13 +21,12 @@ import { Doc as Ydoc } from 'yjs' import { Editor, Extension, mergeAttributes } from '@tiptap/core' - import Collaboration from '@tiptap/extension-collaboration' import { Plugin, PluginKey } from '@tiptap/pm/state' import { DecorationSet } from '@tiptap/pm/view' + import { getEditorKit } from '../../src/kits/editor-kit' import { calculateDecorations, createYdocDocument } from './diff/decorations' import { defaultEditorAttributes } from './editor/editorProps' - import { getEditorKit } from '../../src/kits/editor-kit' export let ydoc: Ydoc export let field: string | undefined = undefined @@ -77,11 +76,19 @@ } onMount(async () => { + const kit = await getEditorKit({ + collaboration: { + collaboration: { document: ydoc, field }, + collaborationCursor: false, + inlineComments: false + } + }) + editor = new Editor({ editorProps: { attributes: mergeAttributes(defaultEditorAttributes, { class: 'flex-grow' }) }, element, editable: false, - extensions: [await getEditorKit(), DecorationExtension, Collaboration.configure({ document: ydoc, field })], + extensions: [kit, DecorationExtension], onContentError: ({ error, disableCollaboration }) => { disableCollaboration() Analytics.handleError(error) diff --git a/plugins/text-editor-resources/src/components/CollaborativeAttributeBox.svelte b/plugins/text-editor-resources/src/components/CollaborativeAttributeBox.svelte index 3742b98e3a..fd97c648a2 100644 --- a/plugins/text-editor-resources/src/components/CollaborativeAttributeBox.svelte +++ b/plugins/text-editor-resources/src/components/CollaborativeAttributeBox.svelte @@ -16,11 +16,10 @@ import { Doc } from '@hcengineering/core' import { IntlString } from '@hcengineering/platform' import { KeyedAttribute } from '@hcengineering/presentation' - import { AnySvelteComponent, registerFocus } from '@hcengineering/ui' import textEditor, { CollaborationUser, RefAction } from '@hcengineering/text-editor' + import { AnySvelteComponent, registerFocus } from '@hcengineering/ui' import CollaborativeTextEditor from './CollaborativeTextEditor.svelte' - import { FocusExtension } from './extension/focus' import { type FileAttachFunction } from './extension/types' export let object: Doc @@ -77,13 +76,6 @@ updateFocus() } } - - const extensions = [ - FocusExtension.configure({ - onCanBlur: (value: boolean) => (canBlur = value), - onFocus: handleFocus - }) - ] { + canBlur = value + }, + onFocus: handleFocus + } + }, + shortcuts: { + fileUpload: false + } }} on:focus on:blur on:update - on:open-document /> diff --git a/plugins/text-editor-resources/src/components/CollaborativeTextEditor.svelte b/plugins/text-editor-resources/src/components/CollaborativeTextEditor.svelte index ac8e26f957..c5913cd0ba 100644 --- a/plugins/text-editor-resources/src/components/CollaborativeTextEditor.svelte +++ b/plugins/text-editor-resources/src/components/CollaborativeTextEditor.svelte @@ -51,10 +51,8 @@ ThrottledCaller } from '@hcengineering/ui' import view from '@hcengineering/view' - import { AnyExtension, Editor, FocusPosition, mergeAttributes } from '@tiptap/core' - import Collaboration, { isChangeOrigin } from '@tiptap/extension-collaboration' - import CollaborationCursor from '@tiptap/extension-collaboration-cursor' - import Placeholder from '@tiptap/extension-placeholder' + import { Editor, FocusPosition, mergeAttributes } from '@tiptap/core' + import { isChangeOrigin } from '@tiptap/extension-collaboration' import { createEventDispatcher, getContext, onDestroy, onMount } from 'svelte' import { Doc as YDoc } from 'yjs' @@ -75,14 +73,6 @@ import { noSelectionRender, renderCursor } from './editor/collaboration' import { defaultEditorAttributes } from './editor/editorProps' import { SavedBoard } from './extension/drawingBoard' - import { EmojiExtension } from './extension/emoji' - import { FileUploadExtension } from './extension/fileUploadExt' - import { ImageUploadExtension } from './extension/imageUploadExt' - import { InlineCommandsExtension } from './extension/inlineCommands' - import { InlineCommentCollaborationExtension } from './extension/inlineComment' - import { LeftMenuExtension } from './extension/leftMenu' - import { mermaidOptions } from './extension/mermaid' - import { referenceConfig, ReferenceExtension } from './extension/reference' import { type FileAttachFunction } from './extension/types' import { inlineCommandsConfig } from './extensions' @@ -99,7 +89,6 @@ export let full: boolean = false export let placeholder: IntlString = textEditor.string.EditorPlaceholder - export let extensions: AnyExtension[] = [] export let refActions: RefAction[] = [] export let editorAttributes: Record = {} @@ -107,14 +96,13 @@ export let boundary: HTMLElement | undefined = undefined export let attachFile: FileAttachFunction | undefined = undefined - export let canShowPopups = true - export let canEmbedFiles = true - export let canEmbedImages = true + export let withSideMenu = true export let withInlineCommands = true + export let kitOptions: Partial = {} + export let requestSideSpace: ((width: number) => void) | undefined = undefined - export let enableInlineComments: boolean = true const client = getClient() const dispatch = createEventDispatcher() @@ -247,60 +235,6 @@ editor.setEditable(editable, emitUpdate) } - const optionalExtensions: AnyExtension[] = [] - - if (attachFile !== undefined) { - if (canEmbedFiles) { - optionalExtensions.push( - FileUploadExtension.configure({ - attachFile - }) - ) - } - if (canEmbedImages) { - optionalExtensions.push( - ImageUploadExtension.configure({ - attachFile, - getFileUrl - }) - ) - } - } - - if (withSideMenu) { - optionalExtensions.push( - LeftMenuExtension.configure({ - width: 20, - height: 20, - marginX: 8, - className: 'tiptap-left-menu', - icon: view.icon.Add, - iconProps: { - className: 'svg-tiny', - fill: 'currentColor' - }, - items: [ - ...(canEmbedImages ? [{ id: 'image', label: textEditor.string.Image, icon: view.icon.Image }] : []), - { id: 'table', label: textEditor.string.Table, icon: view.icon.Table2 }, - { id: 'code-block', label: textEditor.string.CodeBlock, icon: view.icon.CodeBlock }, - { id: 'separator-line', label: textEditor.string.SeparatorLine, icon: view.icon.SeparatorLine }, - { id: 'todo-list', label: textEditor.string.TodoItem, icon: view.icon.TodoList }, - { id: 'drawing-board', label: textEditor.string.DrawingBoard, icon: IconScribble as any }, - { id: 'mermaid', label: textEditor.string.MermaidDiargram, icon: view.icon.Model } - ], - handleSelect: handleLeftMenuClick - }) - ) - } - - if (withInlineCommands) { - optionalExtensions.push( - InlineCommandsExtension.configure( - inlineCommandsConfig(handleLeftMenuClick, attachFile === undefined || !canEmbedImages ? ['image'] : []) - ) - ) - } - let inputImage: HTMLInputElement export function handleAttachImage (): void { @@ -431,67 +365,82 @@ onMount(async () => { await ph - if (enableInlineComments && !isGuest) { - optionalExtensions.push( - InlineCommentCollaborationExtension.configure({ - ydoc, - boundary, - popupContainer: editorPopupContainer, - requestSideSpace - }) - ) - } - // it is recommended to wait for the local provider to be loaded // https://discuss.yjs.dev/t/initial-offline-value-of-a-shared-document/465/4 await localProvider.loaded - editor = new Editor({ - enableContentCheck: true, - element, - editorProps: { attributes: mergeAttributes(defaultEditorAttributes, editorAttributes, { class: 'flex-grow' }) }, - extensions: [ - (await getEditorKit()).configure({ - objectId, - objectClass, - objectSpace, - history: false, + const canAttachFiles = attachFile != null + + const kit = await getEditorKit( + { + objectId, + objectClass, + objectSpace, + + history: false, + shortcuts: { submit: false, - toolbar: { - boundary, - popupContainer: editorPopupContainer + imageUpload: canAttachFiles && { attachFile, getFileUrl }, + fileUpload: canAttachFiles && { attachFile } + }, + toolbar: { + boundary, + popupContainer: editorPopupContainer + }, + codeSnippets: { + codeBlockMermaid: { ydoc, ydocContentField: field } + }, + drawingBoard: { getSavedBoard }, + leftMenu: withSideMenu && { + width: 20, + height: 20, + marginX: 8, + className: 'tiptap-left-menu', + icon: view.icon.Add, + iconProps: { + className: 'svg-tiny', + fill: 'currentColor' }, - mermaid: { - ...mermaidOptions, + items: [ + { id: 'image', label: textEditor.string.Image, icon: view.icon.Image }, + { id: 'table', label: textEditor.string.Table, icon: view.icon.Table2 }, + { id: 'code-block', label: textEditor.string.CodeBlock, icon: view.icon.CodeBlock }, + { id: 'separator-line', label: textEditor.string.SeparatorLine, icon: view.icon.SeparatorLine }, + { id: 'todo-list', label: textEditor.string.TodoItem, icon: view.icon.TodoList }, + { id: 'drawing-board', label: textEditor.string.DrawingBoard, icon: IconScribble as any }, + { id: 'mermaid', label: textEditor.string.MermaidDiargram, icon: view.icon.Model } + ], + handleSelect: handleLeftMenuClick + }, + inlineCommands: + withInlineCommands && inlineCommandsConfig(handleLeftMenuClick, canAttachFiles ? [] : ['image']), + placeholder: { placeholder: placeHolderStr }, + collaboration: { + collaboration: { document: ydoc, field }, + collaborationCursor: { + provider: remoteProvider, + user, + render: renderCursor, + selectionRender: noSelectionRender + }, + inlineComments: !isGuest && { ydoc, - ydocContentField: field - }, - drawingBoard: { - getSavedBoard - }, - ...kitOptions - }), - ...optionalExtensions, - Placeholder.configure({ placeholder: placeHolderStr }), - Collaboration.configure({ - document: ydoc, - field - }), - CollaborationCursor.configure({ - provider: remoteProvider, - user, - render: renderCursor, - selectionRender: noSelectionRender - }), - ReferenceExtension.configure({ - ...referenceConfig, - showDoc (event: MouseEvent, _id: string, _class: string) { - dispatch('open-document', { event, _id, _class }) + boundary, + popupContainer: editorPopupContainer, + requestSideSpace } - }), - EmojiExtension, - ...extensions - ], + } + }, + kitOptions + ) + + editor = new Editor({ + extensions: [kit], + element, + editorProps: { + attributes: mergeAttributes(defaultEditorAttributes, editorAttributes, { class: 'flex-grow' }) + }, + enableContentCheck: true, parseOptions: { preserveWhitespace: 'full' }, diff --git a/plugins/text-editor-resources/src/components/CollaboratorEditor.svelte b/plugins/text-editor-resources/src/components/CollaboratorEditor.svelte index 0ceca720ec..610da2431a 100644 --- a/plugins/text-editor-resources/src/components/CollaboratorEditor.svelte +++ b/plugins/text-editor-resources/src/components/CollaboratorEditor.svelte @@ -18,13 +18,13 @@ import { type Doc } from '@hcengineering/core' import { IntlString } from '@hcengineering/platform' import { KeyedAttribute } from '@hcengineering/presentation' - import { AnySvelteComponent, IconSize, registerFocus } from '@hcengineering/ui' - import { AnyExtension, FocusPosition } from '@tiptap/core' import textEditor, { CollaborationUser, TextEditorCommandHandler } from '@hcengineering/text-editor' + import { AnySvelteComponent, IconSize, registerFocus } from '@hcengineering/ui' + import { FocusPosition } from '@tiptap/core' + import { EditorKitOptions } from '../kits/editor-kit' import CollaborativeTextEditor from './CollaborativeTextEditor.svelte' import { FileAttachFunction } from './extension/types' - import { EditorKitOptions } from '../kits/editor-kit' export let object: Doc export let attribute: KeyedAttribute @@ -39,14 +39,11 @@ export let overflow: 'auto' | 'none' = 'auto' export let editorAttributes: Record = {} - export let onExtensions: () => AnyExtension[] = () => [] export let boundary: HTMLElement | undefined = undefined export let attachFile: FileAttachFunction | undefined = undefined - export let canShowPopups = true export let kitOptions: Partial = {} export let requestSideSpace: ((width: number) => void) | undefined = undefined - export let enableInlineComments: boolean = true let collaborativeEditor: CollaborativeTextEditor @@ -95,15 +92,11 @@ {overflow} {boundary} {attachFile} - extensions={[...onExtensions()]} - {canShowPopups} {editorAttributes} {kitOptions} {requestSideSpace} - {enableInlineComments} on:editor on:update - on:open-document on:blur on:loaded on:focus={handleFocus} diff --git a/plugins/text-editor-resources/src/components/FullDescriptionBox.svelte b/plugins/text-editor-resources/src/components/FullDescriptionBox.svelte index ec2a9c1b2a..f3d28437da 100644 --- a/plugins/text-editor-resources/src/components/FullDescriptionBox.svelte +++ b/plugins/text-editor-resources/src/components/FullDescriptionBox.svelte @@ -14,14 +14,15 @@ // limitations under the License. -->
@@ -172,9 +161,6 @@ bind:this={editor} {autofocus} {boundary} - {canEmbedFiles} - {canEmbedImages} - dropcursor={false} on:content={(ev) => { if (canSubmit) { dispatch('message', ev.detail) @@ -191,13 +177,24 @@ updateFocus() dispatch('focus') }} - extensions={[ - completionPlugin, - EmojiExtension.configure(), - IsEmptyContentExtension.configure({ onChange: (value) => (isEmpty = value) }) - ]} {onPaste} on:update + kitOptions={mergeKitOptions( + { + emoji: true, + dropcursor: false, + reference: { + docClass, + multipleMentions: true + }, + hooks: { + emptyContent: { + onChange: (a) => (isEmpty = a) + } + } + }, + kitOptions + )} placeholder={placeholder ?? textEditor.string.EditorPlaceholder} />
diff --git a/plugins/text-editor-resources/src/components/StyledTextArea.svelte b/plugins/text-editor-resources/src/components/StyledTextArea.svelte index d81999bfbd..c93080d67a 100644 --- a/plugins/text-editor-resources/src/components/StyledTextArea.svelte +++ b/plugins/text-editor-resources/src/components/StyledTextArea.svelte @@ -4,10 +4,8 @@ import { EmptyMarkup } from '@hcengineering/text' import textEditor from '@hcengineering/text-editor' import { ButtonSize, Label } from '@hcengineering/ui' - import { AnyExtension } from '@tiptap/core' import { createEventDispatcher } from 'svelte' - import { referenceConfig, ReferenceExtension } from './extension/reference' import StyledTextEditor from './StyledTextEditor.svelte' export let label: IntlString | undefined = undefined @@ -21,7 +19,6 @@ export let isScrollable: boolean = false export let maxHeight: 'max' | 'card' | 'limited' | string | undefined = undefined export let required = false - export let enableBackReferences = false let rawValue: Markup let oldContent: Markup = EmptyMarkup @@ -50,24 +47,6 @@ editor.focus() needFocus = false } - - function configureExtensions (): AnyExtension[] { - const completionPlugin = ReferenceExtension.configure({ - ...referenceConfig, - showDoc (event: MouseEvent, _id: string, _class: string) { - dispatch('open-document', { event, _id, _class }) - } - }) - - const extensions: AnyExtension[] = [] - if (enableBackReferences) { - extensions.push(completionPlugin) - } - - return extensions - } - - const extensions = configureExtensions() @@ -92,7 +71,6 @@ {buttonSize} {maxHeight} {isScrollable} - {extensions} bind:content={rawValue} bind:this={editor} on:blur={() => { diff --git a/plugins/text-editor-resources/src/components/StyledTextBox.svelte b/plugins/text-editor-resources/src/components/StyledTextBox.svelte index ec9a286a62..f8aa45b61d 100644 --- a/plugins/text-editor-resources/src/components/StyledTextBox.svelte +++ b/plugins/text-editor-resources/src/components/StyledTextBox.svelte @@ -18,19 +18,14 @@ registerFocus, resizeObserver } from '@hcengineering/ui' - import type { AnyExtension } from '@tiptap/core' import { createEventDispatcher } from 'svelte' import StyledTextEditor from './StyledTextEditor.svelte' + import { EditorKitOptions } from '../kits/editor-kit' import { addTableHandler } from '../utils' - import { EmojiExtension } from './extension/emoji' - import { FocusExtension } from './extension/focus' - import { ImageUploadExtension } from './extension/imageUploadExt' - import { InlineCommandsExtension } from './extension/inlineCommands' - import { ReferenceExtension, referenceConfig } from './extension/reference' import { type FileAttachFunction } from './extension/types' - import { InlineCommandId, inlineCommandsConfig } from './extensions' + import { inlineCommandsConfig } from './extensions' export let label: IntlString | undefined = undefined export let content: Markup @@ -47,12 +42,11 @@ export let previewUnlimit: boolean = false export let focusable: boolean = false export let autofocus = false - export let enableBackReferences: boolean = false - export let enableEmojiReplace: boolean = true export let enableInlineCommands: boolean = true export let isScrollable: boolean = true export let boundary: HTMLElement | undefined = undefined export let readonly: boolean = false + export let kitOptions: Partial = {} export let attachFile: FileAttachFunction | undefined = undefined @@ -173,44 +167,6 @@ editor.removeAttachment(id) } - function configureExtensions (): AnyExtension[] { - const imageUploadPlugin = ImageUploadExtension.configure({ - attachFile, - getFileUrl - }) - - const completionPlugin = ReferenceExtension.configure({ - ...referenceConfig, - showDoc (event: MouseEvent, _id: string, _class: string) { - dispatch('open-document', { event, _id, _class }) - } - }) - - const extensions: AnyExtension[] = [] - if (enableBackReferences) { - extensions.push(completionPlugin) - } - extensions.push( - imageUploadPlugin, - FocusExtension.configure({ onCanBlur: (value: boolean) => (canBlur = value), onFocus: handleFocus }) - ) - if (enableEmojiReplace) { - extensions.push(EmojiExtension.configure()) - } - - if (enableInlineCommands) { - const excludedInlineCommands: InlineCommandId[] = ['drawing-board', 'todo-list'] - - if (attachFile === undefined) excludedInlineCommands.push('image') - - extensions.push( - InlineCommandsExtension.configure(inlineCommandsConfig(handleCommandSelected, excludedInlineCommands)) - ) - } - - return extensions - } - async function handleCommandSelected (id: string, pos: number, targetItem?: MouseEvent | HTMLElement): Promise { switch (id) { case 'image': @@ -281,8 +237,6 @@ } inputImage.value = '' } - - const extensions = configureExtensions() (canBlur = value), + onFocus: handleFocus + } + }, + shortcuts: { + imageUpload: { + attachFile, + getFileUrl + } + }, + inlineCommands: inlineCommandsConfig( + handleCommandSelected, + attachFile == null ? ['drawing-board', 'todo-list', 'image'] : ['drawing-board', 'todo-list'] + ) + }} bind:content={rawValue} bind:this={editor} on:value={(evt) => { diff --git a/plugins/text-editor-resources/src/components/StyledTextEditor.svelte b/plugins/text-editor-resources/src/components/StyledTextEditor.svelte index 15b12d2f14..76f2b921d7 100644 --- a/plugins/text-editor-resources/src/components/StyledTextEditor.svelte +++ b/plugins/text-editor-resources/src/components/StyledTextEditor.svelte @@ -16,10 +16,11 @@ import { type Blob, Markup, type Ref } from '@hcengineering/core' import { IntlString } from '@hcengineering/platform' import { EmptyMarkup } from '@hcengineering/text' + import textEditor, { RefAction, TextEditorHandler } from '@hcengineering/text-editor' import { Button, type ButtonSize, Scroller } from '@hcengineering/ui' - import { AnyExtension, mergeAttributes, type Editor } from '@tiptap/core' + import { type Editor, mergeAttributes } from '@tiptap/core' import { createEventDispatcher } from 'svelte' - import textEditor, { RefAction, TextEditorHandler, TextFormatCategory } from '@hcengineering/text-editor' + import { EditorKitOptions } from '../kits/editor-kit' import { defaultRefActions, getModelRefActions } from './editor/actions' import TextEditor from './TextEditor.svelte' @@ -34,10 +35,10 @@ export let maxHeight: 'max' | 'card' | 'limited' | string | undefined = undefined export let autofocus = false export let full = false - export let extensions: AnyExtension[] = [] export let editorAttributes: Record = {} export let extraActions: RefAction[] = [] export let boundary: HTMLElement | undefined = undefined + export let kitOptions: Partial = {} let editor: TextEditor | undefined = undefined @@ -163,7 +164,6 @@ editorAttributes={mergedEditorAttributes} bind:content {placeholder} - {extensions} bind:this={editor} on:value on:content={(ev) => { @@ -173,6 +173,7 @@ }} on:blur on:focus + {kitOptions} supportSubmit={false} /> @@ -181,7 +182,6 @@ editorAttributes={mergedEditorAttributes} bind:content {placeholder} - {extensions} bind:this={editor} on:value on:content={(ev) => { @@ -191,6 +191,7 @@ }} on:blur on:focus + {kitOptions} supportSubmit={false} {boundary} /> diff --git a/plugins/text-editor-resources/src/components/TextEditor.svelte b/plugins/text-editor-resources/src/components/TextEditor.svelte index b6989a30ee..46b8ad8b9c 100644 --- a/plugins/text-editor-resources/src/components/TextEditor.svelte +++ b/plugins/text-editor-resources/src/components/TextEditor.svelte @@ -18,30 +18,26 @@ import { type Blob, Markup, type Ref } from '@hcengineering/core' import { IntlString, translate } from '@hcengineering/platform' import { EmptyMarkup, getMarkup, markupToJSON } from '@hcengineering/text' - import { themeStore } from '@hcengineering/ui' import textEditor from '@hcengineering/text-editor' - import { AnyExtension, Content, Editor, FocusPosition, mergeAttributes } from '@tiptap/core' - import Placeholder from '@tiptap/extension-placeholder' + import { themeStore } from '@hcengineering/ui' + import { Content, Editor, FocusPosition, mergeAttributes } from '@tiptap/core' import { ParseOptions } from '@tiptap/pm/model' import { EditorView } from '@tiptap/pm/view' import { createEventDispatcher, onDestroy, onMount } from 'svelte' + import { EditorKitOptions, getEditorKit } from '../../src/kits/editor-kit' import { deleteAttachment } from '../command/deleteAttachment' import { defaultEditorAttributes } from './editor/editorProps' - import { getEditorKit } from '../../src/kits/editor-kit' export let content: Markup = EmptyMarkup export let placeholder: IntlString = textEditor.string.EditorPlaceholder export let placeholderParams: Record = {} - export let extensions: AnyExtension[] = [] export let supportSubmit = true export let editorAttributes: Record = {} export let boundary: HTMLElement | undefined = undefined export let autofocus: FocusPosition = false - export let canEmbedFiles = true - export let canEmbedImages = true - export let dropcursor: false | undefined = undefined export let onPaste: ((view: EditorView, event: ClipboardEvent) => boolean) | undefined = undefined + export let kitOptions: Partial = {} let element: HTMLElement let editor: Editor @@ -154,54 +150,58 @@ needFocus = false } - onMount(() => { - void ph.then(async () => { - editor = new Editor({ - enableContentCheck: true, - element, - editorProps: { - attributes: mergeAttributes(defaultEditorAttributes, editorAttributes), - handlePaste: onPaste - }, - content: markupToJSON(content), - autofocus, - extensions: [ - (await getEditorKit()).configure({ - mode: 'compact', - file: canEmbedFiles ? {} : false, - dropcursor, - image: canEmbedImages ? {} : false, - drawingBoard: false, - textAlign: false, - submit: supportSubmit ? { submit } : false - }), - Placeholder.configure({ placeholder: placeHolderStr }), - ...extensions - ], - parseOptions: { - preserveWhitespace: 'full' - }, - onTransaction: () => { - // force re-render so `editor.isActive` works as expected - editor = editor - }, - onBlur: () => { - focused = false - dispatch('blur') - }, - onFocus: () => { - focused = true - dispatch('focus') - }, - onUpdate: () => { - content = getContent() - dispatch('value', content) - dispatch('update', content) - }, - onContentError: ({ error }) => { - Analytics.handleError(error) + onMount(async () => { + await ph + + const kit = await getEditorKit( + { + mode: 'compact', + + drawingBoard: false, + textAlign: false, + reference: false, + emoji: false, + placeholder: { placeholder: placeHolderStr }, + shortcuts: { + submit: supportSubmit ? { submit } : false } - }) + }, + kitOptions + ) + + editor = new Editor({ + enableContentCheck: true, + element, + editorProps: { + attributes: mergeAttributes(defaultEditorAttributes, editorAttributes), + handlePaste: onPaste + }, + content: markupToJSON(content), + autofocus, + extensions: [kit], + parseOptions: { + preserveWhitespace: 'full' + }, + onTransaction: () => { + // force re-render so `editor.isActive` works as expected + editor = editor + }, + onBlur: () => { + focused = false + dispatch('blur') + }, + onFocus: () => { + focused = true + dispatch('focus') + }, + onUpdate: () => { + content = getContent() + dispatch('value', content) + dispatch('update', content) + }, + onContentError: ({ error }) => { + Analytics.handleError(error) + } }) }) diff --git a/plugins/text-editor-resources/src/components/extension/codeblock.ts b/plugins/text-editor-resources/src/components/extension/codeSnippets/codeblock.ts similarity index 99% rename from plugins/text-editor-resources/src/components/extension/codeblock.ts rename to plugins/text-editor-resources/src/components/extension/codeSnippets/codeblock.ts index 905fe4eb08..9891999169 100644 --- a/plugins/text-editor-resources/src/components/extension/codeblock.ts +++ b/plugins/text-editor-resources/src/components/extension/codeSnippets/codeblock.ts @@ -22,7 +22,7 @@ import { type Node as ProseMirrorNode } from '@tiptap/pm/model' import { Plugin, PluginKey } from '@tiptap/pm/state' import { Decoration, DecorationSet, type EditorView } from '@tiptap/pm/view' import { createLowlight } from 'lowlight' -import { isChangeEditable } from './editable' +import { isChangeEditable } from '../hooks/editable' type Lowlight = ReturnType diff --git a/plugins/text-editor-resources/src/components/extension/mermaid.ts b/plugins/text-editor-resources/src/components/extension/codeSnippets/mermaid.ts similarity index 99% rename from plugins/text-editor-resources/src/components/extension/mermaid.ts rename to plugins/text-editor-resources/src/components/extension/codeSnippets/mermaid.ts index ba4c7177f6..0ea4b42639 100644 --- a/plugins/text-editor-resources/src/components/extension/mermaid.ts +++ b/plugins/text-editor-resources/src/components/extension/codeSnippets/mermaid.ts @@ -19,7 +19,7 @@ import { type Node as ProseMirrorNode } from '@tiptap/pm/model' import { NodeSelection, Plugin, PluginKey, TextSelection, type Transaction } from '@tiptap/pm/state' import { Decoration, DecorationSet, type EditorView } from '@tiptap/pm/view' import { createLowlight } from 'lowlight' -import { isChangeEditable } from './editable' +import { isChangeEditable } from '../hooks/editable' import type { MermaidConfig } from 'mermaid' import { getCurrentTheme, isThemeDark, themeStore } from '@hcengineering/theme' diff --git a/plugins/text-editor-resources/src/components/extension/editable.ts b/plugins/text-editor-resources/src/components/extension/hooks/editable.ts similarity index 56% rename from plugins/text-editor-resources/src/components/extension/editable.ts rename to plugins/text-editor-resources/src/components/extension/hooks/editable.ts index b2dc5eca75..369a4a10be 100644 --- a/plugins/text-editor-resources/src/components/extension/editable.ts +++ b/plugins/text-editor-resources/src/components/extension/hooks/editable.ts @@ -1,3 +1,18 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + import { Extension } from '@tiptap/core' import { type Transaction } from '@tiptap/pm/state' @@ -15,7 +30,7 @@ export function isChangeEditable (tr: Transaction): boolean { } export const EditableExtension = Extension.create({ - name: 'editable', + name: 'isEditable', addStorage () { return { isEditable: undefined } diff --git a/plugins/text-editor-resources/src/components/extension/focus.ts b/plugins/text-editor-resources/src/components/extension/hooks/focus.ts similarity index 68% rename from plugins/text-editor-resources/src/components/extension/focus.ts rename to plugins/text-editor-resources/src/components/extension/hooks/focus.ts index 7f80b92a4a..16e72dd72f 100644 --- a/plugins/text-editor-resources/src/components/extension/focus.ts +++ b/plugins/text-editor-resources/src/components/extension/hooks/focus.ts @@ -1,3 +1,18 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + import { type Editor, Extension } from '@tiptap/core' const canBlur = (editor: Editor, options: FocusOptions): boolean => { diff --git a/plugins/text-editor-resources/src/components/extension/isEmptyContent.ts b/plugins/text-editor-resources/src/components/extension/hooks/isEmptyContent.ts similarity index 57% rename from plugins/text-editor-resources/src/components/extension/isEmptyContent.ts rename to plugins/text-editor-resources/src/components/extension/hooks/isEmptyContent.ts index 026d88b442..807ed25d71 100644 --- a/plugins/text-editor-resources/src/components/extension/isEmptyContent.ts +++ b/plugins/text-editor-resources/src/components/extension/hooks/isEmptyContent.ts @@ -1,3 +1,18 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + import { Extension } from '@tiptap/core' export interface IsEmptyContentOptions { diff --git a/plugins/text-editor-resources/src/components/extension/imageExt.ts b/plugins/text-editor-resources/src/components/extension/imageExt.ts index b6dbf68af1..124fff20a4 100644 --- a/plugins/text-editor-resources/src/components/extension/imageExt.ts +++ b/plugins/text-editor-resources/src/components/extension/imageExt.ts @@ -12,29 +12,32 @@ // See the License for the specific language governing permissions and // limitations under the License. // +import { notEmpty } from '@hcengineering/core' import { getEmbeddedLabel } from '@hcengineering/platform' -import { FilePreviewPopup, getFileUrl } from '@hcengineering/presentation' +import { FilePreviewPopup, getBlobRef, getFileUrl } from '@hcengineering/presentation' import { ImageNode, type ImageOptions } from '@hcengineering/text' import textEditor from '@hcengineering/text-editor' import { getEventPositionElement, SelectPopup, showPopup } from '@hcengineering/ui' import { type Editor, mergeAttributes, nodeInputRule } from '@tiptap/core' -import { type ResolvedPos, type Node } from '@tiptap/pm/model' +import { type Node, type ResolvedPos } from '@tiptap/pm/model' import { type EditorState, Plugin, PluginKey } from '@tiptap/pm/state' import { CursorSource, getToolbarCursor, registerToolbarProvider, - setLoadingState, type ResolveCursorProps, + setLoadingState, type ToolbarCursor } from './toolbar/toolbar' -import { notEmpty } from '@hcengineering/core' /** * @public */ export const inputRegex = /(?:^|\s)(!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\))$/ +const _loadingImageSrc = + 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxzdmcgd2lkdGg9IjMycHgiIGhlaWdodD0iMzJweCIgdmlld0JveD0iMCAwIDE2IDE2IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg0KICAgIDxwYXRoIGQ9Im0gNCAxIGMgLTEuNjQ0NTMxIDAgLTMgMS4zNTU0NjkgLTMgMyB2IDEgaCAxIHYgLTEgYyAwIC0xLjEwOTM3NSAwLjg5MDYyNSAtMiAyIC0yIGggMSB2IC0xIHogbSAyIDAgdiAxIGggNCB2IC0xIHogbSA1IDAgdiAxIGggMSBjIDEuMTA5Mzc1IDAgMiAwLjg5MDYyNSAyIDIgdiAxIGggMSB2IC0xIGMgMCAtMS42NDQ1MzEgLTEuMzU1NDY5IC0zIC0zIC0zIHogbSAtNSA0IGMgLTAuNTUwNzgxIDAgLTEgMC40NDkyMTkgLTEgMSBzIDAuNDQ5MjE5IDEgMSAxIHMgMSAtMC40NDkyMTkgMSAtMSBzIC0wLjQ0OTIxOSAtMSAtMSAtMSB6IG0gLTUgMSB2IDQgaCAxIHYgLTQgeiBtIDEzIDAgdiA0IGggMSB2IC00IHogbSAtNC41IDIgbCAtMiAyIGwgLTEuNSAtMSBsIC0yIDIgdiAwLjUgYyAwIDAuNSAwLjUgMC41IDAuNSAwLjUgaCA3IHMgMC40NzI2NTYgLTAuMDM1MTU2IDAuNSAtMC41IHYgLTEgeiBtIC04LjUgMyB2IDEgYyAwIDEuNjQ0NTMxIDEuMzU1NDY5IDMgMyAzIGggMSB2IC0xIGggLTEgYyAtMS4xMDkzNzUgMCAtMiAtMC44OTA2MjUgLTIgLTIgdiAtMSB6IG0gMTMgMCB2IDEgYyAwIDEuMTA5Mzc1IC0wLjg5MDYyNSAyIC0yIDIgaCAtMSB2IDEgaCAxIGMgMS42NDQ1MzEgMCAzIC0xLjM1NTQ2OSAzIC0zIHYgLTEgeiBtIC04IDMgdiAxIGggNCB2IC0xIHogbSAwIDAiIGZpbGw9IiMyZTM0MzQiIGZpbGwtb3BhY2l0eT0iMC4zNDkwMiIvPg0KPC9zdmc+DQo=' + /** * @public */ @@ -42,8 +45,9 @@ export const ImageExtension = ImageNode.extend({ addOptions () { return { inline: true, + loadingImgSrc: _loadingImageSrc, HTMLAttributes: {}, - getBlobRef: async () => ({ src: '', srcset: '' }) + getBlobRef } }, diff --git a/plugins/text-editor-resources/src/components/extension/inlineComment.ts b/plugins/text-editor-resources/src/components/extension/inlineComment.ts index d99c469a8d..6b055f0737 100644 --- a/plugins/text-editor-resources/src/components/extension/inlineComment.ts +++ b/plugins/text-editor-resources/src/components/extension/inlineComment.ts @@ -35,7 +35,7 @@ import 'tippy.js/animations/shift-toward.css' import { type Doc as YDoc, type Map as YMap } from 'yjs' import { SvelteRenderer } from '../node-view' -interface InlineCommentExtensionOptions { +export interface InlineCommentExtensionOptions { boundary?: HTMLElement popupContainer?: HTMLElement commentWidth?: number diff --git a/plugins/text-editor-resources/src/components/extension/nodeHighlight.ts b/plugins/text-editor-resources/src/components/extension/qms/qmsInlineComment.ts similarity index 65% rename from plugins/text-editor-resources/src/components/extension/nodeHighlight.ts rename to plugins/text-editor-resources/src/components/extension/qms/qmsInlineComment.ts index 626d890874..f89421d2b2 100644 --- a/plugins/text-editor-resources/src/components/extension/nodeHighlight.ts +++ b/plugins/text-editor-resources/src/components/extension/qms/qmsInlineComment.ts @@ -1,12 +1,31 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type TextEditorCommand } from '@hcengineering/text-editor' import { type Command, type CommandProps, Extension, type Range, getMarkRange, mergeAttributes } from '@tiptap/core' -import { type Node as ProseMirrorNode, type MarkType } from '@tiptap/pm/model' +import { type MarkType, type Node as ProseMirrorNode } from '@tiptap/pm/model' import { Plugin, PluginKey, TextSelection } from '@tiptap/pm/state' import { AddMarkStep, RemoveMarkStep } from '@tiptap/pm/transform' import { Decoration, DecorationSet } from '@tiptap/pm/view' -import { NodeUuidExtension, type NodeUuidOptions, findNodeUuidMark } from './nodeUuid' -import { type TextEditorCommand } from '@hcengineering/text-editor' +import { + QMSInlineCommentMark, + type QMSInlineCommentMarkOptions, + findQMSInlineCommentMark +} from './qmsInlineCommentMark' -export enum NodeHighlightType { +export enum CommentHighlightType { WARNING = 'warning', ADD = 'add', DELETE = 'delete' @@ -16,8 +35,8 @@ export function highlightUpdateCommand (): TextEditorCommand { return ({ commands }) => commands.updateHighlight() } -export interface NodeHighlightExtensionOptions extends NodeUuidOptions { - getNodeHighlight: (uuid: string) => { type: NodeHighlightType, isActive?: boolean } | undefined | null +export interface QMSInlineCommentExtensionOptions extends QMSInlineCommentMarkOptions { + getNodeHighlight: (uuid: string) => { type: CommentHighlightType, isActive?: boolean } | undefined | null isHighlightModeOn: () => boolean isAutoSelect?: () => boolean } @@ -27,7 +46,10 @@ function isRange (range: Range | undefined | null | void): range is Range { return range !== null && range !== undefined } -const generateAttributes = (uuid: string, options: NodeHighlightExtensionOptions): Record | undefined => { +const generateAttributes = ( + uuid: string, + options: QMSInlineCommentExtensionOptions +): Record | undefined => { if (!options.isHighlightModeOn()) { return undefined } @@ -38,11 +60,11 @@ const generateAttributes = (uuid: string, options: NodeHighlightExtensionOptions } const classAttrs: { class?: string } = {} - if (highlight.type === NodeHighlightType.WARNING) { + if (highlight.type === CommentHighlightType.WARNING) { classAttrs.class = 'text-editor-highlighted-node-warning' - } else if (highlight.type === NodeHighlightType.ADD) { + } else if (highlight.type === CommentHighlightType.ADD) { classAttrs.class = 'text-editor-highlighted-node-add' - } else if (highlight.type === NodeHighlightType.DELETE) { + } else if (highlight.type === CommentHighlightType.DELETE) { classAttrs.class = 'text-editor-highlighted-node-delete' } @@ -51,11 +73,10 @@ const generateAttributes = (uuid: string, options: NodeHighlightExtensionOptions : classAttrs } -const NodeHighlight = 'node-highlight' -const NodeHighlightMeta = 'node-highlight' +const ExtensionName = 'qms-inline-comment' -export interface NodeHighlightCommands { - [NodeHighlight]: { +export interface QMSInlineCommentCommands { + [ExtensionName]: { /** * Force all nodes to be re-rendered */ @@ -64,15 +85,12 @@ export interface NodeHighlightCommands { } declare module '@tiptap/core' { - interface Commands extends NodeHighlightCommands {} + interface Commands extends QMSInlineCommentCommands {} } -/** - * Extension allows to highlight nodes based on uuid - */ -export const NodeHighlightExtension: Extension = - Extension.create({ - name: NodeHighlight, +export const QMSInlineCommentExtension: Extension = + Extension.create({ + name: ExtensionName, addProseMirrorPlugins () { const options = this.options @@ -80,7 +98,7 @@ export const NodeHighlightExtension: Extension = const plugins = [ ...(this.parent?.() ?? []), new Plugin({ - key: new PluginKey('node-highlight-click-plugin'), + key: new PluginKey('qms-inline-comment-click-plugin'), props: { handleClick (view, pos) { if (!options.isHighlightModeOn() || options.isAutoSelect?.() !== true) { @@ -93,12 +111,12 @@ export const NodeHighlightExtension: Extension = return false } - const nodeUuidMark = findNodeUuidMark($pos.nodeAfter) - if (nodeUuidMark === undefined) { + const qmsInlineCommentMark = findQMSInlineCommentMark($pos.nodeAfter) + if (qmsInlineCommentMark === undefined) { return false } - const range = getMarkRange($pos, schema.marks[NodeUuidExtension.name], nodeUuidMark.attrs) + const range = getMarkRange($pos, schema.marks[QMSInlineCommentMark.name], qmsInlineCommentMark.attrs) if (!isRange(range)) { return false @@ -115,18 +133,18 @@ export const NodeHighlightExtension: Extension = }), new Plugin({ - key: new PluginKey('node-highlight-decoration-plugin'), + key: new PluginKey('qms-inline-comment-decoration-plugin'), state: { init (_config, state): DecorationSet { const { doc, schema } = state - const markType = schema.marks[NodeUuidExtension.name] + const markType = schema.marks[QMSInlineCommentMark.name] return createDecorations(doc, markType, options) }, apply (tr, decorations, oldState, newState) { - const markType = newState.schema.marks[NodeUuidExtension.name] + const markType = newState.schema.marks[QMSInlineCommentMark.name] - if (tr.getMeta(NodeHighlightMeta) !== undefined) { + if (tr.getMeta(ExtensionName) !== undefined) { return createDecorations(tr.doc, markType, options) } @@ -181,11 +199,11 @@ export const NodeHighlightExtension: Extension = }, addCommands () { - const result: NodeHighlightCommands[typeof NodeHighlight] = { + const result: QMSInlineCommentCommands[typeof ExtensionName] = { updateHighlight: () => ({ view: { dispatch, state } }: CommandProps) => { - dispatch(state.tr.setMeta(NodeHighlightMeta, '')) + dispatch(state.tr.setMeta(ExtensionName, '')) return true } } @@ -194,10 +212,10 @@ export const NodeHighlightExtension: Extension = }, addExtensions () { - const options: NodeHighlightExtensionOptions = this.options + const options: QMSInlineCommentExtensionOptions = this.options return [ - NodeUuidExtension.extend({ - addOptions (): NodeUuidOptions { + QMSInlineCommentMark.extend({ + addOptions (): QMSInlineCommentMarkOptions { return { ...this.parent?.(), ...options @@ -211,22 +229,22 @@ export const NodeHighlightExtension: Extension = const createDecorations = ( doc: ProseMirrorNode, markType: MarkType, - options: NodeHighlightExtensionOptions + options: QMSInlineCommentExtensionOptions ): DecorationSet => { const decorations: Decoration[] = [] doc.descendants((node, pos) => { - const nodeUuidMark = findNodeUuidMark(node) + const qmsInlineCommentMark = findQMSInlineCommentMark(node) - if (nodeUuidMark !== null && nodeUuidMark !== undefined) { - const nodeUuid = nodeUuidMark.attrs[NodeUuidExtension.name] + if (qmsInlineCommentMark !== null && qmsInlineCommentMark !== undefined) { + const nodeUuid = qmsInlineCommentMark.attrs[QMSInlineCommentMark.name] const attributes = generateAttributes(nodeUuid, options) if (attributes === null || attributes === undefined) { return } // the first pos does not contain the mark, so we need to add 1 (pos + 1) to get the correct range - const range = getMarkRange(doc.resolve(pos + 1), markType, nodeUuidMark.attrs) + const range = getMarkRange(doc.resolve(pos + 1), markType, qmsInlineCommentMark.attrs) if (!isRange(range)) { return } diff --git a/plugins/text-editor-resources/src/components/extension/nodeUuid.ts b/plugins/text-editor-resources/src/components/extension/qms/qmsInlineCommentMark.ts similarity index 55% rename from plugins/text-editor-resources/src/components/extension/nodeUuid.ts rename to plugins/text-editor-resources/src/components/extension/qms/qmsInlineCommentMark.ts index 9c81399168..3b76ad5a4e 100644 --- a/plugins/text-editor-resources/src/components/extension/nodeUuid.ts +++ b/plugins/text-editor-resources/src/components/extension/qms/qmsInlineCommentMark.ts @@ -1,3 +1,18 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + import { type CommandProps, type Editor, @@ -10,10 +25,10 @@ import { import { type Node, type Mark as ProseMirrorMark } from '@tiptap/pm/model' import { type EditorState, Plugin, PluginKey, TextSelection } from '@tiptap/pm/state' -export const nodeUuidName = 'node-uuid' -export const nodeElementQuerySelector = (nodeUuid: string): string => `span[${nodeUuidName}='${nodeUuid}']` +export const qmsInlineCommentMarkName = 'node-uuid' +export const nodeElementQuerySelector = (nodeUuid: string): string => `span[${qmsInlineCommentMarkName}='${nodeUuid}']` -export interface NodeUuidOptions { +export interface QMSInlineCommentMarkOptions { HTMLAttributes: Record onNodeSelected?: (uuid: string | null) => void onNodeClicked?: (uuid: string) => void @@ -21,24 +36,18 @@ export interface NodeUuidOptions { declare module '@tiptap/core' { interface Commands { - [nodeUuidName]: { - /** - * Add uuid mark - */ - setNodeUuid: (uuid: string) => ReturnType - /** - * Unset uuid mark - */ - unsetNodeUuid: () => ReturnType + [qmsInlineCommentMarkName]: { + setQMSInlineCommentMark: (uuid: string) => ReturnType + unsetQMSInlineCommentMark: () => ReturnType } } } -export interface NodeUuidStorage { - activeNodeUuid: string | null +export interface QMSInlineCommentMarkStorage { + activeQMSInlineComment: string | null } -const findSelectionNodeUuidMark = (state: EditorState): ProseMirrorMark | undefined => { +const findSelectionQMSInlineCommentMark = (state: EditorState): ProseMirrorMark | undefined => { const { doc, selection } = state if (selection === null || selection === undefined) { @@ -52,7 +61,7 @@ const findSelectionNodeUuidMark = (state: EditorState): ProseMirrorMark | undefi if (nodeUuidMark !== undefined) { return false } - nodeUuidMark = findNodeUuidMark(node) + nodeUuidMark = findQMSInlineCommentMark(node) }) } } @@ -60,12 +69,12 @@ const findSelectionNodeUuidMark = (state: EditorState): ProseMirrorMark | undefi return nodeUuidMark } -export const findNodeUuidMark = (node: Node): ProseMirrorMark | undefined => { +export const findQMSInlineCommentMark = (node: Node): ProseMirrorMark | undefined => { if (node === null || node === undefined) { return } - return node.marks.find((mark) => mark.type.name === nodeUuidName && mark.attrs[nodeUuidName]) + return node.marks.find((mark) => mark.type.name === qmsInlineCommentMarkName && mark.attrs[qmsInlineCommentMarkName]) } export function getNodeElement (editor: Editor, uuid: string): Element | null { @@ -89,7 +98,7 @@ export function selectNode (editor: Editor, uuid: string): void { } const nodeUuidMark = node.marks.find( - (mark) => mark.type.name === NodeUuidExtension.name && mark.attrs[NodeUuidExtension.name] === uuid + (mark) => mark.type.name === QMSInlineCommentMark.name && mark.attrs[QMSInlineCommentMark.name] === uuid ) if (nodeUuidMark === undefined) { @@ -99,7 +108,7 @@ export function selectNode (editor: Editor, uuid: string): void { foundNode = true // the first pos does not contain the mark, so we need to add 1 (pos + 1) to get the correct range - const range = getMarkRange(doc.resolve(pos + 1), schema.marks[NodeUuidExtension.name]) + const range = getMarkRange(doc.resolve(pos + 1), schema.marks[QMSInlineCommentMark.name]) if (range == null || typeof range !== 'object') { return false @@ -111,12 +120,8 @@ export function selectNode (editor: Editor, uuid: string): void { }) } -/** - * This extension allows to add node uuid to the selected text - * Creates span node with attribute node-uuid - */ -export const NodeUuidExtension = Mark.create({ - name: nodeUuidName, +export const QMSInlineCommentMark = Mark.create({ + name: qmsInlineCommentMarkName, exitable: true, inclusive: false, addOptions () { @@ -127,9 +132,9 @@ export const NodeUuidExtension = Mark.create({ addAttributes () { return { - [nodeUuidName]: { + [qmsInlineCommentMarkName]: { default: null, - parseHTML: (el) => (el as HTMLSpanElement).getAttribute(nodeUuidName) + parseHTML: (el) => (el as HTMLSpanElement).getAttribute(qmsInlineCommentMarkName) } } }, @@ -137,9 +142,9 @@ export const NodeUuidExtension = Mark.create({ parseHTML () { return [ { - tag: `span[${nodeUuidName}]`, + tag: `span[${qmsInlineCommentMarkName}]`, getAttrs: (el) => { - const value = (el as HTMLSpanElement).getAttribute(nodeUuidName)?.trim() + const value = (el as HTMLSpanElement).getAttribute(qmsInlineCommentMarkName)?.trim() if (value === null || value === undefined || value.length === 0) { return false } @@ -156,32 +161,33 @@ export const NodeUuidExtension = Mark.create({ addProseMirrorPlugins () { const options = this.options - const storage: NodeUuidStorage = this.storage + const storage: QMSInlineCommentMarkStorage = this.storage const plugins = [ ...(this.parent?.() ?? []), new Plugin({ - key: new PluginKey('handle-node-uuid-click-plugin'), + key: new PluginKey('handle-qms-inline-comment-click-plugin'), props: { handleClick (view, pos) { const from = Math.max(0, pos - 1) const to = Math.min(view.state.doc.content.size, pos + 1) const markRanges = getMarksBetween(from, to, view.state.doc)?.filter( - (markRange) => markRange.mark.type.name === nodeUuidName && markRange.from <= pos && markRange.to >= pos + (markRange) => + markRange.mark.type.name === qmsInlineCommentMarkName && markRange.from <= pos && markRange.to >= pos ) ?? [] let nodeUuid: string | null = null if (markRanges.length > 0) { - nodeUuid = markRanges[0].mark.attrs[nodeUuidName] + nodeUuid = markRanges[0].mark.attrs[qmsInlineCommentMarkName] } if (nodeUuid !== null) { options.onNodeClicked?.(nodeUuid) } - if (storage.activeNodeUuid !== nodeUuid) { - storage.activeNodeUuid = nodeUuid - options.onNodeSelected?.(storage.activeNodeUuid) + if (storage.activeQMSInlineComment !== nodeUuid) { + storage.activeQMSInlineComment = nodeUuid + options.onNodeSelected?.(storage.activeQMSInlineComment) } } } @@ -193,20 +199,20 @@ export const NodeUuidExtension = Mark.create({ addCommands () { return { - setNodeUuid: + setQMSInlineCommentMark: (uuid: string) => ({ commands, state }: CommandProps) => { const { doc, selection } = state if (selection.empty) { return false } - if (doc.rangeHasMark(selection.from, selection.to, getMarkType(nodeUuidName, state.schema))) { + if (doc.rangeHasMark(selection.from, selection.to, getMarkType(qmsInlineCommentMarkName, state.schema))) { return false } - return commands.setMark(this.name, { [nodeUuidName]: uuid }) + return commands.setMark(this.name, { [qmsInlineCommentMarkName]: uuid }) }, - unsetNodeUuid: + unsetQMSInlineCommentMark: () => ({ commands }: CommandProps) => commands.unsetMark(this.name) @@ -215,18 +221,20 @@ export const NodeUuidExtension = Mark.create({ addStorage () { return { - activeNodeUuid: null + activeQMSInlineComment: null } }, onSelectionUpdate () { - const activeNodeUuidMark = findSelectionNodeUuidMark(this.editor.state) - const activeNodeUuid = - activeNodeUuidMark !== null && activeNodeUuidMark !== undefined ? activeNodeUuidMark.attrs[nodeUuidName] : null + const activeQMSInlineCommentMark = findSelectionQMSInlineCommentMark(this.editor.state) + const activeQMSInlineComment = + activeQMSInlineCommentMark !== null && activeQMSInlineCommentMark !== undefined + ? activeQMSInlineCommentMark.attrs[qmsInlineCommentMarkName] + : null - if (this.storage.activeNodeUuid !== activeNodeUuid) { - this.storage.activeNodeUuid = activeNodeUuid - this.options.onNodeSelected?.(this.storage.activeNodeUuid) + if (this.storage.activeQMSInlineComment !== activeQMSInlineComment) { + this.storage.activeQMSInlineComment = activeQMSInlineComment + this.options.onNodeSelected?.(this.storage.activeQMSInlineComment) } } }) diff --git a/plugins/text-editor-resources/src/components/extension/reference.ts b/plugins/text-editor-resources/src/components/extension/reference.ts index b42ac608bf..41186f5011 100644 --- a/plugins/text-editor-resources/src/components/extension/reference.ts +++ b/plugins/text-editor-resources/src/components/extension/reference.ts @@ -34,7 +34,7 @@ export interface ReferenceExtensionOptions extends ReferenceOptions { suggestion: Omit docClass?: Ref> multipleMentions?: boolean - showDoc?: (event: MouseEvent, _id: string, _class: string) => void + openDocument?: (_class: Ref>, _id: Ref, event?: MouseEvent) => void | Promise } export const ReferenceExtension = ReferenceNode.extend({ @@ -117,7 +117,7 @@ export const ReferenceExtension = ReferenceNode.extend = { } } } + }, + openDocument: async (_class, _id, event) => { + const openDocument = await getResource(view.function.OpenDocument) + await openDocument?.(_class, _id) } } diff --git a/plugins/text-editor-resources/src/components/extension/fileUploadExt.ts b/plugins/text-editor-resources/src/components/extension/shortcuts/fileUpload.ts similarity index 94% rename from plugins/text-editor-resources/src/components/extension/fileUploadExt.ts rename to plugins/text-editor-resources/src/components/extension/shortcuts/fileUpload.ts index 96b682ba51..e23725e8ed 100644 --- a/plugins/text-editor-resources/src/components/extension/fileUploadExt.ts +++ b/plugins/text-editor-resources/src/components/extension/shortcuts/fileUpload.ts @@ -16,19 +16,19 @@ import { Extension } from '@tiptap/core' import { Plugin, PluginKey } from '@tiptap/pm/state' import { type EditorView } from '@tiptap/pm/view' -import { type FileAttachFunction } from './types' +import { type FileAttachFunction } from '../types' /** * @public */ -export interface FileUploadOptions { +export interface FileUploadExtensionOptions { attachFile?: FileAttachFunction } /** * @public */ -export const FileUploadExtension = Extension.create({ +export const FileUploadExtension = Extension.create({ name: 'file-upload-ext', addProseMirrorPlugins () { diff --git a/plugins/text-editor-resources/src/components/extension/submit.ts b/plugins/text-editor-resources/src/components/extension/shortcuts/handleSubmit.ts similarity index 80% rename from plugins/text-editor-resources/src/components/extension/submit.ts rename to plugins/text-editor-resources/src/components/extension/shortcuts/handleSubmit.ts index bede6c2675..5b42c06c96 100644 --- a/plugins/text-editor-resources/src/components/extension/submit.ts +++ b/plugins/text-editor-resources/src/components/extension/shortcuts/handleSubmit.ts @@ -1,12 +1,12 @@ import { Extension, type KeyboardShortcutCommand } from '@tiptap/core' -export interface SubmitOptions { +export interface HandleSubmitExtensionOptions { submit: () => void useModKey?: boolean } -export const SubmitExtension = Extension.create({ - name: 'submit', +export const HandleSubmitExtension = Extension.create({ + name: 'handleSubmit', addKeyboardShortcuts () { const shortcuts: Record = { Space: () => { diff --git a/plugins/text-editor-resources/src/components/extension/imageUploadExt.ts b/plugins/text-editor-resources/src/components/extension/shortcuts/imageUpload.ts similarity index 97% rename from plugins/text-editor-resources/src/components/extension/imageUploadExt.ts rename to plugins/text-editor-resources/src/components/extension/shortcuts/imageUpload.ts index b43a6a7ff5..b16615bfc8 100644 --- a/plugins/text-editor-resources/src/components/extension/imageUploadExt.ts +++ b/plugins/text-editor-resources/src/components/extension/shortcuts/imageUpload.ts @@ -18,13 +18,13 @@ import { Extension } from '@tiptap/core' import { Plugin, PluginKey } from '@tiptap/pm/state' import { type EditorView } from '@tiptap/pm/view' -import { type FileAttachFunction } from './types' +import { type FileAttachFunction } from '../types' import type { Blob, Ref } from '@hcengineering/core' /** * @public */ -export interface ImageUploadOptions { +export interface ImageUploadExtensionOptions { attachFile?: FileAttachFunction getFileUrl: (fileId: Ref) => string } @@ -40,7 +40,7 @@ function getType (type: string): 'image' | 'other' { /** * @public */ -export const ImageUploadExtension = Extension.create({ +export const ImageUploadExtension = Extension.create({ name: 'image-upload', addOptions () { diff --git a/plugins/text-editor-resources/src/components/extension/indent.ts b/plugins/text-editor-resources/src/components/extension/shortcuts/indent.ts similarity index 100% rename from plugins/text-editor-resources/src/components/extension/indent.ts rename to plugins/text-editor-resources/src/components/extension/shortcuts/indent.ts diff --git a/plugins/text-editor-resources/src/components/extension/link.ts b/plugins/text-editor-resources/src/components/extension/shortcuts/linkKeymap.ts similarity index 92% rename from plugins/text-editor-resources/src/components/extension/link.ts rename to plugins/text-editor-resources/src/components/extension/shortcuts/linkKeymap.ts index 091c06378a..30e2aacf88 100644 --- a/plugins/text-editor-resources/src/components/extension/link.ts +++ b/plugins/text-editor-resources/src/components/extension/shortcuts/linkKeymap.ts @@ -17,9 +17,9 @@ import { showPopup } from '@hcengineering/ui' import { Extension } from '@tiptap/core' import { type MarkType } from '@tiptap/pm/model' import { Plugin, PluginKey } from '@tiptap/pm/state' -import LinkPopup from '../LinkPopup.svelte' +import LinkPopup from '../../LinkPopup.svelte' -export const LinkUtilsExtension = Extension.create({ +export const LinkKeymapExtension = Extension.create({ name: 'linkUtils', addKeyboardShortcuts () { @@ -48,11 +48,11 @@ export const LinkUtilsExtension = Extension.create({ } }) -interface LinkClickHandlerOptions { +interface LinkClickHandlerPluginOptions { type: MarkType } -export function LinkClickHandlerPlugin (options: LinkClickHandlerOptions): Plugin { +export function LinkClickHandlerPlugin (options: LinkClickHandlerPluginOptions): Plugin { return new Plugin({ key: new PluginKey('handleClickLink'), props: { diff --git a/plugins/text-editor-resources/src/components/extension/listkeymap.ts b/plugins/text-editor-resources/src/components/extension/shortcuts/listKeymap.ts similarity index 99% rename from plugins/text-editor-resources/src/components/extension/listkeymap.ts rename to plugins/text-editor-resources/src/components/extension/shortcuts/listKeymap.ts index f82f8291c9..f836d50057 100644 --- a/plugins/text-editor-resources/src/components/extension/listkeymap.ts +++ b/plugins/text-editor-resources/src/components/extension/shortcuts/listKeymap.ts @@ -1,5 +1,5 @@ // -// Copyright © 2024 Hardcore Engineering Inc. +// Copyright © 2025 Hardcore Engineering Inc. // // Licensed under the Eclipse Public License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. You may diff --git a/plugins/text-editor-resources/src/components/extension/paragraph.ts b/plugins/text-editor-resources/src/components/extension/shortcuts/paragraphKeymap.ts similarity index 85% rename from plugins/text-editor-resources/src/components/extension/paragraph.ts rename to plugins/text-editor-resources/src/components/extension/shortcuts/paragraphKeymap.ts index aca79048ca..17457be7cd 100644 --- a/plugins/text-editor-resources/src/components/extension/paragraph.ts +++ b/plugins/text-editor-resources/src/components/extension/shortcuts/paragraphKeymap.ts @@ -1,6 +1,7 @@ import { Extension } from '@tiptap/core' -export const ParagraphExtension = Extension.create({ +export const ParagraphKeymapExtension = Extension.create({ + name: 'paragraphKeymap', addKeyboardShortcuts () { return { 'Shift-Enter': () => { diff --git a/plugins/text-editor-resources/src/components/extension/paste.ts b/plugins/text-editor-resources/src/components/extension/shortcuts/smartPaste.ts similarity index 98% rename from plugins/text-editor-resources/src/components/extension/paste.ts rename to plugins/text-editor-resources/src/components/extension/shortcuts/smartPaste.ts index 84265d96d5..b0aecd4a2f 100644 --- a/plugins/text-editor-resources/src/components/extension/paste.ts +++ b/plugins/text-editor-resources/src/components/extension/shortcuts/smartPaste.ts @@ -19,7 +19,7 @@ import { Extension } from '@tiptap/core' import { Node, type Schema } from '@tiptap/pm/model' import { Plugin } from '@tiptap/pm/state' -export const TransformPastedContentExtension = Extension.create({ +export const SmartPasteExtension = Extension.create({ name: 'transformPastedContent', addProseMirrorPlugins () { diff --git a/plugins/text-editor-resources/src/components/extension/headings.ts b/plugins/text-editor-resources/src/components/extension/toc.ts similarity index 96% rename from plugins/text-editor-resources/src/components/extension/headings.ts rename to plugins/text-editor-resources/src/components/extension/toc.ts index 6f350a1c2f..7a03e07a53 100644 --- a/plugins/text-editor-resources/src/components/extension/headings.ts +++ b/plugins/text-editor-resources/src/components/extension/toc.ts @@ -20,16 +20,16 @@ import { Decoration, DecorationSet } from '@tiptap/pm/view' import slugify from 'slugify' import { type Heading } from '@hcengineering/text-editor' -export interface HeadingsOptions { +export interface ToCExtensionOptions { prefixId?: string onChange?: (headings: Heading[]) => void } -export interface HeadingsStorage { +export interface ToCExtensionStorage { headings: Heading[] } -export const HeadingsExtension: Extension = Extension.create({ +export const ToCExtension: Extension = Extension.create({ name: 'headings-extension', addStorage () { diff --git a/plugins/time-resources/src/components/text-editor/node-view/ToDoItemNodeView.svelte b/plugins/text-editor-resources/src/components/extension/todo/ToDoItemNodeView.svelte similarity index 87% rename from plugins/time-resources/src/components/text-editor/node-view/ToDoItemNodeView.svelte rename to plugins/text-editor-resources/src/components/extension/todo/ToDoItemNodeView.svelte index bbbfdea616..65de09f06c 100644 --- a/plugins/time-resources/src/components/text-editor/node-view/ToDoItemNodeView.svelte +++ b/plugins/text-editor-resources/src/components/extension/todo/ToDoItemNodeView.svelte @@ -1,15 +1,15 @@ diff --git a/plugins/text-editor-resources/src/components/extension/todo.ts b/plugins/text-editor-resources/src/components/extension/todo/todo.ts similarity index 91% rename from plugins/text-editor-resources/src/components/extension/todo.ts rename to plugins/text-editor-resources/src/components/extension/todo/todo.ts index b2a1222652..667863cc73 100644 --- a/plugins/text-editor-resources/src/components/extension/todo.ts +++ b/plugins/text-editor-resources/src/components/extension/todo/todo.ts @@ -13,14 +13,20 @@ // limitations under the License. // +import core, { type Class, type Doc, type Ref } from '@hcengineering/core' +import { getClient } from '@hcengineering/presentation' +import { type ActionContext } from '@hcengineering/text-editor' import { findParentNode, getNodeType, InputRule, isList, type RawCommands } from '@tiptap/core' -import TaskItem from '@tiptap/extension-task-item' +import TaskItem, { type TaskItemOptions } from '@tiptap/extension-task-item' import TaskList from '@tiptap/extension-task-list' import { Fragment, NodeRange, Slice, type NodeType } from '@tiptap/pm/model' import { Plugin, type Command, type EditorState, type Selection, type Transaction } from '@tiptap/pm/state' import { canJoin, findWrapping, liftTarget, ReplaceAroundStep } from '@tiptap/pm/transform' import { type EditorView } from '@tiptap/pm/view' -import { getDataAttribute } from '../../utils' +import { getDataAttribute } from '../../../utils' +import { SvelteNodeViewRenderer } from '../../node-view' +import ToDoItemNodeView from './ToDoItemNodeView.svelte' +import ToDoListNodeView from './ToDoListNodeView.svelte' declare module '@tiptap/core' { interface Commands { @@ -30,14 +36,20 @@ declare module '@tiptap/core' { } } -export const TodoItemExtension = TaskItem.extend({ +export interface TodoItemExtensionOptions extends TaskItemOptions { + context?: ActionContext +} + +export const TodoItemExtension = TaskItem.extend({ name: 'todoItem', group: 'listItems', addOptions () { return { nested: true, - HTMLAttributes: {}, + HTMLAttributes: { + class: 'todo-item' + }, taskListTypeName: 'todoList' } }, @@ -75,6 +87,15 @@ export const TodoItemExtension = TaskItem.extend({ addProseMirrorPlugins () { return [...(this.parent?.() ?? []), TodoItemDowncastPlugin()] + }, + + addNodeView () { + const { objectId, objectClass, objectSpace } = this.options.context ?? {} + return SvelteNodeViewRenderer(ToDoItemNodeView, { + contentAs: 'li', + contentClass: 'todo-item', + componentProps: objectClass != null && isTodoableClass(objectClass) ? { objectId, objectClass, objectSpace } : {} + }) } }) @@ -84,8 +105,14 @@ export const TodoListExtension = TaskList.extend({ addOptions () { return { itemTypeName: 'todoItem', - HTMLAttributes: {} + HTMLAttributes: { + class: 'todo-list' + } } + }, + + addNodeView () { + return SvelteNodeViewRenderer(ToDoListNodeView, {}) } }) @@ -499,3 +526,14 @@ export function sinkListItem (itemType: NodeType, group: string): Command { return true } } + +function isTodoableClass (objectClass: Ref>): boolean { + const hierarchy = getClient().getHierarchy() + + try { + const todosCollection = hierarchy.getAttribute(objectClass, 'todos') + return todosCollection !== undefined && todosCollection.type._class === core.class.Collection + } catch (e) { + return false + } +} diff --git a/plugins/text-editor-resources/src/components/extension/toolbar/toolbar.ts b/plugins/text-editor-resources/src/components/extension/toolbar/toolbar.ts index 20eded96e2..782c4c74e6 100644 --- a/plugins/text-editor-resources/src/components/extension/toolbar/toolbar.ts +++ b/plugins/text-editor-resources/src/components/extension/toolbar/toolbar.ts @@ -206,7 +206,7 @@ export function ToolbarControlPlugin (editor: Editor, options: ToolbarOptions): placement: viewOptions?.placement, hideOnClick: 'toggle', appendTo: viewOptions?.floating !== true ? options.popupContainer ?? document.body : document.body, - zIndex: 10000, + zIndex: 9998, offset: viewOptions?.offset, popperOptions: { modifiers: [ diff --git a/plugins/text-editor-resources/src/components/node-view/svelte-node-view-renderer.ts b/plugins/text-editor-resources/src/components/node-view/svelte-node-view-renderer.ts index affaba7671..80e68d8a70 100644 --- a/plugins/text-editor-resources/src/components/node-view/svelte-node-view-renderer.ts +++ b/plugins/text-editor-resources/src/components/node-view/svelte-node-view-renderer.ts @@ -28,6 +28,7 @@ import { type Node } from '@tiptap/pm/model' import { type Decoration, DecorationSet } from '@tiptap/pm/view' import { createNodeViewContext } from './context' import { SvelteRenderer } from './svelte-renderer' +import { type AnyComponent } from '@hcengineering/ui' export interface SvelteNodeViewRendererOptions extends NodeViewRendererOptions { update?: (node: Node, decorations: readonly Decoration[]) => boolean @@ -38,7 +39,7 @@ export interface SvelteNodeViewRendererOptions extends NodeViewRendererOptions { export type SvelteNodeViewProps = NodeViewProps & Record -export type SvelteNodeViewComponent = typeof SvelteComponent | ComponentType +export type SvelteNodeViewComponent = typeof SvelteComponent | ComponentType | AnyComponent /** * Svelte NodeView renderer, inspired by React and Vue implementation by Tiptap diff --git a/plugins/text-editor-resources/src/components/node-view/svelte-renderer.ts b/plugins/text-editor-resources/src/components/node-view/svelte-renderer.ts index 23d933de17..14c2d540b6 100644 --- a/plugins/text-editor-resources/src/components/node-view/svelte-renderer.ts +++ b/plugins/text-editor-resources/src/components/node-view/svelte-renderer.ts @@ -14,9 +14,11 @@ // limitations under the License. // +import { getResource } from '@hcengineering/platform' +import { type AnyComponent } from '@hcengineering/ui' import type { ComponentType, SvelteComponent } from 'svelte' -export type SvelteRendererComponent = typeof SvelteComponent | ComponentType +export type SvelteRendererComponent = typeof SvelteComponent | ComponentType | AnyComponent export interface SvelteRendererOptions { element: HTMLElement @@ -25,9 +27,10 @@ export interface SvelteRendererOptions { } export class SvelteRenderer { - private readonly component: SvelteComponent + private component: SvelteComponent | null element: HTMLElement props: Record + destroyed = false constructor (component: SvelteRendererComponent, { element, props, context }: SvelteRendererOptions) { this.element = element @@ -35,25 +38,39 @@ export class SvelteRenderer { this.props = props ?? {} const options = { target: element, props, context } - const Component = component - this.component = new Component(options) + this.component = null + if (typeof component !== 'string') { + const Component = component + this.component = new Component(options) + } else { + void getResource(component) + .then((resource) => { + if (resource == null || this.destroyed) return + const Component = resource + this.component = new Component({ ...options, props: this.props }) + }) + .catch((error) => { + console.error(`Failed to load Svelte component ${component}`, error) + }) + } } updateProps (props: Record): void { - this.component.$set(props) + this.component?.$set(props) } onKeyDown (props: Record): boolean { - if (this.component.onKeyDown !== undefined) { - return this.component.onKeyDown(props.event) + if (this.component?.onKeyDown !== undefined) { + return this.component?.onKeyDown(props.event) } return false } destroy (): void { - if (this.component.done !== undefined) { - this.component.done() + this.destroyed = true + if (this.component?.done !== undefined) { + this.component?.done() } - this.component.$destroy() + this.component?.$destroy() } } diff --git a/plugins/text-editor-resources/src/index.ts b/plugins/text-editor-resources/src/index.ts index 54bd155c3d..7035b2ce3c 100644 --- a/plugins/text-editor-resources/src/index.ts +++ b/plugins/text-editor-resources/src/index.ts @@ -25,8 +25,6 @@ import { openTableOptions, selectTable } from './components/extension/table/table' -import { formatLink } from './kits/default-kit' -import { isEditable, isHeadingVisible } from './kits/editor-kit' import { convertToEmbedPreviewAction, convertToEmbedPreviewActionIsActive, @@ -37,9 +35,10 @@ import { shouldShowCopyPreviewLinkAction, copyPreviewLinkAction } from './components/extension/embed/embed' -export { TransformPastedContentExtension } from './components/extension/paste' +import { formatLink, isEditable, isHeadingVisible } from './utils' +export { SmartPasteExtension as TransformPastedContentExtension } from './components/extension/shortcuts/smartPaste' export { getReferenceFromUrl, getReferenceLabel, getTargetObjectFromUrl } from './components/extension/reference' -export { TodoItemExtension, TodoListExtension } from './components/extension/todo' +export { TodoItemExtension, TodoListExtension } from './components/extension/todo/todo' export * from '@hcengineering/presentation/src/types' export { default as Collaboration } from './components/Collaboration.svelte' @@ -68,29 +67,36 @@ export * from './utils' export * from './command/deleteAttachment' export { EmojiExtension } from './components/extension/emoji' -export { FocusExtension, type FocusOptions, type FocusStorage } from './components/extension/focus' -export { HeadingsExtension, type HeadingsOptions, type HeadingsStorage } from './components/extension/headings' +export { FocusExtension, type FocusOptions, type FocusStorage } from './components/extension/hooks/focus' +export { + ToCExtension as HeadingsExtension, + type ToCExtensionOptions as HeadingsOptions, + type ToCExtensionStorage as HeadingsStorage +} from './components/extension/toc' export { ImageExtension } from './components/extension/imageExt' -export { ImageUploadExtension, type ImageUploadOptions } from './components/extension/imageUploadExt' +export { + ImageUploadExtension, + type ImageUploadExtensionOptions as ImageUploadOptions +} from './components/extension/shortcuts/imageUpload' export { IsEmptyContentExtension, type IsEmptyContentOptions, type IsEmptyContentStorage -} from './components/extension/isEmptyContent' +} from './components/extension/hooks/isEmptyContent' export { highlightUpdateCommand, - NodeHighlightExtension, - NodeHighlightType, - type NodeHighlightExtensionOptions -} from './components/extension/nodeHighlight' + QMSInlineCommentExtension as NodeHighlightExtension, + CommentHighlightType as NodeHighlightType, + type QMSInlineCommentExtensionOptions as NodeHighlightExtensionOptions +} from './components/extension/qms/qmsInlineComment' export { getNodeElement, - NodeUuidExtension, - nodeUuidName, + QMSInlineCommentMark as NodeUuidExtension, + qmsInlineCommentMarkName as nodeUuidName, selectNode, - type NodeUuidOptions, - type NodeUuidStorage -} from './components/extension/nodeUuid' + type QMSInlineCommentMarkOptions as NodeUuidOptions, + type QMSInlineCommentMarkStorage as NodeUuidStorage +} from './components/extension/qms/qmsInlineCommentMark' export { referenceConfig, ReferenceExtension } from './components/extension/reference' export { type Provider } from './provider/types' export { createTiptapCollaborationData } from './provider/utils' diff --git a/plugins/text-editor-resources/src/kits/default-kit.ts b/plugins/text-editor-resources/src/kits/default-kit.ts deleted file mode 100644 index cd85ed9657..0000000000 --- a/plugins/text-editor-resources/src/kits/default-kit.ts +++ /dev/null @@ -1,96 +0,0 @@ -// -// Copyright © 2023, 2024, 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { codeOptions } from '@hcengineering/text' -import { showPopup } from '@hcengineering/ui' -import { type Editor, Extension } from '@tiptap/core' -import type { CodeOptions } from '@tiptap/extension-code' -import type { CodeBlockOptions } from '@tiptap/extension-code-block' -import type { HardBreakOptions } from '@tiptap/extension-hard-break' -import type { Level } from '@tiptap/extension-heading' -import Highlight from '@tiptap/extension-highlight' -import Link from '@tiptap/extension-link' -import Typography from '@tiptap/extension-typography' -import Underline from '@tiptap/extension-underline' -import StarterKit from '@tiptap/starter-kit' - -import BulletList from '@tiptap/extension-bullet-list' -import ListItem from '@tiptap/extension-list-item' -import OrderedList from '@tiptap/extension-ordered-list' -import LinkPopup from '../components/LinkPopup.svelte' -import { CodeBlockHighlighExtension, codeBlockHighlightOptions } from '../components/extension/codeblock' - -export interface DefaultKitOptions { - codeBlock?: Partial | false - code?: Partial | false - hardBreak?: Partial | false - heading?: { - levels?: Level[] - } - history?: false - dropcursor?: false -} - -export const DefaultKit = Extension.create({ - name: 'defaultKit', - - addExtensions () { - return [ - StarterKit.configure({ - blockquote: { - HTMLAttributes: { - class: 'proseBlockQuote' - } - }, - code: this.options.code ?? codeOptions, - codeBlock: false, - hardBreak: this.options.hardBreak, - heading: this.options.heading, - history: this.options.history, - bulletList: false, - orderedList: false, - dropcursor: this.options.dropcursor - }), - ListItem.extend({ group: 'listItems' }), - BulletList.extend({ content: 'listItems+' }), - OrderedList.extend({ content: 'listItems+' }), - Underline, - Highlight.configure({ - multicolor: false - }), - Typography.configure({}), - Link.extend({ inclusive: false }).configure({ - openOnClick: false, - HTMLAttributes: { class: 'cursor-pointer', rel: 'noopener noreferrer', target: '_blank' } - }), - CodeBlockHighlighExtension.configure(codeBlockHighlightOptions) - ] - } -}) - -export async function formatLink (editor: Editor): Promise { - const link = editor.getAttributes('link').href - - // give editor some time to handle blur event - setTimeout(() => { - showPopup(LinkPopup, { link }, undefined, undefined, (newLink) => { - if (newLink === '') { - editor.chain().focus().extendMarkRange('link').unsetLink().run() - } else { - editor.chain().focus().extendMarkRange('link').setLink({ href: newLink }).run() - } - }) - }) -} diff --git a/plugins/text-editor-resources/src/kits/editor-kit.ts b/plugins/text-editor-resources/src/kits/editor-kit.ts index 59c09cf246..5541fb83dc 100644 --- a/plugins/text-editor-resources/src/kits/editor-kit.ts +++ b/plugins/text-editor-resources/src/kits/editor-kit.ts @@ -14,318 +14,267 @@ // import { type Class, type Doc, type Ref, type Space } from '@hcengineering/core' import { getResource } from '@hcengineering/platform' -import { getBlobRef, getClient } from '@hcengineering/presentation' +import { getClient } from '@hcengineering/presentation' import { - BackgroundColor, CodeExtension, codeOptions, CommentNode, - type ImageOptions, - InlineCommentMark, - MarkdownNode, - TextColor, - TextStyle + CommonKitFactory, + CommonListKitFactory, + extensionKit, + mergeKitOptions, + TextColorStylingKit } from '@hcengineering/text' import textEditor, { type ActionContext, type ExtensionCreator, type TextEditorMode } from '@hcengineering/text-editor' -import { type AnyExtension, type Editor, Extension } from '@tiptap/core' -import { type Level } from '@tiptap/extension-heading' +import { type AnyExtension, Extension } from '@tiptap/core' import TableHeader from '@tiptap/extension-table-header' import 'prosemirror-codemark/dist/codemark.css' -import TextAlign, { type TextAlignOptions } from '@tiptap/extension-text-align' -import { CodeBlockHighlighExtension, codeBlockHighlightOptions } from '../components/extension/codeblock' -import { DrawingBoardExtension, type DrawingBoardOptions } from '../components/extension/drawingBoard' -import { EditableExtension } from '../components/extension/editable' -import { EmbedNode, type EmbedNodeOptions } from '../components/extension/embed/embed' +import Collaboration from '@tiptap/extension-collaboration' +import CollaborationCursor from '@tiptap/extension-collaboration-cursor' +import Placeholder from '@tiptap/extension-placeholder' +import { CodeBlockHighlighExtension, codeBlockHighlightOptions } from '../components/extension/codeSnippets/codeblock' +import { MermaidExtension, mermaidOptions } from '../components/extension/codeSnippets/mermaid' +import { DrawingBoardExtension } from '../components/extension/drawingBoard' +import { EmbedNode } from '../components/extension/embed/embed' import { defaultDriveEmbedOptions, DriveEmbedProvider } from '../components/extension/embed/providers/drive' import { defaultYoutubeEmbedUrlOptions, YoutubeEmbedProvider } from '../components/extension/embed/providers/youtube' -import { FileExtension, type FileOptions } from '../components/extension/fileExt' +import { EmojiExtension } from '../components/extension/emoji' +import { FileExtension } from '../components/extension/fileExt' import { HardBreakExtension } from '../components/extension/hardBreak' +import { EditableExtension } from '../components/extension/hooks/editable' +import { FocusExtension } from '../components/extension/hooks/focus' +import { IsEmptyContentExtension } from '../components/extension/hooks/isEmptyContent' import { ImageExtension } from '../components/extension/imageExt' -import { type IndendOptions, IndentExtension, indentExtensionOptions } from '../components/extension/indent' -import { LinkUtilsExtension } from '../components/extension/link' -import { ListKeymapExtension } from '../components/extension/listkeymap' -import { MermaidExtension, type MermaidOptions, mermaidOptions } from '../components/extension/mermaid' -import { NodeUuidExtension } from '../components/extension/nodeUuid' -import { NoteExtension, type NoteOptions } from '../components/extension/note' -import { ParagraphExtension } from '../components/extension/paragraph' -import { TransformPastedContentExtension } from '../components/extension/paste' -import { SubmitExtension, type SubmitOptions } from '../components/extension/submit' +import { InlineCommandsExtension } from '../components/extension/inlineCommands' +import { InlineCommentCollaborationExtension } from '../components/extension/inlineComment' +import { LeftMenuExtension } from '../components/extension/leftMenu' +import { NoteExtension } from '../components/extension/note' +import { QMSInlineCommentExtension } from '../components/extension/qms/qmsInlineComment' +import { QMSInlineCommentMark } from '../components/extension/qms/qmsInlineCommentMark' +import { referenceConfig, ReferenceExtension } from '../components/extension/reference' +import { FileUploadExtension } from '../components/extension/shortcuts/fileUpload' +import { ImageUploadExtension } from '../components/extension/shortcuts/imageUpload' +import { IndentExtension, indentExtensionOptions } from '../components/extension/shortcuts/indent' +import { LinkKeymapExtension } from '../components/extension/shortcuts/linkKeymap' +import { ParagraphKeymapExtension } from '../components/extension/shortcuts/paragraphKeymap' +import { SmartPasteExtension } from '../components/extension/shortcuts/smartPaste' +import { HandleSubmitExtension } from '../components/extension/shortcuts/handleSubmit' import { Table, TableCell, TableRow } from '../components/extension/table' -import { ToolbarExtension, type ToolbarOptions } from '../components/extension/toolbar/toolbar' -import { DefaultKit, type DefaultKitOptions } from './default-kit' +import { ToCExtension } from '../components/extension/toc' +import { TodoItemExtension, TodoListExtension } from '../components/extension/todo/todo' +import { ToolbarExtension } from '../components/extension/toolbar/toolbar' +import { ListKeymapExtension } from '../components/extension/shortcuts/listKeymap' -export interface EditorKitOptions extends DefaultKitOptions { - history?: false - file?: Partial | false - image?: - | (Partial & { - toolbar?: { - element: HTMLElement - boundary?: HTMLElement - appendTo?: HTMLElement | (() => HTMLElement) - isHidden?: () => boolean - } - }) - | false - drawingBoard?: DrawingBoardOptions | false - mermaid?: MermaidOptions | false - indent?: IndendOptions | false - textAlign?: TextAlignOptions | false +export interface EditorKitContext { mode?: 'full' | 'compact' - note?: NoteOptions | false - submit?: SubmitOptions | false + objectId?: Ref objectClass?: Ref> objectSpace?: Ref - toolbar?: Partial | false - embed?: Partial | false } -const headingLevels: Level[] = [1, 2, 3] +export type EditorKitOptions = EditorKitContext & (typeof StaticEditorKit)['options'] -export const tableKitExtensions: KitExtension[] = [ - [ - 10, - Table.configure({ - resizable: true, - HTMLAttributes: { - class: 'proseTable' - } - }) - ], - [20, TableRow.configure({})], - [30, TableHeader.configure({})], - [40, TableCell.configure({})] -] +const StaticEditorKit = extensionKit( + 'static-kit', + (e, context: EditorKitContext) => + ({ + ...CommonKitFactory(e), -/** - * KitExtensionCreator is a tuple of an index and an ExtensionCreator. - */ -export type KitExtensionCreator = [number, ExtensionCreator] -export type KitExtension = [number, AnyExtension] + shortcuts: e(subKits.shortcuts, context), // needs to be loaded as early as possible to override the default behavior -async function getKitExtensionCreators (): Promise { + // =========================================================================================== + // Extensions and kits with separate / shortened implementations in the server-side editor kit + // See file://./../../../../packages/text/src/kits/server-kit.ts + // ============================================================= + + lists: e(subKits.lists, context), + tables: e(subKits.tables), + codeSnippets: e(subKits.codeSnippets), + textColorStyling: e(TextColorStylingKit, context.mode === 'full'), + hardBreak: e(HardBreakExtension, { shortcuts: context.mode }), + reference: e(ReferenceExtension, referenceConfig), + file: e(FileExtension, { inline: true }), + image: e(ImageExtension), + emoji: e(EmojiExtension), + drawingBoard: e(DrawingBoardExtension), + embed: e( + EmbedNode, + context.mode === 'full' && { + providers: [YoutubeEmbedProvider(defaultYoutubeEmbedUrlOptions), DriveEmbedProvider(defaultDriveEmbedOptions)] + } + ), + inlineNote: e(NoteExtension, context.mode === 'full'), // Semi-deprecated, should be removed in the future + commentNode: e(CommentNode, false), + + // ===================================================== + // Extensions and kits designed for client-side use only + // ===================================================== + + toolbar: e(ToolbarExtension, { + providers: [], + context: getActionContext(context) + }), + toc: e(ToCExtension, false), + leftMenu: e(LeftMenuExtension, false), + inlineCommands: e(InlineCommandsExtension, false), + placeholder: e(Placeholder, false), + + collaboration: e(subKits.collaboration, false), + hooks: e(subKits.hooks), // Semi-deprecated, should be removed in the future + qms: e(subKits.qms, false) // Semi-deprecated, should be removed in the future + }) as const +) + +const subKits = { + lists: extensionKit( + 'list-kit', + (e, context: EditorKitContext) => + ({ + ...CommonListKitFactory(e), + todoItem: e(TodoItemExtension, context.mode === 'full' && { context: getActionContext(context) }), + todoList: e(TodoListExtension, context.mode === 'full') + }) as const + ), + + tables: extensionKit( + 'table-kit', + (e) => + ({ + table: e(Table, { resizable: true, HTMLAttributes: { class: 'proseTable' } }), + tableRow: e(TableRow), + tableHeader: e(TableHeader), + tableCell: e(TableCell) + }) as const + ), + + codeSnippets: extensionKit( + 'code-snippets-kit', + (e) => + ({ + codeBlock: e(CodeBlockHighlighExtension, codeBlockHighlightOptions), + codeInline: e(CodeExtension, codeOptions), + codeBlockMermaid: e(MermaidExtension, mermaidOptions) + }) as const + ), + + collaboration: extensionKit( + 'collaboration-kit', + (e) => + ({ + collaboration: e(Collaboration), + collaborationCursor: e(CollaborationCursor), + inlineComments: e(InlineCommentCollaborationExtension) + }) as const + ), + + shortcuts: extensionKit( + 'shortcuts-kit', + (e, context: EditorKitContext) => + ({ + fileUpload: e(FileUploadExtension, false), + imageUpload: e(ImageUploadExtension, false), + submit: e(HandleSubmitExtension, { useModKey: context.mode === 'full' }), + indent: e(IndentExtension, indentExtensionOptions), + smartPaste: e(SmartPasteExtension), + paragraphKeymap: e(ParagraphKeymapExtension, context.mode === 'compact'), + linkKeymap: e(LinkKeymapExtension), + listKeymap: e(ListKeymapExtension, { + listTypes: [ + { itemName: 'listItem', wrapperNames: ['bulletList', 'orderedList'] }, + { itemName: 'taskItem', wrapperNames: ['taskList'] }, + { itemName: 'todoItem', wrapperNames: ['todoList'] } + ] + }) + }) as const + ), + + // Semi-deprecated, should be removed in the future + hooks: extensionKit( + 'hooks-kit', + (e) => + ({ + emptyContent: e(IsEmptyContentExtension, false), + focus: e(FocusExtension, false), + editable: e(EditableExtension) + }) as const + ), + + // Semi-deprecated, should be removed in the future + qms: extensionKit( + 'qms-kit', + (e) => + ({ + qmsInlineCommentMark: e(QMSInlineCommentMark), + qmsInlineComment: e(QMSInlineCommentExtension) + }) as const + ) +} + +async function getModelKitFactories (): Promise { const client = getClient() const extensionFactories = client.getModel().findAllSync(textEditor.class.TextEditorExtensionFactory, {}) - return await Promise.all( + const factories = await Promise.all( extensionFactories.map(async ({ index, create }) => { - return [index, await getResource(create)] + return [index, await getResource(create)] as const }) ) + + return factories.sort((a, b) => a[0] - b[0]).map(([_, ext]) => ext) +} + +async function buildEditorKit (): Promise> { + const modelKitFactories = await getModelKitFactories() + + return Extension.create({ + name: 'editorKit', + + addExtensions () { + const mode: TextEditorMode = this.options.mode ?? 'full' + const modelKit: AnyExtension[] = modelKitFactories + .map((factory) => + factory(mode, { + objectId: this.options.objectId, + objectClass: this.options.objectClass, + objectSpace: this.options.objectSpace + }) + ) + .filter((e) => e != null) + + const extensions = [StaticEditorKit.configure(this.options), ...modelKit] + return extensions + } + }) } let editorKitPromise: Promise> -export async function getEditorKit (): Promise> { +export async function getEditorKit ( + ...options: Array> +): Promise> { if (editorKitPromise === undefined) { editorKitPromise = buildEditorKit() } - return await editorKitPromise + const kit = await editorKitPromise + if ((options ?? []).length < 1) return kit + + let newOptions: Partial = { + mode: 'full' + } + for (const e of options) { + newOptions = mergeKitOptions(newOptions, e) + } + return kit.configure(newOptions) } -async function buildEditorKit (): Promise> { - return await new Promise>((resolve, reject) => { - getKitExtensionCreators() - .then((kitExtensionCreators) => { - resolve( - Extension.create({ - name: 'editorKit', - - addExtensions () { - const mode: TextEditorMode = this.options.mode ?? 'full' - const modelKitExtensions: KitExtension[] = kitExtensionCreators - .map( - ([idx, createExtension]) => - [ - idx, - createExtension(mode, { - objectId: this.options.objectId, - objectClass: this.options.objectClass, - objectSpace: this.options.objectSpace - }) - ] as KitExtension - ) - .filter(([_, ext]) => ext != null) - - const staticKitExtensions: KitExtension[] = [ - [ - 100, - DefaultKit.configure({ - ...this.options, - code: false, - codeBlock: false, - hardBreak: false, - heading: { - levels: headingLevels - } - }) - ], - [110, EditableExtension], - [150, InlineCommentMark.configure({})], - [200, CodeBlockHighlighExtension.configure(codeBlockHighlightOptions)], - [210, CodeExtension.configure(codeOptions)], - [220, HardBreakExtension.configure({ shortcuts: mode })], - [230, CommentNode], - [ - 240, - MarkdownNode.configure({ - HTMLAttributes: { - class: 'proseCodeBlock' - } - }) - ] - ] - - if (this.options.submit !== false) { - staticKitExtensions.push([ - 300, - SubmitExtension.configure({ - useModKey: mode === 'full', - ...this.options.submit - }) - ]) - } - if (this.options.toolbar !== false) { - staticKitExtensions.push([ - 310, - ToolbarExtension.configure({ - providers: [], - context: { - mode, - objectId: this.options.objectId, - objectClass: this.options.objectClass, - objectSpace: this.options.objectSpace - }, - ...this.options.toolbar - }) - ]) - } - - if (mode === 'compact') { - staticKitExtensions.push([400, ParagraphExtension.configure()]) - } - - if (mode === 'full') { - staticKitExtensions.push([410, TextStyle.configure({})]) - staticKitExtensions.push([420, TextColor.configure({})]) - staticKitExtensions.push([430, BackgroundColor.configure({ types: ['tableCell'] })]) - } - - if (mode === 'full' && this.options.embed !== false) { - staticKitExtensions.push([ - 450, - EmbedNode.configure({ - providers: [ - YoutubeEmbedProvider(defaultYoutubeEmbedUrlOptions), - DriveEmbedProvider(defaultDriveEmbedOptions) - ], - ...this.options.embed - }) - ]) - } - - staticKitExtensions.push([ - 500, - ListKeymapExtension.configure({ - listTypes: [ - { - itemName: 'listItem', - wrapperNames: ['bulletList', 'orderedList'] - }, - { - itemName: 'taskItem', - wrapperNames: ['taskList'] - }, - { - itemName: 'todoItem', - wrapperNames: ['todoList'] - } - ] - }) - ]) - - staticKitExtensions.push([600, NodeUuidExtension]) - - if (this.options.file !== false) { - staticKitExtensions.push([ - 700, - FileExtension.configure({ - inline: true, - ...this.options.file - }) - ]) - } - - if (this.options.image !== false) { - const imageOptions: ImageOptions = { - inline: true, - loadingImgSrc: - 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxzdmcgd2lkdGg9IjMycHgiIGhlaWdodD0iMzJweCIgdmlld0JveD0iMCAwIDE2IDE2IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg0KICAgIDxwYXRoIGQ9Im0gNCAxIGMgLTEuNjQ0NTMxIDAgLTMgMS4zNTU0NjkgLTMgMyB2IDEgaCAxIHYgLTEgYyAwIC0xLjEwOTM3NSAwLjg5MDYyNSAtMiAyIC0yIGggMSB2IC0xIHogbSAyIDAgdiAxIGggNCB2IC0xIHogbSA1IDAgdiAxIGggMSBjIDEuMTA5Mzc1IDAgMiAwLjg5MDYyNSAyIDIgdiAxIGggMSB2IC0xIGMgMCAtMS42NDQ1MzEgLTEuMzU1NDY5IC0zIC0zIC0zIHogbSAtNSA0IGMgLTAuNTUwNzgxIDAgLTEgMC40NDkyMTkgLTEgMSBzIDAuNDQ5MjE5IDEgMSAxIHMgMSAtMC40NDkyMTkgMSAtMSBzIC0wLjQ0OTIxOSAtMSAtMSAtMSB6IG0gLTUgMSB2IDQgaCAxIHYgLTQgeiBtIDEzIDAgdiA0IGggMSB2IC00IHogbSAtNC41IDIgbCAtMiAyIGwgLTEuNSAtMSBsIC0yIDIgdiAwLjUgYyAwIDAuNSAwLjUgMC41IDAuNSAwLjUgaCA3IHMgMC40NzI2NTYgLTAuMDM1MTU2IDAuNSAtMC41IHYgLTEgeiBtIC04LjUgMyB2IDEgYyAwIDEuNjQ0NTMxIDEuMzU1NDY5IDMgMyAzIGggMSB2IC0xIGggLTEgYyAtMS4xMDkzNzUgMCAtMiAtMC44OTA2MjUgLTIgLTIgdiAtMSB6IG0gMTMgMCB2IDEgYyAwIDEuMTA5Mzc1IC0wLjg5MDYyNSAyIC0yIDIgaCAtMSB2IDEgaCAxIGMgMS42NDQ1MzEgMCAzIC0xLjM1NTQ2OSAzIC0zIHYgLTEgeiBtIC04IDMgdiAxIGggNCB2IC0xIHogbSAwIDAiIGZpbGw9IiMyZTM0MzQiIGZpbGwtb3BhY2l0eT0iMC4zNDkwMiIvPg0KPC9zdmc+DQo=', - getBlobRef: async (file, name, size) => await getBlobRef(file, name, size), - HTMLAttributes: this.options.image?.HTMLAttributes ?? {}, - ...this.options.image - } - - staticKitExtensions.push([800, ImageExtension.configure(imageOptions)]) - } - - if (this.options.drawingBoard !== false) { - staticKitExtensions.push([840, DrawingBoardExtension.configure(this.options.drawingBoard)]) - } - - if (this.options.mermaid !== false) { - staticKitExtensions.push([850, MermaidExtension.configure(this.options.mermaid ?? mermaidOptions)]) - } - - if (this.options.indent !== false) { - staticKitExtensions.push([ - 860, - IndentExtension.configure(this.options.indent ?? indentExtensionOptions) - ]) - } - - if (this.options.textAlign !== false) { - staticKitExtensions.push([ - 870, - TextAlign.configure( - this.options.textAlign ?? { - types: ['heading', 'paragraph'], - alignments: ['left', 'center', 'right'], - defaultAlignment: null - } - ) - ]) - } - staticKitExtensions.push([950, LinkUtilsExtension.configure({})]) - - if (mode !== 'compact' && this.options.note !== false) { - staticKitExtensions.push([1000, NoteExtension.configure(this.options.note ?? {})]) - } - - staticKitExtensions.push([1100, TransformPastedContentExtension.configure({})]) - - const allKitExtensions = [...tableKitExtensions, ...modelKitExtensions, ...staticKitExtensions] - - allKitExtensions.sort((a, b) => a[0] - b[0]) - - return allKitExtensions.map(([_, ext]) => ext) - } - }) - ) - }) - .catch((err) => { - reject(err) - }) - }) -} - -export async function isEditable (editor: Editor): Promise { - return editor.isEditable -} - -export async function isHeadingVisible (editor: Editor, ctx: ActionContext): Promise { - return (await isEditable(editor)) && ctx.mode === 'full' +function getActionContext (context: EditorKitContext): ActionContext { + return { + mode: context.mode ?? 'full', + objectId: context.objectId, + objectClass: context.objectClass, + objectSpace: context.objectSpace + } } diff --git a/plugins/text-editor-resources/src/utils.ts b/plugins/text-editor-resources/src/utils.ts index c2674fb556..e01ea34e1d 100644 --- a/plugins/text-editor-resources/src/utils.ts +++ b/plugins/text-editor-resources/src/utils.ts @@ -13,10 +13,12 @@ // limitations under the License. // -import { type Attribute } from '@tiptap/core' -import { showPopup, SelectPopup, type PopupAlignment } from '@hcengineering/ui' +import { SelectPopup, showPopup, type PopupAlignment } from '@hcengineering/ui' +import { type Editor, type Attribute } from '@tiptap/core' +import { type ActionContext } from '@hcengineering/text-editor' import { mInsertTable } from './components/extensions' +import LinkPopup from './components/LinkPopup.svelte' export function getDataAttribute ( name: string, @@ -65,3 +67,25 @@ export async function addTableHandler ( } ) } + +export async function isEditable (editor: Editor): Promise { + return editor.isEditable +} +export async function isHeadingVisible (editor: Editor, ctx: ActionContext): Promise { + return (await isEditable(editor)) && ctx.mode === 'full' +} + +export async function formatLink (editor: Editor): Promise { + const link = editor.getAttributes('link').href + + // give editor some time to handle blur event + setTimeout(() => { + showPopup(LinkPopup, { link }, undefined, undefined, (newLink) => { + if (newLink === '') { + editor.chain().focus().extendMarkRange('link').unsetLink().run() + } else { + editor.chain().focus().extendMarkRange('link').setLink({ href: newLink }).run() + } + }) + }) +} diff --git a/plugins/time-resources/src/index.ts b/plugins/time-resources/src/index.ts index a98efa4553..076dfe8c82 100644 --- a/plugins/time-resources/src/index.ts +++ b/plugins/time-resources/src/index.ts @@ -27,7 +27,7 @@ import EditToDo from './components/EditToDo.svelte' import CreateToDoPopup from './components/CreateToDoPopup.svelte' import NotificationToDoPresenter from './components/NotificationToDoPresenter.svelte' import PriorityEditor from './components/PriorityEditor.svelte' -import { ToDoTitleProvider, createTodoItemExtension, createTodoListExtension } from './utils' +import { ToDoTitleProvider } from './utils' export type ToDosMode = 'unplanned' | 'planned' | 'all' | 'tag' | 'date' @@ -48,8 +48,6 @@ export default async (): Promise => ({ PriorityEditor }, function: { - ToDoTitleProvider, - CreateTodoItemExtension: createTodoItemExtension, - CreateTodoListExtension: createTodoListExtension + ToDoTitleProvider } }) diff --git a/plugins/time-resources/src/plugin.ts b/plugins/time-resources/src/plugin.ts index 0ee78f00a2..8050e66f58 100644 --- a/plugins/time-resources/src/plugin.ts +++ b/plugins/time-resources/src/plugin.ts @@ -60,11 +60,6 @@ export default mergeIds(timeId, time, { MyWork: '' as IntlString, WorkSchedule: '' as IntlString, SummaryDuration: '' as IntlString, - TeamPlanner: '' as IntlString, - - UnassignToDo: '' as IntlString, - UnassignToDoConfirm: '' as IntlString, - ReassignToDo: '' as IntlString, - ReassignToDoConfirm: '' as IntlString + TeamPlanner: '' as IntlString } }) diff --git a/plugins/time-resources/src/utils.ts b/plugins/time-resources/src/utils.ts index c0d75da5b6..20fd80456e 100644 --- a/plugins/time-resources/src/utils.ts +++ b/plugins/time-resources/src/utils.ts @@ -1,14 +1,10 @@ -import type { WorkSlot, ToDo } from '@hcengineering/time' -import type { DefSeparators } from '@hcengineering/ui' -import core, { getCurrentAccount, type Class, type Client, type Doc, type Ref } from '@hcengineering/core' -import time from '@hcengineering/time' -import { type TextEditorMode, type AnyExtension } from '@hcengineering/text-editor' -import { SvelteNodeViewRenderer, TodoItemExtension, TodoListExtension } from '@hcengineering/text-editor-resources' +import { getCurrentAccount, type Client, type Ref } from '@hcengineering/core' import { getClient } from '@hcengineering/presentation' +import type { ToDo, WorkSlot } from '@hcengineering/time' +import time from '@hcengineering/time' +import type { DefSeparators } from '@hcengineering/ui' -import ToDoItemNodeView from './components/text-editor/node-view/ToDoItemNodeView.svelte' -import ToDoListNodeView from './components/text-editor/node-view/ToDoListNodeView.svelte' -import calendarPlugin, { AccessLevel, type Calendar, getPrimaryCalendar } from '@hcengineering/calendar' +import calendarPlugin, { AccessLevel, getPrimaryCalendar, type Calendar } from '@hcengineering/calendar' export * from './types' @@ -44,61 +40,6 @@ export async function ToDoTitleProvider (client: Client, ref: Ref, doc?: T return object.title } -function isTodoableClass (objectClass: Ref>): boolean { - const hierarchy = getClient().getHierarchy() - - try { - const todosCollection = hierarchy.getAttribute(objectClass, 'todos') - - return todosCollection !== undefined && todosCollection.type._class === core.class.Collection - } catch (e) { - return false - } -} - -function isTodoable (mode: TextEditorMode): boolean { - return mode === 'full' -} - -export function createTodoItemExtension (mode: TextEditorMode, ctx: any): AnyExtension | undefined { - if (!isTodoable(mode)) { - return - } - - const { objectId, objectClass, objectSpace } = ctx - const componentProps = isTodoableClass(objectClass) ? { objectId, objectClass, objectSpace } : {} - - return TodoItemExtension.extend({ - addNodeView () { - return SvelteNodeViewRenderer(ToDoItemNodeView, { - contentAs: 'li', - contentClass: 'todo-item', - componentProps - }) - } - }).configure({ - HTMLAttributes: { - class: 'todo-item' - } - }) -} - -export function createTodoListExtension (mode: TextEditorMode, ctx: any): AnyExtension | undefined { - if (!isTodoable(mode)) { - return - } - - return TodoListExtension.extend({ - addNodeView () { - return SvelteNodeViewRenderer(ToDoListNodeView, {}) - } - }).configure({ - HTMLAttributes: { - class: 'todo-list' - } - }) -} - export function calculateEventsDuration (events: WorkSlot[]): number { const points = events.flatMap((event) => [ { time: event.date, type: 'start' }, diff --git a/plugins/time/src/index.ts b/plugins/time/src/index.ts index 76d6f01419..ef4f6c8dcd 100644 --- a/plugins/time/src/index.ts +++ b/plugins/time/src/index.ts @@ -161,6 +161,10 @@ export default plugin(timeId, { AddToDo: '' as IntlString, NewToDoDetails: '' as IntlString, ToDo: '' as IntlString, - NewToDo: '' as IntlString + NewToDo: '' as IntlString, + UnassignToDo: '' as IntlString, + UnassignToDoConfirm: '' as IntlString, + ReassignToDo: '' as IntlString, + ReassignToDoConfirm: '' as IntlString } }) diff --git a/plugins/tracker-resources/src/components/CreateIssue.svelte b/plugins/tracker-resources/src/components/CreateIssue.svelte index 73d0f0c5e2..f3e113f663 100644 --- a/plugins/tracker-resources/src/components/CreateIssue.svelte +++ b/plugins/tracker-resources/src/components/CreateIssue.svelte @@ -864,7 +864,7 @@ showButtons={false} kind={'indented'} isScrollable={false} - enableBackReferences={true} + kitOptions={{ reference: true }} enableAttachments={false} bind:content={object.description} placeholder={tracker.string.IssueDescriptionPlaceholder} diff --git a/plugins/training-resources/src/components/TrainingPanelOverview.svelte b/plugins/training-resources/src/components/TrainingPanelOverview.svelte index 918215f45e..d6226a2fd7 100644 --- a/plugins/training-resources/src/components/TrainingPanelOverview.svelte +++ b/plugins/training-resources/src/components/TrainingPanelOverview.svelte @@ -50,7 +50,7 @@ showButtons={false} content={object.description} placeholder={core.string.Description} - enableBackReferences + kitOptions={{ reference: true }} on:blur={onDescriptionChange} /> {:else} diff --git a/plugins/view-resources/src/index.ts b/plugins/view-resources/src/index.ts index 6ac4c01974..4af22c0635 100644 --- a/plugins/view-resources/src/index.ts +++ b/plugins/view-resources/src/index.ts @@ -148,6 +148,7 @@ import { canLeaveSpace, isClipboardAvailable } from './visibilityTester' +import { openDocFromRef } from './utils' export { canArchiveSpace, canDeleteObject, canDeleteSpace, canEditSpace } from './visibilityTester' export { getActions, getContextActions, invokeAction, showMenu } from './actions' export { default as ActionButton } from './components/ActionButton.svelte' @@ -370,6 +371,7 @@ export default async (): Promise => ({ CanLeaveSpace: canLeaveSpace, IsClipboardAvailable: isClipboardAvailable, BlobImageMetadata: blobImageMetadata, - BlobVideoMetadata: blobVideoMetadata + BlobVideoMetadata: blobVideoMetadata, + OpenDocument: openDocFromRef } }) diff --git a/plugins/view-resources/src/utils.ts b/plugins/view-resources/src/utils.ts index debce92fd9..de0ec50b96 100644 --- a/plugins/view-resources/src/utils.ts +++ b/plugins/view-resources/src/utils.ts @@ -1206,6 +1206,16 @@ export async function openDoc (hierarchy: Hierarchy, object: Doc): Promise navigate(loc) } +export async function openDocFromRef (_class: Ref>, _id: Ref): Promise { + const client = getClient() + const doc = await client.findOne(_class, { _id }) + if (doc?._id !== undefined) { + await openDoc(client.getHierarchy(), doc) + return true + } + return false +} + /** * @public */ diff --git a/plugins/view/src/index.ts b/plugins/view/src/index.ts index d0503971b3..db1ff8beb6 100644 --- a/plugins/view/src/index.ts +++ b/plugins/view/src/index.ts @@ -63,7 +63,8 @@ import { ViewletDescriptor, ViewletPreference, LinkIdProvider, - CustomObjectLinkProvider + CustomObjectLinkProvider, + OpenDocumentFunction } from './types' export * from './types' @@ -325,6 +326,9 @@ const view = plugin(viewId, { popup: { PositionElementAlignment: '' as Resource<(e?: Event) => PopupAlignment | undefined> }, + function: { + OpenDocument: '' as Resource + }, actionImpl: { CopyTextToClipboard: '' as ViewAction<{ textProvider: Resource<(doc: Doc, props: Record) => Promise> diff --git a/plugins/view/src/types.ts b/plugins/view/src/types.ts index cc52141054..ce7a35c9c0 100644 --- a/plugins/view/src/types.ts +++ b/plugins/view/src/types.ts @@ -486,6 +486,11 @@ export type ViewActionFunction> = ( */ export type ViewActionAvailabilityFunction = (doc: T | T[] | undefined) => Promise +/** + * @public + */ +export type OpenDocumentFunction = (_class: Ref>, _id: Ref) => Promise + /** * @public */ diff --git a/services/github/pod-github/src/markdown/extensions.ts b/services/github/pod-github/src/markdown/extensions.ts index 3a48f27ca1..82244301d2 100644 --- a/services/github/pod-github/src/markdown/extensions.ts +++ b/services/github/pod-github/src/markdown/extensions.ts @@ -1,6 +1,6 @@ -import { ServerKit } from '@hcengineering/text' +import { extensionKit, ServerKit } from '@hcengineering/text' -import { AnyExtension, Extension, Node, mergeAttributes } from '@tiptap/core' +import { AnyExtension, mergeAttributes, Node } from '@tiptap/core' export interface SubLinkOptions { HTMLAttributes: Record @@ -55,23 +55,17 @@ export const SubLink = Node.create({ } }) -export interface GithubKitOptions { - sub?: Partial | false -} - -export const GithubKit = Extension.create({ - name: 'githubKit', - - addExtensions () { - return [ - ServerKit.configure({ +export const GithubKit = extensionKit( + 'github', + (e) => + ({ + serverKit: e(ServerKit, { image: { getBlobRef: async () => ({ src: '', srcset: '' }) } }), - ...(this.options.sub !== false ? [SubLink.configure({ ...this.options.sub })] : []) - ] - } -}) + sub: e(SubLink) + }) as const +) -export const defaultExtensions: AnyExtension[] = [GithubKit.configure({})] +export const defaultExtensions: AnyExtension[] = [GithubKit] diff --git a/tests/sanity/tests/model/documents/document-content-page.ts b/tests/sanity/tests/model/documents/document-content-page.ts index da8de4c4e8..e279dda5ae 100644 --- a/tests/sanity/tests/model/documents/document-content-page.ts +++ b/tests/sanity/tests/model/documents/document-content-page.ts @@ -261,7 +261,7 @@ export class DocumentContentPage extends CommonPage { } async changeCodeBlockLanguage (text: string, oldLang: string, lang: string): Promise { - await this.codeBlock(text).locator('button.antiButton', { hasText: oldLang }).nth(1).click() + await this.codeBlock(text).locator('button.antiButton', { hasText: oldLang }).click() await this.selectMenuItem(this.page, lang, true) }