From 516d0da41c5ffbbbb2e4fd9f73b84973b5898315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Thu, 29 Oct 2020 09:26:24 +0100 Subject: [PATCH 01/11] nothing works --- packages/core/src/Editor.ts | 1 + packages/core/src/ExtensionManager.ts | 38 ++++++++++- packages/core/src/NodeExtension.ts | 6 ++ packages/core/src/NodeView.ts | 3 + packages/extension-image/index.ts | 7 ++ packages/vue/index.ts | 3 + packages/vue/src/VueRenderer.ts | 96 +++++++++++++++++++++++++++ 7 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 packages/core/src/NodeView.ts create mode 100644 packages/vue/src/VueRenderer.ts diff --git a/packages/core/src/Editor.ts b/packages/core/src/Editor.ts index c49cd3de..e925f422 100644 --- a/packages/core/src/Editor.ts +++ b/packages/core/src/Editor.ts @@ -256,6 +256,7 @@ export class Editor extends EventEmitter { * Creates a ProseMirror view. */ private createView() { + console.log('CREATE VIEW') this.view = new EditorView(this.options.element, { state: EditorState.create({ doc: this.createDocument(this.options.content), diff --git a/packages/core/src/ExtensionManager.ts b/packages/core/src/ExtensionManager.ts index 2da65cd5..ab26ef84 100644 --- a/packages/core/src/ExtensionManager.ts +++ b/packages/core/src/ExtensionManager.ts @@ -1,14 +1,14 @@ import { Plugin } from 'prosemirror-state' import { keymap } from 'prosemirror-keymap' -// import { Schema, Node as ProsemirrorNode } from 'prosemirror-model' +import { Schema, Node as ProsemirrorNode } from 'prosemirror-model' import { inputRules } from 'prosemirror-inputrules' -// import { EditorView, Decoration } from 'prosemirror-view' -import { Schema } from 'prosemirror-model' +import { EditorView, Decoration } from 'prosemirror-view' import { Editor } from './Editor' // import capitalize from './utils/capitalize' import { Extensions } from './types' import getSchema from './utils/getSchema' import getSchemaTypeByName from './utils/getSchemaTypeByName' +import splitExtensions from './utils/splitExtensions' export default class ExtensionManager { @@ -98,6 +98,38 @@ export default class ExtensionManager { } get nodeViews() { + const { nodeExtensions } = splitExtensions(this.extensions) + + return Object.fromEntries(nodeExtensions + .filter(extension => !!extension.addNodeView) + .map(extension => { + const context = { + options: extension.options, + editor: this.editor, + type: getSchemaTypeByName(extension.name, this.schema), + } + + const renderer = extension.addNodeView?.bind(context)?.() + + const nodeview = ( + node: ProsemirrorNode, + view: EditorView, + getPos: (() => number) | boolean, + decorations: Decoration[], + ) => { + // @ts-ignore + return new renderer({ + editor: this.editor, + view, + node, + getPos, + decorations, + }) + } + + return [extension.name, nodeview] + })) + // const { renderer: Renderer } = this.editor // if (!Renderer || !Renderer.type) { diff --git a/packages/core/src/NodeExtension.ts b/packages/core/src/NodeExtension.ts index 76dba816..8de0c678 100644 --- a/packages/core/src/NodeExtension.ts +++ b/packages/core/src/NodeExtension.ts @@ -139,6 +139,11 @@ export interface NodeExtensionSpec extends Overwrit editor: Editor, type: NodeType, }) => Plugin[], + + /** + * Node View + */ + addNodeView?: (() => any) | null, }> {} export type NodeExtension = Required & { @@ -166,6 +171,7 @@ const defaultNode: NodeExtension = { parseHTML: () => null, renderHTML: null, addAttributes: () => ({}), + addNodeView: null, } export function createNode(config: NodeExtensionSpec) { diff --git a/packages/core/src/NodeView.ts b/packages/core/src/NodeView.ts new file mode 100644 index 00000000..30a8e894 --- /dev/null +++ b/packages/core/src/NodeView.ts @@ -0,0 +1,3 @@ +export class NodeView { + +} diff --git a/packages/extension-image/index.ts b/packages/extension-image/index.ts index 424cffc5..bf975076 100644 --- a/packages/extension-image/index.ts +++ b/packages/extension-image/index.ts @@ -1,4 +1,5 @@ import { Command, createNode, nodeInputRule } from '@tiptap/core' +import { VueRenderer } from '@tiptap/vue' export const inputRegex = /!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/ @@ -59,6 +60,12 @@ const Image = createNode({ }), ] }, + + addNodeView() { + return VueRenderer({ + template: '
vue component
', + }) + }, }) export default Image diff --git a/packages/vue/index.ts b/packages/vue/index.ts index 79050a05..f1e855cc 100644 --- a/packages/vue/index.ts +++ b/packages/vue/index.ts @@ -1,7 +1,10 @@ import { Editor as CoreEditor } from '@tiptap/core' import Renderer from './src/Renderer' +import VueRenderer from './src/VueRenderer' export * from '@tiptap/core' +export { Renderer } +export { VueRenderer } export { default as EditorContent } from './src/components/EditorContent' export class Editor extends CoreEditor { diff --git a/packages/vue/src/VueRenderer.ts b/packages/vue/src/VueRenderer.ts new file mode 100644 index 00000000..e58ab726 --- /dev/null +++ b/packages/vue/src/VueRenderer.ts @@ -0,0 +1,96 @@ +// @ts-nocheck + +// export default (component: any) => { +// console.log('vue renderer', component) + +// // return (node, view, getPos) => { +// return what => { + +// console.log(what) +// // return new class ImageView { +// // constructor(node, view, getPos) { +// // this.dom = document.createElement('img') +// // this.dom.src = node.attrs.src +// // this.dom.alt = node.attrs.alt +// // this.dom.addEventListener('click', e => { +// // e.preventDefault() +// // const alt = prompt('New alt text:', '') +// // if (alt) { +// // view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { +// // src: node.attrs.src, +// // alt, +// // })) +// // } +// // }) +// // } + +// // stopEvent() { return true } +// // }(node, view, getPos) + +// } +// } + +import { Editor } from '@tiptap/core' +import { Node as ProsemirrorNode } from 'prosemirror-model' +import { EditorView } from 'prosemirror-view' +import Vue from 'vue' + +export default (component: any) => class ImageView { + + vm!: Vue + + constructor(props: { editor: Editor, node: ProsemirrorNode, view: EditorView, getPos: any }) { + const { + node, editor, getPos, view, + } = props + // const { view } = editor + + // this.dom = document.createElement('div') + // this.dom.innerHTML = 'hello node view' + + this.mount(component) + } + + mount(component: Vue) { + const Component = Vue.extend(component) + + this.vm = new Component({ + // parent: this.parent, + // propsData: props, + }).$mount() + } + + get dom() { + return this.vm.$el + } + + get contentDOM() { + return this.vm.$refs.content + } + + stopEvent() { + return true + } + + // console.log(what) + // return new class ImageView { + // constructor(node, view, getPos) { + // this.dom = document.createElement('img') + // this.dom.src = node.attrs.src + // this.dom.alt = node.attrs.alt + // this.dom.addEventListener('click', e => { + // e.preventDefault() + // const alt = prompt('New alt text:', '') + // if (alt) { + // view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { + // src: node.attrs.src, + // alt, + // })) + // } + // }) + // } + + // stopEvent() { return true } + // }(node, view, getPos) + +} From f9089932ff58a5819e677de9eb5df78ce76fbd70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Fri, 30 Oct 2020 11:08:23 +0100 Subject: [PATCH 02/11] add basic implementation for node views --- packages/core/src/Editor.ts | 11 ++-- packages/core/src/ExtensionManager.ts | 52 +++--------------- packages/core/src/NodeExtension.ts | 4 +- packages/core/src/NodeView.ts | 3 -- packages/core/src/types.ts | 12 +++++ packages/extension-image/index.ts | 7 ++- packages/vue/index.ts | 11 +--- packages/vue/src/Renderer.ts | 32 ----------- packages/vue/src/VueRenderer.ts | 78 +++++---------------------- 9 files changed, 47 insertions(+), 163 deletions(-) delete mode 100644 packages/core/src/NodeView.ts delete mode 100644 packages/vue/src/Renderer.ts diff --git a/packages/core/src/Editor.ts b/packages/core/src/Editor.ts index e925f422..238140df 100644 --- a/packages/core/src/Editor.ts +++ b/packages/core/src/Editor.ts @@ -75,8 +75,6 @@ declare module './Editor' { @magicMethods export class Editor extends EventEmitter { - public renderer!: any - private proxy!: Editor private commandManager!: CommandManager @@ -256,7 +254,6 @@ export class Editor extends EventEmitter { * Creates a ProseMirror view. */ private createView() { - console.log('CREATE VIEW') this.view = new EditorView(this.options.element, { state: EditorState.create({ doc: this.createDocument(this.options.content), @@ -266,10 +263,16 @@ export class Editor extends EventEmitter { ], }), dispatchTransaction: this.dispatchTransaction.bind(this), + }) + + // `editor.view` is not yet available at this time. + // Therefore we will add all node views directly afterwards. + this.view.setProps({ nodeViews: this.extensionManager.nodeViews, }) - // store editor in dom element for better testing + // Let’s store the editor instance in the DOM element. + // So we’ll have access to it for tests. const dom = this.view.dom as HTMLElement dom.editor = this.proxy } diff --git a/packages/core/src/ExtensionManager.ts b/packages/core/src/ExtensionManager.ts index ab26ef84..2d27f087 100644 --- a/packages/core/src/ExtensionManager.ts +++ b/packages/core/src/ExtensionManager.ts @@ -4,8 +4,7 @@ import { Schema, Node as ProsemirrorNode } from 'prosemirror-model' import { inputRules } from 'prosemirror-inputrules' import { EditorView, Decoration } from 'prosemirror-view' import { Editor } from './Editor' -// import capitalize from './utils/capitalize' -import { Extensions } from './types' +import { Extensions, NodeViewRenderer } from './types' import getSchema from './utils/getSchema' import getSchemaTypeByName from './utils/getSchemaTypeByName' import splitExtensions from './utils/splitExtensions' @@ -109,57 +108,22 @@ export default class ExtensionManager { type: getSchemaTypeByName(extension.name, this.schema), } - const renderer = extension.addNodeView?.bind(context)?.() + const renderer = extension.addNodeView?.bind(context)?.() as NodeViewRenderer const nodeview = ( node: ProsemirrorNode, view: EditorView, getPos: (() => number) | boolean, decorations: Decoration[], - ) => { - // @ts-ignore - return new renderer({ - editor: this.editor, - view, - node, - getPos, - decorations, - }) - } + ) => renderer({ + editor: this.editor, + node, + getPos, + decorations, + }) return [extension.name, nodeview] })) - - // const { renderer: Renderer } = this.editor - - // if (!Renderer || !Renderer.type) { - // return {} - // } - - // const prop = `to${capitalize(Renderer.type)}` - - // return collect(this.extensions) - // .where('extensionType', 'node') - // .filter((extension: any) => extension.schema()[prop]) - // .map((extension: any) => { - // return ( - // node: ProsemirrorNode, - // view: EditorView, - // getPos: (() => number) | boolean, - // decorations: Decoration[], - // ) => { - // return new Renderer(extension.schema()[prop], { - // extension, - // editor: this.editor, - // node, - // getPos, - // decorations, - // }) - // } - // }) - // .all() - - return {} } } diff --git a/packages/core/src/NodeExtension.ts b/packages/core/src/NodeExtension.ts index 8de0c678..17d6e91d 100644 --- a/packages/core/src/NodeExtension.ts +++ b/packages/core/src/NodeExtension.ts @@ -3,7 +3,7 @@ import { } from 'prosemirror-model' import { Plugin } from 'prosemirror-state' import { ExtensionSpec, defaultExtension } from './Extension' -import { Attributes, Overwrite } from './types' +import { Attributes, NodeViewRenderer, Overwrite } from './types' import { Editor } from './Editor' export interface NodeExtensionSpec extends Overwrite, { @@ -143,7 +143,7 @@ export interface NodeExtensionSpec extends Overwrit /** * Node View */ - addNodeView?: (() => any) | null, + addNodeView?: (() => NodeViewRenderer) | null, }> {} export type NodeExtension = Required & { diff --git a/packages/core/src/NodeView.ts b/packages/core/src/NodeView.ts deleted file mode 100644 index 30a8e894..00000000 --- a/packages/core/src/NodeView.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class NodeView { - -} diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 5533a4fd..d1385106 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,6 +1,9 @@ +import { Node } from 'prosemirror-model' +import { Decoration, NodeView } from 'prosemirror-view' import { Extension } from './Extension' import { NodeExtension } from './NodeExtension' import { MarkExtension } from './MarkExtension' +import Editor from '..' export type Extensions = (Extension | NodeExtension | MarkExtension)[] @@ -40,3 +43,12 @@ export type Overwrite = Pick> & U; export type AnyObject = { [key: string]: any } + +export type NodeViewRendererProps = { + editor: Editor, + node: Node, + getPos: (() => number) | boolean, + decorations: Decoration[] +} + +export type NodeViewRenderer = (props: NodeViewRendererProps) => NodeView diff --git a/packages/extension-image/index.ts b/packages/extension-image/index.ts index bf975076..fccc8ff8 100644 --- a/packages/extension-image/index.ts +++ b/packages/extension-image/index.ts @@ -1,5 +1,6 @@ import { Command, createNode, nodeInputRule } from '@tiptap/core' import { VueRenderer } from '@tiptap/vue' +import Vue from 'vue' export const inputRegex = /!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/ @@ -62,9 +63,11 @@ const Image = createNode({ }, addNodeView() { - return VueRenderer({ - template: '
vue component
', + const Component = Vue.extend({ + template: '
this is a vue component
', }) + + return VueRenderer(Component) }, }) diff --git a/packages/vue/index.ts b/packages/vue/index.ts index f1e855cc..80cb8873 100644 --- a/packages/vue/index.ts +++ b/packages/vue/index.ts @@ -1,12 +1,3 @@ -import { Editor as CoreEditor } from '@tiptap/core' -import Renderer from './src/Renderer' -import VueRenderer from './src/VueRenderer' - export * from '@tiptap/core' -export { Renderer } -export { VueRenderer } +export { default as VueRenderer } from './src/VueRenderer' export { default as EditorContent } from './src/components/EditorContent' - -export class Editor extends CoreEditor { - renderer = Renderer -} diff --git a/packages/vue/src/Renderer.ts b/packages/vue/src/Renderer.ts deleted file mode 100644 index 67d3e6ec..00000000 --- a/packages/vue/src/Renderer.ts +++ /dev/null @@ -1,32 +0,0 @@ -import Vue from 'vue' -import { ComponentRenderer } from '@tiptap/core' - -export default class Renderer extends ComponentRenderer { - - static type = 'vue' - - vm!: Vue - - constructor(component: Vue) { - super() - this.mount(component) - } - - mount(component: Vue) { - const Component = Vue.extend(component) - - this.vm = new Component({ - // parent: this.parent, - // propsData: props, - }).$mount() - } - - get dom() { - return this.vm.$el - } - - get contentDOM() { - return this.vm.$refs.content - } - -} diff --git a/packages/vue/src/VueRenderer.ts b/packages/vue/src/VueRenderer.ts index e58ab726..9bfd525c 100644 --- a/packages/vue/src/VueRenderer.ts +++ b/packages/vue/src/VueRenderer.ts @@ -1,57 +1,20 @@ -// @ts-nocheck - -// export default (component: any) => { -// console.log('vue renderer', component) - -// // return (node, view, getPos) => { -// return what => { - -// console.log(what) -// // return new class ImageView { -// // constructor(node, view, getPos) { -// // this.dom = document.createElement('img') -// // this.dom.src = node.attrs.src -// // this.dom.alt = node.attrs.alt -// // this.dom.addEventListener('click', e => { -// // e.preventDefault() -// // const alt = prompt('New alt text:', '') -// // if (alt) { -// // view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { -// // src: node.attrs.src, -// // alt, -// // })) -// // } -// // }) -// // } - -// // stopEvent() { return true } -// // }(node, view, getPos) - -// } -// } - -import { Editor } from '@tiptap/core' -import { Node as ProsemirrorNode } from 'prosemirror-model' -import { EditorView } from 'prosemirror-view' +import { NodeViewRendererProps } from '@tiptap/core' +import { NodeView } from 'prosemirror-view' import Vue from 'vue' +import { VueConstructor } from 'vue/types/umd' -export default (component: any) => class ImageView { +class VueNodeView implements NodeView { vm!: Vue - constructor(props: { editor: Editor, node: ProsemirrorNode, view: EditorView, getPos: any }) { - const { - node, editor, getPos, view, - } = props + constructor(component: Vue | VueConstructor, props: NodeViewRendererProps) { + // const { node, editor, getPos } = props // const { view } = editor - // this.dom = document.createElement('div') - // this.dom.innerHTML = 'hello node view' - this.mount(component) } - mount(component: Vue) { + mount(component: Vue | VueConstructor) { const Component = Vue.extend(component) this.vm = new Component({ @@ -65,32 +28,15 @@ export default (component: any) => class ImageView { } get contentDOM() { - return this.vm.$refs.content + return this.vm.$refs.content as Element } stopEvent() { return true } - // console.log(what) - // return new class ImageView { - // constructor(node, view, getPos) { - // this.dom = document.createElement('img') - // this.dom.src = node.attrs.src - // this.dom.alt = node.attrs.alt - // this.dom.addEventListener('click', e => { - // e.preventDefault() - // const alt = prompt('New alt text:', '') - // if (alt) { - // view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { - // src: node.attrs.src, - // alt, - // })) - // } - // }) - // } - - // stopEvent() { return true } - // }(node, view, getPos) - +} + +export default function VueRenderer(component: Vue | VueConstructor) { + return (props: NodeViewRendererProps) => new VueNodeView(component, props) as NodeView } From 3e00fadcaa3f70eb4f881a2b34aa4d645c4e3fd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Fri, 30 Oct 2020 13:19:06 +0100 Subject: [PATCH 03/11] add basic task list and task item extension --- docs/src/demos/Extensions/TaskList/index.vue | 53 +++++++++++++++++ docs/src/docPages/api/extensions/task-list.md | 31 ++++++++++ docs/src/links.yaml | 3 + packages/extension-bullet-list/index.ts | 4 +- packages/extension-list-item/index.ts | 4 +- packages/extension-ordered-list/index.ts | 4 +- packages/extension-task-item/index.ts | 59 +++++++++++++++++++ packages/extension-task-item/package.json | 17 ++++++ packages/extension-task-list/index.ts | 45 ++++++++++++++ packages/extension-task-list/package.json | 17 ++++++ 10 files changed, 232 insertions(+), 5 deletions(-) create mode 100644 docs/src/demos/Extensions/TaskList/index.vue create mode 100644 docs/src/docPages/api/extensions/task-list.md create mode 100644 packages/extension-task-item/index.ts create mode 100644 packages/extension-task-item/package.json create mode 100644 packages/extension-task-list/index.ts create mode 100644 packages/extension-task-list/package.json diff --git a/docs/src/demos/Extensions/TaskList/index.vue b/docs/src/demos/Extensions/TaskList/index.vue new file mode 100644 index 00000000..bae41d95 --- /dev/null +++ b/docs/src/demos/Extensions/TaskList/index.vue @@ -0,0 +1,53 @@ + + + diff --git a/docs/src/docPages/api/extensions/task-list.md b/docs/src/docPages/api/extensions/task-list.md new file mode 100644 index 00000000..3cc62c65 --- /dev/null +++ b/docs/src/docPages/api/extensions/task-list.md @@ -0,0 +1,31 @@ +# TaskList +This extension enables you to use task lists in the editor. They are rendered as `
    ` HTML tags. + +## Installation +::: warning Use with TaskItem +The `TaskList` extension is intended to be used with the [`TaskItem`](/api/extensions/task-item) extension. Make sure to import that one too, otherwise you’ll get a SyntaxError. +::: + +```bash +# With npm +npm install @tiptap/extension-task-list @tiptap/extension-task-item + +# Or: With Yarn +yarn add @tiptap/extension-task-list @tiptap/extension-task-item +``` + +## Settings +| Option | Type | Default | Description | +| ------ | ------ | ------- | -------------------------------------------- | +| class | string | – | Add a custom class to the rendered HTML tag. | + +## Commands +| Command | Options | Description | +| ----------- | ------- | --------------------- | +| task_list | — | Toggle a task list. | + +## Source code +[packages/extension-task-list/](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-task-list/) + +## Usage + diff --git a/docs/src/links.yaml b/docs/src/links.yaml index f5a3d913..4c7cd515 100644 --- a/docs/src/links.yaml +++ b/docs/src/links.yaml @@ -156,6 +156,9 @@ # draft: true - title: Strike link: /api/extensions/strike + - title: TaskList + link: /api/extensions/task-list + draft: true # - title: TableCell # link: /api/extensions/table-cell # draft: true diff --git a/packages/extension-bullet-list/index.ts b/packages/extension-bullet-list/index.ts index 45e6bc18..cb949d70 100644 --- a/packages/extension-bullet-list/index.ts +++ b/packages/extension-bullet-list/index.ts @@ -4,10 +4,10 @@ import { wrappingInputRule } from 'prosemirror-inputrules' const BulletList = createNode({ name: 'bullet_list', - content: 'list_item+', - group: 'block', + content: 'list_item+', + parseHTML() { return [ { tag: 'ul' }, diff --git a/packages/extension-list-item/index.ts b/packages/extension-list-item/index.ts index 3bfa852a..88cfca70 100644 --- a/packages/extension-list-item/index.ts +++ b/packages/extension-list-item/index.ts @@ -9,7 +9,9 @@ const ListItem = createNode({ parseHTML() { return [ - { tag: 'li' }, + { + tag: 'li', + }, ] }, diff --git a/packages/extension-ordered-list/index.ts b/packages/extension-ordered-list/index.ts index 0b678728..6c0aef3b 100644 --- a/packages/extension-ordered-list/index.ts +++ b/packages/extension-ordered-list/index.ts @@ -4,10 +4,10 @@ import { wrappingInputRule } from 'prosemirror-inputrules' const OrderedList = createNode({ name: 'ordered_list', - content: 'list_item+', - group: 'block', + content: 'list_item+', + addAttributes() { return { start: { diff --git a/packages/extension-task-item/index.ts b/packages/extension-task-item/index.ts new file mode 100644 index 00000000..6ac32943 --- /dev/null +++ b/packages/extension-task-item/index.ts @@ -0,0 +1,59 @@ +import { createNode, mergeAttributes } from '@tiptap/core' + +export interface TaskItemOptions { + nested: boolean, +} + +const TaskItem = createNode({ + name: 'task_item', + + content: 'paragraph+', + + // TODO: allow content to be a callback function + // content() { + // return this.options.nested ? '(paragraph|todo_list)+' : 'paragraph+', + // }, + + defining: true, + + defaultOptions: { + nested: false, + }, + + addAttributes() { + return { + done: { + default: false, + }, + } + }, + + parseHTML() { + return [ + { + tag: 'li[data-type="task_item"]', + priority: 51, + }, + ] + }, + + renderHTML({ attributes }) { + return ['li', mergeAttributes(attributes, { 'data-type': 'task_item' }), 0] + }, + + addKeyboardShortcuts() { + return { + Enter: () => this.editor.splitListItem('task_item'), + Tab: () => this.editor.sinkListItem('task_item'), + 'Shift-Tab': () => this.editor.liftListItem('task_item'), + } + }, +}) + +export default TaskItem + +declare module '@tiptap/core/src/Editor' { + interface AllExtensions { + TaskItem: typeof TaskItem, + } +} diff --git a/packages/extension-task-item/package.json b/packages/extension-task-item/package.json new file mode 100644 index 00000000..cb3e4b38 --- /dev/null +++ b/packages/extension-task-item/package.json @@ -0,0 +1,17 @@ +{ + "name": "@tiptap/extension-task-item", + "version": "1.0.0", + "source": "index.ts", + "main": "dist/tiptap-extension-task-item.js", + "umd:main": "dist/tiptap-extension-task-item.umd.js", + "module": "dist/tiptap-extension-task-item.mjs", + "unpkg": "dist/tiptap-extension-task-item.js", + "jsdelivr": "dist/tiptap-extension-task-item.js", + "files": [ + "src", + "dist" + ], + "peerDependencies": { + "@tiptap/core": "2.x" + } +} diff --git a/packages/extension-task-list/index.ts b/packages/extension-task-list/index.ts new file mode 100644 index 00000000..76a61da6 --- /dev/null +++ b/packages/extension-task-list/index.ts @@ -0,0 +1,45 @@ +import { Command, createNode, mergeAttributes } from '@tiptap/core' +import { wrappingInputRule } from 'prosemirror-inputrules' + +const TaskList = createNode({ + name: 'task_list', + + group: 'block', + + content: 'task_item+', + + parseHTML() { + return [ + { + tag: 'ul[data-type="task_list"]', + priority: 51, + }, + ] + }, + + renderHTML({ attributes }) { + return ['ul', mergeAttributes(attributes, { 'data-type': 'task_list' }), 0] + }, + + addCommands() { + return { + taskList: (): Command => ({ commands }) => { + return commands.toggleList('task_list', 'task_item') + }, + } + }, + + addInputRules() { + return [ + wrappingInputRule(/^\s*(\[ \])\s$/, this.type), + ] + }, +}) + +export default TaskList + +declare module '@tiptap/core/src/Editor' { + interface AllExtensions { + TaskList: typeof TaskList, + } +} diff --git a/packages/extension-task-list/package.json b/packages/extension-task-list/package.json new file mode 100644 index 00000000..50add09a --- /dev/null +++ b/packages/extension-task-list/package.json @@ -0,0 +1,17 @@ +{ + "name": "@tiptap/extension-task-list", + "version": "1.0.0", + "source": "index.ts", + "main": "dist/tiptap-extension-task-list.js", + "umd:main": "dist/tiptap-extension-task-list.umd.js", + "module": "dist/tiptap-extension-task-list.mjs", + "unpkg": "dist/tiptap-extension-task-list.js", + "jsdelivr": "dist/tiptap-extension-task-list.js", + "files": [ + "src", + "dist" + ], + "peerDependencies": { + "@tiptap/core": "2.x" + } +} From cf8956bca1e081e8ef60572a99a81b2724bd6ad5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Fri, 30 Oct 2020 13:20:27 +0100 Subject: [PATCH 04/11] remove node view from image extension --- packages/extension-image/index.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/extension-image/index.ts b/packages/extension-image/index.ts index fccc8ff8..424cffc5 100644 --- a/packages/extension-image/index.ts +++ b/packages/extension-image/index.ts @@ -1,6 +1,4 @@ import { Command, createNode, nodeInputRule } from '@tiptap/core' -import { VueRenderer } from '@tiptap/vue' -import Vue from 'vue' export const inputRegex = /!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/ @@ -61,14 +59,6 @@ const Image = createNode({ }), ] }, - - addNodeView() { - const Component = Vue.extend({ - template: '
    this is a vue component
    ', - }) - - return VueRenderer(Component) - }, }) export default Image From b28a322d8be6282e985ac5ba4c430a9627b4943c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Fri, 30 Oct 2020 14:55:48 +0100 Subject: [PATCH 05/11] add node view to task item --- docs/src/demos/Extensions/TaskList/index.vue | 4 +- packages/core/src/ExtensionManager.ts | 24 ++++++--- packages/core/src/extensions/toggleList.ts | 2 +- packages/core/src/types.ts | 3 +- .../core/src/utils/getRenderedAttributes.ts | 4 +- packages/extension-task-item/index.ts | 53 ++++++++++++++++++- 6 files changed, 75 insertions(+), 15 deletions(-) diff --git a/docs/src/demos/Extensions/TaskList/index.vue b/docs/src/demos/Extensions/TaskList/index.vue index bae41d95..06d1f1be 100644 --- a/docs/src/demos/Extensions/TaskList/index.vue +++ b/docs/src/demos/Extensions/TaskList/index.vue @@ -39,8 +39,8 @@ export default { ], content: `
      -
    • A list item
    • -
    • And another one
    • +
    • A list item
    • +
    • And another one
    `, }) diff --git a/packages/core/src/ExtensionManager.ts b/packages/core/src/ExtensionManager.ts index 2d27f087..23b91aac 100644 --- a/packages/core/src/ExtensionManager.ts +++ b/packages/core/src/ExtensionManager.ts @@ -8,6 +8,8 @@ import { Extensions, NodeViewRenderer } from './types' import getSchema from './utils/getSchema' import getSchemaTypeByName from './utils/getSchemaTypeByName' import splitExtensions from './utils/splitExtensions' +import getAttributesFromExtensions from './utils/getAttributesFromExtensions' +import getRenderedAttributes from './utils/getRenderedAttributes' export default class ExtensionManager { @@ -97,14 +99,17 @@ export default class ExtensionManager { } get nodeViews() { + const { editor } = this const { nodeExtensions } = splitExtensions(this.extensions) + const allAttributes = getAttributesFromExtensions(this.extensions) return Object.fromEntries(nodeExtensions .filter(extension => !!extension.addNodeView) .map(extension => { + const extensionAttributes = allAttributes.filter(attribute => attribute.type === extension.name) const context = { options: extension.options, - editor: this.editor, + editor, type: getSchemaTypeByName(extension.name, this.schema), } @@ -115,12 +120,17 @@ export default class ExtensionManager { view: EditorView, getPos: (() => number) | boolean, decorations: Decoration[], - ) => renderer({ - editor: this.editor, - node, - getPos, - decorations, - }) + ) => { + const attributes = getRenderedAttributes(node, extensionAttributes) + + return renderer({ + editor, + node, + getPos, + decorations, + attributes, + }) + } return [extension.name, nodeview] })) diff --git a/packages/core/src/extensions/toggleList.ts b/packages/core/src/extensions/toggleList.ts index 2f7a15d3..26f97ea5 100644 --- a/packages/core/src/extensions/toggleList.ts +++ b/packages/core/src/extensions/toggleList.ts @@ -8,7 +8,7 @@ import getNodeType from '../utils/getNodeType' function isList(node: Node, schema: Schema) { return (node.type === schema.nodes.bullet_list || node.type === schema.nodes.ordered_list - || node.type === schema.nodes.todo_list) + || node.type === schema.nodes.task_list) } export const ToggleList = createExtension({ diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index d1385106..e0f09679 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -48,7 +48,8 @@ export type NodeViewRendererProps = { editor: Editor, node: Node, getPos: (() => number) | boolean, - decorations: Decoration[] + decorations: Decoration[], + attributes: AnyObject, } export type NodeViewRenderer = (props: NodeViewRendererProps) => NodeView diff --git a/packages/core/src/utils/getRenderedAttributes.ts b/packages/core/src/utils/getRenderedAttributes.ts index da92012d..48fd7021 100644 --- a/packages/core/src/utils/getRenderedAttributes.ts +++ b/packages/core/src/utils/getRenderedAttributes.ts @@ -1,8 +1,8 @@ import { Node, Mark } from 'prosemirror-model' -import { ExtensionAttribute } from '../types' +import { ExtensionAttribute, AnyObject } from '../types' import mergeAttributes from './mergeAttributes' -export default function getRenderedAttributes(nodeOrMark: Node | Mark, extensionAttributes: ExtensionAttribute[]): { [key: string]: any } { +export default function getRenderedAttributes(nodeOrMark: Node | Mark, extensionAttributes: ExtensionAttribute[]): AnyObject { return extensionAttributes .filter(item => item.attribute.rendered) .map(item => { diff --git a/packages/extension-task-item/index.ts b/packages/extension-task-item/index.ts index 6ac32943..283296cf 100644 --- a/packages/extension-task-item/index.ts +++ b/packages/extension-task-item/index.ts @@ -22,8 +22,14 @@ const TaskItem = createNode({ addAttributes() { return { - done: { + checked: { default: false, + parseHTML: element => ({ + checked: element.getAttribute('data-checked') === 'true', + }), + renderHTML: attributes => ({ + 'data-checked': attributes.checked, + }), }, } }, @@ -42,12 +48,55 @@ const TaskItem = createNode({ }, addKeyboardShortcuts() { - return { + const shortcuts = { Enter: () => this.editor.splitListItem('task_item'), + } + + if (!this.options.nested) { + return shortcuts + } + + return { + ...shortcuts, Tab: () => this.editor.sinkListItem('task_item'), 'Shift-Tab': () => this.editor.liftListItem('task_item'), } }, + + addNodeView() { + return ({ attributes, getPos, editor }) => { + const { view } = editor + const listItem = document.createElement('li') + const checkbox = document.createElement('input') + const content = document.createElement('div') + + checkbox.type = 'checkbox' + checkbox.addEventListener('change', event => { + const { checked } = event.target as any + + if (typeof getPos === 'function') { + view.dispatch(view.state.tr.setNodeMarkup(getPos(), undefined, { + checked, + })) + } + }) + + if (attributes['data-checked'] === true) { + checkbox.setAttribute('checked', 'checked') + } + + listItem.append(checkbox, content) + + Object.entries(attributes).forEach(([key, value]) => { + listItem.setAttribute(key, value) + }) + + return { + dom: listItem, + contentDOM: content, + } + } + }, }) export default TaskItem From 073ef2ee0df3e708b86f962344f0719f680edd20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Fri, 30 Oct 2020 15:20:10 +0100 Subject: [PATCH 06/11] add optional callback functions to schema fields --- packages/core/src/MarkExtension.ts | 8 +++---- packages/core/src/NodeExtension.ts | 20 ++++++++--------- packages/core/src/utils/callOrReturn.ts | 17 +++++++++++++++ packages/core/src/utils/getSchema.ts | 29 +++++++++++++------------ packages/extension-task-item/index.ts | 9 +++----- 5 files changed, 49 insertions(+), 34 deletions(-) create mode 100644 packages/core/src/utils/callOrReturn.ts diff --git a/packages/core/src/MarkExtension.ts b/packages/core/src/MarkExtension.ts index 4627b681..da1d1f41 100644 --- a/packages/core/src/MarkExtension.ts +++ b/packages/core/src/MarkExtension.ts @@ -10,22 +10,22 @@ export interface MarkExtensionSpec extends Overwrit /** * Inclusive */ - inclusive?: MarkSpec['inclusive'], + inclusive?: MarkSpec['inclusive'] | ((this: { options: Options }) => MarkSpec['inclusive']), /** * Excludes */ - excludes?: MarkSpec['excludes'], + excludes?: MarkSpec['excludes'] | ((this: { options: Options }) => MarkSpec['excludes']), /** * Group */ - group?: MarkSpec['group'], + group?: MarkSpec['group'] | ((this: { options: Options }) => MarkSpec['group']), /** * Spanning */ - spanning?: MarkSpec['spanning'], + spanning?: MarkSpec['spanning'] | ((this: { options: Options }) => MarkSpec['spanning']), /** * Parse HTML diff --git a/packages/core/src/NodeExtension.ts b/packages/core/src/NodeExtension.ts index 17d6e91d..37e6f2fc 100644 --- a/packages/core/src/NodeExtension.ts +++ b/packages/core/src/NodeExtension.ts @@ -15,52 +15,52 @@ export interface NodeExtensionSpec extends Overwrit /** * Content */ - content?: NodeSpec['content'], + content?: NodeSpec['content'] | ((this: { options: Options }) => NodeSpec['content']), /** * Marks */ - marks?: NodeSpec['marks'], + marks?: NodeSpec['marks'] | ((this: { options: Options }) => NodeSpec['marks']), /** * Group */ - group?: NodeSpec['group'], + group?: NodeSpec['group'] | ((this: { options: Options }) => NodeSpec['group']), /** * Inline */ - inline?: NodeSpec['inline'], + inline?: NodeSpec['inline'] | ((this: { options: Options }) => NodeSpec['inline']), /** * Atom */ - atom?: NodeSpec['atom'], + atom?: NodeSpec['atom'] | ((this: { options: Options }) => NodeSpec['atom']), /** * Selectable */ - selectable?: NodeSpec['selectable'], + selectable?: NodeSpec['selectable'] | ((this: { options: Options }) => NodeSpec['selectable']), /** * Draggable */ - draggable?: NodeSpec['draggable'], + draggable?: NodeSpec['draggable'] | ((this: { options: Options }) => NodeSpec['draggable']), /** * Code */ - code?: NodeSpec['code'], + code?: NodeSpec['code'] | ((this: { options: Options }) => NodeSpec['code']), /** * Defining */ - defining?: NodeSpec['defining'], + defining?: NodeSpec['defining'] | ((this: { options: Options }) => NodeSpec['defining']), /** * Isolating */ - isolating?: NodeSpec['isolating'], + isolating?: NodeSpec['isolating'] | ((this: { options: Options }) => NodeSpec['isolating']), /** * Parse HTML diff --git a/packages/core/src/utils/callOrReturn.ts b/packages/core/src/utils/callOrReturn.ts new file mode 100644 index 00000000..55e78e0d --- /dev/null +++ b/packages/core/src/utils/callOrReturn.ts @@ -0,0 +1,17 @@ +/** + * Optionally calls `value` as a function. + * Otherwise it is returned directly. + * @param value Function or any value. + * @param context Optional context to bind to function. + */ +export default function callOrReturn(value: any, context?: any) { + if (typeof value === 'function') { + if (context) { + return value.bind(context)() + } + + return value() + } + + return value +} diff --git a/packages/core/src/utils/getSchema.ts b/packages/core/src/utils/getSchema.ts index 817ed7a8..1a191f50 100644 --- a/packages/core/src/utils/getSchema.ts +++ b/packages/core/src/utils/getSchema.ts @@ -5,6 +5,7 @@ import getAttributesFromExtensions from './getAttributesFromExtensions' import getRenderedAttributes from './getRenderedAttributes' import isEmptyObject from './isEmptyObject' import injectExtensionAttributesToParseRule from './injectExtensionAttributesToParseRule' +import callOrReturn from './callOrReturn' function cleanUpSchemaItem(data: T) { return Object.fromEntries(Object.entries(data).filter(([key, value]) => { @@ -25,16 +26,16 @@ export default function getSchema(extensions: Extensions): Schema { const extensionAttributes = allAttributes.filter(attribute => attribute.type === extension.name) const context = { options: extension.options } const schema: NodeSpec = cleanUpSchemaItem({ - content: extension.content, - marks: extension.marks, - group: extension.group, - inline: extension.inline, - atom: extension.atom, - selectable: extension.selectable, - draggable: extension.draggable, - code: extension.code, - defining: extension.defining, - isolating: extension.isolating, + content: callOrReturn(extension.content, context), + marks: callOrReturn(extension.marks, context), + group: callOrReturn(extension.group, context), + inline: callOrReturn(extension.inline, context), + atom: callOrReturn(extension.atom, context), + selectable: callOrReturn(extension.selectable, context), + draggable: callOrReturn(extension.draggable, context), + code: callOrReturn(extension.code, context), + defining: callOrReturn(extension.defining, context), + isolating: callOrReturn(extension.isolating, context), attrs: Object.fromEntries(extensionAttributes.map(extensionAttribute => { return [extensionAttribute.name, { default: extensionAttribute?.attribute?.default }] })), @@ -60,10 +61,10 @@ export default function getSchema(extensions: Extensions): Schema { const extensionAttributes = allAttributes.filter(attribute => attribute.type === extension.name) const context = { options: extension.options } const schema: MarkSpec = cleanUpSchemaItem({ - inclusive: extension.inclusive, - excludes: extension.excludes, - group: extension.group, - spanning: extension.spanning, + inclusive: callOrReturn(extension.inclusive, context), + excludes: callOrReturn(extension.excludes, context), + group: callOrReturn(extension.group, context), + spanning: callOrReturn(extension.spanning, context), attrs: Object.fromEntries(extensionAttributes.map(extensionAttribute => { return [extensionAttribute.name, { default: extensionAttribute?.attribute?.default }] })), diff --git a/packages/extension-task-item/index.ts b/packages/extension-task-item/index.ts index 283296cf..dc91de8a 100644 --- a/packages/extension-task-item/index.ts +++ b/packages/extension-task-item/index.ts @@ -7,12 +7,9 @@ export interface TaskItemOptions { const TaskItem = createNode({ name: 'task_item', - content: 'paragraph+', - - // TODO: allow content to be a callback function - // content() { - // return this.options.nested ? '(paragraph|todo_list)+' : 'paragraph+', - // }, + content() { + return this.options.nested ? '(paragraph|task_list)+' : 'paragraph+' + }, defining: true, From e5e47aaa48a743c0be7784feaec69f1547bc7fa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Fri, 30 Oct 2020 15:46:15 +0100 Subject: [PATCH 07/11] add list option to nodes --- packages/core/src/NodeExtension.ts | 6 ++++++ packages/core/src/extensions/toggleList.ts | 16 ++++++---------- packages/core/src/utils/isList.ts | 13 +++++++++++++ packages/extension-bullet-list/index.ts | 2 ++ packages/extension-ordered-list/index.ts | 2 ++ packages/extension-task-list/index.ts | 2 ++ 6 files changed, 31 insertions(+), 10 deletions(-) create mode 100644 packages/core/src/utils/isList.ts diff --git a/packages/core/src/NodeExtension.ts b/packages/core/src/NodeExtension.ts index 37e6f2fc..cfb304d9 100644 --- a/packages/core/src/NodeExtension.ts +++ b/packages/core/src/NodeExtension.ts @@ -12,6 +12,11 @@ export interface NodeExtensionSpec extends Overwrit */ topNode?: boolean, + /** + * List + */ + list?: boolean, + /** * Content */ @@ -158,6 +163,7 @@ const defaultNode: NodeExtension = { type: 'node', name: 'node', topNode: false, + list: false, content: null, marks: null, group: null, diff --git a/packages/core/src/extensions/toggleList.ts b/packages/core/src/extensions/toggleList.ts index 26f97ea5..a0bc3288 100644 --- a/packages/core/src/extensions/toggleList.ts +++ b/packages/core/src/extensions/toggleList.ts @@ -1,23 +1,19 @@ import { wrapInList, liftListItem } from 'prosemirror-schema-list' import { findParentNode } from 'prosemirror-utils' -import { Node, NodeType, Schema } from 'prosemirror-model' +import { NodeType } from 'prosemirror-model' import { Command } from '../Editor' import { createExtension } from '../Extension' import getNodeType from '../utils/getNodeType' - -function isList(node: Node, schema: Schema) { - return (node.type === schema.nodes.bullet_list - || node.type === schema.nodes.ordered_list - || node.type === schema.nodes.task_list) -} +import isList from '../utils/isList' export const ToggleList = createExtension({ addCommands() { return { toggleList: (listTypeOrName: string | NodeType, itemTypeOrName: string | NodeType): Command => ({ tr, state, dispatch }) => { + const { extensions } = this.editor.options const listType = getNodeType(listTypeOrName, state.schema) const itemType = getNodeType(itemTypeOrName, state.schema) - const { schema, selection } = state + const { selection } = state const { $from, $to } = selection const range = $from.blockRange($to) @@ -25,14 +21,14 @@ export const ToggleList = createExtension({ return false } - const parentList = findParentNode(node => isList(node, schema))(selection) + const parentList = findParentNode(node => isList(node.type.name, extensions))(selection) if (range.depth >= 1 && parentList && range.depth - parentList.depth <= 1) { if (parentList.node.type === listType) { return liftListItem(itemType)(state, dispatch) } - if (isList(parentList.node, schema) && listType.validContent(parentList.node.content)) { + if (isList(parentList.node.type.name, extensions) && listType.validContent(parentList.node.content)) { tr.setNodeMarkup(parentList.pos, listType) return false diff --git a/packages/core/src/utils/isList.ts b/packages/core/src/utils/isList.ts new file mode 100644 index 00000000..3a5a2305 --- /dev/null +++ b/packages/core/src/utils/isList.ts @@ -0,0 +1,13 @@ +import { Extensions } from '../types' +import splitExtensions from './splitExtensions' + +export default function isList(name: string, extensions: Extensions) { + const { nodeExtensions } = splitExtensions(extensions) + const extension = nodeExtensions.find(item => item.name === name) + + if (!extension) { + return false + } + + return extension.list +} diff --git a/packages/extension-bullet-list/index.ts b/packages/extension-bullet-list/index.ts index cb949d70..d4d57246 100644 --- a/packages/extension-bullet-list/index.ts +++ b/packages/extension-bullet-list/index.ts @@ -4,6 +4,8 @@ import { wrappingInputRule } from 'prosemirror-inputrules' const BulletList = createNode({ name: 'bullet_list', + list: true, + group: 'block', content: 'list_item+', diff --git a/packages/extension-ordered-list/index.ts b/packages/extension-ordered-list/index.ts index 6c0aef3b..3255ad56 100644 --- a/packages/extension-ordered-list/index.ts +++ b/packages/extension-ordered-list/index.ts @@ -4,6 +4,8 @@ import { wrappingInputRule } from 'prosemirror-inputrules' const OrderedList = createNode({ name: 'ordered_list', + list: true, + group: 'block', content: 'list_item+', diff --git a/packages/extension-task-list/index.ts b/packages/extension-task-list/index.ts index 76a61da6..28f5a3d1 100644 --- a/packages/extension-task-list/index.ts +++ b/packages/extension-task-list/index.ts @@ -4,6 +4,8 @@ import { wrappingInputRule } from 'prosemirror-inputrules' const TaskList = createNode({ name: 'task_list', + list: true, + group: 'block', content: 'task_item+', From b99cdcdb6fb79bded3d142e6d85f41d7766b482c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Fri, 30 Oct 2020 16:06:24 +0100 Subject: [PATCH 08/11] improve default styling for node views --- packages/core/src/style.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/core/src/style.ts b/packages/core/src/style.ts index ce819062..e98e74b8 100644 --- a/packages/core/src/style.ts +++ b/packages/core/src/style.ts @@ -9,6 +9,14 @@ const style = `.ProseMirror { font-variant-ligatures: none; } +.ProseMirror [contenteditable="false"] { + white-space: normal; +} + +.ProseMirror [contenteditable="false"] [contenteditable="true"] { + white-space: pre-wrap; +} + .ProseMirror pre { white-space: pre-wrap; } From 62149cc5b2f3b16369eb5e4e1056f48f9e2a5db3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Fri, 30 Oct 2020 16:06:30 +0100 Subject: [PATCH 09/11] refactoring --- packages/extension-bullet-list/index.ts | 4 +++- packages/extension-ordered-list/index.ts | 4 +++- packages/extension-task-list/index.ts | 5 ++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/extension-bullet-list/index.ts b/packages/extension-bullet-list/index.ts index d4d57246..e116ca3c 100644 --- a/packages/extension-bullet-list/index.ts +++ b/packages/extension-bullet-list/index.ts @@ -1,6 +1,8 @@ import { Command, createNode } from '@tiptap/core' import { wrappingInputRule } from 'prosemirror-inputrules' +export const inputRegex = /^\s*([-+*])\s$/ + const BulletList = createNode({ name: 'bullet_list', @@ -36,7 +38,7 @@ const BulletList = createNode({ addInputRules() { return [ - wrappingInputRule(/^\s*([-+*])\s$/, this.type), + wrappingInputRule(inputRegex, this.type), ] }, }) diff --git a/packages/extension-ordered-list/index.ts b/packages/extension-ordered-list/index.ts index 3255ad56..e9d6e469 100644 --- a/packages/extension-ordered-list/index.ts +++ b/packages/extension-ordered-list/index.ts @@ -1,6 +1,8 @@ import { Command, createNode } from '@tiptap/core' import { wrappingInputRule } from 'prosemirror-inputrules' +export const inputRegex = /^(\d+)\.\s$/ + const OrderedList = createNode({ name: 'ordered_list', @@ -56,7 +58,7 @@ const OrderedList = createNode({ addInputRules() { return [ wrappingInputRule( - /^(\d+)\.\s$/, + inputRegex, this.type, match => ({ order: +match[1] }), (match, node) => node.childCount + node.attrs.order === +match[1], diff --git a/packages/extension-task-list/index.ts b/packages/extension-task-list/index.ts index 28f5a3d1..cada9581 100644 --- a/packages/extension-task-list/index.ts +++ b/packages/extension-task-list/index.ts @@ -1,6 +1,9 @@ import { Command, createNode, mergeAttributes } from '@tiptap/core' import { wrappingInputRule } from 'prosemirror-inputrules' +// TODO: add suport for [ ] and [x] +export const inputRegex = /^\s*(\[ \])\s$/ + const TaskList = createNode({ name: 'task_list', @@ -33,7 +36,7 @@ const TaskList = createNode({ addInputRules() { return [ - wrappingInputRule(/^\s*(\[ \])\s$/, this.type), + wrappingInputRule(inputRegex, this.type), ] }, }) From a8216d08407fc529c76baea40da48efa2df11763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Fri, 30 Oct 2020 16:51:25 +0100 Subject: [PATCH 10/11] move inputrule from task_list to task_item --- packages/extension-task-item/index.ts | 15 +++++++++++++++ packages/extension-task-list/index.ts | 10 ---------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/extension-task-item/index.ts b/packages/extension-task-item/index.ts index dc91de8a..ac64dceb 100644 --- a/packages/extension-task-item/index.ts +++ b/packages/extension-task-item/index.ts @@ -1,4 +1,7 @@ import { createNode, mergeAttributes } from '@tiptap/core' +import { wrappingInputRule } from 'prosemirror-inputrules' + +export const inputRegex = /^\s*(\[([ |x])\])\s$/ export interface TaskItemOptions { nested: boolean, @@ -94,6 +97,18 @@ const TaskItem = createNode({ } } }, + + addInputRules() { + return [ + wrappingInputRule( + inputRegex, + this.type, + match => ({ + checked: match[match.length - 1] === 'x', + }), + ), + ] + }, }) export default TaskItem diff --git a/packages/extension-task-list/index.ts b/packages/extension-task-list/index.ts index cada9581..bd73d1e2 100644 --- a/packages/extension-task-list/index.ts +++ b/packages/extension-task-list/index.ts @@ -1,8 +1,4 @@ import { Command, createNode, mergeAttributes } from '@tiptap/core' -import { wrappingInputRule } from 'prosemirror-inputrules' - -// TODO: add suport for [ ] and [x] -export const inputRegex = /^\s*(\[ \])\s$/ const TaskList = createNode({ name: 'task_list', @@ -33,12 +29,6 @@ const TaskList = createNode({ }, } }, - - addInputRules() { - return [ - wrappingInputRule(inputRegex, this.type), - ] - }, }) export default TaskList From 3e3dd26ca59578fcf825f5142dc518c773bdf1ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Fri, 30 Oct 2020 16:57:55 +0100 Subject: [PATCH 11/11] add inline option to image node --- docs/src/docPages/api/extensions/image.md | 7 ++++++- packages/extension-image/index.ts | 16 ++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/docs/src/docPages/api/extensions/image.md b/docs/src/docPages/api/extensions/image.md index 3c463ea5..1c3c9132 100644 --- a/docs/src/docPages/api/extensions/image.md +++ b/docs/src/docPages/api/extensions/image.md @@ -9,8 +9,13 @@ npm install @tiptap/extension-image yarn add @tiptap/extension-image ``` +## Settings +| Option | Type | Default | Description | +| ------ | ------- | ------- | ------------------------ | +| inline | boolean | false | Renders the node inline. | + ## Source code [packages/extension-image/](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-image/) ## Usage - + diff --git a/packages/extension-image/index.ts b/packages/extension-image/index.ts index 424cffc5..67e9c04a 100644 --- a/packages/extension-image/index.ts +++ b/packages/extension-image/index.ts @@ -1,13 +1,25 @@ import { Command, createNode, nodeInputRule } from '@tiptap/core' +export interface ImageOptions { + inline: boolean, +} + export const inputRegex = /!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/ const Image = createNode({ name: 'image', - inline: true, + defaultOptions: { + inline: false, + }, - group: 'inline', + inline() { + return this.options.inline + }, + + group() { + return this.options.inline ? 'inline' : 'block' + }, draggable: true,