From c52e7416f80b0ef078d999c28750d146a58c543a Mon Sep 17 00:00:00 2001 From: Eduard Metzger Date: Thu, 13 Jul 2023 16:30:41 -0500 Subject: [PATCH 01/48] Add tasks, quotes and style everything NotePlan style, the tasks still lack some definitions for serialization --- examples/editor/index.html | 7 +- examples/editor/src/App.tsx | 9 +- packages/core/src/editor.module.css | 17 +- .../extensions/Blocks/api/defaultBlocks.ts | 10 + .../extensions/Blocks/nodes/Block.module.css | 155 ++++++++++-- .../BulletListItemBlockContent.ts | 2 +- .../QuoteBlockContent.ts | 94 ++++++++ .../TaskListItemBlockContent.ts | 227 ++++++++++++++++++ 8 files changed, 485 insertions(+), 36 deletions(-) create mode 100644 packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/QuoteListItemBlockContent/QuoteBlockContent.ts create mode 100644 packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts diff --git a/examples/editor/index.html b/examples/editor/index.html index 1f8f0c6f25..f713d58f2d 100644 --- a/examples/editor/index.html +++ b/examples/editor/index.html @@ -4,7 +4,12 @@ - BlockNote demo + BlockNote demo 123 + +
diff --git a/examples/editor/src/App.tsx b/examples/editor/src/App.tsx index 55ce63eb93..f51d0db7c3 100644 --- a/examples/editor/src/App.tsx +++ b/examples/editor/src/App.tsx @@ -5,10 +5,17 @@ import styles from "./App.module.css"; type WindowWithProseMirror = Window & typeof globalThis & { ProseMirror: any }; +// Gets the previously stored editor contents. +const initialContent: string | null = localStorage.getItem("editorContent"); + function App() { const editor = useBlockNote({ + initialContent: initialContent ? JSON.parse(initialContent) : undefined, onEditorContentChange: (editor) => { - console.log(editor.topLevelBlocks); + localStorage.setItem( + "editorContent", + JSON.stringify(editor.topLevelBlocks) + ); }, editorDOMAttributes: { class: styles.editor, diff --git a/packages/core/src/editor.module.css b/packages/core/src/editor.module.css index c6fdd22160..dc7577a164 100644 --- a/packages/core/src/editor.module.css +++ b/packages/core/src/editor.module.css @@ -1,5 +1,3 @@ -@import url("./assets/fonts-inter.css"); - .bnEditor { outline: none; padding-inline: 54px; @@ -47,22 +45,21 @@ Tippy popups that are appended to document.body directly .defaultStyles { font-size: 16px; - font-weight: normal; - font-family: "Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont, - "Open Sans", "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", - "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + font-weight: 500; + font-family: "-apple-system", "system-ui", "BlinkMacSystemFont", + "Helvetica Neue"; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } [data-theme="light"] { - background-color: #FFFFFF; - color: #3F3F3F; + background-color: #ffffff; + color: #3f3f3f; } [data-theme="dark"] { - background-color: #1F1F1F; - color: #CFCFCF; + background-color: #1f1f1f; + color: #cfcfcf; } .dragPreview { diff --git a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts index d60d716cce..2f61bb2a7f 100644 --- a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts +++ b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts @@ -1,6 +1,8 @@ import { HeadingBlockContent } from "../nodes/BlockContent/HeadingBlockContent/HeadingBlockContent"; import { BulletListItemBlockContent } from "../nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent"; import { NumberedListItemBlockContent } from "../nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent"; +import { QuoteBlockContent } from "../nodes/BlockContent/ListItemBlockContent/QuoteListItemBlockContent/QuoteBlockContent"; +import { TaskListItemBlockContent } from "../nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent"; import { ParagraphBlockContent } from "../nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent"; import { PropSchema, TypesMatch } from "./blockTypes"; @@ -35,6 +37,14 @@ export const defaultBlockSchema = { propSchema: defaultProps, node: BulletListItemBlockContent, }, + taskListItem: { + propSchema: defaultProps, + node: TaskListItemBlockContent, + }, + quote: { + propSchema: defaultProps, + node: QuoteBlockContent, + }, numberedListItem: { propSchema: defaultProps, node: NumberedListItemBlockContent, diff --git a/packages/core/src/extensions/Blocks/nodes/Block.module.css b/packages/core/src/extensions/Blocks/nodes/Block.module.css index 91c960b757..58a80765bf 100644 --- a/packages/core/src/extensions/Blocks/nodes/Block.module.css +++ b/packages/core/src/extensions/Blocks/nodes/Block.module.css @@ -1,10 +1,22 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; +@tailwind colors; + +:root { + --blue-noteplan: #0091f8; + --orange-noteplan: #d87001; + --orange-noteplan-hsl: 31, 99%, 43%; +} + /* BASIC STYLES */ .blockOuter { - line-height: 1.5; + line-height: 1.4; transition: margin 0.2s; + padding-top: 0.55em; } /*Ensures blocks & block content spans editor width*/ @@ -20,7 +32,6 @@ BASIC STYLES } .blockContent { - padding: 3px 0; flex-grow: 1; transition: font-size 0.2s; /* @@ -40,7 +51,7 @@ NESTED BLOCKS */ .blockGroup .blockGroup { - margin-left: 1.5em; + margin-left: 2em; } .blockGroup .blockGroup > .blockOuter { @@ -51,8 +62,9 @@ NESTED BLOCKS content: " "; display: inline; position: absolute; - left: -20px; - height: 100%; + left: -1.45rem; + top: 5px; + bottom: 0px; transition: all 0.2s 0.1s; } @@ -60,14 +72,16 @@ NESTED BLOCKS .blockGroup .blockGroup > .blockOuter:not([data-prev-depth-changed])::before { - border-left: 1px solid #AFAFAF; + width: 1px; + background: #e5e5e5; } [data-theme="dark"] .blockGroup .blockGroup > .blockOuter:not([data-prev-depth-changed])::before { - border-left: 1px solid #7F7F7F; + width: 1px; + background: #7f7f7f; } .blockGroup .blockGroup > .blockOuter[data-prev-depth-change="-2"]::before { @@ -159,6 +173,7 @@ NESTED BLOCKS /* Ordered */ [data-content-type="numberedListItem"] { --index: attr(data-index); + padding-left: 1.2em; } [data-prev-type="numberedListItem"] { @@ -168,38 +183,79 @@ NESTED BLOCKS .blockOuter[data-prev-type="numberedListItem"]:not([data-prev-index="none"]) > .block > .blockContent::before { - margin-right: 1.2em; + margin-right: 0.6em; + margin-left: -1.2em; content: var(--prev-index) "."; } .blockOuter:not([data-prev-type]) > .block > .blockContent[data-content-type="numberedListItem"]::before { - margin-right: 1.2em; + margin-right: 0.6em; + margin-left: -1.2em; content: var(--index) "."; } +/* Quotes */ +[data-content-type="quote"] { + font-style: italic; + position: relative; + padding-left: 1.63em; + color: #666666; +} + +.blockOuter:not([data-prev-type]) + > .block + > .blockContent[data-content-type="quote"]::before { + content: " "; + margin-right: 1.2em; + + position: absolute; + top: 0px; + bottom: 0px; + left: 0.45rem; + width: 5px; + background: #95c8fc; + border-radius: 10px; +} + /* Unordered */ /* No list nesting */ -.blockOuter[data-prev-type="bulletListItem"] > .block > .blockContent::before { - margin-right: 1.2em; - content: "•"; +[data-content-type="bulletListItem"] { + position: relative; + padding-left: 1.62em; } +/* +.blockOuter[data-prev-type="bulletListItem"] > .block > .blockContent::before { + position: absolute; + top: 1.05rem; + left: 0.45rem; + content: "\e122"; + font: var(--fa-font-solid); + color: var(--orange-noteplan); + font-size: 8px; +} */ .blockOuter:not([data-prev-type]) > .block > .blockContent[data-content-type="bulletListItem"]::before { - margin-right: 1.2em; - content: "•"; + position: absolute; + top: 0.45rem; + left: 0.45rem; + content: "\e122"; + font: var(--fa-font-solid); + color: var(--orange-noteplan); + font-size: 8px; } /* 1 level of list nesting */ -[data-content-type="bulletListItem"] +/* [data-content-type="bulletListItem"] ~ .blockGroup > .blockOuter[data-prev-type="bulletListItem"] > .block > .blockContent::before { - margin-right: 1.2em; + margin-right: 0.77em; + margin-left: -1.22em; content: "◦"; } @@ -208,11 +264,11 @@ NESTED BLOCKS > .blockOuter:not([data-prev-type]) > .block > .blockContent[data-content-type="bulletListItem"]::before { - margin-right: 1.2em; + margin-right: 0.77em; + margin-left: -1.22em; content: "◦"; } -/* 2 levels of list nesting */ [data-content-type="bulletListItem"] ~ .blockGroup [data-content-type="bulletListItem"] @@ -220,7 +276,8 @@ NESTED BLOCKS > .blockOuter[data-prev-type="bulletListItem"] > .block > .blockContent::before { - margin-right: 1.2em; + margin-right: 0.77em; + margin-left: -1.22em; content: "▪"; } @@ -231,9 +288,61 @@ NESTED BLOCKS > .blockOuter:not([data-prev-type]) > .block > .blockContent[data-content-type="bulletListItem"]::before { - margin-right: 1.2em; + margin-right: 0.6em; + margin-left: -1.2em; content: "▪"; } +*/ + +/* Tasks */ +[data-content-type="taskListItem"] { + padding-left: 1.62em; + position: relative; + + &[data-checked="false"] > label:before { + content: "\f111"; + font: var(--fa-font-regular); + color: var(--orange-noteplan); + } + + &[data-checked="true"] > label:before { + content: "\f058"; + font: var(--fa-font-regular); + } + + &[data-checked="true"] { + opacity: 0.4; + } + + > label { + user-select: none; + font: var(--fa-font-regular); + display: inline-block; + position: absolute; + top: 0.2rem; + left: 0.12rem; + } + + > label > input[type="checkbox"] { + cursor: pointer; + display: none; + } +} + +/* .blockOuter[data-prev-type="taskListItem"] > .block > .blockContent::before { + margin-right: 0.6em; + margin-left: -1.2em; + +} */ + +/* .blockOuter:not([data-prev-type]) + > .block + > .blockContent[data-content-type="taskListItem"]::before { + margin-right: 0.6em; + margin-left: -1.2em; + content: "\f058"; + font: var(--fa-font-regular); +} */ /* PLACEHOLDERS*/ @@ -250,12 +359,12 @@ NESTED BLOCKS [data-theme="light"] .isEmpty .inlineContent:before, .isFilter .inlineContent:before { - color: #CFCFCF; + color: #cfcfcf; } [data-theme="dark"] .isEmpty .inlineContent:before, .isFilter .inlineContent:before { - color: #7F7F7F; + color: #7f7f7f; } /* TODO: would be nicer if defined from code */ @@ -274,7 +383,7 @@ NESTED BLOCKS .blockContent[data-content-type="bulletListItem"].isEmpty .inlineContent:before, .blockContent[data-content-type="numberedListItem"].isEmpty -.inlineContent:before { + .inlineContent:before { content: "List"; } diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts index 58ef2002ff..51b79cedf7 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts @@ -11,7 +11,7 @@ export const BulletListItemBlockContent = createTipTapBlock<"bulletListItem">({ return [ // Creates an unordered list when starting with "-", "+", or "*". new InputRule({ - find: new RegExp(`^[-+*]\\s$`), + find: new RegExp(`^[-]\\s$`), handler: ({ state, chain, range }) => { chain() .BNUpdateBlock(state.selection.from, { diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/QuoteListItemBlockContent/QuoteBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/QuoteListItemBlockContent/QuoteBlockContent.ts new file mode 100644 index 0000000000..1928369fe3 --- /dev/null +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/QuoteListItemBlockContent/QuoteBlockContent.ts @@ -0,0 +1,94 @@ +import { InputRule, mergeAttributes } from "@tiptap/core"; +import { createTipTapBlock } from "../../../../api/block"; +import { handleEnter } from "../ListItemKeyboardShortcuts"; +import styles from "../../../Block.module.css"; + +export const QuoteBlockContent = createTipTapBlock<"quote">({ + name: "quote", + content: "inline*", + + addInputRules() { + return [ + // Creates an unordered list when starting with "-", "+", or "*". + new InputRule({ + find: new RegExp(`^[>]\\s$`), + handler: ({ state, chain, range }) => { + chain() + .BNUpdateBlock(state.selection.from, { + type: "quote", + props: {}, + }) + // Removes the "-", "+", or "*" character used to set the list. + .deleteRange({ from: range.from, to: range.to }); + }, + }), + ]; + }, + + addKeyboardShortcuts() { + return { + Enter: () => handleEnter(this.editor), + }; + }, + + parseHTML() { + return [ + // Case for regular HTML list structure. + { + tag: "li", + getAttrs: (element) => { + if (typeof element === "string") { + return false; + } + + const parent = element.parentElement; + + if (parent === null) { + return false; + } + + if (parent.tagName === "UL") { + return {}; + } + + return false; + }, + node: "bulletListItem", + }, + // Case for BlockNote list structure. + { + tag: "p", + getAttrs: (element) => { + if (typeof element === "string") { + return false; + } + + const parent = element.parentElement; + + if (parent === null) { + return false; + } + + if (parent.getAttribute("data-content-type") === "quote") { + return {}; + } + + return false; + }, + priority: 300, + node: "quote", + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return [ + "div", + mergeAttributes(HTMLAttributes, { + class: styles.blockContent, + "data-content-type": this.name, + }), + ["p", { class: styles.inlineContent }, 0], + ]; + }, +}); diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts new file mode 100644 index 0000000000..40246f221f --- /dev/null +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts @@ -0,0 +1,227 @@ +import { InputRule, mergeAttributes } from "@tiptap/core"; +import { createTipTapBlock } from "../../../../api/block"; +import { handleEnter } from "../ListItemKeyboardShortcuts"; +import styles from "../../../Block.module.css"; + +import { NodeView } from "prosemirror-view"; +import { Node } from "prosemirror-model"; + +function addTaskListItemBlockContentView( + node: Node, + editor: EditorView, + getPos: () => number +): NodeView { + const dom = document.createElement("div"); + const checked = node.attrs.checked || false; + let altKey = false; + + dom.className = "blockContent"; + dom.dataset.contentType = "taskListItem"; + dom.dataset.checked = checked; + + const label = document.createElement("label"); + const input = document.createElement("input"); + const span = document.createElement("span"); + const content = document.createElement("div"); + + input.type = "checkbox"; + input.checked = checked; + + content.classList.add(styles.inlineContent); + + input.addEventListener("click", (event) => { + altKey = event.altKey; + }); + + input.addEventListener("change", (event) => { + const { checked } = event.target; + if (editor.isEditable && typeof getPos === "function") { + editor + .chain() + .focus(undefined, { scrollIntoView: false }) + .command(({ tr }) => { + const position = getPos(); + const currentNode = tr.doc.nodeAt(position); + tr.setNodeMarkup(position, undefined, { + ...(currentNode === null || currentNode === void 0 + ? void 0 + : currentNode.attrs), + checked: altKey ? true : checked, + cancelled: altKey ? true : false, + }); + return true; + }) + .run(); + } + if (!editor.isEditable && this.options.onReadOnlyChecked) { + // Reset state if onReadOnlyChecked returns false + if (!this.options.onReadOnlyChecked(node, checked)) { + checkbox.checked = !checkbox.checked; + } + } + }); + + label.appendChild(input); + label.appendChild(span); + dom.appendChild(label); + dom.appendChild(content); + + return { + dom: dom, + contentDOM: content, + update: (updatedNode: Node) => { + if (updatedNode.type !== node.type) { + return false; + } + + dom.dataset.checked = updatedNode.attrs.checked || false; + + if (updatedNode.attrs.checked) { + input.setAttribute("checked", "checked"); + } else { + input.removeAttribute("checked"); + } + + return true; + }, + }; +} + +export const TaskListItemBlockContent = createTipTapBlock<"taskListItem">({ + name: "taskListItem", + content: "inline*", + + addInputRules() { + return [ + // Creates an unordered list when starting with "*". + new InputRule({ + find: new RegExp(`^[*]\\s$`), + handler: ({ state, chain, range }) => { + chain() + .BNUpdateBlock(state.selection.from, { + type: "taskListItem", + props: {}, + }) + // Removes the "-", "+", or "*" character used to set the list. + .deleteRange({ from: range.from, to: range.to }); + }, + }), + ]; + }, + + addKeyboardShortcuts() { + return { + Enter: () => handleEnter(this.editor), + }; + }, + + addAttributes() { + return { + checked: { + default: false, + keepOnSplit: false, + parseHTML: (element) => element.getAttribute("data-checked") === "true", + renderHTML: (attributes) => ({ + "data-checked": attributes.checked, + }), + }, + cancelled: { + default: false, + keepOnSplit: false, + parseHTML: (element) => + element.getAttribute("data-cancelled") === "true", + renderHTML: (attributes) => ({ + "data-cancelled": attributes.cancelled, + }), + }, + checklist: { + default: false, + keepOnSplit: true, + parseHTML: (element) => + element.getAttribute("data-checklist") === "true", + renderHTML: (attributes) => ({ + "data-checklist": attributes.checklist, + }), + }, + }; + }, + + parseHTML() { + return [ + // Case for regular HTML list structure. + { + tag: "li", + getAttrs: (element) => { + if (typeof element === "string") { + return false; + } + + const parent = element.parentElement; + + if (parent === null) { + return false; + } + + if (parent.tagName === "UL") { + return {}; + } + + return false; + }, + node: "taskListItem", + }, + // Case for BlockNote list structure. + { + tag: "p", + getAttrs: (element) => { + if (typeof element === "string") { + return false; + } + + const parent = element.parentElement; + + if (parent === null) { + return false; + } + + if (parent.getAttribute("data-content-type") === "taskListItem") { + return {}; + } + + return false; + }, + priority: 300, + node: "taskListItem", + }, + ]; + }, + + renderHTML({ HTMLAttributes, node }) { + const checked = node.attrs.checked || false; + + return [ + "div", + mergeAttributes(HTMLAttributes, { + class: styles.blockContent, + "data-content-type": this.name, + }), + [ + "label", + [ + "input", + { + type: "checkbox", + }, + ], + ["span"], + ], + ["div", { class: styles.inlineContent }, 0], + ]; + }, + + addNodeView() { + return ({ node, HTMLAttributes, getPos, editor }) => { + return addTaskListItemBlockContentView(node, editor, getPos); + }; + }, +}); From a9a9345d4169cbf03bcfcaedde2f1a8bd1cb4586 Mon Sep 17 00:00:00 2001 From: Eduard Metzger Date: Fri, 14 Jul 2023 09:57:02 -0500 Subject: [PATCH 02/48] make more adjustments, fix errors and warnings, make tasks work --- .../extensions/Blocks/api/defaultBlocks.ts | 9 +++++++ .../extensions/Blocks/nodes/Block.module.css | 4 +-- .../BulletListItemBlockContent.ts | 5 ++-- .../ListItemKeyboardShortcuts.ts | 25 ++++++++++++++++++ .../TaskListItemBlockContent.ts | 26 +++++++++---------- 5 files changed, 51 insertions(+), 18 deletions(-) diff --git a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts index 2f61bb2a7f..c4a72dd4c2 100644 --- a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts +++ b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts @@ -17,6 +17,15 @@ export const defaultProps = { default: "left" as const, values: ["left", "center", "right", "justify"] as const, }, + checked: { + default: "false" as const, + }, + cancelled: { + default: "false", + }, + checklist: { + default: "false", + }, } satisfies PropSchema; export type DefaultProps = typeof defaultProps; diff --git a/packages/core/src/extensions/Blocks/nodes/Block.module.css b/packages/core/src/extensions/Blocks/nodes/Block.module.css index 58a80765bf..6847406c4f 100644 --- a/packages/core/src/extensions/Blocks/nodes/Block.module.css +++ b/packages/core/src/extensions/Blocks/nodes/Block.module.css @@ -1,7 +1,7 @@ -@tailwind base; +/* @tailwind base; @tailwind components; @tailwind utilities; -@tailwind colors; +@tailwind colors; */ :root { --blue-noteplan: #0091f8; diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts index 51b79cedf7..ee81bf47b7 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts @@ -7,9 +7,10 @@ export const BulletListItemBlockContent = createTipTapBlock<"bulletListItem">({ name: "bulletListItem", content: "inline*", + // This is needed to detect when the user types "-", so it gets converted into a bullet item. addInputRules() { return [ - // Creates an unordered list when starting with "-", "+", or "*". + // Creates an unordered list when starting with "-",. new InputRule({ find: new RegExp(`^[-]\\s$`), handler: ({ state, chain, range }) => { @@ -18,7 +19,7 @@ export const BulletListItemBlockContent = createTipTapBlock<"bulletListItem">({ type: "bulletListItem", props: {}, }) - // Removes the "-", "+", or "*" character used to set the list. + // Removes the "-" character used to set the list. .deleteRange({ from: range.from, to: range.to }); }, }), diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts index 01f7474eab..22b5ea7c29 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts @@ -45,3 +45,28 @@ export const handleEnter = (editor: Editor) => { }), ]); }; + +export const handleCheckingTask = (editor: Editor) => { + const node = editor.state.selection.$from.node(-1); + + console.log(node.attrs); + if (node) { + // transaction to toggle checked attribute + return editor + .chain() + .command(({ tr }) => { + tr.setNodeMarkup( + editor.state.selection.$from.before(-1) + 1, + undefined, + { + checked: !node.attrs.checked, + cancelled: false, + checklist: node.attrs.checklist, + } + ); + return true; + }) + .run(); + } + return false; +}; diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts index 40246f221f..44e3f71ca5 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts @@ -1,6 +1,6 @@ -import { InputRule, mergeAttributes } from "@tiptap/core"; +import { Editor, InputRule, mergeAttributes } from "@tiptap/core"; import { createTipTapBlock } from "../../../../api/block"; -import { handleEnter } from "../ListItemKeyboardShortcuts"; +import { handleEnter, handleCheckingTask } from "../ListItemKeyboardShortcuts"; import styles from "../../../Block.module.css"; import { NodeView } from "prosemirror-view"; @@ -8,8 +8,8 @@ import { Node } from "prosemirror-model"; function addTaskListItemBlockContentView( node: Node, - editor: EditorView, - getPos: () => number + editor: Editor, + getPos: (() => number) | boolean ): NodeView { const dom = document.createElement("div"); const checked = node.attrs.checked || false; @@ -34,7 +34,7 @@ function addTaskListItemBlockContentView( }); input.addEventListener("change", (event) => { - const { checked } = event.target; + const checked = (event.target as HTMLInputElement).checked; if (editor.isEditable && typeof getPos === "function") { editor .chain() @@ -53,11 +53,9 @@ function addTaskListItemBlockContentView( }) .run(); } - if (!editor.isEditable && this.options.onReadOnlyChecked) { + if (!editor.isEditable) { // Reset state if onReadOnlyChecked returns false - if (!this.options.onReadOnlyChecked(node, checked)) { - checkbox.checked = !checkbox.checked; - } + input.checked = !input.checked; } }); @@ -91,6 +89,7 @@ export const TaskListItemBlockContent = createTipTapBlock<"taskListItem">({ name: "taskListItem", content: "inline*", + // This is needed to detect when the user types "*", so it gets converted into a task item. addInputRules() { return [ // Creates an unordered list when starting with "*". @@ -102,7 +101,7 @@ export const TaskListItemBlockContent = createTipTapBlock<"taskListItem">({ type: "taskListItem", props: {}, }) - // Removes the "-", "+", or "*" character used to set the list. + // Removes the "*" character used to set the list. .deleteRange({ from: range.from, to: range.to }); }, }), @@ -112,6 +111,7 @@ export const TaskListItemBlockContent = createTipTapBlock<"taskListItem">({ addKeyboardShortcuts() { return { Enter: () => handleEnter(this.editor), + "Cmd-d": () => handleCheckingTask(this.editor), }; }, @@ -196,9 +196,7 @@ export const TaskListItemBlockContent = createTipTapBlock<"taskListItem">({ ]; }, - renderHTML({ HTMLAttributes, node }) { - const checked = node.attrs.checked || false; - + renderHTML({ HTMLAttributes }) { return [ "div", mergeAttributes(HTMLAttributes, { @@ -220,7 +218,7 @@ export const TaskListItemBlockContent = createTipTapBlock<"taskListItem">({ }, addNodeView() { - return ({ node, HTMLAttributes, getPos, editor }) => { + return ({ node, getPos, editor }) => { return addTaskListItemBlockContentView(node, editor, getPos); }; }, From b2f7ccdab0dd76e0679b6b1dc2571991aa5c8b37 Mon Sep 17 00:00:00 2001 From: Eduard Metzger Date: Fri, 14 Jul 2023 16:41:04 -0500 Subject: [PATCH 03/48] Fix CMD+D to complete tasks and add CMD+S to cancel tasks --- examples/editor/src/App.tsx | 4 +- .../extensions/Blocks/nodes/Block.module.css | 11 +++++- .../ListItemKeyboardShortcuts.ts | 39 +++++++++++++------ .../TaskListItemBlockContent.ts | 13 +++++-- 4 files changed, 50 insertions(+), 17 deletions(-) diff --git a/examples/editor/src/App.tsx b/examples/editor/src/App.tsx index f51d0db7c3..56239899fd 100644 --- a/examples/editor/src/App.tsx +++ b/examples/editor/src/App.tsx @@ -3,7 +3,8 @@ import "@blocknote/core/style.css"; import { BlockNoteView, useBlockNote } from "@blocknote/react"; import styles from "./App.module.css"; -type WindowWithProseMirror = Window & typeof globalThis & { ProseMirror: any }; +type WindowWithProseMirror = Window & + typeof globalThis & { ProseMirror: any; editor?: any }; // Gets the previously stored editor contents. const initialContent: string | null = localStorage.getItem("editorContent"); @@ -26,6 +27,7 @@ function App() { // Give tests a way to get prosemirror instance (window as WindowWithProseMirror).ProseMirror = editor?._tiptapEditor; + (window as WindowWithProseMirror).editor = editor; return ; } diff --git a/packages/core/src/extensions/Blocks/nodes/Block.module.css b/packages/core/src/extensions/Blocks/nodes/Block.module.css index 6847406c4f..ce0ca68225 100644 --- a/packages/core/src/extensions/Blocks/nodes/Block.module.css +++ b/packages/core/src/extensions/Blocks/nodes/Block.module.css @@ -299,6 +299,11 @@ NESTED BLOCKS padding-left: 1.62em; position: relative; + &[data-checked="true"], + &[data-cancelled="true"] { + opacity: 0.4; + } + &[data-checked="false"] > label:before { content: "\f111"; font: var(--fa-font-regular); @@ -310,8 +315,10 @@ NESTED BLOCKS font: var(--fa-font-regular); } - &[data-checked="true"] { - opacity: 0.4; + &[data-cancelled="true"] > label:before { + content: "\f057"; + font: var(--fa-font-regular); + color: inherit; } > label { diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts index 22b5ea7c29..fd351ea198 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts @@ -46,8 +46,8 @@ export const handleEnter = (editor: Editor) => { ]); }; -export const handleCheckingTask = (editor: Editor) => { - const node = editor.state.selection.$from.node(-1); +export const handleComplete = (editor: Editor) => { + const node = editor.state.selection.$from.node(); console.log(node.attrs); if (node) { @@ -55,15 +55,32 @@ export const handleCheckingTask = (editor: Editor) => { return editor .chain() .command(({ tr }) => { - tr.setNodeMarkup( - editor.state.selection.$from.before(-1) + 1, - undefined, - { - checked: !node.attrs.checked, - cancelled: false, - checklist: node.attrs.checklist, - } - ); + tr.setNodeMarkup(editor.state.selection.$from.before(), undefined, { + checked: !node.attrs.checked, + cancelled: false, + checklist: node.attrs.checklist, + }); + return true; + }) + .run(); + } + return false; +}; + +export const handleCancel = (editor: Editor) => { + const node = editor.state.selection.$from.node(); + + console.log(node.attrs); + if (node) { + // transaction to toggle checked attribute + return editor + .chain() + .command(({ tr }) => { + tr.setNodeMarkup(editor.state.selection.$from.before(), undefined, { + checked: false, + cancelled: !node.attrs.cancelled, + checklist: node.attrs.checklist, + }); return true; }) .run(); diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts index 44e3f71ca5..b4d365408f 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts @@ -1,6 +1,10 @@ import { Editor, InputRule, mergeAttributes } from "@tiptap/core"; import { createTipTapBlock } from "../../../../api/block"; -import { handleEnter, handleCheckingTask } from "../ListItemKeyboardShortcuts"; +import { + handleEnter, + handleComplete, + handleCancel, +} from "../ListItemKeyboardShortcuts"; import styles from "../../../Block.module.css"; import { NodeView } from "prosemirror-view"; @@ -18,6 +22,7 @@ function addTaskListItemBlockContentView( dom.className = "blockContent"; dom.dataset.contentType = "taskListItem"; dom.dataset.checked = checked; + dom.dataset.cancelled = node.attrs.cancelled || false; const label = document.createElement("label"); const input = document.createElement("input"); @@ -73,6 +78,7 @@ function addTaskListItemBlockContentView( } dom.dataset.checked = updatedNode.attrs.checked || false; + dom.dataset.cancelled = updatedNode.attrs.cancelled || false; if (updatedNode.attrs.checked) { input.setAttribute("checked", "checked"); @@ -111,7 +117,8 @@ export const TaskListItemBlockContent = createTipTapBlock<"taskListItem">({ addKeyboardShortcuts() { return { Enter: () => handleEnter(this.editor), - "Cmd-d": () => handleCheckingTask(this.editor), + "Cmd-d": () => handleComplete(this.editor), + "Cmd-s": () => handleCancel(this.editor), }; }, @@ -119,7 +126,7 @@ export const TaskListItemBlockContent = createTipTapBlock<"taskListItem">({ return { checked: { default: false, - keepOnSplit: false, + keepOnSplit: false, // When you hit enter and create a new task in the middle or empty, don't keep the checked attribute parseHTML: (element) => element.getAttribute("data-checked") === "true", renderHTML: (attributes) => ({ "data-checked": attributes.checked, From 95192e03bbd6be667ceed8c7f12c8fb34dacda8d Mon Sep 17 00:00:00 2001 From: Eduard Metzger Date: Fri, 14 Jul 2023 18:51:07 -0500 Subject: [PATCH 04/48] Add checklists --- .../extensions/Blocks/nodes/Block.module.css | 17 ++++++++++++++ .../TaskListItemBlockContent.ts | 22 ++++++++++++++----- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/packages/core/src/extensions/Blocks/nodes/Block.module.css b/packages/core/src/extensions/Blocks/nodes/Block.module.css index ce0ca68225..dbcab2be3a 100644 --- a/packages/core/src/extensions/Blocks/nodes/Block.module.css +++ b/packages/core/src/extensions/Blocks/nodes/Block.module.css @@ -321,6 +321,18 @@ NESTED BLOCKS color: inherit; } + &[data-checked="false"][data-checklist="true"] > label:before { + content: "\f0c8"; + } + + &[data-checked="true"][data-checklist="true"] > label:before { + content: "\f14a"; + } + + &[data-cancelled="true"][data-checklist="true"] > label:before { + content: "\f2d3"; + } + > label { user-select: none; font: var(--fa-font-regular); @@ -334,6 +346,11 @@ NESTED BLOCKS cursor: pointer; display: none; } + + &[data-checklist="true"] { + left: 0.06rem; + padding-left: 1.56em; + } } /* .blockOuter[data-prev-type="taskListItem"] > .block > .blockContent::before { diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts index b4d365408f..af67c5e912 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts @@ -1,4 +1,9 @@ -import { Editor, InputRule, mergeAttributes } from "@tiptap/core"; +import { + Editor, + InputRule, + wrappingInputRule, + mergeAttributes, +} from "@tiptap/core"; import { createTipTapBlock } from "../../../../api/block"; import { handleEnter, @@ -23,6 +28,7 @@ function addTaskListItemBlockContentView( dom.dataset.contentType = "taskListItem"; dom.dataset.checked = checked; dom.dataset.cancelled = node.attrs.cancelled || false; + dom.dataset.checklist = node.attrs.checklist || false; const label = document.createElement("label"); const input = document.createElement("input"); @@ -79,6 +85,7 @@ function addTaskListItemBlockContentView( dom.dataset.checked = updatedNode.attrs.checked || false; dom.dataset.cancelled = updatedNode.attrs.cancelled || false; + dom.dataset.checklist = updatedNode.attrs.checklist || false; if (updatedNode.attrs.checked) { input.setAttribute("checked", "checked"); @@ -100,12 +107,17 @@ export const TaskListItemBlockContent = createTipTapBlock<"taskListItem">({ return [ // Creates an unordered list when starting with "*". new InputRule({ - find: new RegExp(`^[*]\\s$`), - handler: ({ state, chain, range }) => { + find: new RegExp(`^([*+])\\s$`), + handler: ({ state, chain, range, match }) => { + const isCheckList = match && match.length > 0 ? match[1] : false; chain() .BNUpdateBlock(state.selection.from, { - type: "taskListItem", - props: {}, + type: this.name, + props: { + checked: "false", + canceled: "false", + checklist: isCheckList === "+" ? "true" : "false", + }, }) // Removes the "*" character used to set the list. .deleteRange({ from: range.from, to: range.to }); From 60b1432ca34be6896ef7f77425df77972f989acf Mon Sep 17 00:00:00 2001 From: Eduard Metzger Date: Fri, 14 Jul 2023 19:25:32 -0500 Subject: [PATCH 05/48] update quotes so they copy when hitting enter at the end --- .../src/extensions/Blocks/api/defaultBlocks.ts | 6 +++--- .../src/extensions/Blocks/nodes/Block.module.css | 4 ++-- .../ListItemKeyboardShortcuts.ts | 2 -- .../QuoteBlockContent.ts | 16 ++++++++-------- .../TaskListItemBlockContent.ts | 14 +++++--------- 5 files changed, 18 insertions(+), 24 deletions(-) diff --git a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts index c4a72dd4c2..abd2921c81 100644 --- a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts +++ b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts @@ -1,7 +1,7 @@ import { HeadingBlockContent } from "../nodes/BlockContent/HeadingBlockContent/HeadingBlockContent"; import { BulletListItemBlockContent } from "../nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent"; import { NumberedListItemBlockContent } from "../nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent"; -import { QuoteBlockContent } from "../nodes/BlockContent/ListItemBlockContent/QuoteListItemBlockContent/QuoteBlockContent"; +import { QuotListItemBlockContent } from "../nodes/BlockContent/ListItemBlockContent/QuoteListItemBlockContent/QuoteBlockContent"; import { TaskListItemBlockContent } from "../nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent"; import { ParagraphBlockContent } from "../nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent"; import { PropSchema, TypesMatch } from "./blockTypes"; @@ -50,9 +50,9 @@ export const defaultBlockSchema = { propSchema: defaultProps, node: TaskListItemBlockContent, }, - quote: { + quoteListItem: { propSchema: defaultProps, - node: QuoteBlockContent, + node: QuotListItemBlockContent, }, numberedListItem: { propSchema: defaultProps, diff --git a/packages/core/src/extensions/Blocks/nodes/Block.module.css b/packages/core/src/extensions/Blocks/nodes/Block.module.css index dbcab2be3a..35084ca466 100644 --- a/packages/core/src/extensions/Blocks/nodes/Block.module.css +++ b/packages/core/src/extensions/Blocks/nodes/Block.module.css @@ -197,7 +197,7 @@ NESTED BLOCKS } /* Quotes */ -[data-content-type="quote"] { +[data-content-type="quoteListItem"] { font-style: italic; position: relative; padding-left: 1.63em; @@ -206,7 +206,7 @@ NESTED BLOCKS .blockOuter:not([data-prev-type]) > .block - > .blockContent[data-content-type="quote"]::before { + > .blockContent[data-content-type="quoteListItem"]::before { content: " "; margin-right: 1.2em; diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts index fd351ea198..10d86462a7 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts @@ -69,8 +69,6 @@ export const handleComplete = (editor: Editor) => { export const handleCancel = (editor: Editor) => { const node = editor.state.selection.$from.node(); - - console.log(node.attrs); if (node) { // transaction to toggle checked attribute return editor diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/QuoteListItemBlockContent/QuoteBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/QuoteListItemBlockContent/QuoteBlockContent.ts index 1928369fe3..48bc496740 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/QuoteListItemBlockContent/QuoteBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/QuoteListItemBlockContent/QuoteBlockContent.ts @@ -3,22 +3,22 @@ import { createTipTapBlock } from "../../../../api/block"; import { handleEnter } from "../ListItemKeyboardShortcuts"; import styles from "../../../Block.module.css"; -export const QuoteBlockContent = createTipTapBlock<"quote">({ - name: "quote", +export const QuotListItemBlockContent = createTipTapBlock<"quoteListItem">({ + name: "quoteListItem", content: "inline*", addInputRules() { return [ - // Creates an unordered list when starting with "-", "+", or "*". + // Creates an unordered list when starting with ">". new InputRule({ find: new RegExp(`^[>]\\s$`), handler: ({ state, chain, range }) => { chain() .BNUpdateBlock(state.selection.from, { - type: "quote", + type: this.name, props: {}, }) - // Removes the "-", "+", or "*" character used to set the list. + // Removes the ">" character used to set the list. .deleteRange({ from: range.from, to: range.to }); }, }), @@ -53,7 +53,7 @@ export const QuoteBlockContent = createTipTapBlock<"quote">({ return false; }, - node: "bulletListItem", + node: this.name, }, // Case for BlockNote list structure. { @@ -69,14 +69,14 @@ export const QuoteBlockContent = createTipTapBlock<"quote">({ return false; } - if (parent.getAttribute("data-content-type") === "quote") { + if (parent.getAttribute("data-content-type") === this.name) { return {}; } return false; }, priority: 300, - node: "quote", + node: this.name, }, ]; }, diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts index af67c5e912..78b5c6c955 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts @@ -1,9 +1,4 @@ -import { - Editor, - InputRule, - wrappingInputRule, - mergeAttributes, -} from "@tiptap/core"; +import { Editor, InputRule, mergeAttributes } from "@tiptap/core"; import { createTipTapBlock } from "../../../../api/block"; import { handleEnter, @@ -20,6 +15,7 @@ function addTaskListItemBlockContentView( editor: Editor, getPos: (() => number) | boolean ): NodeView { + console.log("creat a list"); const dom = document.createElement("div"); const checked = node.attrs.checked || false; let altKey = false; @@ -187,7 +183,7 @@ export const TaskListItemBlockContent = createTipTapBlock<"taskListItem">({ return false; }, - node: "taskListItem", + node: this.name, }, // Case for BlockNote list structure. { @@ -203,14 +199,14 @@ export const TaskListItemBlockContent = createTipTapBlock<"taskListItem">({ return false; } - if (parent.getAttribute("data-content-type") === "taskListItem") { + if (parent.getAttribute("data-content-type") === this.name) { return {}; } return false; }, priority: 300, - node: "taskListItem", + node: this.name, }, ]; }, From e42dc709893e1b7e86618117c2b3070f1c88c9b9 Mon Sep 17 00:00:00 2001 From: Eduard Metzger Date: Sat, 15 Jul 2023 12:24:55 -0500 Subject: [PATCH 06/48] fixed creating a new task using enter at the end copied also the checked style. Split up the task and checklist items into two classes --- .../extensions/Blocks/api/defaultBlocks.ts | 9 +- .../extensions/Blocks/nodes/Block.module.css | 34 +++-- .../extensions/Blocks/nodes/BlockContainer.ts | 6 +- .../CheckListItemBlockContent.ts | 144 ++++++++++++++++++ .../TaskListItemBlockContent.ts | 108 +------------ .../TaskListItemNodeView.ts | 88 +++++++++++ 6 files changed, 266 insertions(+), 123 deletions(-) create mode 100644 packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/CheckListItemBlockContent.ts create mode 100644 packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemNodeView.ts diff --git a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts index abd2921c81..10b4f41ede 100644 --- a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts +++ b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts @@ -3,6 +3,7 @@ import { BulletListItemBlockContent } from "../nodes/BlockContent/ListItemBlockC import { NumberedListItemBlockContent } from "../nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent"; import { QuotListItemBlockContent } from "../nodes/BlockContent/ListItemBlockContent/QuoteListItemBlockContent/QuoteBlockContent"; import { TaskListItemBlockContent } from "../nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent"; +import { CheckListItemBlockContent } from "../nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/CheckListItemBlockContent"; import { ParagraphBlockContent } from "../nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent"; import { PropSchema, TypesMatch } from "./blockTypes"; @@ -21,10 +22,10 @@ export const defaultProps = { default: "false" as const, }, cancelled: { - default: "false", + default: "false" as const, }, checklist: { - default: "false", + default: "false" as const, }, } satisfies PropSchema; @@ -50,6 +51,10 @@ export const defaultBlockSchema = { propSchema: defaultProps, node: TaskListItemBlockContent, }, + checkListItem: { + propSchema: defaultProps, + node: CheckListItemBlockContent, + }, quoteListItem: { propSchema: defaultProps, node: QuotListItemBlockContent, diff --git a/packages/core/src/extensions/Blocks/nodes/Block.module.css b/packages/core/src/extensions/Blocks/nodes/Block.module.css index 35084ca466..db7e16b3aa 100644 --- a/packages/core/src/extensions/Blocks/nodes/Block.module.css +++ b/packages/core/src/extensions/Blocks/nodes/Block.module.css @@ -295,7 +295,8 @@ NESTED BLOCKS */ /* Tasks */ -[data-content-type="taskListItem"] { +[data-content-type="taskListItem"], +[data-content-type="checkListItem"] { padding-left: 1.62em; position: relative; @@ -321,18 +322,6 @@ NESTED BLOCKS color: inherit; } - &[data-checked="false"][data-checklist="true"] > label:before { - content: "\f0c8"; - } - - &[data-checked="true"][data-checklist="true"] > label:before { - content: "\f14a"; - } - - &[data-cancelled="true"][data-checklist="true"] > label:before { - content: "\f2d3"; - } - > label { user-select: none; font: var(--fa-font-regular); @@ -346,10 +335,23 @@ NESTED BLOCKS cursor: pointer; display: none; } +} + +/* Checklist */ +[data-content-type="checkListItem"] { + left: 0.06rem; + padding-left: 1.56em; - &[data-checklist="true"] { - left: 0.06rem; - padding-left: 1.56em; + &[data-checked="false"] > label:before { + content: "\f0c8"; + } + + &[data-checked="true"] > label:before { + content: "\f14a"; + } + + &[data-cancelled="true"] > label:before { + content: "\f2d3"; } } diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts b/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts index 3120ba8253..64790320dc 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts @@ -311,7 +311,7 @@ export const BlockContainer = Node.create({ return false; } - const { contentNode, contentType, startPos, endPos, depth } = + const { /*contentNode,*/ contentType, startPos, endPos, depth } = blockInfo; const originalBlockContent = state.doc.cut(startPos + 1, posInBlock); @@ -348,8 +348,8 @@ export const BlockContainer = Node.create({ state.tr.setBlockType( newBlockContentPos, newBlockContentPos, - state.schema.node(contentType).type, - contentNode.attrs + state.schema.node(contentType).type + // contentNode.attrs // Don't keep the attributes or we run into the issue that we the new task is completed or canceled ); } diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/CheckListItemBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/CheckListItemBlockContent.ts new file mode 100644 index 0000000000..cc9a63b2a8 --- /dev/null +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/CheckListItemBlockContent.ts @@ -0,0 +1,144 @@ +import { InputRule, mergeAttributes } from "@tiptap/core"; +import { createTipTapBlock } from "../../../../api/block"; +import { + handleEnter, + handleComplete, + handleCancel, +} from "../ListItemKeyboardShortcuts"; +import styles from "../../../Block.module.css"; +import { TaskListItemNodeView } from "./TaskListItemNodeView"; + +export const CheckListItemBlockContent = createTipTapBlock<"checkListItem">({ + name: "checkListItem", + content: "inline*", + + // This is needed to detect when the user types "*", so it gets converted into a task item. + addInputRules() { + return [ + // Creates an unordered list when starting with "*". + new InputRule({ + find: new RegExp(`^\\+\\s$`), + handler: ({ state, chain, range }) => { + chain() + .BNUpdateBlock(state.selection.from, { + type: this.name, + props: { + checked: "false", + canceled: "false", + }, + }) + // Removes the "*" character used to set the list. + .deleteRange({ from: range.from, to: range.to }); + }, + }), + ]; + }, + + addKeyboardShortcuts() { + return { + Enter: () => handleEnter(this.editor), + "Cmd-d": () => handleComplete(this.editor), + "Cmd-s": () => handleCancel(this.editor), + }; + }, + + addAttributes() { + console.log("adding attributes"); + return { + checked: { + default: false, + keepOnSplit: false, // When you hit enter and create a new task in the middle or empty, don't keep the checked attribute + parseHTML: (element) => element.getAttribute("data-checked") === "true", + renderHTML: (attributes) => ({ + "data-checked": attributes.checked, + }), + }, + cancelled: { + default: false, + keepOnSplit: false, + parseHTML: (element) => + element.getAttribute("data-cancelled") === "true", + renderHTML: (attributes) => ({ + "data-cancelled": attributes.cancelled, + }), + }, + }; + }, + + parseHTML() { + return [ + // Case for regular HTML list structure. + { + tag: "li", + getAttrs: (element) => { + if (typeof element === "string") { + return false; + } + + const parent = element.parentElement; + + if (parent === null) { + return false; + } + + if (parent.tagName === "UL") { + return {}; + } + + return false; + }, + node: this.name, + }, + // Case for BlockNote list structure. + { + tag: "p", + getAttrs: (element) => { + if (typeof element === "string") { + return false; + } + + const parent = element.parentElement; + + if (parent === null) { + return false; + } + + if (parent.getAttribute("data-content-type") === this.name) { + return {}; + } + + return false; + }, + priority: 300, + node: this.name, + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return [ + "div", + mergeAttributes(HTMLAttributes, { + class: styles.blockContent, + "data-content-type": this.name, + }), + [ + "label", + [ + "input", + { + type: "checkbox", + }, + ], + ["span"], + ], + ["div", { class: styles.inlineContent }, 0], + ]; + }, + + addNodeView() { + return ({ node, getPos, editor }) => { + return TaskListItemNodeView(node, editor, getPos, this.name); + }; + }, +}); diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts index 78b5c6c955..bd789f4092 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts @@ -1,4 +1,4 @@ -import { Editor, InputRule, mergeAttributes } from "@tiptap/core"; +import { InputRule, mergeAttributes } from "@tiptap/core"; import { createTipTapBlock } from "../../../../api/block"; import { handleEnter, @@ -6,93 +6,7 @@ import { handleCancel, } from "../ListItemKeyboardShortcuts"; import styles from "../../../Block.module.css"; - -import { NodeView } from "prosemirror-view"; -import { Node } from "prosemirror-model"; - -function addTaskListItemBlockContentView( - node: Node, - editor: Editor, - getPos: (() => number) | boolean -): NodeView { - console.log("creat a list"); - const dom = document.createElement("div"); - const checked = node.attrs.checked || false; - let altKey = false; - - dom.className = "blockContent"; - dom.dataset.contentType = "taskListItem"; - dom.dataset.checked = checked; - dom.dataset.cancelled = node.attrs.cancelled || false; - dom.dataset.checklist = node.attrs.checklist || false; - - const label = document.createElement("label"); - const input = document.createElement("input"); - const span = document.createElement("span"); - const content = document.createElement("div"); - - input.type = "checkbox"; - input.checked = checked; - - content.classList.add(styles.inlineContent); - - input.addEventListener("click", (event) => { - altKey = event.altKey; - }); - - input.addEventListener("change", (event) => { - const checked = (event.target as HTMLInputElement).checked; - if (editor.isEditable && typeof getPos === "function") { - editor - .chain() - .focus(undefined, { scrollIntoView: false }) - .command(({ tr }) => { - const position = getPos(); - const currentNode = tr.doc.nodeAt(position); - tr.setNodeMarkup(position, undefined, { - ...(currentNode === null || currentNode === void 0 - ? void 0 - : currentNode.attrs), - checked: altKey ? true : checked, - cancelled: altKey ? true : false, - }); - return true; - }) - .run(); - } - if (!editor.isEditable) { - // Reset state if onReadOnlyChecked returns false - input.checked = !input.checked; - } - }); - - label.appendChild(input); - label.appendChild(span); - dom.appendChild(label); - dom.appendChild(content); - - return { - dom: dom, - contentDOM: content, - update: (updatedNode: Node) => { - if (updatedNode.type !== node.type) { - return false; - } - - dom.dataset.checked = updatedNode.attrs.checked || false; - dom.dataset.cancelled = updatedNode.attrs.cancelled || false; - dom.dataset.checklist = updatedNode.attrs.checklist || false; - - if (updatedNode.attrs.checked) { - input.setAttribute("checked", "checked"); - } else { - input.removeAttribute("checked"); - } - - return true; - }, - }; -} +import { TaskListItemNodeView } from "./TaskListItemNodeView"; export const TaskListItemBlockContent = createTipTapBlock<"taskListItem">({ name: "taskListItem", @@ -103,16 +17,14 @@ export const TaskListItemBlockContent = createTipTapBlock<"taskListItem">({ return [ // Creates an unordered list when starting with "*". new InputRule({ - find: new RegExp(`^([*+])\\s$`), - handler: ({ state, chain, range, match }) => { - const isCheckList = match && match.length > 0 ? match[1] : false; + find: new RegExp(`^\\*\\s$`), + handler: ({ state, chain, range }) => { chain() .BNUpdateBlock(state.selection.from, { type: this.name, props: { checked: "false", canceled: "false", - checklist: isCheckList === "+" ? "true" : "false", }, }) // Removes the "*" character used to set the list. @@ -131,6 +43,7 @@ export const TaskListItemBlockContent = createTipTapBlock<"taskListItem">({ }, addAttributes() { + console.log("adding attributes"); return { checked: { default: false, @@ -149,15 +62,6 @@ export const TaskListItemBlockContent = createTipTapBlock<"taskListItem">({ "data-cancelled": attributes.cancelled, }), }, - checklist: { - default: false, - keepOnSplit: true, - parseHTML: (element) => - element.getAttribute("data-checklist") === "true", - renderHTML: (attributes) => ({ - "data-checklist": attributes.checklist, - }), - }, }; }, @@ -234,7 +138,7 @@ export const TaskListItemBlockContent = createTipTapBlock<"taskListItem">({ addNodeView() { return ({ node, getPos, editor }) => { - return addTaskListItemBlockContentView(node, editor, getPos); + return TaskListItemNodeView(node, editor, getPos, this.name); }; }, }); diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemNodeView.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemNodeView.ts new file mode 100644 index 0000000000..cf59a593dd --- /dev/null +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemNodeView.ts @@ -0,0 +1,88 @@ +import { Editor } from "@tiptap/core"; +import styles from "../../../Block.module.css"; + +import { NodeView } from "prosemirror-view"; +import { Node } from "prosemirror-model"; + +export function TaskListItemNodeView( + node: Node, + editor: Editor, + getPos: (() => number) | boolean, + type: string +): NodeView { + console.log("new task list"); + const dom = document.createElement("div"); + const checked = node.attrs.checked || false; + let altKey = false; + + dom.className = "blockContent"; + dom.dataset.contentType = type; + dom.dataset.checked = checked; + dom.dataset.cancelled = node.attrs.cancelled || false; + + const label = document.createElement("label"); + const input = document.createElement("input"); + const span = document.createElement("span"); + const content = document.createElement("div"); + + input.type = "checkbox"; + input.checked = checked; + + content.classList.add(styles.inlineContent); + + input.addEventListener("click", (event) => { + altKey = event.altKey; + }); + + input.addEventListener("change", (event) => { + const checked = (event.target as HTMLInputElement).checked; + if (editor.isEditable && typeof getPos === "function") { + editor + .chain() + .focus(undefined, { scrollIntoView: false }) + .command(({ tr }) => { + const position = getPos(); + const currentNode = tr.doc.nodeAt(position); + tr.setNodeMarkup(position, undefined, { + ...(currentNode === null || currentNode === void 0 + ? void 0 + : currentNode.attrs), + checked: altKey ? true : checked, + cancelled: altKey ? true : false, + }); + return true; + }) + .run(); + } + if (!editor.isEditable) { + // Reset state if onReadOnlyChecked returns false + input.checked = !input.checked; + } + }); + + label.appendChild(input); + label.appendChild(span); + dom.appendChild(label); + dom.appendChild(content); + + return { + dom: dom, + contentDOM: content, + update: (updatedNode: Node) => { + if (updatedNode.type !== node.type) { + return false; + } + + dom.dataset.checked = updatedNode.attrs.checked || false; + dom.dataset.cancelled = updatedNode.attrs.cancelled || false; + + if (updatedNode.attrs.checked) { + input.setAttribute("checked", "checked"); + } else { + input.removeAttribute("checked"); + } + + return true; + }, + }; +} From 687b2ee47c2dcb46258576743deb8658093b3229 Mon Sep 17 00:00:00 2001 From: Eduard Metzger Date: Sat, 15 Jul 2023 15:02:54 -0500 Subject: [PATCH 07/48] Fix copying tasks and checklists, it didn't copy the icon, just a plain paragraph, the issue was that the tags (p) in parseHTML didn't match the tags in renderHTML and addNodeView --- packages/core/src/BlockNoteEditor.ts | 1 + packages/core/src/BlockNoteExtensions.ts | 1 + .../QuoteBlockContent.ts | 5 +- .../CheckListItemBlockContent.ts | 52 ++----------------- .../TaskListItemBlockContent.ts | 52 ++----------------- .../TaskListItemHTMLParser.ts | 49 +++++++++++++++++ .../TaskListItemNodeView.ts | 3 +- 7 files changed, 61 insertions(+), 102 deletions(-) create mode 100644 packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemHTMLParser.ts diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts index 87fe240136..c67996b7c5 100644 --- a/packages/core/src/BlockNoteEditor.ts +++ b/packages/core/src/BlockNoteEditor.ts @@ -419,6 +419,7 @@ export class BlockNoteEditor { * Gets a snapshot of the current selection. */ public getSelection(): Selection | undefined { + console.log("Get selection"); if ( this._tiptapEditor.state.selection.from === this._tiptapEditor.state.selection.to diff --git a/packages/core/src/BlockNoteExtensions.ts b/packages/core/src/BlockNoteExtensions.ts index f26deb9774..084432e32e 100644 --- a/packages/core/src/BlockNoteExtensions.ts +++ b/packages/core/src/BlockNoteExtensions.ts @@ -110,6 +110,7 @@ export const getBlockNoteExtensions = (opts: { ...Object.values(opts.blockSchema).map((blockSpec) => blockSpec.node.configure({ editor: opts.editor }) ), + CustomBlockSerializerExtension, Dropcursor.configure({ width: 5, color: "#ddeeff" }), diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/QuoteListItemBlockContent/QuoteBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/QuoteListItemBlockContent/QuoteBlockContent.ts index 48bc496740..f02ca806d4 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/QuoteListItemBlockContent/QuoteBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/QuoteListItemBlockContent/QuoteBlockContent.ts @@ -31,6 +31,7 @@ export const QuotListItemBlockContent = createTipTapBlock<"quoteListItem">({ }; }, + // Parsed into tip tap nodes parseHTML() { return [ // Case for regular HTML list structure. @@ -57,7 +58,7 @@ export const QuotListItemBlockContent = createTipTapBlock<"quoteListItem">({ }, // Case for BlockNote list structure. { - tag: "p", + tag: "p", // This has to overlap with what is defined in the BlockContent node (the node type, here p) getAttrs: (element) => { if (typeof element === "string") { return false; @@ -88,7 +89,7 @@ export const QuotListItemBlockContent = createTipTapBlock<"quoteListItem">({ class: styles.blockContent, "data-content-type": this.name, }), - ["p", { class: styles.inlineContent }, 0], + ["p", { class: styles.inlineContent }, 0], // This p has to overlap with the tags in parseHTML ]; }, }); diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/CheckListItemBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/CheckListItemBlockContent.ts index cc9a63b2a8..c8e2d03630 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/CheckListItemBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/CheckListItemBlockContent.ts @@ -7,6 +7,7 @@ import { } from "../ListItemKeyboardShortcuts"; import styles from "../../../Block.module.css"; import { TaskListItemNodeView } from "./TaskListItemNodeView"; +import { TaskListItemHTMLParser } from "./TaskListItemHTMLParser"; export const CheckListItemBlockContent = createTipTapBlock<"checkListItem">({ name: "checkListItem", @@ -43,7 +44,6 @@ export const CheckListItemBlockContent = createTipTapBlock<"checkListItem">({ }, addAttributes() { - console.log("adding attributes"); return { checked: { default: false, @@ -66,53 +66,7 @@ export const CheckListItemBlockContent = createTipTapBlock<"checkListItem">({ }, parseHTML() { - return [ - // Case for regular HTML list structure. - { - tag: "li", - getAttrs: (element) => { - if (typeof element === "string") { - return false; - } - - const parent = element.parentElement; - - if (parent === null) { - return false; - } - - if (parent.tagName === "UL") { - return {}; - } - - return false; - }, - node: this.name, - }, - // Case for BlockNote list structure. - { - tag: "p", - getAttrs: (element) => { - if (typeof element === "string") { - return false; - } - - const parent = element.parentElement; - - if (parent === null) { - return false; - } - - if (parent.getAttribute("data-content-type") === this.name) { - return {}; - } - - return false; - }, - priority: 300, - node: this.name, - }, - ]; + return TaskListItemHTMLParser(this.name); }, renderHTML({ HTMLAttributes }) { @@ -132,7 +86,7 @@ export const CheckListItemBlockContent = createTipTapBlock<"checkListItem">({ ], ["span"], ], - ["div", { class: styles.inlineContent }, 0], + ["p", { class: styles.inlineContent }, 0], // This has to be a 'p' here and in parseHTML where we define the tags ]; }, diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts index bd789f4092..2311d3cb74 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts @@ -7,6 +7,7 @@ import { } from "../ListItemKeyboardShortcuts"; import styles from "../../../Block.module.css"; import { TaskListItemNodeView } from "./TaskListItemNodeView"; +import { TaskListItemHTMLParser } from "./TaskListItemHTMLParser"; export const TaskListItemBlockContent = createTipTapBlock<"taskListItem">({ name: "taskListItem", @@ -43,7 +44,6 @@ export const TaskListItemBlockContent = createTipTapBlock<"taskListItem">({ }, addAttributes() { - console.log("adding attributes"); return { checked: { default: false, @@ -66,53 +66,7 @@ export const TaskListItemBlockContent = createTipTapBlock<"taskListItem">({ }, parseHTML() { - return [ - // Case for regular HTML list structure. - { - tag: "li", - getAttrs: (element) => { - if (typeof element === "string") { - return false; - } - - const parent = element.parentElement; - - if (parent === null) { - return false; - } - - if (parent.tagName === "UL") { - return {}; - } - - return false; - }, - node: this.name, - }, - // Case for BlockNote list structure. - { - tag: "p", - getAttrs: (element) => { - if (typeof element === "string") { - return false; - } - - const parent = element.parentElement; - - if (parent === null) { - return false; - } - - if (parent.getAttribute("data-content-type") === this.name) { - return {}; - } - - return false; - }, - priority: 300, - node: this.name, - }, - ]; + return TaskListItemHTMLParser(this.name); }, renderHTML({ HTMLAttributes }) { @@ -132,7 +86,7 @@ export const TaskListItemBlockContent = createTipTapBlock<"taskListItem">({ ], ["span"], ], - ["div", { class: styles.inlineContent }, 0], + ["p", { class: styles.inlineContent }, 0], // This has to be a 'p' here and in parseHTML where we define the tags ]; }, diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemHTMLParser.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemHTMLParser.ts new file mode 100644 index 0000000000..64ba1cd06f --- /dev/null +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemHTMLParser.ts @@ -0,0 +1,49 @@ +export function TaskListItemHTMLParser(name: string) { + return [ + // Case for regular HTML list structure. + { + tag: "li", + getAttrs: (element: HTMLElement | string) => { + if (typeof element === "string") { + return false; + } + + const parent = element.parentElement; + + if (parent === null) { + return false; + } + + if (parent.tagName === "UL") { + return {}; + } + + return false; + }, + node: name, + }, + // Case for BlockNote list structure. + { + tag: "p", + getAttrs: (element: HTMLElement | string) => { + if (typeof element === "string") { + return false; + } + + const parent = element.parentElement; + + if (parent === null) { + return false; + } + + if (parent.getAttribute("data-content-type") === name) { + return {}; + } + + return false; + }, + priority: 300, + node: name, + }, + ]; +} diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemNodeView.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemNodeView.ts index cf59a593dd..4a98b29a41 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemNodeView.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemNodeView.ts @@ -10,7 +10,6 @@ export function TaskListItemNodeView( getPos: (() => number) | boolean, type: string ): NodeView { - console.log("new task list"); const dom = document.createElement("div"); const checked = node.attrs.checked || false; let altKey = false; @@ -23,7 +22,7 @@ export function TaskListItemNodeView( const label = document.createElement("label"); const input = document.createElement("input"); const span = document.createElement("span"); - const content = document.createElement("div"); + const content = document.createElement("p"); input.type = "checkbox"; input.checked = checked; From 7f157d9263cbea32c062b5f9db6f0d65031686a0 Mon Sep 17 00:00:00 2001 From: Eduard Metzger Date: Sat, 15 Jul 2023 17:18:13 -0500 Subject: [PATCH 08/48] Fix copying completed or canceled items didn't copy forward the state --- packages/core/src/BlockNoteEditor.ts | 1 - .../extensions/Blocks/api/defaultBlocks.ts | 3 -- .../CheckListItemBlockContent.ts | 30 +++-------------- .../TaskListItemBlockContent.ts | 32 +++++-------------- .../TaskListItemHTMLParser.ts | 2 +- .../TaskListItemHTMLRender.ts | 31 ++++++++++++++++++ .../TaskListItemNodeView.ts | 2 +- 7 files changed, 46 insertions(+), 55 deletions(-) create mode 100644 packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemHTMLRender.ts diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts index c67996b7c5..87fe240136 100644 --- a/packages/core/src/BlockNoteEditor.ts +++ b/packages/core/src/BlockNoteEditor.ts @@ -419,7 +419,6 @@ export class BlockNoteEditor { * Gets a snapshot of the current selection. */ public getSelection(): Selection | undefined { - console.log("Get selection"); if ( this._tiptapEditor.state.selection.from === this._tiptapEditor.state.selection.to diff --git a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts index 10b4f41ede..acc1f4703a 100644 --- a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts +++ b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts @@ -24,9 +24,6 @@ export const defaultProps = { cancelled: { default: "false" as const, }, - checklist: { - default: "false" as const, - }, } satisfies PropSchema; export type DefaultProps = typeof defaultProps; diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/CheckListItemBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/CheckListItemBlockContent.ts index c8e2d03630..d95679b6a1 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/CheckListItemBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/CheckListItemBlockContent.ts @@ -1,13 +1,13 @@ -import { InputRule, mergeAttributes } from "@tiptap/core"; +import { InputRule } from "@tiptap/core"; import { createTipTapBlock } from "../../../../api/block"; import { handleEnter, handleComplete, handleCancel, } from "../ListItemKeyboardShortcuts"; -import styles from "../../../Block.module.css"; import { TaskListItemNodeView } from "./TaskListItemNodeView"; import { TaskListItemHTMLParser } from "./TaskListItemHTMLParser"; +import { TaskListItemListHTMLRender } from "./TaskListItemHTMLRender"; export const CheckListItemBlockContent = createTipTapBlock<"checkListItem">({ name: "checkListItem", @@ -47,17 +47,14 @@ export const CheckListItemBlockContent = createTipTapBlock<"checkListItem">({ return { checked: { default: false, - keepOnSplit: false, // When you hit enter and create a new task in the middle or empty, don't keep the checked attribute - parseHTML: (element) => element.getAttribute("data-checked") === "true", + parseHTML: (element) => element.getAttribute("data-checked"), renderHTML: (attributes) => ({ "data-checked": attributes.checked, }), }, cancelled: { default: false, - keepOnSplit: false, - parseHTML: (element) => - element.getAttribute("data-cancelled") === "true", + parseHTML: (element) => element.getAttribute("data-cancelled"), renderHTML: (attributes) => ({ "data-cancelled": attributes.cancelled, }), @@ -70,24 +67,7 @@ export const CheckListItemBlockContent = createTipTapBlock<"checkListItem">({ }, renderHTML({ HTMLAttributes }) { - return [ - "div", - mergeAttributes(HTMLAttributes, { - class: styles.blockContent, - "data-content-type": this.name, - }), - [ - "label", - [ - "input", - { - type: "checkbox", - }, - ], - ["span"], - ], - ["p", { class: styles.inlineContent }, 0], // This has to be a 'p' here and in parseHTML where we define the tags - ]; + return TaskListItemListHTMLRender(this.name, HTMLAttributes); }, addNodeView() { diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts index 2311d3cb74..2144622976 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts @@ -1,13 +1,13 @@ -import { InputRule, mergeAttributes } from "@tiptap/core"; +import { InputRule } from "@tiptap/core"; import { createTipTapBlock } from "../../../../api/block"; import { handleEnter, handleComplete, handleCancel, } from "../ListItemKeyboardShortcuts"; -import styles from "../../../Block.module.css"; import { TaskListItemNodeView } from "./TaskListItemNodeView"; import { TaskListItemHTMLParser } from "./TaskListItemHTMLParser"; +import { TaskListItemListHTMLRender } from "./TaskListItemHTMLRender"; export const TaskListItemBlockContent = createTipTapBlock<"taskListItem">({ name: "taskListItem", @@ -47,8 +47,7 @@ export const TaskListItemBlockContent = createTipTapBlock<"taskListItem">({ return { checked: { default: false, - keepOnSplit: false, // When you hit enter and create a new task in the middle or empty, don't keep the checked attribute - parseHTML: (element) => element.getAttribute("data-checked") === "true", + parseHTML: (element) => element.getAttribute("data-checked"), renderHTML: (attributes) => ({ "data-checked": attributes.checked, }), @@ -56,8 +55,7 @@ export const TaskListItemBlockContent = createTipTapBlock<"taskListItem">({ cancelled: { default: false, keepOnSplit: false, - parseHTML: (element) => - element.getAttribute("data-cancelled") === "true", + parseHTML: (element) => element.getAttribute("data-cancelled"), renderHTML: (attributes) => ({ "data-cancelled": attributes.cancelled, }), @@ -70,24 +68,10 @@ export const TaskListItemBlockContent = createTipTapBlock<"taskListItem">({ }, renderHTML({ HTMLAttributes }) { - return [ - "div", - mergeAttributes(HTMLAttributes, { - class: styles.blockContent, - "data-content-type": this.name, - }), - [ - "label", - [ - "input", - { - type: "checkbox", - }, - ], - ["span"], - ], - ["p", { class: styles.inlineContent }, 0], // This has to be a 'p' here and in parseHTML where we define the tags - ]; + return TaskListItemListHTMLRender( + this.name, + HTMLAttributes + ) as DOMOutputSpec; }, addNodeView() { diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemHTMLParser.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemHTMLParser.ts index 64ba1cd06f..09a2a20c36 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemHTMLParser.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemHTMLParser.ts @@ -24,7 +24,7 @@ export function TaskListItemHTMLParser(name: string) { }, // Case for BlockNote list structure. { - tag: "p", + tag: "div", getAttrs: (element: HTMLElement | string) => { if (typeof element === "string") { return false; diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemHTMLRender.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemHTMLRender.ts new file mode 100644 index 0000000000..3d956a528d --- /dev/null +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemHTMLRender.ts @@ -0,0 +1,31 @@ +import { DOMOutputSpec } from "@tiptap/pm/model"; +import styles from "../../../Block.module.css"; +import { mergeAttributes } from "@tiptap/core"; + +export function TaskListItemListHTMLRender( + name: string, + HTMLAttributes: any +): DOMOutputSpec { + return [ + "div", + { class: styles.blockContent, "data-content-type": name }, + [ + "label", + [ + "input", + { + type: "checkbox", + }, + ], + ["span"], + ], + [ + "div", + mergeAttributes(HTMLAttributes, { + // This has to be here or we can't copy the styles + class: styles.inlineContent, + }), + 0, + ], // This has to be a 'div' here and in parseHTML where we define the tags + ]; +} diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemNodeView.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemNodeView.ts index 4a98b29a41..5145b7cf06 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemNodeView.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemNodeView.ts @@ -22,7 +22,7 @@ export function TaskListItemNodeView( const label = document.createElement("label"); const input = document.createElement("input"); const span = document.createElement("span"); - const content = document.createElement("p"); + const content = document.createElement("div"); input.type = "checkbox"; input.checked = checked; From 42d073a20f21333607b2ff85bb4d3145697e9ad8 Mon Sep 17 00:00:00 2001 From: Eduard Metzger Date: Sat, 15 Jul 2023 17:25:34 -0500 Subject: [PATCH 09/48] Fix weird animation when creating a bullet point --- .../extensions/Blocks/nodes/Block.module.css | 57 +++++++++++++------ .../TaskListItemBlockContent.ts | 5 +- 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/packages/core/src/extensions/Blocks/nodes/Block.module.css b/packages/core/src/extensions/Blocks/nodes/Block.module.css index db7e16b3aa..d4644660ba 100644 --- a/packages/core/src/extensions/Blocks/nodes/Block.module.css +++ b/packages/core/src/extensions/Blocks/nodes/Block.module.css @@ -225,8 +225,18 @@ NESTED BLOCKS position: relative; padding-left: 1.62em; } -/* -.blockOuter[data-prev-type="bulletListItem"] > .block > .blockContent::before { + +[data-content-type="bulletListItem"]::before { + position: absolute; + top: 0.45rem; + left: 0.45rem; + content: "\e122"; + font: var(--fa-font-solid); + color: var(--orange-noteplan); + font-size: 8px; +} + +/* .blockOuter[data-prev-type="bulletListItem"] > .block > .blockContent::before { position: absolute; top: 1.05rem; left: 0.45rem; @@ -254,9 +264,13 @@ NESTED BLOCKS > .blockOuter[data-prev-type="bulletListItem"] > .block > .blockContent::before { - margin-right: 0.77em; - margin-left: -1.22em; - content: "◦"; + position: absolute; + top: 0.45rem; + left: 0.45rem; + content: "\e122"; + font: var(--fa-font-solid); + color: var(--orange-noteplan); + font-size: 8px; } [data-content-type="bulletListItem"] @@ -264,9 +278,13 @@ NESTED BLOCKS > .blockOuter:not([data-prev-type]) > .block > .blockContent[data-content-type="bulletListItem"]::before { - margin-right: 0.77em; - margin-left: -1.22em; - content: "◦"; + position: absolute; + top: 0.45rem; + left: 0.45rem; + content: "\e122"; + font: var(--fa-font-solid); + color: var(--orange-noteplan); + font-size: 8px; } [data-content-type="bulletListItem"] @@ -276,9 +294,13 @@ NESTED BLOCKS > .blockOuter[data-prev-type="bulletListItem"] > .block > .blockContent::before { - margin-right: 0.77em; - margin-left: -1.22em; - content: "▪"; + position: absolute; + top: 0.45rem; + left: 0.45rem; + content: "\e122"; + font: var(--fa-font-solid); + color: var(--orange-noteplan); + font-size: 8px; } [data-content-type="bulletListItem"] @@ -288,11 +310,14 @@ NESTED BLOCKS > .blockOuter:not([data-prev-type]) > .block > .blockContent[data-content-type="bulletListItem"]::before { - margin-right: 0.6em; - margin-left: -1.2em; - content: "▪"; -} -*/ + position: absolute; + top: 0.45rem; + left: 0.45rem; + content: "\e122"; + font: var(--fa-font-solid); + color: var(--orange-noteplan); + font-size: 8px; +} */ /* Tasks */ [data-content-type="taskListItem"], diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts index 2144622976..910afec4cf 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts @@ -68,10 +68,7 @@ export const TaskListItemBlockContent = createTipTapBlock<"taskListItem">({ }, renderHTML({ HTMLAttributes }) { - return TaskListItemListHTMLRender( - this.name, - HTMLAttributes - ) as DOMOutputSpec; + return TaskListItemListHTMLRender(this.name, HTMLAttributes); }, addNodeView() { From 9bdb2c6546e3905778317caa3515df5e521afb47 Mon Sep 17 00:00:00 2001 From: Eduard Metzger Date: Sun, 16 Jul 2023 16:46:29 -0500 Subject: [PATCH 10/48] add custom noteplan markdown to html converter, call it from the window for testing: and it returns and html string which we could in theory use with window.editor.HTMLtoBlocks --- examples/editor/src/App.tsx | 4 + .../formatConversions/formatConversions.ts | 6 +- .../npMarkdownConversions.ts | 746 ++++++++++++++++++ .../BulletListItemBlockContent.ts | 5 +- 4 files changed, 757 insertions(+), 4 deletions(-) create mode 100644 packages/core/src/api/formatConversions/npMarkdownConversions.ts diff --git a/examples/editor/src/App.tsx b/examples/editor/src/App.tsx index 56239899fd..5e2ae4e925 100644 --- a/examples/editor/src/App.tsx +++ b/examples/editor/src/App.tsx @@ -2,6 +2,7 @@ import "@blocknote/core/style.css"; import { BlockNoteView, useBlockNote } from "@blocknote/react"; import styles from "./App.module.css"; +import { parseMarkdown } from "../../../packages/core/src/api/formatConversions/npMarkdownConversions"; type WindowWithProseMirror = Window & typeof globalThis & { ProseMirror: any; editor?: any }; @@ -17,6 +18,8 @@ function App() { "editorContent", JSON.stringify(editor.topLevelBlocks) ); + + console.log(editor.topLevelBlocks); }, editorDOMAttributes: { class: styles.editor, @@ -28,6 +31,7 @@ function App() { // Give tests a way to get prosemirror instance (window as WindowWithProseMirror).ProseMirror = editor?._tiptapEditor; (window as WindowWithProseMirror).editor = editor; + window.parseMarkdown = parseMarkdown; return ; } diff --git a/packages/core/src/api/formatConversions/formatConversions.ts b/packages/core/src/api/formatConversions/formatConversions.ts index 0f956446c8..50a1fbcbef 100644 --- a/packages/core/src/api/formatConversions/formatConversions.ts +++ b/packages/core/src/api/formatConversions/formatConversions.ts @@ -30,7 +30,11 @@ export async function blocksToHTML( .use(rehypeParse, { fragment: true }) .use(simplifyBlocks, { orderedListItemBlockTypes: new Set(["numberedListItem"]), - unorderedListItemBlockTypes: new Set(["bulletListItem"]), + unorderedListItemBlockTypes: new Set([ + "bulletListItem", + "taskListItem", + "checkListItem", + ]), }) .use(rehypeStringify) .process(htmlParentElement.innerHTML); diff --git a/packages/core/src/api/formatConversions/npMarkdownConversions.ts b/packages/core/src/api/formatConversions/npMarkdownConversions.ts new file mode 100644 index 0000000000..3f5f6205b0 --- /dev/null +++ b/packages/core/src/api/formatConversions/npMarkdownConversions.ts @@ -0,0 +1,746 @@ +import ReactDOM from "react-dom"; + +const headerIDs = new Set(); +var attachmentCount = 0; +var imageCount = 0; +var attachments = []; +const imageExtension = ["png", "jpeg", "jpg", "bmp", "tiff", "ico", "svg"]; + +const fileLinkRegex = /\!\[(file)\]\(([^\(\)]+)\)/g; +const imageLinkRegex = /\!\[(image)\]\(([^\(\)]+)\)/g; +const namedLinkRegex = /\[([^\[\]]*)\]\(([^\(\)]+)\)/g; +const linkRegex = + /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&\/\/=]*)/g; +const boldRegex = + /(^|\W)(?:(?!\\1)|(?=^))((\*|_)\3)(?=\S)(.*?[^*_])(\3\3)(?!\2)(?=\W|$)/g; +const italicRegex = + /(^|\W)(?:(?!\\1)|(?=^))((\*|_))(?=\S)(.*?[^*_])(\3)(?!\2)(?=\W|$)/g; +const strikethroughRegex = + /(^|\W)(?:(?!\\1)|(?=^))((~)\3)(?=\S)(.*?[^*_])(\3\3)(?!\2)(?=\W|$)/g; +const highlightRegex = + /(^|\W)(?:(?!\\1)|(?=^))((:)\3)(?=\S)(.*?[^*_])(\3\3)(?!\2)(?=\W|$)/g; +const codeRegex = + /(^|\W)(?:(?!\\1)|(?=^))((`))(?=\S)(.*?[^*_])(\3)(?!\2)(?=\W|$)/g; +const doneDateRegex = + /@done\((([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|1[0-9]|2[0-9]|3[0-1]))( ((0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]( ?[aApP][mM])?))?\)/g; +const hashtagRegex = + /(\s|^|[\\"\'\(\[\{\*\_])(?!#[\d!"#$%&'()*+,\-.\/:;<=>?@[\]^_`{|}~]+(\s|$))(#([^!"#$%&'()*+,\-.\/:;<=>?@[\]^_`{|}~\s]|[\-_\/])+?\\(.*?\\)|#([^!"#$%&'()*+,\-.\/:;<=>?@[\]^_`{|}~\s]|[\-_\/])+)/g; +const atRegex = + /(\s|^|[\\"\'\(\[\{\*\_])(?!@[\d!"#$%&'()*+,\-.\/:;<=>?@[\]^_`{|}~]+(\s|$))(@([^!"#$%&'()*+,\-.\/:;<=>?@[\]^_`{|}~\s]|[\-_\/])+?\\(.*?\\)|@([^!"#$%&'()*+,\-.\/:;<=>?@[\]^_`{|}~\s]|[\-_\/])+)/g; +const codeFenceOpenRegex = /^```/; +const codeFenceCloseRegex = /^```\s*$/; +const separatorRegex = + /(?:^|\v)([\-\_]{3,}|\*\*\* |(\-(\h|$)){3,}|\*{5,})(?:$|\v)/g; + +window.matchMedia = + window.matchMedia || + function () { + return { + matches: false, + addListener: function () {}, + removeListener: function () {}, + addEventListener: function () {}, + removeEventListener: function () {}, + }; + }; + +function generateHeaderID(text) { + let id = text + .toLowerCase() + .replace(/\s+/g, "-") + .replace(/[^\w-]/g, ""); + + let count = 0; + + while (true) { + if (!headerIDs.has(id)) { + return id; + } + + count += 1; + id = id + count.toString(); + } +} + +function shouldIgnore(ignoreRanges, offset) { + let length = ignoreRanges.length; + + for (let i = 0; i < length; i++) { + let range = ignoreRanges[i]; + + if (offset >= range[0] && offset <= range[1]) { + return true; + } + } + + return false; +} + +function fileLinkReplacer(ignoreRanges, isImageRegex) { + // If we find an attachment, be in an image or file, we donwload it from CloudKit, then display it. That's the only way. + // It would be better if we can simply link file attachments, but we get an odd downloadURL from Apple, which downloads the + // file when clicking on it with a weird filename. + return function (match, title, url, offset) { + let currentAttachmentCount = isImageRegex ? attachmentCount : imageCount; + + // Get the downloadURL from the asset object from CloudKit + if (!attachments || !attachments[currentAttachmentCount]) { + return "N/A"; + } + + // Extract the extension (so we can check if its an image) and the filename. + + let split = url.split("."); + let ext = split[split.length - 1]; + let isImage = true; + + split = url.split("/"); + let filename = split[split.length - 1]; + + if (ext) { + isImage = imageExtension.includes(ext); + } + + let downloadUrl = attachments[currentAttachmentCount].downloadURL; + + let f = fetch(downloadUrl) + .then((response) => { + return response.blob(); + }) + .then((blob) => { + // Create an URL to the blob, which can be opened by the browser (PDF) or displayed (image) + const url = window.URL.createObjectURL( + new Blob([blob], { type: "application/" + ext }) + ); + + // Find the placeholder which we are showing before the file is downloaded. + const item = document.getElementById( + "attachment-" + currentAttachmentCount + ); + + // Either set the image source or link up the attachment. + if (isImage) { + item.src = url; + } else { + item.href = url; + item.className = "note-attachment"; // Indicate the attachment is clickable now + if (ext != "pdf") { + // If it's a PDF we can open it directly, no need to download + item.download = filename; + } + } + + // Hide the loading indicator + document + .getElementById("attachment-loading-" + currentAttachmentCount) + ?.remove(); + }); + + // Create the placeholder html object while we load the attachments + let placeholderId = "attachment-" + currentAttachmentCount; + let placeholderLoadingId = "attachment-loading-" + currentAttachmentCount; + let replace = isImage + ? "
" + : ""; + + ignoreRanges.push([offset, offset + replace.length]); + + // Attachment counter to walk through the assets array + if (isImageRegex) { + attachmentCount += 1; + } else { + imageCount += 1; + } + + return replace; + }; +} + +function separatorReplacer(ignoreRanges) { + return function (match, offset) { + if (shouldIgnore(ignoreRanges, offset)) { + return match; + } + + let replace = "
"; + ignoreRanges.push([offset, offset + replace.length]); + return replace; + }; +} + +function prependHttpsIfNeeded(url) { + const urlPattern = new RegExp( + /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/ + ); + + if (urlPattern.test(url)) { + //string is url + + ///clear http && https from string + url = url.replace("https://", "").replace("http://", ""); + + //add https to string + url = `https://${url}`; + } + + return url; +} + +function namedLinkReplacer(ignoreRanges) { + return function (match, title, url, offset) { + var secureUrl = prependHttpsIfNeeded(url); + let replace = + "" + + title + + ""; + + ignoreRanges.push([offset, offset + replace.length]); + return replace; + }; +} + +function linkReplacer(ignoreRanges) { + return function (match, g1, g2, offset) { + if (shouldIgnore(ignoreRanges, offset)) { + return match; + } + + let replace = + "" + + match + + ""; + ignoreRanges.push([offset, offset + replace.length]); + return replace; + }; +} + +function tagReplacer(ignoreRanges, tag, groupIndecesLeft, groupIndecesInside) { + return function (match) { + let offset = arguments[arguments.length - 2]; + + if (shouldIgnore(ignoreRanges, offset)) { + return match; + } + + let replacementsInside = ""; + groupIndecesInside.forEach( + (index) => (replacementsInside += arguments[index]) + ); + + let replacementsLeft = ""; + groupIndecesLeft.forEach((index) => (replacementsLeft += arguments[index])); + + return ( + replacementsLeft + "<" + tag + ">" + replacementsInside + "" + ); + }; +} + +function doneDateReplacer(ignoreRanges) { + return function (match) { + let offset = arguments[arguments.length - 2]; + + if (shouldIgnore(ignoreRanges, offset)) { + return match; + } + + return "" + match + ""; + }; +} + +function spanReplacer( + ignoreRanges, + styleClass, + groupIndecesLeft, + groupIndecesInside +) { + return function (match) { + let offset = arguments[arguments.length - 2]; + + if (shouldIgnore(ignoreRanges, offset)) { + return match; + } + + let replacementsInside = ""; + groupIndecesInside.forEach( + (index) => (replacementsInside += arguments[index]) + ); + + let replacementsLeft = ""; + groupIndecesLeft.forEach((index) => (replacementsLeft += arguments[index])); + + return ( + replacementsLeft + + "" + + replacementsInside + + "" + ); + }; +} + +function createElement(name: string, innerText: string) { + let ignoreRanges: any[] = []; + let text = document.createTextNode(innerText); + let node = document.createElement(name); + + node.appendChild(text); + + // Parses line by line + let parsed = node.innerHTML + .replace(separatorRegex, separatorReplacer(ignoreRanges)) + .replace(boldRegex, tagReplacer(ignoreRanges, "strong", [1], [4])) + .replace(italicRegex, tagReplacer(ignoreRanges, "em", [1], [4])) + .replace(strikethroughRegex, tagReplacer(ignoreRanges, "del", [1], [4])) + .replace(highlightRegex, tagReplacer(ignoreRanges, "mark", [1], [4])) + .replace(codeRegex, tagReplacer(ignoreRanges, "code", [1], [4])) + .replace(doneDateRegex, doneDateReplacer(ignoreRanges)) + .replace(hashtagRegex, spanReplacer(ignoreRanges, "hashtag", [1], [3])) + .replace(atRegex, spanReplacer(ignoreRanges, "at", [1], [3])) + .replace(imageLinkRegex, fileLinkReplacer(ignoreRanges, true)) + .replace(fileLinkRegex, fileLinkReplacer(ignoreRanges, false)) + .replace(namedLinkRegex, namedLinkReplacer(ignoreRanges)) + .replace(linkRegex, linkReplacer(ignoreRanges)); + + node.innerHTML = parsed; + return node; +} + +function parseHeader(line, regex, type) { + let matches = regex.exec(line); + + if (matches == null) { + return null; + } + + let substring = line.substring(matches[0].length); + let header = createElement(type, substring); + header.id = generateHeaderID(substring); + + return header; +} + +function parseIndentedBlock(line, regex, outterType, innerType) { + let matches = regex.exec(line); + + if (matches == null) { + return null; + } + + let substring = line.substring(matches[0].length); + let leadingWhitespace = matches[1].length; + + let div = document.createElement(outterType); + if (leadingWhitespace > 0) { + div.style.marginLeft = (leadingWhitespace * 36).toString() + "px"; + } + + let node = createElement(innerType, substring); + div.appendChild(node); + return div; +} + +function parseList(line, regex) { + let matches = regex.exec(line); + + if (matches == null) { + return null; + } + + let substring = line.substring(matches[0].length); + let leadingWhitespace = matches[1].length; + + let li = createElement("li", substring); + if (leadingWhitespace > 0) { + li.style.marginLeft = (leadingWhitespace * 36).toString() + "px"; + } + + return li; +} + +function parseParagraph(line) { + if (line.length === 0) { + let node = document.createElement("div"); + node.classList.add("empty-line"); + return node; + } + + return createElement("p", line); +} + +function parseHeader1(line) { + return parseHeader(line, /^#\s+/, "h1"); +} + +function parseHeader2(line) { + return parseHeader(line, /^##\s+/, "h2"); +} + +function parseHeader3(line) { + return parseHeader(line, /^###\s+/, "h3"); +} + +function parseHeader4(line) { + return parseHeader(line, /^#+\s+/, "h4"); +} + +function parseQuote(line) { + return parseIndentedBlock(line, /^(\s*?)>\s+/, "div", "blockquote"); +} + +function parseListWithIcon(line, regex, fontWeight, iconName) { + let li = parseList(line, regex); + + if (li == null) { + return null; + } + + let ul = document.createElement("ul"); + ul.classList.add("fa-ul"); + + let span = document.createElement("span"); + span.classList.add("fa-li"); + + let icon = document.createElement("i"); + icon.classList.add(fontWeight); + icon.classList.add(iconName); + + let argc = arguments.length; + + for (let i = 4; i < argc; i++) { + let name = arguments[i]; + icon.classList.add(name + "-icon"); + li.classList.add(name); + } + + span.appendChild(icon); + li.insertBefore(span, li.childNodes[0]); + ul.appendChild(li); + + return ul; +} + +function parseOrderedList(line) { + let matches = /^(\s*?)(\d+)\.\s+/.exec(line); + + if (matches == null) { + return null; + } + + let substring = line.substring(matches[0].length); + let leadingWhitespace = matches[1].length; + + let li = createElement("li", substring); + if (leadingWhitespace > 0) { + li.style.marginLeft = (leadingWhitespace * 36).toString() + "px"; + } + + li.value = matches[2]; + + let ul = document.createElement("ol"); + ul.classList.add("ordered-list"); + ul.appendChild(li); + + return ul; +} + +function parseToDoHyphen(line) { + return parseListWithIcon( + line, + /^(\s*?)- \[ \]\s+/, + "far", + "fa-circle", + "to-do" + ); +} + +function parseToDoHyphenCancelled(line) { + return parseListWithIcon( + line, + /^(\s*?)- \[-\]\s+/, + "far", + "fa-times-circle", + "to-do-cancelled", + "to-do" + ); +} + +function parseToDoHyphenComplete(line) { + return parseListWithIcon( + line, + /^(\s*?)- \[x\]\s+/, + "far", + "fa-check-circle", + "to-do-complete", + "to-do" + ); +} + +function parseToDoHyphenScheduled(line) { + return parseListWithIcon( + line, + /^(\s*?)- \[>\]\s+/, + "far", + "fa-clock", + "to-do-scheduled", + "to-do" + ); +} + +function parseToDoAsteriskCancelled(line) { + return parseListWithIcon( + line, + /^(\s*?)\* \[-\]\s+/, + "far", + "fa-times-circle", + "to-do-cancelled", + "to-do" + ); +} + +function parseToDoAsteriskComplete(line) { + return parseListWithIcon( + line, + /^(\s*?)\* \[x\]\s+/, + "far", + "fa-check-circle", + "to-do-complete", + "to-do" + ); +} + +function parseToDoAsteriskScheduled(line) { + return parseListWithIcon( + line, + /^(\s*?)\* \[>\]\s+/, + "far", + "fa-clock", + "to-do-scheduled", + "to-do" + ); +} + +function parseToDoAsterisk(line) { + return parseListWithIcon( + line, + /^(\s*?)\*( \[ \])?\s+/, + "far", + "fa-circle", + "to-do" + ); +} + +function parseChecklistPlusCancelled(line) { + return parseListWithIcon( + line, + /^(\s*?)\+ \[-\]\s+/, + "far", + "fa-square-xmark", + "to-do-cancelled", + "to-do" + ); +} + +function parseChecklistPlusComplete(line) { + return parseListWithIcon( + line, + /^(\s*?)\+ \[x\]\s+/, + "far", + "fa-square-check", + "to-do-complete", + "to-do" + ); +} + +function parseChecklistPlusScheduled(line) { + return parseListWithIcon( + line, + /^(\s*?)\+ \[>\]\s+/, + "far", + "fa-clock", + "to-do-scheduled", + "to-do" + ); +} + +function parseChecklistPlus(line) { + return parseListWithIcon( + line, + /^(\s*?)\+( \[ \])?\s+/, + "far", + "fa-square", + "to-do" + ); +} + +function parseUnorderedList(line) { + return parseListWithIcon( + line, + /^(\s*?)-\s+/, + "fas", + "fa-circle", + "unordered-list" + ); +} + +const parseFunctions = [ + parseHeader1, + parseHeader2, + parseHeader3, + parseHeader4, + parseQuote, + parseToDoHyphen, + parseToDoHyphenComplete, + parseToDoHyphenCancelled, + parseToDoHyphenScheduled, + parseToDoAsteriskComplete, + parseToDoAsteriskCancelled, + parseToDoAsteriskScheduled, + parseToDoAsterisk, + parseChecklistPlusComplete, + parseChecklistPlusCancelled, + parseChecklistPlusScheduled, + parseChecklistPlus, + parseUnorderedList, + // parseOrderedList, + parseParagraph, +]; + +function parseMarkdownLine(line: string) { + let parseFunctionsLength = parseFunctions.length; + + for (let i = 0; i < parseFunctionsLength; ++i) { + let node = parseFunctions[i](line); + + if (node != null) { + return node; + } + } +} + +function parseTable(markdown: string): string { + const tableRegex = /\|(.+)\|\n\|( *[-:]+[-| :]*)+\|\n((\|.*\|\n)+)/gm; + let match; + + while ((match = tableRegex.exec(markdown)) !== null) { + const rows = match[0].trim().split("\n"); + const headerCells = rows[0] + .split("|") + .map((cell) => cell.trim()) + .slice(1, -1); + const numColumns = headerCells.length; + + let html = ""; + for (const cell of headerCells) { + html += ``; + } + html += ""; + + for (let i = 2; i < rows.length; i++) { + const cells = rows[i] + .split("|") + .map((cell) => cell.trim()) + .slice(1, -1); + if (cells.length !== numColumns) { + throw new Error( + `Row ${i} has ${cells.length} cells, expected ${numColumns}` + ); + } + html += ""; + for (const cell of cells) { + html += ``; + } + html += ""; + } + + html += "
${cell}
${cell}
"; + markdown = markdown.replace( + match[0], + "
" + html + "

