mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 21:50:34 +03:00
UBER-1117 Add ToDo to wiki documents (#3948)
Signed-off-by: Alexander Onnikov <alexander.onnikov@xored.com>
This commit is contained in:
parent
c9aceeaa8f
commit
1db7ec4d79
@ -82,8 +82,8 @@
|
||||
"@tiptap/extension-code": "^2.1.12",
|
||||
"@tiptap/extension-bubble-menu": "^2.1.12",
|
||||
"@tiptap/extension-underline": "^2.1.12",
|
||||
"@hocuspocus/provider": "^2.5.0",
|
||||
"@tiptap/extension-list-keymap": "^2.1.12",
|
||||
"@hocuspocus/provider": "^2.5.0",
|
||||
"slugify": "^1.6.6"
|
||||
}
|
||||
}
|
||||
|
@ -97,9 +97,9 @@ export const Completion = Node.create<CompletionOptions>({
|
||||
|
||||
addAttributes () {
|
||||
return {
|
||||
id: getDataAttribute('id', null),
|
||||
label: getDataAttribute('label', null),
|
||||
objectclass: getDataAttribute('objectclass', null)
|
||||
id: getDataAttribute('id'),
|
||||
label: getDataAttribute('label'),
|
||||
objectclass: getDataAttribute('objectclass')
|
||||
}
|
||||
},
|
||||
|
||||
|
35
packages/text-editor/src/components/extension/todo.ts
Normal file
35
packages/text-editor/src/components/extension/todo.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { TaskItem } from '@tiptap/extension-task-item'
|
||||
import { TaskList } from '@tiptap/extension-task-list'
|
||||
|
||||
import { getDataAttribute } from '../../utils'
|
||||
|
||||
export const TodoItemExtension = TaskItem.extend({
|
||||
name: 'todoItem',
|
||||
|
||||
addOptions () {
|
||||
return {
|
||||
nested: false,
|
||||
HTMLAttributes: {},
|
||||
taskListTypeName: 'todoList'
|
||||
}
|
||||
},
|
||||
|
||||
addAttributes () {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
todoid: getDataAttribute('todoid', { default: null, keepOnSplit: false }),
|
||||
userid: getDataAttribute('userid', { default: null, keepOnSplit: false })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const TodoListExtension = TaskList.extend({
|
||||
name: 'todoList',
|
||||
|
||||
addOptions () {
|
||||
return {
|
||||
itemTypeName: 'todoItem',
|
||||
HTMLAttributes: {}
|
||||
}
|
||||
}
|
||||
})
|
@ -19,8 +19,8 @@ import Link from '@tiptap/extension-link'
|
||||
import Typography from '@tiptap/extension-typography'
|
||||
import { CompletionOptions } from '../Completion'
|
||||
import MentionList from './MentionList.svelte'
|
||||
import { SvelteRenderer } from './SvelteRenderer'
|
||||
import { NodeUuidExtension } from './extension/nodeUuid'
|
||||
import { SvelteRenderer } from './node-view'
|
||||
|
||||
export const tableExtensions = [
|
||||
Table.configure({
|
||||
@ -164,9 +164,12 @@ export const completionConfig: Partial<CompletionOptions> = {
|
||||
return {
|
||||
onStart: (props: any) => {
|
||||
component = new SvelteRenderer(MentionList, {
|
||||
...props,
|
||||
close: () => {
|
||||
component.destroy()
|
||||
element: document.body,
|
||||
props: {
|
||||
...props,
|
||||
close: () => {
|
||||
component.destroy()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
@ -0,0 +1,33 @@
|
||||
<!--
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { getNodeViewContext } from './context'
|
||||
|
||||
export let as = 'div'
|
||||
|
||||
const { onContentElement } = getNodeViewContext()
|
||||
|
||||
let element: HTMLElement
|
||||
$: if (element) {
|
||||
element.style.whiteSpace = 'pre-wrap'
|
||||
onContentElement(element)
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:element this={as} bind:this={element} {...$$restProps}>
|
||||
<slot />
|
||||
</svelte:element>
|
@ -0,0 +1,42 @@
|
||||
<!--
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { onMount, tick } from 'svelte'
|
||||
import { getNodeViewContext } from './context'
|
||||
|
||||
export let as = 'div'
|
||||
|
||||
const { onDragStart } = getNodeViewContext()
|
||||
|
||||
let element: HTMLElement
|
||||
|
||||
onMount(async () => {
|
||||
await tick()
|
||||
element.style.whiteSpace = 'normal'
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:element
|
||||
this={as}
|
||||
bind:this={element}
|
||||
data-node-view-wrapper=""
|
||||
on:dragstart={onDragStart}
|
||||
role="none"
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</svelte:element>
|
33
packages/text-editor/src/components/node-view/context.ts
Normal file
33
packages/text-editor/src/components/node-view/context.ts
Normal file
@ -0,0 +1,33 @@
|
||||
//
|
||||
// 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 { getContext } from 'svelte'
|
||||
|
||||
const key = 'tiptap-node-view-context'
|
||||
|
||||
export interface TiptapNodeViewContext {
|
||||
onDragStart: (event: DragEvent) => void
|
||||
onContentElement: (element: HTMLElement) => void
|
||||
}
|
||||
|
||||
export function getNodeViewContext (): TiptapNodeViewContext {
|
||||
return getContext(key)
|
||||
}
|
||||
|
||||
export function createNodeViewContext (value: TiptapNodeViewContext): Map<any, any> {
|
||||
const context = new Map()
|
||||
context.set(key, value)
|
||||
return context
|
||||
}
|
24
packages/text-editor/src/components/node-view/index.ts
Normal file
24
packages/text-editor/src/components/node-view/index.ts
Normal file
@ -0,0 +1,24 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
export { default as NodeViewContent } from './NodeViewContent.svelte'
|
||||
export { default as NodeViewWrapper } from './NodeViewWrapper.svelte'
|
||||
export {
|
||||
default as SvelteNodeViewRenderer,
|
||||
SvelteNodeViewComponent,
|
||||
SvelteNodeViewProps as NodeViewProps,
|
||||
SvelteNodeViewRendererOptions
|
||||
} from './svelte-node-view-renderer'
|
||||
export { SvelteRenderer, SvelteRendererComponent, SvelteRendererOptions } from './svelte-renderer'
|
@ -0,0 +1,155 @@
|
||||
//
|
||||
// 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 {
|
||||
DecorationWithType,
|
||||
Editor,
|
||||
NodeView,
|
||||
NodeViewProps,
|
||||
NodeViewRenderer,
|
||||
NodeViewRendererOptions
|
||||
} from '@tiptap/core'
|
||||
import type { Node as ProseMirrorNode } from '@tiptap/pm/model'
|
||||
import type { ComponentType, SvelteComponent } from 'svelte'
|
||||
|
||||
import { createNodeViewContext } from './context'
|
||||
import { SvelteRenderer } from './svelte-renderer'
|
||||
|
||||
export interface SvelteNodeViewRendererOptions extends NodeViewRendererOptions {
|
||||
update?: (node: ProseMirrorNode, decorations: DecorationWithType[]) => boolean
|
||||
contentAs?: string
|
||||
contentDOMElementAs?: string
|
||||
componentProps?: Record<string, any>
|
||||
}
|
||||
|
||||
export type SvelteNodeViewProps = NodeViewProps & {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export type SvelteNodeViewComponent = typeof SvelteComponent | ComponentType
|
||||
|
||||
/**
|
||||
* Svelte NodeView renderer, inspired by React and Vue implementation by Tiptap
|
||||
* https://tiptap.dev/guide/node-views/react/
|
||||
*/
|
||||
class SvelteNodeView extends NodeView<SvelteNodeViewComponent, Editor, SvelteNodeViewRendererOptions> {
|
||||
renderer!: SvelteRenderer
|
||||
|
||||
contentDOMElement!: HTMLElement | null
|
||||
|
||||
override mount (): void {
|
||||
const props: SvelteNodeViewProps = {
|
||||
editor: this.editor,
|
||||
node: this.node,
|
||||
decorations: this.decorations,
|
||||
selected: false,
|
||||
extension: this.extension,
|
||||
getPos: () => this.getPos(),
|
||||
updateAttributes: (attributes = {}) => this.updateAttributes(attributes),
|
||||
deleteNode: () => this.deleteNode(),
|
||||
...(this.options.componentProps ?? {})
|
||||
}
|
||||
|
||||
if (this.node.isLeaf) {
|
||||
this.contentDOMElement = null
|
||||
} else if (this.options.contentDOMElementAs !== undefined) {
|
||||
this.contentDOMElement = document.createElement(this.options.contentDOMElementAs)
|
||||
} else {
|
||||
this.contentDOMElement = document.createElement(this.node.isInline ? 'span' : 'div')
|
||||
}
|
||||
|
||||
if (this.contentDOMElement !== null) {
|
||||
// For some reason the whiteSpace prop is not inherited properly in Chrome and Safari
|
||||
// With this fix it seems to work fine
|
||||
// See: https://github.com/ueberdosis/tiptap/issues/1197
|
||||
this.contentDOMElement.style.whiteSpace = 'inherit'
|
||||
}
|
||||
|
||||
const contentAs = this.options.contentAs ?? (this.node.isInline ? 'span' : 'div')
|
||||
|
||||
const target = document.createElement(contentAs)
|
||||
target.classList.add(`node-${this.node.type.name}`)
|
||||
|
||||
const context = createNodeViewContext({
|
||||
onDragStart: this.onDragStart.bind(this),
|
||||
onContentElement: (element) => {
|
||||
if (this.contentDOMElement !== null && !element.contains(this.contentDOMElement)) {
|
||||
element.appendChild(this.contentDOMElement)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.renderer = new SvelteRenderer(this.component, { element: target, props, context })
|
||||
}
|
||||
|
||||
override get dom (): HTMLElement {
|
||||
if (this.renderer.element.firstElementChild?.hasAttribute('data-node-view-wrapper') === false) {
|
||||
throw Error('Please use the NodeViewWrapper component for your node view.')
|
||||
}
|
||||
|
||||
return this.renderer.element
|
||||
}
|
||||
|
||||
override get contentDOM (): HTMLElement | null {
|
||||
if (this.node.isLeaf) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.contentDOMElement
|
||||
}
|
||||
|
||||
update (node: ProseMirrorNode, decorations: DecorationWithType[]): boolean {
|
||||
if (typeof this.options.update === 'function') {
|
||||
return this.options.update(node, decorations)
|
||||
}
|
||||
|
||||
if (node.type !== this.node.type) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (node === this.node && this.decorations === decorations) {
|
||||
return true
|
||||
}
|
||||
|
||||
this.node = node
|
||||
this.decorations = decorations
|
||||
|
||||
this.renderer.updateProps({ node, decorations })
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
selectNode (): void {
|
||||
this.renderer.updateProps({ selected: true })
|
||||
}
|
||||
|
||||
deselectNode (): void {
|
||||
this.renderer.updateProps({ selected: false })
|
||||
}
|
||||
|
||||
destroy (): void {
|
||||
this.renderer.destroy()
|
||||
this.contentDOMElement = null
|
||||
}
|
||||
}
|
||||
|
||||
const SvelteNodeViewRenderer = (
|
||||
component: SvelteNodeViewComponent,
|
||||
options: Partial<SvelteNodeViewRendererOptions>
|
||||
): NodeViewRenderer => {
|
||||
return (props) => new SvelteNodeView(component, props, options)
|
||||
}
|
||||
|
||||
export default SvelteNodeViewRenderer
|
@ -1,6 +1,6 @@
|
||||
//
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 Hardcore Engineering Inc.
|
||||
// Copyright © 2021, 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
|
||||
@ -14,14 +14,27 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { ComponentType, SvelteComponent } from 'svelte'
|
||||
import type { ComponentType, SvelteComponent } from 'svelte'
|
||||
|
||||
export type SvelteRendererComponent = typeof SvelteComponent | ComponentType
|
||||
|
||||
export interface SvelteRendererOptions {
|
||||
element: HTMLElement
|
||||
props?: any
|
||||
context?: any
|
||||
}
|
||||
|
||||
export class SvelteRenderer {
|
||||
private readonly component: SvelteComponent
|
||||
element: HTMLElement
|
||||
|
||||
constructor (comp: typeof SvelteComponent | ComponentType, props: any) {
|
||||
const options = { target: document.body, props }
|
||||
this.component = new (comp as any)(options)
|
||||
constructor (component: SvelteRendererComponent, { element, props, context }: SvelteRendererOptions) {
|
||||
this.element = element
|
||||
this.element.classList.add('svelte-renderer')
|
||||
|
||||
const options = { target: element, props, context }
|
||||
const Component = component
|
||||
this.component = new Component(options)
|
||||
}
|
||||
|
||||
updateProps (props: Record<string, any>): void {
|
@ -1,6 +1,6 @@
|
||||
//
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 Hardcore Engineering Inc.
|
||||
// Copyright © 2021, 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
|
||||
@ -30,6 +30,7 @@ export { default as TextEditor } from './components/TextEditor.svelte'
|
||||
export { default as TextEditorStyleToolbar } from './components/TextEditorStyleToolbar.svelte'
|
||||
export { default as AttachIcon } from './components/icons/Attach.svelte'
|
||||
export { default as TableOfContents } from './components/toc/TableOfContents.svelte'
|
||||
export * from './components/node-view'
|
||||
export { default } from './plugin'
|
||||
export * from './types'
|
||||
export * from './utils'
|
||||
@ -58,5 +59,6 @@ export {
|
||||
type InlineStyleToolbarStorage
|
||||
} from './components/extension/inlineStyleToolbar'
|
||||
export { ImageExtension, type ImageOptions } from './components/extension/imageExt'
|
||||
export { TodoItemExtension, TodoListExtension } from './components/extension/todo'
|
||||
|
||||
export { textEditorId }
|
||||
|
@ -78,11 +78,14 @@ export function copyDocumentContent (
|
||||
provider.copyContent(documentId, snapshotId)
|
||||
}
|
||||
|
||||
export function getDataAttribute (name: string, def?: unknown | null): Partial<Attribute> {
|
||||
export function getDataAttribute (
|
||||
name: string,
|
||||
options?: Omit<Attribute, 'parseHTML' | 'renderHTML'>
|
||||
): Partial<Attribute> {
|
||||
const dataName = `data-${name}`
|
||||
|
||||
return {
|
||||
default: def,
|
||||
default: null,
|
||||
parseHTML: (element) => element.getAttribute(dataName),
|
||||
renderHTML: (attributes) => {
|
||||
// eslint-disable-next-line
|
||||
@ -93,6 +96,7 @@ export function getDataAttribute (name: string, def?: unknown | null): Partial<A
|
||||
return {
|
||||
[dataName]: attributes[name]
|
||||
}
|
||||
}
|
||||
},
|
||||
...(options !== undefined ? options : {})
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ import TaskItem from '@tiptap/extension-task-item'
|
||||
import TaskList from '@tiptap/extension-task-list'
|
||||
import Typography from '@tiptap/extension-typography'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import { ImageNode, ReferenceNode } from './nodes'
|
||||
import { ImageNode, ReferenceNode, TodoItemNode, TodoListNode } from './nodes'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -97,4 +97,10 @@ export const defaultExtensions: AnyExtension[] = [
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const serverExtensions: AnyExtension[] = [...defaultExtensions, ImageNode, ReferenceNode]
|
||||
export const serverExtensions: AnyExtension[] = [
|
||||
...defaultExtensions,
|
||||
ImageNode,
|
||||
ReferenceNode,
|
||||
TodoItemNode,
|
||||
TodoListNode
|
||||
]
|
||||
|
@ -15,4 +15,5 @@
|
||||
|
||||
export * from './image'
|
||||
export * from './reference'
|
||||
export * from './todo'
|
||||
export { getDataAttribute } from './utils'
|
||||
|
35
packages/text/src/nodes/todo.ts
Normal file
35
packages/text/src/nodes/todo.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { TaskItem } from '@tiptap/extension-task-item'
|
||||
import { TaskList } from '@tiptap/extension-task-list'
|
||||
|
||||
import { getDataAttribute } from './utils'
|
||||
|
||||
export const TodoItemNode = TaskItem.extend({
|
||||
name: 'todoItem',
|
||||
|
||||
addOptions () {
|
||||
return {
|
||||
nested: false,
|
||||
HTMLAttributes: {},
|
||||
taskListTypeName: 'todoList'
|
||||
}
|
||||
},
|
||||
|
||||
addAttributes () {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
todoid: getDataAttribute('todoid', { default: null, keepOnSplit: false }),
|
||||
userid: getDataAttribute('userid', { default: null, keepOnSplit: false })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const TodoListNode = TaskList.extend({
|
||||
name: 'todoList',
|
||||
|
||||
addOptions () {
|
||||
return {
|
||||
itemTypeName: 'todoItem',
|
||||
HTMLAttributes: {}
|
||||
}
|
||||
}
|
||||
})
|
@ -18,11 +18,14 @@ import { Attribute } from '@tiptap/core'
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function getDataAttribute (name: string, def?: unknown | null): Partial<Attribute> {
|
||||
export function getDataAttribute (
|
||||
name: string,
|
||||
options?: Omit<Attribute, 'parseHTML' | 'renderHTML'>
|
||||
): Partial<Attribute> {
|
||||
const dataName = `data-${name}`
|
||||
|
||||
return {
|
||||
default: def,
|
||||
default: null,
|
||||
parseHTML: (element) => element.getAttribute(dataName),
|
||||
renderHTML: (attributes) => {
|
||||
// eslint-disable-next-line
|
||||
@ -33,6 +36,7 @@ export function getDataAttribute (name: string, def?: unknown | null): Partial<A
|
||||
return {
|
||||
[dataName]: attributes[name]
|
||||
}
|
||||
}
|
||||
},
|
||||
...(options !== undefined ? options : {})
|
||||
}
|
||||
}
|
||||
|
@ -76,6 +76,17 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul[data-type="todoList"] {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ol ol { list-style: lower-alpha; }
|
||||
ol ol ol { list-style: lower-roman; }
|
||||
ol ol ol ol { list-style: decimal; }
|
||||
|
Loading…
Reference in New Issue
Block a user