Simplified editor plugin configuration management (#9485)

Signed-off-by: Victor Ilyushchenko <alt13ri@gmail.com>
This commit is contained in:
Victor Ilyushchenko
2025-07-08 17:30:27 +07:00
committed by GitHub
parent 7b921eeeac
commit 99feb2e894
84 changed files with 1321 additions and 1330 deletions
+40 -3
View File
@@ -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))
-1
View File
@@ -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,
+12 -23
View File
@@ -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'
+4 -7
View File
@@ -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<NotificationType>
},
function: {
ToDoTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>,
CreateTodoItemExtension: '' as Resource<ExtensionCreator>,
CreateTodoListExtension: '' as Resource<ExtensionCreator>
ToDoTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>
}
})
+10
View File
@@ -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",
+3 -2
View File
@@ -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'
+89
View File
@@ -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 = <T extends AnyExtension = AnyExtension>(
extension: T,
options?: Partial<T['options']> | boolean
) => ExtensionSpec<T>
export interface ExtensionSpec<T extends AnyExtension = AnyExtension> {
extension: T
options: Partial<T['options']> | boolean
}
export type ExtensionSpecOptions<T> = {
[K in keyof T]: T[K] extends ExtensionSpec<infer E> ? Partial<E['options']> | boolean : never
}
export function extensionKit<O, K> (
name: string,
fn: (e: ExtensionFactory, o: O) => K
): Extension<O & ExtensionSpecOptions<K>> {
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<AnyExtension>
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<T extends Record<string, any>> (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
}
+135
View File
@@ -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
)
-73
View File
@@ -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<CodeBlockOptions> | false
code?: Partial<CodeOptions> | false
heading?: {
levels?: Level[]
}
history?: false
}
export const DefaultKit = Extension.create<DefaultKitOptions>({
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' }
})
]
}
})
+29 -93
View File
@@ -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<FileOptions> | false
image: Partial<ImageOptions> | false
}
lists: e(ListKit),
codeSnippets: e(CodeSnippetsKit),
tables: e(TableKit),
textColorStyling: e(TextColorStylingKit),
export const ServerKit = Extension.create<ServerKitOptions>({
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)
@@ -5,7 +5,7 @@ const NAME = 'node-uuid'
/**
* @public
*/
export const NodeUuid = Mark.create({
export const QMSInlineCommentMark = Mark.create({
name: NAME,
inline: true,
+43
View File
@@ -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'
@@ -13,11 +13,11 @@
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { Doc, Ref, Space, Class } from '@hcengineering/core'
import { Button, Label, IconDownOutline, tooltip } from '@hcengineering/ui'
import { Class, Doc, Ref, Space } from '@hcengineering/core'
import textEditorPlugin from '@hcengineering/text-editor'
import { IsEmptyContentExtension, TextEditor } from '@hcengineering/text-editor-resources'
import { TextEditor } from '@hcengineering/text-editor-resources'
import { Button, IconDownOutline, Label, tooltip } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import type { AccordionItem } from '..'
import AttachmentStyledBox from './AttachmentStyledBox.svelte'
@@ -95,7 +95,13 @@
<TextEditor
bind:content={item.content}
bind:this={edits[i]}
extensions={[IsEmptyContentExtension.configure({ onChange: (value) => (isEmpty[i] = value) })]}
kitOptions={{
hooks: {
emptyContent: {
onChange: (a) => (isEmpty[i] = a)
}
}
}}
on:value={(ev) => {
dispatch('update', { item, value: ev.detail })
}}
@@ -493,8 +493,6 @@
loading={loading || progress}
{boundary}
{docClass}
canEmbedFiles={false}
canEmbedImages={false}
extraActions={[
...extraActions,
{
@@ -516,6 +514,10 @@
on:update={onUpdate}
onPaste={pasteAction}
{placeholder}
kitOptions={{
file: false,
image: false
}}
>
<div slot="header">
{#if attachments.size > 0 || progress}
@@ -37,7 +37,7 @@
} from '@hcengineering/text-editor-resources'
import { AnySvelteComponent, getEventPositionElement, getPopupPositionElement } from '@hcengineering/ui'
import { uploadFiles, type FileUploadCallbackParams } from '@hcengineering/uploader'
import { getCollaborationUser, getObjectId, openDoc } from '@hcengineering/view-resources'
import { getCollaborationUser, getObjectId } from '@hcengineering/view-resources'
import AttachmentsGrid from './AttachmentsGrid.svelte'
@@ -320,12 +320,6 @@
{refActions}
{readonly}
{attachFile}
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:focus
on:blur
on:update
@@ -17,9 +17,9 @@
import { IntlString } from '@hcengineering/platform'
import { createQuery, getAttribute, getClient, KeyedAttribute } from '@hcengineering/presentation'
import { openDoc } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, onDestroy } from 'svelte'
import AttachmentStyledBox from './AttachmentStyledBox.svelte'
import { EditorKitOptions } from '@hcengineering/text-editor-resources'
export let object: Doc
export let key: KeyedAttribute
@@ -109,12 +109,16 @@
export function setEditable (editable: boolean): void {
descriptionBox.setEditable(editable)
}
onDestroy(() => {
void save(object, description)
})
</script>
{#key object?._id}
<AttachmentStyledBox
{focusIndex}
enableBackReferences={true}
kitOptions={{ reference: true }}
bind:this={descriptionBox}
useAttachmentPreview={false}
isScrollable={false}
@@ -131,12 +135,5 @@
bind:content={description}
{placeholder}
{boundary}
on:open-document={async (event) => {
save(object, description)
const doc = await client.findOne(event.detail._class, { _id: event.detail._id })
if (doc != null) {
await openDoc(client.getHierarchy(), doc)
}
}}
/>
{/key}
@@ -15,17 +15,17 @@
<script lang="ts">
import { Attachment } from '@hcengineering/attachment'
import {
type Blob,
BlobMetadata,
Class,
Doc,
generateId,
Markup,
PersonId,
Ref,
Space,
toIdMap,
type Blob,
TxOperations,
PersonId,
BlobMetadata
TxOperations
} from '@hcengineering/core'
import { IntlString, setPlatformStatus, unknownError } from '@hcengineering/platform'
import {
@@ -40,7 +40,7 @@
} from '@hcengineering/presentation'
import { EmptyMarkup } from '@hcengineering/text'
import textEditor, { type RefAction } from '@hcengineering/text-editor'
import { AttachIcon, StyledTextBox } from '@hcengineering/text-editor-resources'
import { AttachIcon, EditorKitOptions, StyledTextBox } from '@hcengineering/text-editor-resources'
import { ButtonSize } from '@hcengineering/ui'
import { type FileUploadCallbackParams, uploadFiles } from '@hcengineering/uploader'
import { createEventDispatcher, onDestroy } from 'svelte'
@@ -64,8 +64,8 @@
export let useAttachmentPreview = false
export let focusIndex: number | undefined = -1
export let enableAttachments: boolean = true
export let enableBackReferences: boolean = false
export let isScrollable = true
export let kitOptions: Partial<EditorKitOptions> = {}
export let useDirectAttachDelete = false
export let boundary: HTMLElement | undefined = undefined
@@ -426,7 +426,7 @@
{maxHeight}
{focusable}
{kind}
{enableBackReferences}
{kitOptions}
{isScrollable}
{boundary}
{extraActions}
@@ -434,7 +434,6 @@
on:changeContent
on:blur
on:focus
on:open-document
{attachFile}
/>
{#if attachments.size > 0 && enableAttachments}
@@ -19,10 +19,10 @@
import contact from '@hcengineering/contact'
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: Card
@@ -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(card.class.Card, '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}
/>
@@ -20,7 +20,6 @@
import { getClient } from '@hcengineering/presentation'
import { Heading } from '@hcengineering/text-editor'
import { TableOfContents } from '@hcengineering/text-editor-resources'
import { openDoc } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import ContentEditor from './ContentEditor.svelte'
@@ -102,12 +101,6 @@
headings = evt.detail
dispatch('headings', headings)
}}
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)
}
}}
bind:this={editor}
on:loaded
/>
@@ -11,21 +11,15 @@
<!-- See the License for the specific language governing permissions and -->
<!-- limitations under the License. -->
<script lang="ts">
import { Markup, type Ref, type Blob } from '@hcengineering/core'
import { Markup, type Blob, type Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { EmptyMarkup, isEmptyMarkup, areEqualMarkups } from '@hcengineering/text'
import { areEqualMarkups, EmptyMarkup, isEmptyMarkup } from '@hcengineering/text'
import { TextEditorHandler } from '@hcengineering/text-editor'
import { Button, ButtonIcon, handler, IconComponent, registerFocus, IconSend } from '@hcengineering/ui'
import { TextEditor } from '@hcengineering/text-editor-resources'
import { Button, ButtonIcon, handler, IconComponent, IconSend, registerFocus } from '@hcengineering/ui'
import { FocusPosition } from '@tiptap/core'
import { createEventDispatcher } from 'svelte'
import { EditorView } from '@tiptap/pm/view'
import {
EmojiExtension,
IsEmptyContentExtension,
referenceConfig,
ReferenceExtension,
TextEditor
} from '@hcengineering/text-editor-resources'
import { createEventDispatcher } from 'svelte'
import communication from '../plugin'
import { TextInputAction } from '../types'
@@ -129,12 +123,6 @@
focusManager?.setFocus(idx)
}
}
const completionPlugin = ReferenceExtension.configure({
...referenceConfig,
showDoc (event: MouseEvent, _id: string, _class: string) {
dispatch('open-document', { event, _id, _class })
}
})
</script>
<div class="text-input">
@@ -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
/>
</div>
@@ -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
+2 -1
View File
@@ -396,7 +396,8 @@ export default async (): Promise<Resources> => ({
ChannelIcon,
SpaceMembersEditor,
ContactNamePresenter,
PersonIdFilter
PersonIdFilter,
AssigneePopup
},
completion: {
EmployeeQuery: async (
+3 -1
View File
@@ -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<ChannelProvider>,
@@ -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)
}}
@@ -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) {
@@ -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}
/>
@@ -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
}}
@@ -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}
@@ -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
@@ -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
@@ -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
+3 -1
View File
@@ -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"
}
}
@@ -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)
@@ -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
})
]
</script>
<CollaborativeTextEditor
@@ -93,18 +85,26 @@
{user}
{userComponent}
{refActions}
{extensions}
{attachFile}
{placeholder}
{boundary}
{readonly}
canEmbedFiles={false}
withSideMenu={false}
kitOptions={{
note: false
inlineNote: false,
hooks: {
focus: {
onCanBlur: (value) => {
canBlur = value
},
onFocus: handleFocus
}
},
shortcuts: {
fileUpload: false
}
}}
on:focus
on:blur
on:update
on:open-document
/>
@@ -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<string, string> = {}
@@ -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<EditorKitOptions> = {}
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'
},
@@ -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<string, string> = {}
export let onExtensions: () => AnyExtension[] = () => []
export let boundary: HTMLElement | undefined = undefined
export let attachFile: FileAttachFunction | undefined = undefined
export let canShowPopups = true
export let kitOptions: Partial<EditorKitOptions> = {}
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}
@@ -14,14 +14,15 @@
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { Markup } from '@hcengineering/core'
import { IntlString, Asset } from '@hcengineering/platform'
import { Asset, IntlString } from '@hcengineering/platform'
import { EmptyMarkup } from '@hcengineering/text'
import { Label, Icon } from '@hcengineering/ui'
import type { AnySvelteComponent } from '@hcengineering/ui'
import textEditor from '@hcengineering/text-editor'
import type { AnySvelteComponent } from '@hcengineering/ui'
import { Icon, Label } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { EditorKitOptions } from '../kits/editor-kit'
import StyledTextBox from './StyledTextBox.svelte'
import IconDescription from './icons/Description.svelte'
@@ -29,7 +30,7 @@
export let icon: Asset | AnySvelteComponent = IconDescription
export let content: Markup = EmptyMarkup
export let maxHeight: string = '40vh'
export let enableBackReferences = false
export let kitOptions: Partial<EditorKitOptions> = {}
const dispatch = createEventDispatcher()
@@ -56,6 +57,6 @@
hideExtraButtons
{maxHeight}
on:value={checkValue}
{enableBackReferences}
{kitOptions}
/>
</div>
@@ -16,15 +16,15 @@
-->
<script lang="ts">
import { Class, Doc, Ref } from '@hcengineering/core'
import { MarkupNode, ReferenceNode, CommentNode, jsonToPmNode } from '@hcengineering/text'
import { jsonToPmNode, MarkupNode } from '@hcengineering/text'
import { Editor, Extension, mergeAttributes } from '@tiptap/core'
import { Plugin, PluginKey } from '@tiptap/pm/state'
import { DecorationSet } from '@tiptap/pm/view'
import { onDestroy, onMount } from 'svelte'
import { getEditorKit } from '../../src/kits/editor-kit'
import { calculateDecorations } from './diff/decorations'
import { defaultEditorAttributes } from './editor/editorProps'
import { getEditorKit } from '../../src/kits/editor-kit'
export let content: MarkupNode
export let comparedVersion: MarkupNode | undefined = undefined
@@ -55,6 +55,7 @@
}
}
// TODO: should be implemented as regular plugin
const DecorationExtension = Extension.create({
addProseMirrorPlugins () {
return [
@@ -76,19 +77,17 @@
}
onMount(async () => {
const kit = await getEditorKit({
objectClass,
commentNode: true
})
editor = new Editor({
editorProps: { attributes: mergeAttributes(defaultEditorAttributes, { class: 'flex-grow' }) },
element,
content,
editable: false,
extensions: [
(await getEditorKit()).configure({
objectClass
}),
ReferenceNode,
DecorationExtension,
CommentNode
],
extensions: [kit, DecorationExtension],
onTransaction: () => {
// force re-render so `editor.isActive` works as expected
editor = editor
@@ -13,9 +13,9 @@
// limitations under the License.
-->
<script lang="ts">
import { Class, Doc, Markup, Ref, Blob } from '@hcengineering/core'
import { Blob, Class, Doc, Markup, Ref } from '@hcengineering/core'
import { Asset, IntlString } from '@hcengineering/platform'
import { EmptyMarkup, isEmptyMarkup } from '@hcengineering/text'
import { EmptyMarkup, isEmptyMarkup, mergeKitOptions } from '@hcengineering/text'
import textEditor, { RefAction, TextEditorHandler } from '@hcengineering/text-editor'
import {
AnySvelteComponent,
@@ -34,10 +34,8 @@
import view from '@hcengineering/view'
import TextEditor from './TextEditor.svelte'
import { defaultRefActions, getModelRefActions } from './editor/actions'
import { EmojiExtension } from './extension/emoji'
import { IsEmptyContentExtension } from './extension/isEmptyContent'
import { referenceConfig, ReferenceExtension } from './extension/reference'
import Send from './icons/Send.svelte'
import { EditorKitOptions } from '../kits/editor-kit'
export let content: Markup = EmptyMarkup
export let showHeader = false
@@ -57,11 +55,10 @@
export let noborder: boolean = false
export let boundary: HTMLElement | undefined = undefined
export let autofocus: FocusPosition = false
export let canEmbedFiles = true
export let canEmbedImages = true
export let onPaste: ((view: EditorView, event: ClipboardEvent) => boolean) | undefined = undefined
export let onCancel: (() => void) | undefined = undefined
export let docClass: Ref<Class<Doc>> | undefined = undefined
export let kitOptions: Partial<EditorKitOptions> = {}
const dispatch = createEventDispatcher()
const buttonSize = 'medium'
@@ -150,14 +147,6 @@
focusManager?.setFocus(idx)
}
}
const completionPlugin = ReferenceExtension.configure({
...referenceConfig,
docClass,
multipleMentions: true,
showDoc (event: MouseEvent, _id: string, _class: string) {
dispatch('open-document', { event, _id, _class })
}
})
</script>
<div class="ref-container" class:focusable class:noborder>
@@ -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}
/>
</div>
@@ -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()
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
@@ -92,7 +71,6 @@
{buttonSize}
{maxHeight}
{isScrollable}
{extensions}
bind:content={rawValue}
bind:this={editor}
on:blur={() => {
@@ -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<EditorKitOptions> = {}
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<void> {
switch (id) {
case 'image':
@@ -281,8 +237,6 @@
}
inputImage.value = ''
}
const extensions = configureExtensions()
</script>
<input
@@ -324,9 +278,27 @@
{focusable}
{autofocus}
{isScrollable}
{extensions}
{extraActions}
{boundary}
kitOptions={{
emoji: true,
hooks: {
focus: {
onCanBlur: (value) => (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) => {
@@ -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<string, string> = {}
export let extraActions: RefAction[] = []
export let boundary: HTMLElement | undefined = undefined
export let kitOptions: Partial<EditorKitOptions> = {}
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}
/>
</Scroller>
@@ -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}
/>
@@ -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<string, any> = {}
export let extensions: AnyExtension[] = []
export let supportSubmit = true
export let editorAttributes: Record<string, string> = {}
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<EditorKitOptions> = {}
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)
}
})
})
@@ -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<typeof createLowlight>
@@ -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'
@@ -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<EditableOptions, EditableStorage>({
name: 'editable',
name: 'isEditable',
addStorage () {
return { isEditable: undefined }
@@ -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 => {
@@ -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 {
@@ -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<ImageOptions>({
addOptions () {
return {
inline: true,
loadingImgSrc: _loadingImageSrc,
HTMLAttributes: {},
getBlobRef: async () => ({ src: '', srcset: '' })
getBlobRef
}
},
@@ -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
@@ -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<string, any> | undefined => {
const generateAttributes = (
uuid: string,
options: QMSInlineCommentExtensionOptions
): Record<string, any> | 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<ReturnType> {
[NodeHighlight]: {
export interface QMSInlineCommentCommands<ReturnType> {
[ExtensionName]: {
/**
* Force all nodes to be re-rendered
*/
@@ -64,15 +85,12 @@ export interface NodeHighlightCommands<ReturnType> {
}
declare module '@tiptap/core' {
interface Commands<ReturnType> extends NodeHighlightCommands<ReturnType> {}
interface Commands<ReturnType> extends QMSInlineCommentCommands<ReturnType> {}
}
/**
* Extension allows to highlight nodes based on uuid
*/
export const NodeHighlightExtension: Extension<NodeHighlightExtensionOptions> =
Extension.create<NodeHighlightExtensionOptions>({
name: NodeHighlight,
export const QMSInlineCommentExtension: Extension<QMSInlineCommentExtensionOptions> =
Extension.create<QMSInlineCommentExtensionOptions>({
name: ExtensionName,
addProseMirrorPlugins () {
const options = this.options
@@ -80,7 +98,7 @@ export const NodeHighlightExtension: Extension<NodeHighlightExtensionOptions> =
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<NodeHighlightExtensionOptions> =
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<NodeHighlightExtensionOptions> =
}),
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<NodeHighlightExtensionOptions> =
},
addCommands () {
const result: NodeHighlightCommands<Command>[typeof NodeHighlight] = {
const result: QMSInlineCommentCommands<Command>[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<NodeHighlightExtensionOptions> =
},
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<NodeHighlightExtensionOptions> =
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
}
@@ -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<string, any>
onNodeSelected?: (uuid: string | null) => void
onNodeClicked?: (uuid: string) => void
@@ -21,24 +36,18 @@ export interface NodeUuidOptions {
declare module '@tiptap/core' {
interface Commands<ReturnType> {
[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<NodeUuidOptions, NodeUuidStorage>({
name: nodeUuidName,
export const QMSInlineCommentMark = Mark.create<QMSInlineCommentMarkOptions, QMSInlineCommentMarkStorage>({
name: qmsInlineCommentMarkName,
exitable: true,
inclusive: false,
addOptions () {
@@ -127,9 +132,9 @@ export const NodeUuidExtension = Mark.create<NodeUuidOptions, NodeUuidStorage>({
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<NodeUuidOptions, NodeUuidStorage>({
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<NodeUuidOptions, NodeUuidStorage>({
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<NodeUuidOptions, NodeUuidStorage>({
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<NodeUuidOptions, NodeUuidStorage>({
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)
}
}
})
@@ -34,7 +34,7 @@ export interface ReferenceExtensionOptions extends ReferenceOptions {
suggestion: Omit<SuggestionOptions, 'editor'>
docClass?: Ref<Class<Doc>>
multipleMentions?: boolean
showDoc?: (event: MouseEvent, _id: string, _class: string) => void
openDocument?: (_class: Ref<Class<Doc>>, _id: Ref<Doc>, event?: MouseEvent) => void | Promise<void>
}
export const ReferenceExtension = ReferenceNode.extend<ReferenceExtensionOptions>({
@@ -117,7 +117,7 @@ export const ReferenceExtension = ReferenceNode.extend<ReferenceExtensionOptions
const _class = objectclass
const _id = id
if (_id != null && _class != null) {
this.options.showDoc?.(event, _id, _class)
void this.options.openDocument?.(_class, _id, event)
}
})
@@ -300,6 +300,10 @@ export const referenceConfig: Partial<ReferenceExtensionOptions> = {
}
}
}
},
openDocument: async (_class, _id, event) => {
const openDocument = await getResource(view.function.OpenDocument)
await openDocument?.(_class, _id)
}
}
@@ -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<FileUploadOptions>({
export const FileUploadExtension = Extension.create<FileUploadExtensionOptions>({
name: 'file-upload-ext',
addProseMirrorPlugins () {
@@ -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<SubmitOptions>({
name: 'submit',
export const HandleSubmitExtension = Extension.create<HandleSubmitExtensionOptions>({
name: 'handleSubmit',
addKeyboardShortcuts () {
const shortcuts: Record<string, KeyboardShortcutCommand> = {
Space: () => {
@@ -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<Blob>) => string
}
@@ -40,7 +40,7 @@ function getType (type: string): 'image' | 'other' {
/**
* @public
*/
export const ImageUploadExtension = Extension.create<ImageUploadOptions>({
export const ImageUploadExtension = Extension.create<ImageUploadExtensionOptions>({
name: 'image-upload',
addOptions () {
@@ -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<any>({
export const LinkKeymapExtension = Extension.create<any>({
name: 'linkUtils',
addKeyboardShortcuts () {
@@ -48,11 +48,11 @@ export const LinkUtilsExtension = Extension.create<any>({
}
})
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: {
@@ -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
@@ -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': () => {
@@ -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 () {
@@ -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<HeadingsOptions, HeadingsStorage> = Extension.create<HeadingsOptions>({
export const ToCExtension: Extension<ToCExtensionOptions, ToCExtensionStorage> = Extension.create<ToCExtensionOptions>({
name: 'headings-extension',
addStorage () {
@@ -1,15 +1,15 @@
<script lang="ts">
import { Employee, Person } from '@hcengineering/contact'
import { AssigneePopup, EmployeePresenter } from '@hcengineering/contact-resources'
import contact, { Employee, Person } from '@hcengineering/contact'
import { type Class, type Doc, type Ref, type Space, SortingOrder } from '@hcengineering/core'
import { MessageBox, createQuery, getClient } from '@hcengineering/presentation'
import { makeRank } from '@hcengineering/rank'
import { NodeViewContent, NodeViewProps, NodeViewWrapper } from '@hcengineering/text-editor-resources'
import time, { ToDo, ToDoPriority } from '@hcengineering/time'
import { CheckBox, getEventPositionElement, showPopup } from '@hcengineering/ui'
import { onDestroy, onMount } from 'svelte'
import timeRes from '../../../plugin'
import { NodeViewProps } from '../../node-view'
import NodeViewWrapper from '../../node-view/NodeViewWrapper.svelte'
import NodeViewContent from '../../node-view/NodeViewContent.svelte'
import Component from '@hcengineering/ui/src/components/Component.svelte'
export let node: NodeViewProps['node']
export let editor: NodeViewProps['editor']
@@ -148,8 +148,8 @@
showPopup(
MessageBox,
{
label: timeRes.string.ReassignToDo,
message: timeRes.string.ReassignToDoConfirm,
label: time.string.ReassignToDo,
message: time.string.ReassignToDoConfirm,
action: async () => {
await assignTodo(user)
}
@@ -162,8 +162,8 @@
showPopup(
MessageBox,
{
label: timeRes.string.UnassignToDo,
message: timeRes.string.UnassignToDoConfirm,
label: time.string.UnassignToDo,
message: time.string.UnassignToDoConfirm,
action: async () => {
await unassignTodo()
}
@@ -189,7 +189,7 @@
hovered = true
showPopup(
AssigneePopup,
contact.component.AssigneePopup,
{
selected: userId
},
@@ -215,13 +215,16 @@
>
{#if todoable}
<div class="flex-center assignee" contenteditable="false">
<EmployeePresenter
value={userId}
disabled={readonly}
avatarSize={'card'}
shouldShowName={false}
shouldShowPlaceholder
onEmployeeEdit={handleAssigneeEdit}
<Component
is={contact.component.EmployeePresenter}
props={{
value: userId,
disabled: readonly,
avatarSize: 'card',
shouldShowName: false,
shouldShowPlaceholder: true,
onEmployeeEdit: handleAssigneeEdit
}}
/>
</div>
{/if}
@@ -1,5 +1,6 @@
<script lang="ts">
import { NodeViewContent, NodeViewWrapper } from '@hcengineering/text-editor-resources'
import NodeViewContent from '../../node-view/NodeViewContent.svelte'
import NodeViewWrapper from '../../node-view/NodeViewWrapper.svelte'
</script>
<NodeViewWrapper data-drag-handle="">
@@ -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<ReturnType> {
@@ -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<TodoItemExtensionOptions>({
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<Class<Doc>>): 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
}
}
@@ -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: [
@@ -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<string, any>
export type SvelteNodeViewComponent = typeof SvelteComponent | ComponentType
export type SvelteNodeViewComponent = typeof SvelteComponent | ComponentType | AnyComponent
/**
* Svelte NodeView renderer, inspired by React and Vue implementation by Tiptap
@@ -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<string, any>
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<string, any>): void {
this.component.$set(props)
this.component?.$set(props)
}
onKeyDown (props: Record<string, any>): 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()
}
}
+23 -17
View File
@@ -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'
@@ -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<CodeBlockOptions> | false
code?: Partial<CodeOptions> | false
hardBreak?: Partial<HardBreakOptions> | false
heading?: {
levels?: Level[]
}
history?: false
dropcursor?: false
}
export const DefaultKit = Extension.create<DefaultKitOptions>({
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<void> {
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()
}
})
})
}
@@ -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<FileOptions> | false
image?:
| (Partial<ImageOptions> & {
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<Doc>
objectClass?: Ref<Class<Doc>>
objectSpace?: Ref<Space>
toolbar?: Partial<ToolbarOptions> | false
embed?: Partial<EmbedNodeOptions> | 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<KitExtensionCreator[]> {
// ===========================================================================================
// 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<ExtensionCreator[]> {
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<Extension<EditorKitOptions, any>> {
const modelKitFactories = await getModelKitFactories()
return Extension.create<EditorKitOptions>({
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<Extension<EditorKitOptions, any>>
export async function getEditorKit (): Promise<Extension<EditorKitOptions, any>> {
export async function getEditorKit (
...options: Array<Partial<EditorKitOptions>>
): Promise<Extension<EditorKitOptions, any>> {
if (editorKitPromise === undefined) {
editorKitPromise = buildEditorKit()
}
return await editorKitPromise
const kit = await editorKitPromise
if ((options ?? []).length < 1) return kit
let newOptions: Partial<EditorKitOptions> = {
mode: 'full'
}
for (const e of options) {
newOptions = mergeKitOptions(newOptions, e)
}
return kit.configure(newOptions)
}
async function buildEditorKit (): Promise<Extension<EditorKitOptions, any>> {
return await new Promise<Extension<EditorKitOptions, any>>((resolve, reject) => {
getKitExtensionCreators()
.then((kitExtensionCreators) => {
resolve(
Extension.create<EditorKitOptions>({
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<boolean> {
return editor.isEditable
}
export async function isHeadingVisible (editor: Editor, ctx: ActionContext): Promise<boolean> {
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
}
}
+26 -2
View File
@@ -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<boolean> {
return editor.isEditable
}
export async function isHeadingVisible (editor: Editor, ctx: ActionContext): Promise<boolean> {
return (await isEditable(editor)) && ctx.mode === 'full'
}
export async function formatLink (editor: Editor): Promise<void> {
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()
}
})
})
}
+2 -4
View File
@@ -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<Resources> => ({
PriorityEditor
},
function: {
ToDoTitleProvider,
CreateTodoItemExtension: createTodoItemExtension,
CreateTodoListExtension: createTodoListExtension
ToDoTitleProvider
}
})
+1 -6
View File
@@ -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
}
})
+5 -64
View File
@@ -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<ToDo>, doc?: T
return object.title
}
function isTodoableClass (objectClass: Ref<Class<Doc>>): 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' },
+5 -1
View File
@@ -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
}
})
@@ -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}
@@ -50,7 +50,7 @@
showButtons={false}
content={object.description}
placeholder={core.string.Description}
enableBackReferences
kitOptions={{ reference: true }}
on:blur={onDescriptionChange}
/>
{:else}
+3 -1
View File
@@ -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<Resources> => ({
CanLeaveSpace: canLeaveSpace,
IsClipboardAvailable: isClipboardAvailable,
BlobImageMetadata: blobImageMetadata,
BlobVideoMetadata: blobVideoMetadata
BlobVideoMetadata: blobVideoMetadata,
OpenDocument: openDocFromRef
}
})
+10
View File
@@ -1206,6 +1206,16 @@ export async function openDoc (hierarchy: Hierarchy, object: Doc): Promise<void>
navigate(loc)
}
export async function openDocFromRef<T extends Doc = Doc> (_class: Ref<Class<T>>, _id: Ref<T>): Promise<boolean> {
const client = getClient()
const doc = await client.findOne<Doc>(_class, { _id })
if (doc?._id !== undefined) {
await openDoc(client.getHierarchy(), doc)
return true
}
return false
}
/**
* @public
*/
+5 -1
View File
@@ -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<OpenDocumentFunction>
},
actionImpl: {
CopyTextToClipboard: '' as ViewAction<{
textProvider: Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>
+5
View File
@@ -486,6 +486,11 @@ export type ViewActionFunction<T extends Doc = Doc, P = Record<string, any>> = (
*/
export type ViewActionAvailabilityFunction<T extends Doc = Doc> = (doc: T | T[] | undefined) => Promise<boolean>
/**
* @public
*/
export type OpenDocumentFunction<T extends Doc = Doc> = (_class: Ref<Class<T>>, _id: Ref<T>) => Promise<boolean>
/**
* @public
*/
@@ -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<string, any>
@@ -55,23 +55,17 @@ export const SubLink = Node.create<SubLinkOptions>({
}
})
export interface GithubKitOptions {
sub?: Partial<SubLinkOptions> | false
}
export const GithubKit = Extension.create<GithubKitOptions>({
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]
@@ -261,7 +261,7 @@ export class DocumentContentPage extends CommonPage {
}
async changeCodeBlockLanguage (text: string, oldLang: string, lang: string): Promise<void> {
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)
}