Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/text-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
6 changes: 3 additions & 3 deletions packages/text-editor/src/Completion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}
},

Expand Down
35 changes: 35 additions & 0 deletions packages/text-editor/src/components/extension/todo.ts
Original file line number Diff line number Diff line change
@@ -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: {}
}
}
})
11 changes: 7 additions & 4 deletions packages/text-editor/src/components/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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()
}
}
})
},
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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 changes: 33 additions & 0 deletions packages/text-editor/src/components/node-view/context.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 24 additions & 0 deletions packages/text-editor/src/components/node-view/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Original file line number Diff line number Diff line change
@@ -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
Loading