mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-17 18:05:42 +02:00
Simplified editor plugin configuration management (#9485)
Signed-off-by: Victor Ilyushchenko <alt13ri@gmail.com>
This commit is contained in:
Generated
+40
-3
@@ -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))
|
||||
|
||||
@@ -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
@@ -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'
|
||||
|
||||
@@ -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>>
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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' }
|
||||
})
|
||||
]
|
||||
}
|
||||
})
|
||||
@@ -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)
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ const NAME = 'node-uuid'
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const NodeUuid = Mark.create({
|
||||
export const QMSInlineCommentMark = Mark.create({
|
||||
name: NAME,
|
||||
inline: true,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -396,7 +396,8 @@ export default async (): Promise<Resources> => ({
|
||||
ChannelIcon,
|
||||
SpaceMembersEditor,
|
||||
ContactNamePresenter,
|
||||
PersonIdFilter
|
||||
PersonIdFilter,
|
||||
AssigneePopup
|
||||
},
|
||||
completion: {
|
||||
EmployeeQuery: async (
|
||||
|
||||
@@ -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>,
|
||||
|
||||
+41
-58
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+1
-1
@@ -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'
|
||||
|
||||
+16
-1
@@ -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 }
|
||||
+15
@@ -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 => {
|
||||
+15
@@ -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
|
||||
|
||||
+57
-39
@@ -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
|
||||
}
|
||||
+56
-48
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -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 () {
|
||||
+3
-3
@@ -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: () => {
|
||||
+3
-3
@@ -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 () {
|
||||
+4
-4
@@ -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
-1
@@ -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
|
||||
+2
-1
@@ -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': () => {
|
||||
+1
-1
@@ -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 () {
|
||||
+3
-3
@@ -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 () {
|
||||
+20
-17
@@ -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}
|
||||
+2
-1
@@ -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="">
|
||||
+43
-5
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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>>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user