" + ); + } + + return markdown; +} + +function findCodeFenceClose(lines: string[], start: number) { + let linesLength = lines.length; + + for (let i = start; i < linesLength; i++) { + if (codeFenceCloseRegex.test(lines[i])) { + return i; + } + } + + return -1; +} + +function createCodeFence( + lines: string[], + first: number, + last: number +): HTMLElement { + let div = document.createElement("div"); + div.classList.add("code-fence"); + + let code = ""; + for (let i = first; i < last; i++) { + code += lines[i] + "\n"; + } + + // Render the React element into the div + // ReactDOM.render({code}, div); + + return div; +} + +export function parseMarkdown(markdown: string): string { + // Count the images beforehand, because in the attachments array (where the images and files are stored), the images come first, then the other files, so we need to keep track of the index + imageCount = (markdown.match(imageLinkRegex) || []).length; + markdown = parseTable(markdown); + + let lines = markdown.split(/\r?\n/); + let linesCount = lines.length; + let html = ""; + + for (let i = 0; i < linesCount; i++) { + let line = lines[i]; + + if (codeFenceOpenRegex.test(line)) { + let first = i + 1; + let last = findCodeFenceClose(lines, first); + + if (last !== -1) { + let codeFenceNode = createCodeFence(lines, first, last); + html += codeFenceNode.outerHTML; + i = last; + continue; + } + } + + // If the line starts with , ignore it + if (line.startsWith("
")) { + html += line; + continue; + } + + html += parseMarkdownLine(lines[i]).outerHTML; + } + + return html; +} diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts index ee81bf47b7..8a675fa55c 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts @@ -34,7 +34,6 @@ export const BulletListItemBlockContent = createTipTapBlock<"bulletListItem">({ parseHTML() { return [ - // Case for regular HTML list structure. { tag: "li", getAttrs: (element) => { @@ -54,7 +53,7 @@ export const BulletListItemBlockContent = createTipTapBlock<"bulletListItem">({ return false; }, - node: "bulletListItem", + node: this.name, }, // Case for BlockNote list structure. { @@ -77,7 +76,7 @@ export const BulletListItemBlockContent = createTipTapBlock<"bulletListItem">({ return false; }, priority: 300, - node: "bulletListItem", + node: this.name, }, ]; }, From ef0af0fcbfe30e0851f92fd326fff90a3d29d7b1 Mon Sep 17 00:00:00 2001 From: Eduard Metzger Date: Mon, 17 Jul 2023 09:43:46 -0500 Subject: [PATCH 11/48] added scheduled as icon and prop --- examples/editor/src/App.tsx | 6 ++---- .../src/extensions/Blocks/api/defaultBlocks.ts | 3 +++ .../src/extensions/Blocks/nodes/Block.module.css | 14 +++++++++++++- .../ListItemKeyboardShortcuts.ts | 4 ++-- .../CheckListItemBlockContent.ts | 7 +++++++ .../TaskListItemBlockContent.ts | 8 ++++++++ .../TaskListItemNodeView.ts | 3 +++ 7 files changed, 38 insertions(+), 7 deletions(-) diff --git a/examples/editor/src/App.tsx b/examples/editor/src/App.tsx index 5e2ae4e925..737067555f 100644 --- a/examples/editor/src/App.tsx +++ b/examples/editor/src/App.tsx @@ -2,7 +2,7 @@ import "@blocknote/core/style.css"; import { BlockNoteView, useBlockNote } from "@blocknote/react"; import styles from "./App.module.css"; -import { parseMarkdown } from "../../../packages/core/src/api/formatConversions/npMarkdownConversions"; +// import { parseMarkdown } from "../../../packages/core/src/api/formatConversions/npMarkdownConversions"; type WindowWithProseMirror = Window & typeof globalThis & { ProseMirror: any; editor?: any }; @@ -18,8 +18,6 @@ function App() { "editorContent", JSON.stringify(editor.topLevelBlocks) ); - - console.log(editor.topLevelBlocks); }, editorDOMAttributes: { class: styles.editor, @@ -31,7 +29,7 @@ function App() { // Give tests a way to get prosemirror instance (window as WindowWithProseMirror).ProseMirror = editor?._tiptapEditor; (window as WindowWithProseMirror).editor = editor; - window.parseMarkdown = parseMarkdown; + // window.parseMarkdown = parseMarkdown; return ; } diff --git a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts index acc1f4703a..05b53c6bb7 100644 --- a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts +++ b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts @@ -24,6 +24,9 @@ export const defaultProps = { cancelled: { default: "false" as const, }, + scheduled: { + default: "false" as const, + }, } satisfies PropSchema; export type DefaultProps = typeof defaultProps; diff --git a/packages/core/src/extensions/Blocks/nodes/Block.module.css b/packages/core/src/extensions/Blocks/nodes/Block.module.css index d4644660ba..df82a77ee2 100644 --- a/packages/core/src/extensions/Blocks/nodes/Block.module.css +++ b/packages/core/src/extensions/Blocks/nodes/Block.module.css @@ -326,7 +326,8 @@ NESTED BLOCKS position: relative; &[data-checked="true"], - &[data-cancelled="true"] { + &[data-cancelled="true"], + &[data-scheduled="true"] { opacity: 0.4; } @@ -347,6 +348,12 @@ NESTED BLOCKS color: inherit; } + &[data-scheduled="true"] > label:before { + content: "\f017"; + font: var(--fa-font-regular); + color: inherit; + } + > label { user-select: none; font: var(--fa-font-regular); @@ -378,6 +385,11 @@ NESTED BLOCKS &[data-cancelled="true"] > label:before { content: "\f2d3"; } + + /* TODO: Need to get the custom icon working here from the native apps */ + &[data-scheduled="true"] > label:before { + content: "\f017"; + } } /* .blockOuter[data-prev-type="taskListItem"] > .block > .blockContent::before { diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts index 10d86462a7..db9869a78a 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts @@ -58,7 +58,7 @@ export const handleComplete = (editor: Editor) => { tr.setNodeMarkup(editor.state.selection.$from.before(), undefined, { checked: !node.attrs.checked, cancelled: false, - checklist: node.attrs.checklist, + scheduled: false, }); return true; }) @@ -77,7 +77,7 @@ export const handleCancel = (editor: Editor) => { tr.setNodeMarkup(editor.state.selection.$from.before(), undefined, { checked: false, cancelled: !node.attrs.cancelled, - checklist: node.attrs.checklist, + scheduled: false, }); return true; }) diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/CheckListItemBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/CheckListItemBlockContent.ts index d95679b6a1..eeb0fe0955 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/CheckListItemBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/CheckListItemBlockContent.ts @@ -59,6 +59,13 @@ export const CheckListItemBlockContent = createTipTapBlock<"checkListItem">({ "data-cancelled": attributes.cancelled, }), }, + scheduled: { + default: false, + parseHTML: (element) => element.getAttribute("data-scheduled"), + renderHTML: (attributes) => ({ + "data-scheduled": attributes.scheduled, + }), + }, }; }, diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts index 910afec4cf..d86054c673 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts @@ -60,6 +60,14 @@ export const TaskListItemBlockContent = createTipTapBlock<"taskListItem">({ "data-cancelled": attributes.cancelled, }), }, + scheduled: { + default: false, + keepOnSplit: false, + parseHTML: (element) => element.getAttribute("data-scheduled"), + renderHTML: (attributes) => ({ + "data-scheduled": attributes.scheduled, + }), + }, }; }, diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemNodeView.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemNodeView.ts index 5145b7cf06..c7fea5c57f 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemNodeView.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemNodeView.ts @@ -18,6 +18,7 @@ export function TaskListItemNodeView( dom.dataset.contentType = type; dom.dataset.checked = checked; dom.dataset.cancelled = node.attrs.cancelled || false; + dom.dataset.scheduled = node.attrs.scheduled || false; const label = document.createElement("label"); const input = document.createElement("input"); @@ -48,6 +49,7 @@ export function TaskListItemNodeView( : currentNode.attrs), checked: altKey ? true : checked, cancelled: altKey ? true : false, + scheduled: false, }); return true; }) @@ -74,6 +76,7 @@ export function TaskListItemNodeView( dom.dataset.checked = updatedNode.attrs.checked || false; dom.dataset.cancelled = updatedNode.attrs.cancelled || false; + dom.dataset.scheduled = updatedNode.attrs.scheduled || false; if (updatedNode.attrs.checked) { input.setAttribute("checked", "checked"); From ce70ffd15f9c2e1d4fc4ea6cec68124f45f3f205 Mon Sep 17 00:00:00 2001 From: Eduard Metzger Date: Mon, 17 Jul 2023 15:42:42 -0500 Subject: [PATCH 12/48] support checking off multiple tasks --- .../ListItemKeyboardShortcuts.ts | 81 ++++++++++--------- .../CheckListItemBlockContent.ts | 10 +-- .../TaskListItemBlockContent.ts | 10 +-- 3 files changed, 50 insertions(+), 51 deletions(-) diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts index db9869a78a..0496c334c9 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts @@ -46,42 +46,49 @@ export const handleEnter = (editor: Editor) => { ]); }; -export const handleComplete = (editor: Editor) => { - const node = editor.state.selection.$from.node(); - - console.log(node.attrs); - if (node) { - // transaction to toggle checked attribute - return editor - .chain() - .command(({ tr }) => { - tr.setNodeMarkup(editor.state.selection.$from.before(), undefined, { - checked: !node.attrs.checked, - cancelled: false, - scheduled: false, - }); - return true; - }) - .run(); - } - return false; -}; +export const handleAttribute = ( + editor: Editor, + attribute: "checked" | "cancelled" | "scheduled" +) => { + const { from, to } = editor.state.selection; -export const handleCancel = (editor: Editor) => { - const node = editor.state.selection.$from.node(); - if (node) { - // transaction to toggle checked attribute - return editor - .chain() - .command(({ tr }) => { - tr.setNodeMarkup(editor.state.selection.$from.before(), undefined, { - checked: false, - cancelled: !node.attrs.cancelled, - scheduled: false, - }); - return true; - }) - .run(); - } - return false; + // First, find if any node in the range has attribute = false + let anyUnset = false; + editor.state.doc.nodesBetween(from, to, (node) => { + if (node.attrs[attribute] !== undefined && !node.attrs[attribute]) { + anyUnset = true; + } + }); + + // Then, set all nodes' attribute based on anyUnset + editor.state.doc.nodesBetween(from, to, (node, pos) => { + if (node.attrs[attribute] !== undefined) { + const newAttributes = { + ...node.attrs, + [attribute]: anyUnset, // if any node had attribute = false, all nodes will have it set to true + }; + + if (attribute !== "cancelled") { + newAttributes.cancelled = false; + } + + if (attribute !== "scheduled") { + newAttributes.scheduled = false; + } + + if (attribute !== "checked") { + newAttributes.checked = false; + } + + editor + .chain() + .command(({ tr }) => { + tr.setNodeMarkup(pos, undefined, newAttributes); + return true; + }) + .run(); + } + }); + + return true; }; diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/CheckListItemBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/CheckListItemBlockContent.ts index eeb0fe0955..f03138f93d 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/CheckListItemBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/CheckListItemBlockContent.ts @@ -1,10 +1,6 @@ import { InputRule } from "@tiptap/core"; import { createTipTapBlock } from "../../../../api/block"; -import { - handleEnter, - handleComplete, - handleCancel, -} from "../ListItemKeyboardShortcuts"; +import { handleEnter, handleAttribute } from "../ListItemKeyboardShortcuts"; import { TaskListItemNodeView } from "./TaskListItemNodeView"; import { TaskListItemHTMLParser } from "./TaskListItemHTMLParser"; import { TaskListItemListHTMLRender } from "./TaskListItemHTMLRender"; @@ -38,8 +34,8 @@ export const CheckListItemBlockContent = createTipTapBlock<"checkListItem">({ addKeyboardShortcuts() { return { Enter: () => handleEnter(this.editor), - "Cmd-d": () => handleComplete(this.editor), - "Cmd-s": () => handleCancel(this.editor), + "Cmd-d": () => handleAttribute(this.editor, "checked"), + "Cmd-s": () => handleAttribute(this.editor, "cancelled"), }; }, diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts index d86054c673..f9c6d8fe12 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts @@ -1,10 +1,6 @@ import { InputRule } from "@tiptap/core"; import { createTipTapBlock } from "../../../../api/block"; -import { - handleEnter, - handleComplete, - handleCancel, -} from "../ListItemKeyboardShortcuts"; +import { handleEnter, handleAttribute } from "../ListItemKeyboardShortcuts"; import { TaskListItemNodeView } from "./TaskListItemNodeView"; import { TaskListItemHTMLParser } from "./TaskListItemHTMLParser"; import { TaskListItemListHTMLRender } from "./TaskListItemHTMLRender"; @@ -38,8 +34,8 @@ export const TaskListItemBlockContent = createTipTapBlock<"taskListItem">({ addKeyboardShortcuts() { return { Enter: () => handleEnter(this.editor), - "Cmd-d": () => handleComplete(this.editor), - "Cmd-s": () => handleCancel(this.editor), + "Cmd-d": () => handleAttribute(this.editor, "checked"), + "Cmd-s": () => handleAttribute(this.editor, "cancelled"), }; }, From c554fbdeb3712db91d2618bc5688e8b064679618 Mon Sep 17 00:00:00 2001 From: Eduard Metzger Date: Mon, 17 Jul 2023 18:43:51 -0500 Subject: [PATCH 13/48] add shorcuts for moving one or more lines up and down (control+option+up/down arrows), still needs a tooltip somewhere to explain this --- .../ListItemKeyboardShortcuts.ts | 100 ++++++++++++++++++ .../CheckListItemBlockContent.ts | 1 + .../ParagraphBlockContent.ts | 8 ++ 3 files changed, 109 insertions(+) diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts index 0496c334c9..b918d5fe73 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts @@ -92,3 +92,103 @@ export const handleAttribute = ( return true; }; + +export const handleMove = (editor: Editor, direction: "up" | "down") => { + const { from, to } = editor.state.selection; + + const nodes: any[] = []; + editor.state.doc.nodesBetween(from, to, (node) => { + // Check if defaultBlockSchema has the node type + if (node.type.name === "blockContainer") { + nodes.push(node); + } + }); + + // Save first and last nodes for setting the cursor later + const startNode = editor.state.selection.$from.node(-1); + const endNode = editor.state.selection.$to.node(-1); + + // Save the cursor offset within the taskNode before moving + const cursorOffsetStart = editor.state.selection.$from.parentOffset; + const cursorOffsetEnd = editor.state.selection.$to.parentOffset; + + // Get task or check node of the text node and its index + const nodeIndexStart = editor.state.selection.$from.index(-2); + const nodeIndexEnd = editor.state.selection.$to.index(-2); + + // Determine the target index based on the direction + const targetIndex = + direction === "up" ? nodeIndexStart - 1 : nodeIndexEnd + 2; + + // Ensure movement is possible (not out of bounds) + const listNode = editor.state.selection.$from.node(-2); + const isWithinBounds = + direction === "up" + ? nodeIndexStart > 0 + : nodeIndexEnd < listNode.childCount - 1; + + if (nodes.length > 0 && isWithinBounds) { + // Start a transaction + let tr = editor.state.tr; + + // Delete the range + const deleteAction = () => { + tr = tr.deleteRange( + editor.state.selection.$from.posAtIndex(nodeIndexStart, -2), + editor.state.selection.$to.posAtIndex(nodeIndexEnd + 1, -2) + ); + }; + + const insertAction = () => { + // Insert the fragment at the target index + // Loop through all nodes in nodes + for (let i = nodes.length - 1; i >= 0; i--) { + const node = nodes[i]; + + // Insert each node at the target index + tr = tr.insert( + editor.state.selection.$from.posAtIndex(targetIndex, -2), + node + ); + } + }; + + if (direction === "up") { + deleteAction(); + insertAction(); + } else { + insertAction(); + deleteAction(); + } + + // Apply the transaction + editor.view.dispatch(tr); + + // Fix the cursor selection or it's always at the end of the node, which is not good if the node has children + // First find the first and last node in the list to fetch the start and end pos + let startPos = -1; + let endPos = -1; + editor.state.doc.descendants((node, pos) => { + if (node.attrs.id === startNode.attrs.id) { + startPos = pos; + } + + if (node.attrs.id === endNode.attrs.id) { + endPos = pos; + } + }); + + // Set the cursor selection to the pos + if (startPos > -1 && endPos > -1) { + editor + .chain() + .setTextSelection({ + from: startPos + cursorOffsetStart + 2, + to: endPos + cursorOffsetEnd + 2, + }) + .run(); + } + } + + return true; +}; diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/CheckListItemBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/CheckListItemBlockContent.ts index f03138f93d..d2e189d4f5 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/CheckListItemBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/CheckListItemBlockContent.ts @@ -22,6 +22,7 @@ export const CheckListItemBlockContent = createTipTapBlock<"checkListItem">({ props: { checked: "false", canceled: "false", + scheduled: "false", }, }) // Removes the "*" character used to set the list. diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent.ts index 9d44f9bb7a..9bc244a9e7 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent.ts @@ -1,11 +1,19 @@ import { mergeAttributes } from "@tiptap/core"; import { createTipTapBlock } from "../../../api/block"; import styles from "../../Block.module.css"; +import { handleMove } from "../ListItemBlockContent/ListItemKeyboardShortcuts"; export const ParagraphBlockContent = createTipTapBlock<"paragraph">({ name: "paragraph", content: "inline*", + addKeyboardShortcuts() { + return { + "Control-alt-ArrowDown": () => handleMove(this.editor, "down"), + "Control-alt-ArrowUp": () => handleMove(this.editor, "up"), + }; + }, + parseHTML() { return [ { From 91d99efe230c8872c1d84168aadc6eda046bf96e Mon Sep 17 00:00:00 2001 From: Eduard Metzger Date: Tue, 18 Jul 2023 17:14:16 -0500 Subject: [PATCH 14/48] adjust headings sizes --- packages/core/src/extensions/Blocks/nodes/Block.module.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/extensions/Blocks/nodes/Block.module.css b/packages/core/src/extensions/Blocks/nodes/Block.module.css index df82a77ee2..ff7a03afb6 100644 --- a/packages/core/src/extensions/Blocks/nodes/Block.module.css +++ b/packages/core/src/extensions/Blocks/nodes/Block.module.css @@ -132,10 +132,10 @@ NESTED BLOCKS /* HEADINGS*/ [data-level="1"] { - --level: 3em; + --level: 1.9em; } [data-level="2"] { - --level: 2em; + --level: 1.6em; } [data-level="3"] { --level: 1.3em; From 50e5836f97744d759774a710751ed2f1285892e3 Mon Sep 17 00:00:00 2001 From: Eduard Metzger Date: Wed, 19 Jul 2023 18:57:36 -0500 Subject: [PATCH 15/48] add hashtags/mentions --- packages/core/src/BlockNoteExtensions.ts | 2 + .../api/nodeConversions/nodeConversions.ts | 1 + .../Blocks/api/inlineContentTypes.ts | 1 + .../core/src/extensions/Blocks/inline/tags.ts | 104 ++++++++++++++++++ .../extensions/Blocks/nodes/Block.module.css | 17 +++ 5 files changed, 125 insertions(+) create mode 100644 packages/core/src/extensions/Blocks/inline/tags.ts diff --git a/packages/core/src/BlockNoteExtensions.ts b/packages/core/src/BlockNoteExtensions.ts index 084432e32e..9140bce455 100644 --- a/packages/core/src/BlockNoteExtensions.ts +++ b/packages/core/src/BlockNoteExtensions.ts @@ -4,6 +4,7 @@ import { BlockNoteEditor } from "./BlockNoteEditor"; import { Bold } from "@tiptap/extension-bold"; import { Code } from "@tiptap/extension-code"; +import { Hashtag } from "./extensions/Blocks/inline/tags"; import Collaboration from "@tiptap/extension-collaboration"; import CollaborationCursor from "@tiptap/extension-collaboration-cursor"; import { Dropcursor } from "@tiptap/extension-dropcursor"; @@ -99,6 +100,7 @@ export const getBlockNoteExtensions = (opts: { Italic, Strike, Underline, + Hashtag, TextColorMark, TextColorExtension, BackgroundColorMark, diff --git a/packages/core/src/api/nodeConversions/nodeConversions.ts b/packages/core/src/api/nodeConversions/nodeConversions.ts index dc7f53c226..09aab9559d 100644 --- a/packages/core/src/api/nodeConversions/nodeConversions.ts +++ b/packages/core/src/api/nodeConversions/nodeConversions.ts @@ -26,6 +26,7 @@ const toggleStyles = new Set([ "underline", "strike", "code", + "hashtag", ]); const colorStyles = new Set(["textColor", "backgroundColor"]); diff --git a/packages/core/src/extensions/Blocks/api/inlineContentTypes.ts b/packages/core/src/extensions/Blocks/api/inlineContentTypes.ts index 9d63930d95..1895c2df6b 100644 --- a/packages/core/src/extensions/Blocks/api/inlineContentTypes.ts +++ b/packages/core/src/extensions/Blocks/api/inlineContentTypes.ts @@ -6,6 +6,7 @@ export type Styles = { code?: true; textColor?: string; backgroundColor?: string; + hashtag?: true; }; export type ToggledStyle = { diff --git a/packages/core/src/extensions/Blocks/inline/tags.ts b/packages/core/src/extensions/Blocks/inline/tags.ts new file mode 100644 index 0000000000..078c88970d --- /dev/null +++ b/packages/core/src/extensions/Blocks/inline/tags.ts @@ -0,0 +1,104 @@ +import { InputRule, Mark, markPasteRule } from "@tiptap/core"; +import { Plugin } from "prosemirror-state"; + +export interface HashtagOptions { + HTMLAttributes: Record; +} + +export const hashtagInputRegex = /(?<=(^|\s))(((#|@)(?:[\w-_/]+)))$/; +export const hashtagPasteRegex = /(?<=(^|\s))(((#|@)(?:[\w-_/]+)))/g; + +export const Hashtag = Mark.create({ + name: "hashtag", + + parseHTML() { + return [ + { + tag: "span", + getAttrs: (element: HTMLElement | string) => { + if (typeof element === "string") { + return false; + } + + if (element.getAttribute("data-hashtag") === this.name) { + return {}; + } + + return false; + }, + }, + ]; + }, + + renderHTML() { + return ["span", { "data-hashtag": this.name }, 0]; + }, + + addAttributes() { + return { + hashtag: { + default: false, + parseHTML: (element) => element.getAttribute("data-hashtag"), + renderHTML: () => ({ + "data-hashtag": true, + }), + }, + }; + }, + + addInputRules() { + return [ + new InputRule({ + find: new RegExp(hashtagInputRegex), + handler: ({ state, match, range }) => { + const attrs = { hashtag: true }; + const tr = state.tr.insertText(match[0], range.from, range.to); + state.tr.addMark(range.from, range.to + 1, this.type.create(attrs)); + state.applyTransaction(tr); + }, + }), + ]; + }, + + addPasteRules() { + return [ + markPasteRule({ + find: hashtagPasteRegex, + type: this.type, + }), + ]; + }, + + addProseMirrorPlugins() { + // this plugin will clear the hashtag mark when a space is typed + return [ + new Plugin({ + props: { + handleTextInput: (view, from, to, text) => { + const { state } = view; + const { $from } = state.selection; + const markType = this.type; + + console.log($from.marks().some((mark) => mark.type === markType)); + + if ( + text === " " && + $from.marks().some((mark) => mark.type === markType) + ) { + const transaction = state.tr.replaceWith( + from, + to, + state.schema.text(text) + ); + view.dispatch(transaction); + + return true; + } + + return false; + }, + }, + }), + ]; + }, +}); diff --git a/packages/core/src/extensions/Blocks/nodes/Block.module.css b/packages/core/src/extensions/Blocks/nodes/Block.module.css index ff7a03afb6..7efda543f4 100644 --- a/packages/core/src/extensions/Blocks/nodes/Block.module.css +++ b/packages/core/src/extensions/Blocks/nodes/Block.module.css @@ -450,6 +450,13 @@ NESTED BLOCKS content: "List"; } +/* Inline Code */ +code { + color: #0091f9; + font-family: "SF Mono", Menlo, Monaco, "Courier New", Courier, monospace; + font-weight: 550; +} + /* TEXT COLORS */ [data-text-color="gray"] { color: #9b9a97; @@ -540,3 +547,13 @@ NESTED BLOCKS [data-text-alignment="justify"] { text-align: justify; } + +/* HASHTAG */ +span[data-hashtag] { + color: var(--orange-noteplan); + cursor: pointer; + padding: 2px 5px; /* adjust as needed */ + border-radius: 10px; /* this gives the pill shape */ + background-color: #d8700120; /* replace with your desired background color */ + text-decoration: none; /* optional: removes underline if your hashtags are also links */ +} From 36307c77c8fcfdef2636ad77a7a21d22efc47e43 Mon Sep 17 00:00:00 2001 From: Eduard Metzger Date: Wed, 19 Jul 2023 19:08:09 -0500 Subject: [PATCH 16/48] Fix tag pasting issues --- packages/core/src/extensions/Blocks/inline/tags.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/core/src/extensions/Blocks/inline/tags.ts b/packages/core/src/extensions/Blocks/inline/tags.ts index 078c88970d..11b0c36eb3 100644 --- a/packages/core/src/extensions/Blocks/inline/tags.ts +++ b/packages/core/src/extensions/Blocks/inline/tags.ts @@ -5,8 +5,9 @@ export interface HashtagOptions { HTMLAttributes: Record; } -export const hashtagInputRegex = /(?<=(^|\s))(((#|@)(?:[\w-_/]+)))$/; -export const hashtagPasteRegex = /(?<=(^|\s))(((#|@)(?:[\w-_/]+)))/g; +export const hashtagInputRegex = /((#|@)[\w-_/]+)$/; +export const hashtagPasteRegex = /((^|\s)#[\w-_/]+)/g; +export const mentionPasteRegex = /((^|\s)@[\w-_/]+)/g; export const Hashtag = Mark.create({ name: "hashtag", @@ -66,6 +67,10 @@ export const Hashtag = Mark.create({ find: hashtagPasteRegex, type: this.type, }), + markPasteRule({ + find: mentionPasteRegex, + type: this.type, + }), ]; }, @@ -79,8 +84,6 @@ export const Hashtag = Mark.create({ const { $from } = state.selection; const markType = this.type; - console.log($from.marks().some((mark) => mark.type === markType)); - if ( text === " " && $from.marks().some((mark) => mark.type === markType) From 9481874b80e6cd10af8006e2079843e328724897 Mon Sep 17 00:00:00 2001 From: Eduard Metzger Date: Wed, 19 Jul 2023 19:10:32 -0500 Subject: [PATCH 17/48] fix in tags it should have space before the tag --- packages/core/src/extensions/Blocks/inline/tags.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/extensions/Blocks/inline/tags.ts b/packages/core/src/extensions/Blocks/inline/tags.ts index 11b0c36eb3..e8e1b95030 100644 --- a/packages/core/src/extensions/Blocks/inline/tags.ts +++ b/packages/core/src/extensions/Blocks/inline/tags.ts @@ -5,7 +5,7 @@ export interface HashtagOptions { HTMLAttributes: Record; } -export const hashtagInputRegex = /((#|@)[\w-_/]+)$/; +export const hashtagInputRegex = /(?<=(^|\s))((#|@)[\w-_/]+)$/; export const hashtagPasteRegex = /((^|\s)#[\w-_/]+)/g; export const mentionPasteRegex = /((^|\s)@[\w-_/]+)/g; From 93146234de67b4d7be1889a58f8ecd8432bd58c6 Mon Sep 17 00:00:00 2001 From: Eduard Metzger Date: Wed, 19 Jul 2023 22:15:09 -0500 Subject: [PATCH 18/48] fixed tagging issue --- .../core/src/extensions/Blocks/inline/tags.ts | 107 ++++++++++-------- 1 file changed, 58 insertions(+), 49 deletions(-) diff --git a/packages/core/src/extensions/Blocks/inline/tags.ts b/packages/core/src/extensions/Blocks/inline/tags.ts index e8e1b95030..18bd4e600b 100644 --- a/packages/core/src/extensions/Blocks/inline/tags.ts +++ b/packages/core/src/extensions/Blocks/inline/tags.ts @@ -4,10 +4,11 @@ import { Plugin } from "prosemirror-state"; export interface HashtagOptions { HTMLAttributes: Record; } +// export const hashtagInputRegex = /(?<=(^|\s))((#|@)[\w-_/]+)$/; -export const hashtagInputRegex = /(?<=(^|\s))((#|@)[\w-_/]+)$/; -export const hashtagPasteRegex = /((^|\s)#[\w-_/]+)/g; -export const mentionPasteRegex = /((^|\s)@[\w-_/]+)/g; +export const hashtagInputRegex = /(?<=(^|\s))((#|@)[\w-_/]+)/g; +export const hashtagPasteRegex = /(?:^|\s)(#[\w-_/]+)/g; +export const mentionPasteRegex = /(?:^|\s)(@[\w-_/]+)/g; export const Hashtag = Mark.create({ name: "hashtag", @@ -47,59 +48,67 @@ export const Hashtag = Mark.create({ }; }, - addInputRules() { - return [ - new InputRule({ - find: new RegExp(hashtagInputRegex), - handler: ({ state, match, range }) => { - const attrs = { hashtag: true }; - const tr = state.tr.insertText(match[0], range.from, range.to); - state.tr.addMark(range.from, range.to + 1, this.type.create(attrs)); - state.applyTransaction(tr); - }, - }), - ]; - }, + // addInputRules() { + // return [ + // new InputRule({ + // find: new RegExp(hashtagInputRegex), + // handler: ({ state, match, range }) => { + // const attrs = { hashtag: true }; + // const tr = state.tr.insertText(match[0], range.from, range.to); + // state.tr.addMark(range.from, range.to + 1, this.type.create(attrs)); + // state.applyTransaction(tr); + // }, + // }), + // ]; + // }, - addPasteRules() { - return [ - markPasteRule({ - find: hashtagPasteRegex, - type: this.type, - }), - markPasteRule({ - find: mentionPasteRegex, - type: this.type, - }), - ]; - }, + // addPasteRules() { + // return [ + // markPasteRule({ + // find: hashtagPasteRegex, + // type: this.type, + // }), + // markPasteRule({ + // find: mentionPasteRegex, + // type: this.type, + // }), + // ]; + // }, addProseMirrorPlugins() { // this plugin will clear the hashtag mark when a space is typed + // and also detect valid mentions and hashtags that become valid due to space insertion return [ new Plugin({ - props: { - handleTextInput: (view, from, to, text) => { - const { state } = view; - const { $from } = state.selection; - const markType = this.type; - - if ( - text === " " && - $from.marks().some((mark) => mark.type === markType) - ) { - const transaction = state.tr.replaceWith( - from, - to, - state.schema.text(text) - ); - view.dispatch(transaction); - - return true; - } + props: {}, + appendTransaction: (_transactions, _oldState, newState) => { + const state = newState; + const markType = this.type; + const { tr } = state; - return false; - }, + // Start of line position + const lineStart = state.doc + .resolve(state.selection.$from.pos) + .start(-1); + // End of line position + const lineEnd = state.doc.resolve(state.selection.$from.pos).end(-1); + + // Remove all hashtag marks in the line + tr.removeMark(lineStart, lineEnd, markType); + + // Scan the whole line for valid mentions or hashtags + const lineText = state.doc.textBetween(lineStart, lineEnd, "\n"); + + let match; + while ((match = hashtagInputRegex.exec(lineText)) !== null) { + // If there is a valid mention or hashtag, add the mark + const fromHashtag = lineStart + match.index + 1; + const toHashtag = fromHashtag + match[0].length; + const attrs = { hashtag: true }; + tr.addMark(fromHashtag, toHashtag, markType.create(attrs)); + } + + return tr; }, }), ]; From 928451c1394f00af17362c73f110abd5d9e902a3 Mon Sep 17 00:00:00 2001 From: Anh Tuan Van <217000+anhtuanvan@users.noreply.github.com> Date: Thu, 20 Jul 2023 20:23:15 +0900 Subject: [PATCH 19/48] WIP noteplan to blocks conversion and test page --- examples/editor/src/App.tsx | 62 +- package-lock.json | 301 +++---- packages/core/package.json | 1 + .../formatConversions/notePlanConversions.ts | 747 ++++++++++++++++++ 4 files changed, 917 insertions(+), 194 deletions(-) create mode 100644 packages/core/src/api/formatConversions/notePlanConversions.ts diff --git a/examples/editor/src/App.tsx b/examples/editor/src/App.tsx index 5e2ae4e925..613ecb4009 100644 --- a/examples/editor/src/App.tsx +++ b/examples/editor/src/App.tsx @@ -2,24 +2,30 @@ import "@blocknote/core/style.css"; import { BlockNoteView, useBlockNote } from "@blocknote/react"; import styles from "./App.module.css"; -import { parseMarkdown } from "../../../packages/core/src/api/formatConversions/npMarkdownConversions"; +import { parseNoteToBlocks } from "../../../packages/core/src/api/formatConversions/notePlanConversions"; + +import { useEffect, useState } from "react"; +import { BlockNoteEditor, BlockSchema, PartialBlock } from "@blocknote/core"; type WindowWithProseMirror = Window & typeof globalThis & { ProseMirror: any; editor?: any }; -// Gets the previously stored editor contents. -const initialContent: string | null = localStorage.getItem("editorContent"); - function App() { - const editor = useBlockNote({ - initialContent: initialContent ? JSON.parse(initialContent) : undefined, - onEditorContentChange: (editor) => { - localStorage.setItem( - "editorContent", - JSON.stringify(editor.topLevelBlocks) - ); - - console.log(editor.topLevelBlocks); + const [input, setInput] = useState(""); + const [markdown, setMarkdown] = useState(""); + const [json, setJSON] = useState(""); + + const editor: BlockNoteEditor | null = useBlockNote({ + onEditorContentChange: (editor: BlockNoteEditor) => { + const saveBlocksAsMarkdown = async () => { + const markdown: string = await editor.blocksToMarkdown( + editor.topLevelBlocks + ); + setMarkdown(markdown); + const json: string = JSON.stringify(editor.topLevelBlocks, null, 2); + setJSON(json); + }; + saveBlocksAsMarkdown(); }, editorDOMAttributes: { class: styles.editor, @@ -28,12 +34,38 @@ function App() { theme: "light", }); + useEffect(() => { + if (editor) { + // Whenever the current Markdown content changes, converts it to an array + // of PartialBlock objects and replaces the editor's content with them. + const getBlocks = async () => { + const blocks: PartialBlock[] = parseNoteToBlocks(input); + console.log(blocks); + editor.replaceBlocks(editor.topLevelBlocks, blocks); + }; + getBlocks(); + } + }, [editor, input]); + // Give tests a way to get prosemirror instance (window as WindowWithProseMirror).ProseMirror = editor?._tiptapEditor; (window as WindowWithProseMirror).editor = editor; - window.parseMarkdown = parseMarkdown; - return ; + return ( +
+

Input

+