")) {
+ html += line;
+ continue;
+ }
+
+ html += parseMarkdownLine(lines[i]).outerHTML;
+ }
+
+ return html;
+}
diff --git a/packages/core/src/api/nodeConversions/nodeConversions.ts b/packages/core/src/api/nodeConversions/nodeConversions.ts
index dc7f53c226..481adac99c 100644
--- a/packages/core/src/api/nodeConversions/nodeConversions.ts
+++ b/packages/core/src/api/nodeConversions/nodeConversions.ts
@@ -8,6 +8,7 @@ import {
import { defaultProps } from "../../extensions/Blocks/api/defaultBlocks";
import {
+ ContentAttributes,
ColorStyle,
InlineContent,
PartialInlineContent,
@@ -23,9 +24,15 @@ import { UnreachableCaseError } from "../../shared/utils";
const toggleStyles = new Set
([
"bold",
"italic",
- "underline",
- "strike",
+ "underlined",
+ "strikethrough",
"code",
+ "hashtag",
+ "wikilink",
+ "inlineImage",
+ "inlineFile",
+ "datelink",
+ "highlighted",
]);
const colorStyles = new Set(["textColor", "backgroundColor"]);
@@ -35,10 +42,9 @@ const colorStyles = new Set(["textColor", "backgroundColor"]);
*/
function styledTextToNodes(styledText: StyledText, schema: Schema): Node[] {
const marks: Mark[] = [];
-
for (const [style, value] of Object.entries(styledText.styles)) {
if (toggleStyles.has(style as ToggledStyle)) {
- marks.push(schema.mark(style));
+ marks.push(schema.mark(style, styledText.attrs));
} else if (colorStyles.has(style as ColorStyle)) {
marks.push(schema.mark(style, { color: value }));
}
@@ -103,6 +109,7 @@ function styledTextArrayToNodes(
for (const styledText of content) {
nodes.push(...styledTextToNodes(styledText, schema));
}
+
return nodes;
}
@@ -207,6 +214,7 @@ function contentNodeToInlineContent(contentNode: Node) {
currentContent = {
type: "text",
text: "\n",
+ attrs: {},
styles: {},
};
}
@@ -215,6 +223,7 @@ function contentNodeToInlineContent(contentNode: Node) {
}
const styles: Styles = {};
+ let attrs: ContentAttributes = {};
let linkMark: Mark | undefined;
for (const mark of node.marks) {
@@ -222,8 +231,10 @@ function contentNodeToInlineContent(contentNode: Node) {
linkMark = mark;
} else if (toggleStyles.has(mark.type.name as ToggledStyle)) {
styles[mark.type.name as ToggledStyle] = true;
+ attrs = mark.attrs;
} else if (colorStyles.has(mark.type.name as ColorStyle)) {
styles[mark.type.name as ColorStyle] = mark.attrs.color;
+ attrs = mark.attrs;
} else {
throw Error("Mark is of an unrecognized type: " + mark.type.name);
}
@@ -248,6 +259,7 @@ function contentNodeToInlineContent(contentNode: Node) {
type: "text",
text: node.textContent,
styles,
+ attrs,
};
}
} else {
@@ -261,6 +273,7 @@ function contentNodeToInlineContent(contentNode: Node) {
type: "text",
text: node.textContent,
styles,
+ attrs,
},
],
};
@@ -285,6 +298,7 @@ function contentNodeToInlineContent(contentNode: Node) {
type: "text",
text: node.textContent,
styles,
+ attrs,
});
}
} else {
@@ -298,6 +312,7 @@ function contentNodeToInlineContent(contentNode: Node) {
type: "text",
text: node.textContent,
styles,
+ attrs,
},
],
};
@@ -309,6 +324,7 @@ function contentNodeToInlineContent(contentNode: Node) {
type: "text",
text: node.textContent,
styles,
+ attrs,
};
}
}
@@ -321,6 +337,7 @@ function contentNodeToInlineContent(contentNode: Node) {
type: "text",
text: node.textContent,
styles,
+ attrs,
};
}
// Node is a link.
@@ -333,6 +350,7 @@ function contentNodeToInlineContent(contentNode: Node) {
type: "text",
text: node.textContent,
styles,
+ attrs,
},
],
};
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/blockTypes.ts b/packages/core/src/extensions/Blocks/api/blockTypes.ts
index ee49d9921d..b9b405eed1 100644
--- a/packages/core/src/extensions/Blocks/api/blockTypes.ts
+++ b/packages/core/src/extensions/Blocks/api/blockTypes.ts
@@ -34,8 +34,8 @@ export type TipTapNode<
// Defines a single prop spec, which includes the default value the prop should
// take and possible values it can take.
export type PropSpec = {
- values?: readonly string[];
- default: string;
+ values?: readonly any[];
+ default: any;
};
// Defines multiple block prop specs. The key of each prop is the name of the
@@ -49,9 +49,9 @@ export type PropSchema = Record;
// each prop spec into a union type of its possible values, or a string if no
// values are specified.
export type Props = {
- [PType in keyof PSchema]: PSchema[PType]["values"] extends readonly string[]
+ [PType in keyof PSchema]: PSchema[PType]["values"] extends readonly any[]
? PSchema[PType]["values"][number]
- : string;
+ : any;
};
// Defines the config for a single block. Meant to be used as an argument to
diff --git a/packages/core/src/extensions/Blocks/api/createLinkMark.ts b/packages/core/src/extensions/Blocks/api/createLinkMark.ts
new file mode 100644
index 0000000000..24a8f5c280
--- /dev/null
+++ b/packages/core/src/extensions/Blocks/api/createLinkMark.ts
@@ -0,0 +1,88 @@
+import { Mark } from "@tiptap/core";
+import { InlineParser } from "./inlineParser";
+
+export interface LinkOptions {
+ HTMLAttributes: Record;
+}
+
+interface LinkMarkOptions {
+ name: string;
+ regex: RegExp;
+ dataAttr: string;
+ hrefPrefix: string | undefined;
+ attrsMap: { [attr: string]: number };
+}
+
+export const createLinkMark = ({
+ name,
+ regex,
+ dataAttr,
+ hrefPrefix,
+ attrsMap,
+}: LinkMarkOptions) =>
+ Mark.create({
+ name,
+
+ parseHTML() {
+ return [
+ {
+ tag: "a",
+ getAttrs: (element: HTMLElement | string) => {
+ if (typeof element === "string") {
+ return false;
+ }
+
+ if (element.getAttribute(dataAttr) === this.name) {
+ return {
+ href: element.getAttribute("href") || null,
+ };
+ }
+
+ return false;
+ },
+ },
+ ];
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return [
+ "a",
+ {
+ ...HTMLAttributes,
+ href: "/?" + (hrefPrefix ?? "search") + "=" + HTMLAttributes.href,
+ target: "_self",
+ [dataAttr]: this.name,
+ },
+ 0,
+ ];
+ },
+
+ addAttributes() {
+ return {
+ href: {
+ default: null,
+ parseHTML: (element) => element.getAttribute("href"),
+ renderHTML: (attributes) => ({
+ href: attributes.href,
+ }),
+ },
+ [name]: {
+ default: false,
+ parseHTML: (element) => element.getAttribute(dataAttr),
+ renderHTML: () => ({
+ [dataAttr]: true,
+ }),
+ },
+ };
+ },
+
+ addProseMirrorPlugins() {
+ return [
+ new InlineParser({
+ markType: this.type,
+ regex,
+ attrsMap: attrsMap,
+ }).plugin,
+ ];
+ },
+ });
diff --git a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts
index d60d716cce..e0b15e9257 100644
--- a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts
+++ b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts
@@ -1,8 +1,25 @@
import { HeadingBlockContent } from "../nodes/BlockContent/HeadingBlockContent/HeadingBlockContent";
+import { SeparatorBlockContent } from "../nodes/BlockContent/SeparatorBlockContent/SeparatorBlockContent";
import { BulletListItemBlockContent } from "../nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent";
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";
+import { PropSchema, TipTapNode, TypesMatch } from "./blockTypes";
+import { TableBlockContent } from "../nodes/BlockContent/TableBlockContent/TableBlockContent";
+import { CodeBlockLowlightBlockContent } from "../nodes/BlockContent/CodeFenceBlockContent/CodeFenceBlockContentItem";
+import { lowlight } from "lowlight";
+
+import css from "highlight.js/lib/languages/css";
+import js from "highlight.js/lib/languages/javascript";
+import ts from "highlight.js/lib/languages/typescript";
+import html from "highlight.js/lib/languages/xml";
+
+lowlight.registerLanguage("html", html);
+lowlight.registerLanguage("css", css);
+lowlight.registerLanguage("js", js);
+lowlight.registerLanguage("ts", ts);
export const defaultProps = {
backgroundColor: {
@@ -19,9 +36,42 @@ export const defaultProps = {
export type DefaultProps = typeof defaultProps;
+const taskAndCheckProps = {
+ checked: {
+ default: false,
+ },
+ cancelled: {
+ default: false,
+ },
+ scheduled: {
+ default: false,
+ },
+} satisfies PropSchema;
+
+const tableProps = {
+ data: {
+ default: "" as const,
+ },
+} satisfies PropSchema;
+
+const codeProps = {
+ language: {
+ default: "javascript" as const,
+ },
+} satisfies PropSchema;
+
+const levelProps = {
+ level: {
+ default: "0" as const,
+ },
+} satisfies PropSchema;
+
export const defaultBlockSchema = {
paragraph: {
- propSchema: defaultProps,
+ propSchema: {
+ ...defaultProps,
+ ...levelProps,
+ },
node: ParagraphBlockContent,
},
heading: {
@@ -31,14 +81,62 @@ export const defaultBlockSchema = {
},
node: HeadingBlockContent,
},
+ separator: {
+ propSchema: {
+ ...defaultProps,
+ },
+ node: SeparatorBlockContent,
+ },
bulletListItem: {
- propSchema: defaultProps,
+ propSchema: {
+ ...defaultProps,
+ ...levelProps,
+ },
node: BulletListItemBlockContent,
},
+ taskListItem: {
+ propSchema: {
+ ...defaultProps,
+ ...levelProps,
+ ...taskAndCheckProps,
+ },
+ node: TaskListItemBlockContent,
+ },
+ checkListItem: {
+ propSchema: {
+ ...defaultProps,
+ ...taskAndCheckProps,
+ },
+ node: CheckListItemBlockContent,
+ },
+ quoteListItem: {
+ propSchema: {
+ ...defaultProps,
+ },
+ node: QuotListItemBlockContent,
+ },
numberedListItem: {
- propSchema: defaultProps,
+ propSchema: {
+ ...defaultProps,
+ },
node: NumberedListItemBlockContent,
},
+ tableBlockItem: {
+ propSchema: {
+ ...defaultProps,
+ ...tableProps,
+ },
+ node: TableBlockContent,
+ },
+ codefence: {
+ propSchema: {
+ ...defaultProps,
+ ...codeProps,
+ },
+ node: CodeBlockLowlightBlockContent.configure({
+ lowlight,
+ }) as TipTapNode<"codefence">,
+ },
} as const;
export type DefaultBlockSchema = TypesMatch;
diff --git a/packages/core/src/extensions/Blocks/api/inlineContentTypes.ts b/packages/core/src/extensions/Blocks/api/inlineContentTypes.ts
index 9d63930d95..2b433561e2 100644
--- a/packages/core/src/extensions/Blocks/api/inlineContentTypes.ts
+++ b/packages/core/src/extensions/Blocks/api/inlineContentTypes.ts
@@ -1,15 +1,28 @@
export type Styles = {
- bold?: true;
- italic?: true;
- underline?: true;
- strike?: true;
- code?: true;
+ task?: boolean;
+ checkbox?: boolean;
+ bold?: boolean;
+ italic?: boolean;
+ strikethrough?: boolean;
+ inlineImage?: boolean;
+ inlineFile?: boolean;
+ code?: boolean;
textColor?: string;
backgroundColor?: string;
+ highlighted?: boolean;
+ underlined?: boolean;
+ hashtag?: boolean;
+ wikilink?: boolean;
+ datelink?: boolean;
+};
+
+export type ContentAttributes = {
+ src?: string;
+ href?: string;
};
export type ToggledStyle = {
- [K in keyof Styles]-?: Required[K] extends true ? K : never;
+ [K in keyof Styles]-?: Required[K] extends boolean ? K : never;
}[keyof Styles];
export type ColorStyle = {
@@ -20,6 +33,7 @@ export type StyledText = {
type: "text";
text: string;
styles: Styles;
+ attrs?: ContentAttributes;
};
export type Link = {
diff --git a/packages/core/src/extensions/Blocks/api/inlineParser.ts b/packages/core/src/extensions/Blocks/api/inlineParser.ts
new file mode 100644
index 0000000000..5cd074002f
--- /dev/null
+++ b/packages/core/src/extensions/Blocks/api/inlineParser.ts
@@ -0,0 +1,173 @@
+import { EditorState, Plugin } from "prosemirror-state";
+import { MarkType } from "@tiptap/pm/model";
+import { Node } from "prosemirror-model";
+
+// Define the mark options which include the MarkType and regex to be used for parsing the content.
+interface MarkOptions {
+ markType: MarkType;
+ regex: RegExp;
+ attrsMap: { [attr: string]: number };
+}
+
+// Class to manage the key of a Mark.
+class MarkKey {
+ constructor(
+ public from: number,
+ public to: number,
+ public attrs: { [attr: string]: string | Boolean }
+ ) {}
+
+ // Convert the MarkKey instance into a string representation.
+ toString() {
+ return JSON.stringify(this);
+ }
+}
+
+// Main class for the inline parser.
+export class InlineParser {
+ private markType: MarkType;
+ private regex: RegExp;
+ private attrsMap: { [attr: string]: number };
+
+ constructor({ markType, regex, attrsMap }: MarkOptions) {
+ this.markType = markType;
+ this.regex = regex;
+ this.attrsMap = attrsMap;
+ }
+
+ // Extract the existing marks within the given range.
+ private extractExistingMarks(
+ state: EditorState,
+ lineStart: number,
+ lineEnd: number
+ ): Map {
+ let existingMarks = new Map();
+
+ // Traverse the nodes within the line.
+ state.doc.nodesBetween(lineStart, lineEnd, (node: Node, pos: number) => {
+ // If the node has the markType, create a MarkKey and add it to the map.
+ node.marks.forEach((mark) => {
+ if (mark.type === this.markType) {
+ const key = new MarkKey(pos, pos + node.nodeSize, mark.attrs);
+ existingMarks.set(key.toString(), key);
+ }
+ });
+ });
+
+ return existingMarks;
+ }
+
+ // Extract the new marks from the content using regex.
+ private extractNewMarks(
+ state: EditorState,
+ lineStart: number,
+ lineEnd: number
+ ): Map {
+ const lineText = state.doc.textBetween(lineStart, lineEnd, "\n");
+ let newMarks = new Map();
+
+ // Use the regex to find the new marks.
+ let match;
+ while ((match = this.regex.exec(lineText)) !== null) {
+ let attrs: { [key: string]: string | Boolean } = {};
+
+ for (const [attrKey, groupIndex] of Object.entries(this.attrsMap)) {
+ if (match[groupIndex] !== undefined) {
+ attrs[attrKey] = match[groupIndex];
+
+ const from =
+ lineStart + match.index + match[0].indexOf(match[groupIndex]);
+ const to = from + match[groupIndex].length;
+
+ const key = new MarkKey(from + 1, to + 1, attrs);
+ newMarks.set(key.toString(), key);
+ }
+ }
+ }
+
+ return newMarks;
+ }
+
+ // Generate a ProseMirror Plugin for handling the marks.
+ get plugin() {
+ // Helper function to ensure lineStart and lineEnd are within bounds of document
+ const validateBounds = (
+ lineStart: number,
+ lineEnd: number,
+ docSize: number
+ ) => {
+ return !(lineStart >= docSize || lineEnd > docSize);
+ };
+
+ // Helper function to handle mark changes
+ const handleMarkChanges = (
+ tr: any,
+ existingMarks: Map,
+ newMarks: Map
+ ) => {
+ // Remove marks that no longer exist
+ for (let [key, markKey] of existingMarks) {
+ if (!newMarks.has(key)) {
+ tr = tr.removeMark(markKey.from, markKey.to, this.markType);
+ }
+ }
+
+ // Add new marks
+ for (let [key, markKey] of newMarks) {
+ if (!existingMarks.has(key)) {
+ tr = tr.addMark(
+ markKey.from,
+ markKey.to,
+ this.markType.create(markKey.attrs)
+ );
+ }
+ }
+ return tr;
+ };
+
+ return new Plugin({
+ props: {},
+ appendTransaction: (_transactions, oldState, newState) => {
+ // Start and end of line positions
+ const lineStart = newState.doc
+ .resolve(newState.selection.$from.pos)
+ .start(-1);
+ const lineEnd = newState.doc
+ .resolve(newState.selection.$from.pos)
+ .end(-1);
+
+ // If lineStart and lineEnd are not within the bounds of the oldState document, return early
+ if (!validateBounds(lineStart, lineEnd, oldState.doc.content.size)) {
+ return null;
+ }
+
+ // Get the old and new line text
+ const oldLineText = oldState.doc.textBetween(lineStart, lineEnd, "\n");
+ const newLineText = newState.doc.textBetween(lineStart, lineEnd, "\n");
+
+ // If the line text has not changed, return early
+ if (oldLineText === newLineText) {
+ return null;
+ }
+
+ // Get the existing and new marks.
+ let existingMarks = this.extractExistingMarks(
+ newState,
+ lineStart,
+ lineEnd
+ );
+ let newMarks = this.extractNewMarks(newState, lineStart, lineEnd);
+
+ // Handle mark changes
+ let tr = handleMarkChanges(newState.tr, existingMarks, newMarks);
+
+ // Only return the transaction if the document has changed to avoid an infinite loop
+ if (tr.docChanged) {
+ return tr;
+ } else {
+ return null;
+ }
+ },
+ });
+ }
+}
diff --git a/packages/core/src/extensions/Blocks/inline/datelink.ts b/packages/core/src/extensions/Blocks/inline/datelink.ts
new file mode 100644
index 0000000000..eb4210fe87
--- /dev/null
+++ b/packages/core/src/extensions/Blocks/inline/datelink.ts
@@ -0,0 +1,13 @@
+import { createLinkMark } from "../api/createLinkMark";
+
+export const DateLink = createLinkMark({
+ name: "datelink",
+ regex:
+ /[>@](today|tomorrow|yesterday|(([0-9]{4})(-((0[1-9]|1[0-2])(-(0[1-9]|1[0-9]|2[0-9]|3[0-1]))?|Q[1-4]|W0[1-9]|W[1-4]\d|W5[0-3]))?))/g,
+ dataAttr: "data-datelink",
+ hrefPrefix: "date",
+ attrsMap: {
+ datelink: 0,
+ href: 1,
+ },
+});
diff --git a/packages/core/src/extensions/Blocks/inline/hashtags.ts b/packages/core/src/extensions/Blocks/inline/hashtags.ts
new file mode 100644
index 0000000000..5302308cf5
--- /dev/null
+++ b/packages/core/src/extensions/Blocks/inline/hashtags.ts
@@ -0,0 +1,12 @@
+import { createLinkMark } from "../api/createLinkMark";
+
+export const Hashtag = createLinkMark({
+ name: "hashtag",
+ regex: /(?<=(^|\s))((#|@)[\w-_/]+)/g,
+ dataAttr: "data-hashtag",
+ hrefPrefix: undefined,
+ attrsMap: {
+ hashtag: 0,
+ href: 0,
+ },
+});
diff --git a/packages/core/src/extensions/Blocks/inline/highlighted.ts b/packages/core/src/extensions/Blocks/inline/highlighted.ts
new file mode 100644
index 0000000000..b14d5f447a
--- /dev/null
+++ b/packages/core/src/extensions/Blocks/inline/highlighted.ts
@@ -0,0 +1,96 @@
+import {
+ Mark,
+ markInputRule,
+ markPasteRule,
+ mergeAttributes,
+} from "@tiptap/core";
+
+export interface BoldOptions {
+ HTMLAttributes: Record;
+}
+
+declare module "@tiptap/core" {
+ interface Commands {
+ highlighted: {
+ /**
+ * Set a highlighted mark
+ */
+ setHighlighted: () => ReturnType;
+ /**
+ * Toggle a highlighted mark
+ */
+ toggleHighlighted: () => ReturnType;
+ /**
+ * Unset a highlighted mark
+ */
+ unsetHighlighted: () => ReturnType;
+ };
+ }
+}
+
+export const highlightedInputRegex = /(?:^|\s)((?:==)((?:[^=`]+))(?:==))$/;
+export const highlightedPasteRegex = /(?:^|\s)((?:==)((?:[^=`]+))(?:==))/g;
+
+export const Highlighted = Mark.create({
+ name: "highlighted",
+
+ defaultOptions: {
+ HTMLAttributes: {},
+ },
+
+ parseHTML() {
+ return [{ tag: "mark" }];
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return [
+ "mark",
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
+ 0,
+ ];
+ },
+
+ addCommands() {
+ return {
+ setHighlighted:
+ () =>
+ ({ commands }) => {
+ return commands.setMark(this.name);
+ },
+ toggleHighlighted:
+ () =>
+ ({ commands }) => {
+ return commands.toggleMark(this.name);
+ },
+ unsetHighlighted:
+ () =>
+ ({ commands }) => {
+ return commands.unsetMark(this.name);
+ },
+ };
+ },
+
+ addKeyboardShortcuts() {
+ return {
+ "Mod-Shift-M": () => this.editor.commands.toggleHighlighted(),
+ };
+ },
+
+ addInputRules() {
+ return [
+ markInputRule({
+ find: highlightedInputRegex,
+ type: this.type,
+ }),
+ ];
+ },
+
+ addPasteRules() {
+ return [
+ markPasteRule({
+ find: highlightedPasteRegex,
+ type: this.type,
+ }),
+ ];
+ },
+});
diff --git a/packages/core/src/extensions/Blocks/inline/inlineFile.ts b/packages/core/src/extensions/Blocks/inline/inlineFile.ts
new file mode 100644
index 0000000000..6c11b1c5a8
--- /dev/null
+++ b/packages/core/src/extensions/Blocks/inline/inlineFile.ts
@@ -0,0 +1,62 @@
+import { Mark } from "@tiptap/core";
+
+export const InlineFile = Mark.create({
+ name: "inlineFile",
+ selectable: true,
+
+ addAttributes() {
+ return {
+ href: {
+ default: null,
+ parseHTML: (element) => element.getAttribute("href"),
+ renderHTML: (attributes) => ({
+ href: attributes.href,
+ }),
+ },
+ };
+ },
+
+ // addInputRules() {
+ // return [
+
+ // ];
+ // },
+
+ parseHTML() {
+ return [
+ {
+ tag: "a",
+ getAttrs: (element: HTMLElement | string) => {
+ if (typeof element === "string") {
+ return false;
+ }
+
+ if (element.getAttribute("data-content-type") === this.name) {
+ return {
+ href: element.getAttribute("href") || null,
+ };
+ }
+
+ return false;
+ },
+ },
+ ];
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return [
+ "div",
+ { "data-content-type": this.name },
+ [
+ "a",
+ {
+ ...HTMLAttributes,
+ href: HTMLAttributes.href,
+ target: "_blank",
+ },
+ ["i", { class: "far fa-paperclip" }],
+ ["span", 0],
+ ],
+ ];
+ },
+});
diff --git a/packages/core/src/extensions/Blocks/inline/inlineImage.ts b/packages/core/src/extensions/Blocks/inline/inlineImage.ts
new file mode 100644
index 0000000000..c75d03700f
--- /dev/null
+++ b/packages/core/src/extensions/Blocks/inline/inlineImage.ts
@@ -0,0 +1,56 @@
+import { Mark } from "@tiptap/core";
+
+export const InlineImage = Mark.create({
+ name: "inlineImage",
+
+ addAttributes() {
+ return {
+ src: {
+ default: null,
+ parseHTML: (element) => element.getAttribute("src"),
+ renderHTML: (attributes) => ({
+ src: attributes.src,
+ }),
+ },
+ };
+ },
+
+ // addInputRules() {
+ // return [
+
+ // ];
+ // },
+
+ parseHTML() {
+ return [
+ {
+ tag: "img",
+ getAttrs: (element: HTMLElement | string) => {
+ if (typeof element === "string") {
+ return false;
+ }
+
+ if (element.getAttribute("data-inline-image") === this.name) {
+ return {
+ src: element.getAttribute("src") || null,
+ };
+ }
+
+ return false;
+ },
+ },
+ ];
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return [
+ "img",
+ {
+ ...HTMLAttributes,
+ src: HTMLAttributes.src,
+ "data-inline-image": this.name,
+ },
+ 0,
+ ];
+ },
+});
diff --git a/packages/core/src/extensions/Blocks/inline/strikethrough.ts b/packages/core/src/extensions/Blocks/inline/strikethrough.ts
new file mode 100644
index 0000000000..098ba8bc62
--- /dev/null
+++ b/packages/core/src/extensions/Blocks/inline/strikethrough.ts
@@ -0,0 +1,96 @@
+import {
+ Mark,
+ markInputRule,
+ markPasteRule,
+ mergeAttributes,
+} from "@tiptap/core";
+
+export interface BoldOptions {
+ HTMLAttributes: Record;
+}
+
+declare module "@tiptap/core" {
+ interface Commands {
+ strikethrough: {
+ /**
+ * Set a strikethrough mark
+ */
+ setStrikethrough: () => ReturnType;
+ /**
+ * Toggle a strikethrough mark
+ */
+ toggleStrikethrough: () => ReturnType;
+ /**
+ * Unset a strikethrough mark
+ */
+ unsetStrikethrough: () => ReturnType;
+ };
+ }
+}
+
+export const strikethroughInputRegex = /(?:^|\s)((?:~~)((?:[^~`]+))(?:~~))$/;
+export const strikethroughPasteRegex = /(?:^|\s)((?:~~)((?:[^~`]+))(?:~~))/g;
+
+export const Strikethrough = Mark.create({
+ name: "strikethrough",
+
+ defaultOptions: {
+ HTMLAttributes: {},
+ },
+
+ parseHTML() {
+ return [{ tag: "del" }];
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return [
+ "del",
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
+ 0,
+ ];
+ },
+
+ addCommands() {
+ return {
+ setStrikethrough:
+ () =>
+ ({ commands }) => {
+ return commands.setMark(this.name);
+ },
+ toggleStrikethrough:
+ () =>
+ ({ commands }) => {
+ return commands.toggleMark(this.name);
+ },
+ unsetStrikethrough:
+ () =>
+ ({ commands }) => {
+ return commands.unsetMark(this.name);
+ },
+ };
+ },
+
+ addKeyboardShortcuts() {
+ return {
+ "Mod-Shift-S": () => this.editor.commands.toggleStrikethrough(),
+ };
+ },
+
+ addInputRules() {
+ return [
+ markInputRule({
+ find: strikethroughInputRegex,
+ type: this.type,
+ }),
+ ];
+ },
+
+ addPasteRules() {
+ return [
+ markPasteRule({
+ find: strikethroughPasteRegex,
+ type: this.type,
+ }),
+ ];
+ },
+});
diff --git a/packages/core/src/extensions/Blocks/inline/underlined.ts b/packages/core/src/extensions/Blocks/inline/underlined.ts
new file mode 100644
index 0000000000..dc48c34534
--- /dev/null
+++ b/packages/core/src/extensions/Blocks/inline/underlined.ts
@@ -0,0 +1,96 @@
+import {
+ Mark,
+ markInputRule,
+ markPasteRule,
+ mergeAttributes,
+} from "@tiptap/core";
+
+export interface BoldOptions {
+ HTMLAttributes: Record;
+}
+
+declare module "@tiptap/core" {
+ interface Commands {
+ underlined: {
+ /**
+ * Set a underlined mark
+ */
+ setUnderlined: () => ReturnType;
+ /**
+ * Toggle a underlined mark
+ */
+ toggleUnderlined: () => ReturnType;
+ /**
+ * Unset a underlined mark
+ */
+ unsetUnderlined: () => ReturnType;
+ };
+ }
+}
+
+export const underlinedInputRegex = /(?:^|\s)((?:~)((?:[^~]+))(?:~))$/;
+export const underlinedPasteRegex = /(?:^|\s)((?:~)((?:[^~]+))(?:~))/g;
+
+export const Underlined = Mark.create({
+ name: "underlined",
+
+ defaultOptions: {
+ HTMLAttributes: {},
+ },
+
+ parseHTML() {
+ return [{ tag: "u" }];
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return [
+ "u",
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
+ 0,
+ ];
+ },
+
+ addCommands() {
+ return {
+ setUnderlined:
+ () =>
+ ({ commands }) => {
+ return commands.setMark(this.name);
+ },
+ toggleUnderlined:
+ () =>
+ ({ commands }) => {
+ return commands.toggleMark(this.name);
+ },
+ unsetUnderlined:
+ () =>
+ ({ commands }) => {
+ return commands.unsetMark(this.name);
+ },
+ };
+ },
+
+ addKeyboardShortcuts() {
+ return {
+ "Mod-U": () => this.editor.commands.toggleUnderlined(),
+ };
+ },
+
+ addInputRules() {
+ return [
+ markInputRule({
+ find: underlinedInputRegex,
+ type: this.type,
+ }),
+ ];
+ },
+
+ addPasteRules() {
+ return [
+ markPasteRule({
+ find: underlinedPasteRegex,
+ type: this.type,
+ }),
+ ];
+ },
+});
diff --git a/packages/core/src/extensions/Blocks/inline/wikiLinks.ts b/packages/core/src/extensions/Blocks/inline/wikiLinks.ts
new file mode 100644
index 0000000000..4f4a887d51
--- /dev/null
+++ b/packages/core/src/extensions/Blocks/inline/wikiLinks.ts
@@ -0,0 +1,12 @@
+import { createLinkMark } from "../api/createLinkMark";
+
+export const WikiLink = createLinkMark({
+ name: "wikilink",
+ regex: /(\[{2})(.*?)(\]{2})/g,
+ dataAttr: "data-wikilink",
+ hrefPrefix: "title",
+ attrsMap: {
+ wikilink: 2,
+ href: 2,
+ },
+});
diff --git a/packages/core/src/extensions/Blocks/nodes/Block.module.css b/packages/core/src/extensions/Blocks/nodes/Block.module.css
index 91c960b757..70e8a167f6 100644
--- a/packages/core/src/extensions/Blocks/nodes/Block.module.css
+++ b/packages/core/src/extensions/Blocks/nodes/Block.module.css
@@ -1,10 +1,17 @@
+: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 +27,6 @@ BASIC STYLES
}
.blockContent {
- padding: 3px 0;
flex-grow: 1;
transition: font-size 0.2s;
/*
@@ -40,7 +46,7 @@ NESTED BLOCKS
*/
.blockGroup .blockGroup {
- margin-left: 1.5em;
+ margin-left: 2em;
}
.blockGroup .blockGroup > .blockOuter {
@@ -51,8 +57,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 +67,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 {
@@ -118,10 +127,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;
@@ -159,6 +168,7 @@ NESTED BLOCKS
/* Ordered */
[data-content-type="numberedListItem"] {
--index: attr(data-index);
+ padding-left: 1.2em;
}
[data-prev-type="numberedListItem"] {
@@ -168,39 +178,94 @@ 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="quoteListItem"] {
+ font-style: italic;
+ position: relative;
+ padding-left: 1.63em;
+ color: #666666;
+}
+
+.blockOuter:not([data-prev-type])
+ > .block
+ > .blockContent[data-content-type="quoteListItem"]::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;
}
+[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;
+ 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;
- 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"]
@@ -208,11 +273,15 @@ NESTED BLOCKS
> .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;
}
-/* 2 levels of list nesting */
[data-content-type="bulletListItem"]
~ .blockGroup
[data-content-type="bulletListItem"]
@@ -220,8 +289,13 @@ NESTED BLOCKS
> .blockOuter[data-prev-type="bulletListItem"]
> .block
> .blockContent::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;
}
[data-content-type="bulletListItem"]
@@ -231,9 +305,102 @@ NESTED BLOCKS
> .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;
+} */
+
+/* Tasks */
+[data-content-type="taskListItem"],
+[data-content-type="checkListItem"] {
+ padding-left: 1.62em;
+ position: relative;
+
+ &[data-checked="true"],
+ &[data-cancelled="true"],
+ &[data-scheduled="true"] {
+ opacity: 0.4;
+ }
+
+ &[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-cancelled="true"] > label:before {
+ content: "\f057";
+ font: var(--fa-font-regular);
+ 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);
+ display: inline-block;
+ position: absolute;
+ top: 0.2rem;
+ left: 0.12rem;
+ }
+
+ > label > input[type="checkbox"] {
+ cursor: pointer;
+ display: none;
+ }
+}
+
+/* Checklist */
+[data-content-type="checkListItem"] {
+ 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";
+ }
+
+ /* 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 {
+ 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 +417,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,10 +441,17 @@ NESTED BLOCKS
.blockContent[data-content-type="bulletListItem"].isEmpty .inlineContent:before,
.blockContent[data-content-type="numberedListItem"].isEmpty
-.inlineContent:before {
+ .inlineContent:before {
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;
@@ -368,3 +542,204 @@ NESTED BLOCKS
[data-text-alignment="justify"] {
text-align: justify;
}
+
+/* HASHTAG */
+a[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 */
+}
+
+/* WIKILINK */
+a[data-wikilink] {
+ color: var(--orange-noteplan);
+ cursor: pointer;
+
+ text-decoration: none; /* optional: removes underline if your hashtags are also links */
+}
+
+/* DATELINK */
+a[data-datelink] {
+ color: var(--orange-noteplan);
+ cursor: pointer;
+
+ text-decoration: none; /* optional: removes underline if your hashtags are also links */
+}
+
+/* SEPARATOR */
+[data-content-type="separator"] {
+ width: 100%;
+ margin: 0;
+ border: 0px;
+ height: 50%;
+ position: relative;
+}
+
+[data-content-type="separator"] > hr {
+ border: 0px;
+ height: 50%;
+ margin: 0;
+}
+
+[data-content-type="separator"] > hr::before {
+ content: "";
+ display: block;
+ position: absolute;
+ border-top: 2px solid rgb(215, 215, 215);
+ margin: 0 auto;
+ left: 0;
+ right: 0;
+ top: 50%;
+}
+
+[data-content-type="separator"].hasAnchor > hr::before {
+ border-top: 2px solid rgb(156, 192, 255);
+}
+
+/* TABLE */
+
+[data-content-type="tableBlockItem"] {
+ border-width: 1.5px;
+ border-color: #8e8e8e44;
+ border-style: solid;
+ text-align: left;
+ border-radius: 10px;
+}
+
+[data-content-type="tableBlockItem"] table {
+ line-height: 1.5;
+ font-size: 1rem;
+ background-color: transparent;
+ padding-top: 0.25rem;
+ padding-bottom: 0.25rem;
+ overflow: hidden;
+ width: 100%;
+ border-collapse: collapse;
+ table-layout: fixed;
+}
+
+[data-content-type="tableBlockItem"] table th,
+td {
+ border-width: 1.5px;
+ border-color: #8e8e8e44;
+ border-style: solid;
+ padding: 0.5rem;
+ text-align: left;
+ outline: none;
+ vertical-align: top;
+}
+
+[data-content-type="tableBlockItem"].hasAnchor {
+ border-color: rgb(156, 192, 255);
+}
+
+[data-content-type="tableBlockItem"] tr th:first-child {
+ border-left: none;
+}
+[data-content-type="tableBlockItem"] tr th:last-child {
+ border-right: none;
+}
+[data-content-type="tableBlockItem"] tr:first-child th {
+ border-top: none;
+}
+[data-content-type="tableBlockItem"] tr:last-child td {
+ border-bottom: none;
+}
+[data-content-type="tableBlockItem"] tr td:first-child {
+ border-left: none;
+}
+[data-content-type="tableBlockItem"] tr td:last-child {
+ border-right: none;
+}
+
+/* HIGHLIGHTED */
+
+.blockContent mark {
+ background-color: #84ff0066;
+ border-radius: 6px;
+ padding: 1px 3px;
+}
+
+del {
+ opacity: 0.8;
+ text-decoration: line-through;
+ text-decoration-color: #333333;
+}
+
+u {
+ text-decoration: underline;
+ text-decoration-color: #ffcc66;
+ text-decoration-thickness: 2px;
+}
+
+@media (prefers-color-scheme: dark) {
+ mark {
+ background-color: #5ccfe677;
+ }
+
+ del {
+ text-decoration-color: #ccccc6;
+ }
+}
+
+/* ATTACHMENTS */
+[data-content-type="inlineFile"] {
+ display: inline-block;
+}
+
+/* .note-attachment-disabled {
+ @apply text-gray-700 dark:text-gray-300;
+} */
+
+/* .note-attachment-disabled i {
+ display: none;
+} */
+
+[data-content-type="inlineFile"] a i {
+ opacity: 0.7;
+ color: #0091f8;
+}
+
+[data-content-type="inlineFile"] a {
+ color: var(--orange-noteplan);
+ text-decoration: none;
+}
+
+[data-content-type="inlineFile"] a span {
+ border-width: 2px;
+ border-style: solid;
+ border-color: rgba(0, 0, 0, 0.2);
+ border-radius: 0.5rem;
+ padding-right: 0.25rem;
+ padding-left: 0.25rem;
+ margin-left: 0.25rem;
+}
+
+/* [data-content-type="inlineFile"] a span.hasAnchor {
+ border-color: rgb(156, 192, 255);
+} */
+
+@media (prefers-color-scheme: dark) {
+ [data-content-type="inlineFile"] a span {
+ border-color: rgb(255, 255, 255, 0.2);
+ }
+}
+
+/* Code Blocks */
+pre {
+ background: #0d0d0d;
+ border-radius: 0.5rem;
+ color: #fff;
+ font-family: "JetBrainsMono", monospace;
+ padding: 0.75rem 1rem;
+}
+
+code {
+ background: none;
+ color: inherit;
+ font-size: 0.8rem;
+ padding: 0;
+}
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/CodeFenceBlockContent/CodeFenceBlockContentItem.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/CodeFenceBlockContent/CodeFenceBlockContentItem.ts
new file mode 100644
index 0000000000..2957dc53ac
--- /dev/null
+++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/CodeFenceBlockContent/CodeFenceBlockContentItem.ts
@@ -0,0 +1,113 @@
+import CodeBlockLowlight, {
+ CodeBlockLowlightOptions,
+} from "@tiptap/extension-code-block-lowlight";
+
+// export const CodeFenceBlockContent = createTipTapBlock<"codefence">({
+// name: "codefence",
+// content: "inline*",
+
+// addInputRules() {
+// return [
+// // Creates a code block when starting with "```".
+// new InputRule({
+// find: new RegExp(/^`{3}.*?$/),
+// // handler: ({ state, chain, range }) => {
+// // chain().deleteRange({ from: range.from, to: range.to }).run();
+
+// // this.editor.commands.insertContent({
+// // type: "codeBlockLowlight",
+// // content: [
+// // {
+// // type: "text",
+// // text: "var a = b",
+// // },
+// // ],
+// // });
+
+// // this.editor.chain().focus().setCodeBlock().run();
+// // },
+// handler: ({ state, chain, range }) => {
+// chain()
+// .BNUpdateBlock(state.selection.from, {
+// type: this.name,
+// })
+// // Removes the "#" character(s) used to set the heading.
+// .deleteRange({ from: range.from, to: range.to })
+// .insertContentAt(range.to + 1, {
+// type: "paragraph",
+// props: {},
+// });
+// // chain()
+// // .setCodeBlock()
+// // .deleteRange({ from: range.from, to: range.to })
+// // .insertContentAt(range.to + 1, {
+// // type: "paragraph",
+// // props: {},
+// // });
+// },
+// }),
+// ];
+// },
+
+// // addKeyboardShortcuts() {
+// // return {
+// // ArrowUp: () => handleSelectAboveBelow(this.editor, "above", this.name),
+// // ArrowDown: () => handleSelectAboveBelow(this.editor, "below", this.name),
+// // };
+// // },
+
+// parseHTML() {
+// return [
+// {
+// tag: "pre",
+// },
+// ];
+// },
+
+// renderHTML({ HTMLAttributes }) {
+// return [
+// "div",
+// mergeAttributes(HTMLAttributes, {
+// class: styles.blockContent,
+// "data-content-type": this.name,
+// }),
+// [
+// "pre",
+// [
+// "code",
+// mergeAttributes(HTMLAttributes, {
+// class: "hljs",
+// }),
+// 0,
+// ],
+// ],
+// ];
+// },
+// });
+
+// TODO: When I rename this, don't forget to rename it also in the handleEnter func which is in the shortcuts file
+export const CodeBlockLowlightBlockContent =
+ CodeBlockLowlight.extend({
+ name: "codefence",
+ group: "blockContent",
+
+ // renderHTML({ HTMLAttributes }) {
+ // return [
+ // "div",
+ // mergeAttributes(HTMLAttributes, {
+ // class: styles.blockContent,
+ // "data-content-type": this.name,
+ // }),
+ // [
+ // "pre",
+ // [
+ // "code",
+ // mergeAttributes(HTMLAttributes, {
+ // // class: "hljs",
+ // }),
+ // 0,
+ // ],
+ // ],
+ // ];
+ // },
+ });
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..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
@@ -7,18 +7,19 @@ 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$`),
+ find: new RegExp(`^[-]\\s$`),
handler: ({ state, chain, range }) => {
chain()
.BNUpdateBlock(state.selection.from, {
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 });
},
}),
@@ -33,7 +34,6 @@ export const BulletListItemBlockContent = createTipTapBlock<"bulletListItem">({
parseHTML() {
return [
- // Case for regular HTML list structure.
{
tag: "li",
getAttrs: (element) => {
@@ -53,7 +53,7 @@ export const BulletListItemBlockContent = createTipTapBlock<"bulletListItem">({
return false;
},
- node: "bulletListItem",
+ node: this.name,
},
// Case for BlockNote list structure.
{
@@ -76,7 +76,7 @@ export const BulletListItemBlockContent = createTipTapBlock<"bulletListItem">({
return false;
},
priority: 300,
- node: "bulletListItem",
+ node: this.name,
},
];
},
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..cda697e998 100644
--- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts
+++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts
@@ -1,5 +1,6 @@
import { Editor } from "@tiptap/core";
import { getBlockInfoFromPos } from "../../../helpers/getBlockInfoFromPos";
+import { TextSelection } from "prosemirror-state";
export const handleEnter = (editor: Editor) => {
const { node, contentType } = getBlockInfoFromPos(
@@ -10,6 +11,11 @@ export const handleEnter = (editor: Editor) => {
const selectionEmpty =
editor.state.selection.anchor === editor.state.selection.head;
+ if (contentType.name === "codefence") {
+ editor.commands.insertContent("\n");
+ return true;
+ }
+
if (!contentType.name.endsWith("ListItem") || !selectionEmpty) {
return false;
}
@@ -45,3 +51,202 @@ export const handleEnter = (editor: Editor) => {
}),
]);
};
+
+export const handleSelectAboveBelow = (
+ editor: Editor,
+ direction: "above" | "below",
+ contentType: string
+) => {
+ const { selection } = editor.state;
+
+ const blockInfo = getBlockInfoFromPos(
+ editor.state.doc,
+ editor.state.selection.from
+ );
+
+ if (blockInfo?.contentType.name !== contentType) {
+ return false;
+ }
+
+ const $position = direction === "above" ? selection.$from : selection.$to;
+ let targetPos = null;
+
+ if (direction === "above") {
+ // For above, we need to get the node before the current one
+ if ($position.depth > 0) {
+ targetPos = $position.before($position.depth - 1);
+ }
+ } else {
+ // For below, we need to get the node after the current one
+ targetPos = $position.after($position.depth - 1);
+ }
+
+ // If no node is found, return false
+ if (targetPos === null) {
+ console.log("target pos is nul");
+ return false;
+ }
+
+ // Create a new selection around the node
+ const nodeSelection = TextSelection.create(editor.state.doc, targetPos);
+
+ // Apply the new selection
+ editor.view.dispatch(editor.state.tr.setSelection(nodeSelection));
+
+ return true;
+};
+
+export const handleAttribute = (
+ editor: Editor,
+ attribute: "checked" | "cancelled" | "scheduled"
+) => {
+ const { from, to } = editor.state.selection;
+
+ // 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;
+};
+
+export const handleMove = (editor: Editor, direction: "up" | "down") => {
+ const { $anchor, $head } = editor.state.selection;
+
+ // Look for items with this as the parent node
+ const parentNode = $head.node(-2);
+
+ const nodes: any[] = [];
+ editor.state.doc.nodesBetween(
+ $anchor.pos,
+ $head.pos,
+ (node, _pos, parent) => {
+ if (parent !== null && parent.eq(parentNode)) {
+ nodes.push(node);
+ return false;
+ }
+ return;
+ }
+ );
+
+ // 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/QuoteListItemBlockContent/QuoteBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/QuoteListItemBlockContent/QuoteBlockContent.ts
new file mode 100644
index 0000000000..f02ca806d4
--- /dev/null
+++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/QuoteListItemBlockContent/QuoteBlockContent.ts
@@ -0,0 +1,95 @@
+import { InputRule, mergeAttributes } from "@tiptap/core";
+import { createTipTapBlock } from "../../../../api/block";
+import { handleEnter } from "../ListItemKeyboardShortcuts";
+import styles from "../../../Block.module.css";
+
+export const QuotListItemBlockContent = createTipTapBlock<"quoteListItem">({
+ name: "quoteListItem",
+ 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: this.name,
+ props: {},
+ })
+ // Removes the ">" character used to set the list.
+ .deleteRange({ from: range.from, to: range.to });
+ },
+ }),
+ ];
+ },
+
+ addKeyboardShortcuts() {
+ return {
+ Enter: () => handleEnter(this.editor),
+ };
+ },
+
+ // Parsed into tip tap nodes
+ 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", // 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;
+ }
+
+ 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,
+ }),
+ ["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
new file mode 100644
index 0000000000..da63e44124
--- /dev/null
+++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/CheckListItemBlockContent.ts
@@ -0,0 +1,82 @@
+import { InputRule } from "@tiptap/core";
+import { createTipTapBlock } from "../../../../api/block";
+import { handleEnter, handleAttribute } from "../ListItemKeyboardShortcuts";
+import { TaskListItemNodeView } from "./TaskListItemNodeView";
+import { TaskListItemHTMLParser } from "./TaskListItemHTMLParser";
+import { TaskListItemListHTMLRender } from "./TaskListItemHTMLRender";
+
+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,
+ scheduled: false,
+ },
+ })
+ // Removes the "*" character used to set the list.
+ .deleteRange({ from: range.from, to: range.to });
+ },
+ }),
+ ];
+ },
+
+ addKeyboardShortcuts() {
+ return {
+ Enter: () => handleEnter(this.editor),
+ "Cmd-d": () => handleAttribute(this.editor, "checked"),
+ "Cmd-s": () => handleAttribute(this.editor, "cancelled"),
+ };
+ },
+
+ addAttributes() {
+ return {
+ checked: {
+ default: false,
+ parseHTML: (element) => element.getAttribute("data-checked"),
+ renderHTML: (attributes) => ({
+ "data-checked": attributes.checked,
+ }),
+ },
+ cancelled: {
+ default: false,
+ parseHTML: (element) => element.getAttribute("data-cancelled"),
+ renderHTML: (attributes) => ({
+ "data-cancelled": attributes.cancelled,
+ }),
+ },
+ scheduled: {
+ default: false,
+ parseHTML: (element) => element.getAttribute("data-scheduled"),
+ renderHTML: (attributes) => ({
+ "data-scheduled": attributes.scheduled,
+ }),
+ },
+ };
+ },
+
+ parseHTML() {
+ return TaskListItemHTMLParser(this.name);
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return TaskListItemListHTMLRender(this.name, HTMLAttributes);
+ },
+
+ 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
new file mode 100644
index 0000000000..62a7df11fa
--- /dev/null
+++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemBlockContent.ts
@@ -0,0 +1,84 @@
+import { InputRule } from "@tiptap/core";
+import { createTipTapBlock } from "../../../../api/block";
+import { handleEnter, handleAttribute } from "../ListItemKeyboardShortcuts";
+import { TaskListItemNodeView } from "./TaskListItemNodeView";
+import { TaskListItemHTMLParser } from "./TaskListItemHTMLParser";
+import { TaskListItemListHTMLRender } from "./TaskListItemHTMLRender";
+
+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 "*".
+ new InputRule({
+ find: new RegExp(`^\\*\\s$`),
+ handler: ({ state, chain, range }) => {
+ chain()
+ .BNUpdateBlock(state.selection.from, {
+ type: this.name,
+ props: {
+ checked: false,
+ canceled: false,
+ scheduled: false,
+ },
+ })
+ // Removes the "*" character used to set the list.
+ .deleteRange({ from: range.from, to: range.to });
+ },
+ }),
+ ];
+ },
+
+ addKeyboardShortcuts() {
+ return {
+ Enter: () => handleEnter(this.editor),
+ "Cmd-d": () => handleAttribute(this.editor, "checked"),
+ "Cmd-s": () => handleAttribute(this.editor, "cancelled"),
+ };
+ },
+
+ addAttributes() {
+ return {
+ checked: {
+ default: false,
+ parseHTML: (element) => element.getAttribute("data-checked"),
+ renderHTML: (attributes) => ({
+ "data-checked": attributes.checked,
+ }),
+ },
+ cancelled: {
+ default: false,
+ keepOnSplit: false,
+ parseHTML: (element) => element.getAttribute("data-cancelled"),
+ renderHTML: (attributes) => ({
+ "data-cancelled": attributes.cancelled,
+ }),
+ },
+ scheduled: {
+ default: false,
+ keepOnSplit: false,
+ parseHTML: (element) => element.getAttribute("data-scheduled"),
+ renderHTML: (attributes) => ({
+ "data-scheduled": attributes.scheduled,
+ }),
+ },
+ };
+ },
+
+ parseHTML() {
+ return TaskListItemHTMLParser(this.name);
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return TaskListItemListHTMLRender(this.name, HTMLAttributes);
+ },
+
+ addNodeView() {
+ return ({ node, getPos, editor }) => {
+ return TaskListItemNodeView(node, editor, getPos, this.name);
+ };
+ },
+});
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..09a2a20c36
--- /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: "div",
+ 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/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
new file mode 100644
index 0000000000..c7fea5c57f
--- /dev/null
+++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/TaskListItemBlockContent/TaskListItemNodeView.ts
@@ -0,0 +1,90 @@
+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 {
+ 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;
+ dom.dataset.scheduled = node.attrs.scheduled || 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,
+ scheduled: 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.scheduled = updatedNode.attrs.scheduled || false;
+
+ if (updatedNode.attrs.checked) {
+ input.setAttribute("checked", "checked");
+ } else {
+ input.removeAttribute("checked");
+ }
+
+ return true;
+ },
+ };
+}
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 [
{
diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/SeparatorBlockContent/SeparatorBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/SeparatorBlockContent/SeparatorBlockContent.ts
new file mode 100644
index 0000000000..6c7e4709d2
--- /dev/null
+++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/SeparatorBlockContent/SeparatorBlockContent.ts
@@ -0,0 +1,57 @@
+import { InputRule, mergeAttributes } from "@tiptap/core";
+import { createTipTapBlock } from "../../../api/block";
+import styles from "../../Block.module.css";
+import { handleSelectAboveBelow } from "../ListItemBlockContent/ListItemKeyboardShortcuts";
+
+export const SeparatorBlockContent = createTipTapBlock<"separator">({
+ name: "separator",
+ content: "inline*",
+ selectable: true,
+
+ addInputRules() {
+ return [
+ // Creates a heading of appropriate level when starting with "#", "##", or "###".
+ new InputRule({
+ find: new RegExp(/^\s*([-*]\s*){3,}$/),
+ handler: ({ state, chain, range }) => {
+ chain()
+ .BNUpdateBlock(state.selection.from, {
+ type: this.name,
+ })
+ // Removes the "#" character(s) used to set the heading.
+ .deleteRange({ from: range.from, to: range.to })
+ .insertContentAt(range.to + 1, {
+ type: "paragraph",
+ props: {},
+ });
+ },
+ }),
+ ];
+ },
+
+ addKeyboardShortcuts() {
+ return {
+ ArrowUp: () => handleSelectAboveBelow(this.editor, "above", this.name),
+ ArrowDown: () => handleSelectAboveBelow(this.editor, "below", this.name),
+ };
+ },
+
+ parseHTML() {
+ return [
+ {
+ tag: "hr",
+ },
+ ];
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return [
+ "div",
+ mergeAttributes(HTMLAttributes, {
+ class: styles.blockContent,
+ "data-content-type": this.name,
+ }),
+ ["hr", { contenteditable: false }, 0],
+ ];
+ },
+});
diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/TableBlockContent/TableBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/TableBlockContent/TableBlockContent.ts
new file mode 100644
index 0000000000..c48c066a49
--- /dev/null
+++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/TableBlockContent/TableBlockContent.ts
@@ -0,0 +1,159 @@
+import { InputRule, mergeAttributes } from "@tiptap/core";
+
+import { createTipTapBlock } from "../../../api/block";
+import styles from "../../Block.module.css";
+import { handleSelectAboveBelow } from "../ListItemBlockContent/ListItemKeyboardShortcuts";
+
+export const TableBlockContent = createTipTapBlock<"tableBlockItem">({
+ name: "tableBlockItem",
+ content: "inline*",
+ selectable: true,
+
+ addAttributes() {
+ return {
+ data: {
+ default: [
+ ["", "", ""],
+ ["", "", ""],
+ ["", "", ""],
+ ],
+ },
+ };
+ },
+
+ addInputRules() {
+ return [
+ // Creates a table when typing "===".
+ new InputRule({
+ find: /^(={3,})\s$/,
+ handler: ({ state, match, chain, range }) => {
+ const matchLength = match[1].length;
+
+ chain()
+ .BNUpdateBlock(state.selection.from, {
+ type: this.name,
+ props: {
+ data: Array.from({ length: matchLength }, () =>
+ Array(matchLength).fill("")
+ ),
+ },
+ })
+ // Removes the "=== " character(s) used to set the table.
+ .deleteRange({ from: range.from, to: range.to })
+ .insertContentAt(range.to + 1, {
+ type: "paragraph",
+ props: {},
+ });
+
+ // WIP: We need to create the table instead as a node and then add the table cells individually also as block items
+ // Otherwise, we can't get the content of the cells as the user types them into the table into the node
+ // To do this, we need to use NoteType.createChecked(null, cells), do this also with rows.
+ // See: https://github.com/ueberdosis/tiptap/blob/main/packages/extension-table/src/utilities/createTable.ts
+
+ // Current issue with this: It creates the table node as a child of a paragraph
+ // const node = this.type.createChecked(null);
+
+ // // Add this node to the documen
+ // const tr = state.tr;
+ // // tr.deleteRange(range.from, range.to);
+ // tr.insert(range.from, node);
+ // const newState = state.apply(tr);
+
+ // // Update the editor with the new state.
+ // this.editor.view.updateState(newState);
+ },
+ }),
+ ];
+ },
+
+ addKeyboardShortcuts() {
+ return {
+ ArrowUp: () => handleSelectAboveBelow(this.editor, "above", this.name),
+ ArrowDown: () => handleSelectAboveBelow(this.editor, "below", this.name),
+ // Tab: () => {
+ // if (this.editor.commands.goToNextCell()) {
+ // return true
+ // }
+
+ // if (!this.editor.can().addRowAfter()) {
+ // return false
+ // }
+
+ // return this.editor.chain().addRowAfter().goToNextCell().run()
+ // },
+ // 'Shift-Tab': () => this.editor.commands.goToPreviousCell(),
+ // Backspace: deleteTableWhenAllCellsSelected,
+ // 'Mod-Backspace': deleteTableWhenAllCellsSelected,
+ // Delete: deleteTableWhenAllCellsSelected,
+ // 'Mod-Delete': deleteTableWhenAllCellsSelected,
+ };
+ },
+
+ parseHTML() {
+ return [
+ {
+ tag: "table",
+ },
+ ];
+ },
+
+ renderHTML({ node, HTMLAttributes }) {
+ const tableData = node.attrs.data || [[]];
+ const tableRows = [];
+
+ // Generate the rows with td elements
+ for (let i = 0; i < tableData.length; i++) {
+ const cells = tableData[i];
+ const tableCells = cells.map((cell: string) => [
+ i === 0 ? "th" : "td",
+ { contenteditable: true },
+ cell,
+ ]);
+ tableRows.push(["tr", ...tableCells]);
+ }
+
+ return [
+ "div",
+ mergeAttributes(HTMLAttributes, {
+ class: styles.blockContent,
+ "data-content-type": this.name,
+ }),
+ ["table", ...tableRows],
+ ];
+ },
+
+ // Set the data prop here after the user changes the table
+ // This is another approach that doesn't quite work. We are able to get the data.
+ // But we are not able to find the node to add it:
+ // addProseMirrorPlugins() {
+ // return [
+ // new Plugin({
+ // props: {
+ // handleDOMEvents: {
+ // input(view, event) {
+ // // As the user types into the table cells, get the content
+ // const tableData = [];
+ // const tableRows = view.dom.querySelectorAll("tr");
+
+ // for (let i = 0; i < tableRows.length; i++) {
+ // const row = tableRows[i];
+ // const rowData = [];
+ // const cells = row.querySelectorAll("td, th");
+ // for (let j = 0; j < cells.length; j++) {
+ // const cell = cells[j];
+ // rowData.push(cell.textContent);
+ // }
+ // tableData.push(rowData);
+ // }
+
+ // // How to access the current node
+ // console.log(view.state.selection.$from.nodeBefore);
+
+ // // this.props.attributes.data = tableData;
+ // },
+ // },
+ // },
+ // }),
+ // ];
+ // },
+});
diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/TableBlockContent/TableBlockContentCell.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/TableBlockContent/TableBlockContentCell.ts
new file mode 100644
index 0000000000..a6778ec786
--- /dev/null
+++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/TableBlockContent/TableBlockContentCell.ts
@@ -0,0 +1,36 @@
+import { mergeAttributes, Node } from "@tiptap/core";
+
+// WIP: This is work in progress, when we create a table, we need to add this as a cell. Also, rows are still missing.
+// See: https://tiptap.dev/api/nodes/table#allow-table-node-selection
+// See: https://github.com/ueberdosis/tiptap/blob/main/packages/extension-table-cell/src/table-cell.ts
+
+export interface TableCellOptions {
+ HTMLAttributes: Record;
+}
+
+export const TableCell = Node.create({
+ name: "tableCell",
+
+ addOptions() {
+ return {
+ HTMLAttributes: {},
+ };
+ },
+
+ content: "block+",
+ tableRole: "cell",
+
+ isolating: true,
+
+ parseHTML() {
+ return [{ tag: "td" }];
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return [
+ "td",
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
+ 0,
+ ];
+ },
+});
diff --git a/packages/core/src/extensions/SlashMenu/defaultSlashMenuItems.tsx b/packages/core/src/extensions/SlashMenu/defaultSlashMenuItems.tsx
index 2375996560..3005a3d693 100644
--- a/packages/core/src/extensions/SlashMenu/defaultSlashMenuItems.tsx
+++ b/packages/core/src/extensions/SlashMenu/defaultSlashMenuItems.tsx
@@ -26,37 +26,34 @@ function insertOrUpdateBlock(
* An array containing commands for creating all default blocks.
*/
export const defaultSlashMenuItems = [
- // Command for creating a level 1 heading
+ // Command for creating a task item
new BaseSlashMenuItem(
- "Heading",
+ "Task",
(editor) =>
insertOrUpdateBlock(editor, {
- type: "heading",
- props: { level: "1" },
+ type: "taskListItem",
}),
- ["h", "heading1", "h1"]
+ ["task", "taskList", "task list"]
),
- // Command for creating a level 2 heading
+ // Command for creating a task item
new BaseSlashMenuItem(
- "Heading 2",
+ "Checklist",
(editor) =>
insertOrUpdateBlock(editor, {
- type: "heading",
- props: { level: "2" },
+ type: "checkListItem",
}),
- ["h2", "heading2", "subheading"]
+ ["check", "checkbox", "checkList", "check list"]
),
- // Command for creating a level 3 heading
+ // Command for creating a bullet list
new BaseSlashMenuItem(
- "Heading 3",
+ "Bullet Point",
(editor) =>
insertOrUpdateBlock(editor, {
- type: "heading",
- props: { level: "3" },
+ type: "bulletListItem",
}),
- ["h3", "heading3", "subheading"]
+ ["ul", "list", "bulletPoint", "bullet point"]
),
// Command for creating an ordered list
@@ -66,29 +63,52 @@ export const defaultSlashMenuItems = [
insertOrUpdateBlock(editor, {
type: "numberedListItem",
}),
- ["li", "list", "numberedlist", "numbered list"]
+ ["li", "numberedlist", "numbered list"]
),
- // Command for creating a bullet list
+ // Command for creating a level 1 heading
new BaseSlashMenuItem(
- "Bullet List",
+ "Heading",
(editor) =>
insertOrUpdateBlock(editor, {
- type: "bulletListItem",
+ type: "heading",
+ props: { level: "1" },
}),
- ["ul", "list", "bulletlist", "bullet list"]
+ ["h", "heading1", "h1"]
),
- // Command for creating a paragraph (pretty useless)
+ // Command for creating a level 2 heading
+ new BaseSlashMenuItem(
+ "Heading 2",
+ (editor) =>
+ insertOrUpdateBlock(editor, {
+ type: "heading",
+ props: { level: "2" },
+ }),
+ ["h2", "heading2", "subheading"]
+ ),
+
+ // Command for creating a level 3 heading
new BaseSlashMenuItem(
- "Paragraph",
+ "Heading 3",
(editor) =>
insertOrUpdateBlock(editor, {
- type: "paragraph",
+ type: "heading",
+ props: { level: "3" },
}),
- ["p"]
+ ["h3", "heading3", "subheading"]
),
+ // Command for creating a paragraph (pretty useless)
+ // new BaseSlashMenuItem(
+ // "Paragraph",
+ // (editor) =>
+ // insertOrUpdateBlock(editor, {
+ // type: "paragraph",
+ // }),
+ // ["p"]
+ // ),
+
// replaceRangeWithNode(editor, range, node);
// return true;
diff --git a/packages/react/src/BlockNoteTheme.ts b/packages/react/src/BlockNoteTheme.ts
index e840718141..c0a47fd2d4 100644
--- a/packages/react/src/BlockNoteTheme.ts
+++ b/packages/react/src/BlockNoteTheme.ts
@@ -78,6 +78,17 @@ export const getBlockNoteTheme = (
? blockNoteColorScheme[5]
: blockNoteColorScheme[3];
+ //TODO: Implement NotePlan themes for the approach used above for BlockNote
+ const slashMenuitemColor = "#EADACE";
+ const backgroundSlashMenuColor = "#F2F4F5";
+ const slashMenuIconColor = "#A4A5A7";
+ const slashMenuItemTextColor = "#363638";
+ const slashMenuLabelTextColor = "#858689";
+ const slashMenuBorderColor = "#D8D9DC";
+ const toolbarBackground = "rgb(254 215 170)";
+ const toolbarColor = "rgb(55, 65, 81)";
+
+
return {
activeStyles: {
// Removes button press effect.
@@ -127,19 +138,20 @@ export const getBlockNoteTheme = (
Menu: {
styles: () => ({
dropdown: {
- backgroundColor: primaryBackground,
+ font: "var(--fa-font-solid)",
+ backgroundColor: backgroundSlashMenuColor,
border: border,
borderRadius: "6px",
boxShadow: boxShadow,
color: primaryText,
padding: "2px",
".mantine-Menu-item": {
- backgroundColor: primaryBackground,
+ backgroundColor: slashMenuitemColor,
border: "none",
color: primaryText,
},
".mantine-Menu-item[data-hovered]": {
- backgroundColor: hoveredBackground,
+ backgroundColor: slashMenuitemColor,
border: "none",
color: hoveredText,
},
@@ -205,26 +217,25 @@ export const getBlockNoteTheme = (
borderRadius: "6px",
flexWrap: "nowrap",
gap: "2px",
- padding: "2px",
+ padding: "5px 8px 6px 8px",
width: "fit-content",
// Button (including dropdown target)
".mantine-UnstyledButton-root": {
backgroundColor: primaryBackground,
border: "none",
borderRadius: "4px",
- color: primaryText,
+ color: toolbarColor,
+ fontWeight: 600,
},
// Hovered button
".mantine-UnstyledButton-root:hover": {
- backgroundColor: hoveredBackground,
+ backgroundColor: toolbarBackground,
border: "none",
- color: hoveredText,
},
// Selected button
".mantine-UnstyledButton-root[data-selected]": {
- backgroundColor: selectedBackground,
+ backgroundColor: toolbarBackground,
border: "none",
- color: selectedText,
},
// Disabled button
".mantine-UnstyledButton-root[data-disabled]": {
@@ -252,15 +263,15 @@ export const getBlockNoteTheme = (
Tooltip: {
styles: () => ({
root: {
- backgroundColor: primaryBackground,
+ backgroundColor: "black",
border: border,
borderRadius: "6px",
boxShadow: boxShadow,
- color: primaryText,
+ color: "white",
padding: "4px 10px",
textAlign: "center",
"div ~ div": {
- color: secondaryText,
+ color: "white",
},
},
}),
@@ -269,26 +280,58 @@ export const getBlockNoteTheme = (
styles: () => ({
root: {
position: "relative",
+ background: backgroundSlashMenuColor,
+ padding: "14px 0",
+ border: `1px solid ${slashMenuBorderColor}`,
+ outline: "none",
+ maxHeight: 360,
+ minWidth: 450,
+ width: 450,
+ overflow: "hidden",
".mantine-Menu-item": {
// Icon
+ height: 26,
+ padding: 0,
+ width: "100%",
+ display: "flex",
+ color: slashMenuItemTextColor,
+ backgroundColor: "transparent",
+ alignItems: "flex-end",
+ cursor: "pointer",
+ fontWeight: 400,
+ paddingBottom: 2,
+ fontSize: 13,
+ ":hover,:active": {
+ backgroundColor: slashMenuitemColor,
+ },
".mantine-Menu-itemIcon": {
- backgroundColor: secondaryBackground,
+ backgroundColor: "transparent",
borderRadius: "4px",
- color: primaryText,
- padding: "8px",
+ color: slashMenuIconColor,
+ width: 47,
+ boxSizing: "content-box",
+ fontSize: 15,
+ marginRight: 6,
+ marginBottom: 1,
},
// Text
".mantine-Menu-itemLabel": {
- paddingRight: "16px",
".mantine-Stack-root": {
gap: "0",
},
},
- // Badge (keyboard shortcut)
+ // Badge (markdown hint)
".mantine-Menu-itemRightSection": {
+ marginLeft: "auto",
+ textTransform: "lowercase",
".mantine-Badge-root": {
- backgroundColor: secondaryBackground,
- color: primaryText,
+ backgroundColor: "transparent",
+ color: slashMenuLabelTextColor,
+ fontSize: 10,
+ fontWeight: 400,
+ textTransform: "none",
+ marginBottom: 3,
+ marginRight: 19,
},
},
},
diff --git a/packages/react/src/FormattingToolbar/components/DefaultButtons/CreateLinkButton.tsx b/packages/react/src/FormattingToolbar/components/DefaultButtons/CreateLinkButton.tsx
index 016b6f4d3e..e92a35b500 100644
--- a/packages/react/src/FormattingToolbar/components/DefaultButtons/CreateLinkButton.tsx
+++ b/packages/react/src/FormattingToolbar/components/DefaultButtons/CreateLinkButton.tsx
@@ -1,9 +1,9 @@
import { useCallback } from "react";
import { BlockNoteEditor, BlockSchema } from "@blocknote/core";
-import { RiLink } from "react-icons/ri";
import LinkToolbarButton from "../LinkToolbarButton";
import { formatKeyboardShortcut } from "../../../utils";
-
+import iconsData from "../FontIcons";
+const { link } = iconsData;
export const CreateLinkButton = (props: {
editor: BlockNoteEditor;
}) => {
@@ -20,7 +20,7 @@ export const CreateLinkButton = (props: {
isSelected={!!props.editor.getSelectedLinkUrl()}
mainTooltip="Link"
secondaryTooltip={formatKeyboardShortcut("Mod+K")}
- icon={RiLink}
+ icon={link}
hyperlinkIsActive={!!props.editor.getSelectedLinkUrl()}
activeHyperlinkUrl={props.editor.getSelectedLinkUrl() || ""}
activeHyperlinkText={props.editor.getSelectedText()}
diff --git a/packages/react/src/FormattingToolbar/components/DefaultButtons/TextAlignButton.tsx b/packages/react/src/FormattingToolbar/components/DefaultButtons/TextAlignButton.tsx
index dde4fe6342..5131693cbe 100644
--- a/packages/react/src/FormattingToolbar/components/DefaultButtons/TextAlignButton.tsx
+++ b/packages/react/src/FormattingToolbar/components/DefaultButtons/TextAlignButton.tsx
@@ -6,21 +6,17 @@ import {
} from "@blocknote/core";
import { useCallback, useMemo } from "react";
import { IconType } from "react-icons";
-import {
- RiAlignCenter,
- RiAlignJustify,
- RiAlignLeft,
- RiAlignRight,
-} from "react-icons/ri";
import { ToolbarButton } from "../../../SharedComponents/Toolbar/components/ToolbarButton";
+import iconsData from "../FontIcons";
+const { left, center, right, justify } = iconsData;
type TextAlignment = DefaultProps["textAlignment"]["values"][number];
const icons: Record = {
- left: RiAlignLeft,
- center: RiAlignCenter,
- right: RiAlignRight,
- justify: RiAlignJustify,
+ left,
+ center,
+ right,
+ justify,
};
export const TextAlignButton = (props: {
diff --git a/packages/react/src/FormattingToolbar/components/DefaultButtons/ToggledStyleButton.tsx b/packages/react/src/FormattingToolbar/components/DefaultButtons/ToggledStyleButton.tsx
index 375d9475ab..ee572c3271 100644
--- a/packages/react/src/FormattingToolbar/components/DefaultButtons/ToggledStyleButton.tsx
+++ b/packages/react/src/FormattingToolbar/components/DefaultButtons/ToggledStyleButton.tsx
@@ -1,36 +1,57 @@
import { ToolbarButton } from "../../../SharedComponents/Toolbar/components/ToolbarButton";
import { formatKeyboardShortcut } from "../../../utils";
-import {
- RiBold,
- RiCodeFill,
- RiItalic,
- RiStrikethrough,
- RiUnderline,
-} from "react-icons/ri";
import { BlockNoteEditor, BlockSchema, ToggledStyle } from "@blocknote/core";
import { IconType } from "react-icons";
+import iconsData from "../FontIcons";
+const {
+ bold,
+ link,
+ italic,
+ underlined,
+ strikethrough,
+ highlighted,
+ code,
+ hashtag,
+ task,
+ checkbox,
+} = iconsData;
const shortcuts: Record = {
+ task: "",
+ checkbox: "",
bold: "Mod+B",
italic: "Mod+I",
- underline: "Mod+U",
- strike: "Mod+Shift+X",
+ underlined: "Mod+U",
+ strikethrough: "Mod+Shift+X",
+ highlighted: "Mod+Shift+H",
code: "",
+ hashtag: "",
+ wikilink: "",
+ datelink: "",
+ inlineFile: "",
+ inlineImage: "",
};
const icons: Record = {
- bold: RiBold,
- italic: RiItalic,
- underline: RiUnderline,
- strike: RiStrikethrough,
- code: RiCodeFill,
+ task,
+ checkbox,
+ bold,
+ italic,
+ underlined,
+ strikethrough,
+ highlighted,
+ code,
+ hashtag,
+ wikilink: link,
+ datelink: link,
+ inlineFile: link,
+ inlineImage: link,
};
export const ToggledStyleButton = (props: {
editor: BlockNoteEditor;
toggledStyle: ToggledStyle;
}) => {
-
const toggleStyle = (style: ToggledStyle) => {
props.editor.focus();
props.editor.toggleStyles({ [style]: true });
diff --git a/packages/react/src/FormattingToolbar/components/FontIcons.tsx b/packages/react/src/FormattingToolbar/components/FontIcons.tsx
new file mode 100644
index 0000000000..7b416b4c5a
--- /dev/null
+++ b/packages/react/src/FormattingToolbar/components/FontIcons.tsx
@@ -0,0 +1,27 @@
+const getIcon = (
+ name: string | number,
+ type: string = "solid",
+ family: string = ""
+) => ;
+
+const iconsData = {
+ task: () => getIcon("circle-check", "regular"),
+ checkbox: () => getIcon("square-check", "regular"),
+ bold: () => getIcon("bold"),
+ italic: () => getIcon("italic"),
+ underlined: () => getIcon("underline"),
+ strikethrough: () => getIcon("strikethrough"),
+ highlighted: () => getIcon("highlighter", "regular", "sharp"),
+ hashtag: () => getIcon("hashtag"),
+ code: () => getIcon("code-simple"),
+ link: () => getIcon("link"),
+ getIcon,
+ bullet: () => getIcon("circle-small"),
+ heading: () => getIcon("heading"),
+ numList: () => getIcon("list-ol"),
+ left: () => getIcon("align-left"),
+ center: () => getIcon("align-center"),
+ right: () => getIcon("align-right"),
+ justify: () => getIcon("align-justify"),
+};
+export default iconsData;
diff --git a/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx b/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx
index 85b3650d0b..463c9388af 100644
--- a/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx
+++ b/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx
@@ -1,36 +1,23 @@
import { BlockNoteEditor, BlockSchema } from "@blocknote/core";
import { Toolbar } from "../../SharedComponents/Toolbar/components/Toolbar";
-import { ColorStyleButton } from "./DefaultButtons/ColorStyleButton";
import { CreateLinkButton } from "./DefaultButtons/CreateLinkButton";
-import {
- NestBlockButton,
- UnnestBlockButton,
-} from "./DefaultButtons/NestBlockButtons";
-import { TextAlignButton } from "./DefaultButtons/TextAlignButton";
import { ToggledStyleButton } from "./DefaultButtons/ToggledStyleButton";
-import { BlockTypeDropdown } from "./DefaultDropdowns/BlockTypeDropdown";
export const FormattingToolbar = (props: {
editor: BlockNoteEditor;
}) => {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
);
diff --git a/packages/react/src/SharedComponents/Tooltip/components/TooltipContent.tsx b/packages/react/src/SharedComponents/Tooltip/components/TooltipContent.tsx
index ddd5782822..8ad484bd91 100644
--- a/packages/react/src/SharedComponents/Tooltip/components/TooltipContent.tsx
+++ b/packages/react/src/SharedComponents/Tooltip/components/TooltipContent.tsx
@@ -3,14 +3,19 @@ import { createStyles, Stack, Text } from "@mantine/core";
export const TooltipContent = (props: {
mainTooltip: string;
secondaryTooltip?: string;
+ showMainTooltip?: boolean;
}) => {
const { classes } = createStyles({ root: {} })(undefined, {
name: "Tooltip",
});
-
+ if (!props.secondaryTooltip) {
+ return null;
+ }
return (
- {props.mainTooltip}
+ {props.showMainTooltip ? (
+ {props.mainTooltip}
+ ) : null}
{props.secondaryTooltip && (
{props.secondaryTooltip}
)}
diff --git a/packages/react/src/SlashMenu/ReactSlashMenuItem.ts b/packages/react/src/SlashMenu/ReactSlashMenuItem.ts
index 44ff73c2a1..05799d8131 100644
--- a/packages/react/src/SlashMenu/ReactSlashMenuItem.ts
+++ b/packages/react/src/SlashMenu/ReactSlashMenuItem.ts
@@ -14,7 +14,8 @@ export class ReactSlashMenuItem<
public readonly group: string,
public readonly icon: JSX.Element,
public readonly hint?: string,
- public readonly shortcut?: string
+ public readonly shortcut?: string,
+ public readonly markdownHint?: string
) {
super(name, execute, aliases);
}
diff --git a/packages/react/src/SlashMenu/components/SlashMenu.tsx b/packages/react/src/SlashMenu/components/SlashMenu.tsx
index f775ec7728..c5d07e2bdd 100644
--- a/packages/react/src/SlashMenu/components/SlashMenu.tsx
+++ b/packages/react/src/SlashMenu/components/SlashMenu.tsx
@@ -22,18 +22,14 @@ export function SlashMenu(
const groups = _.groupBy(props.items, (i) => i.group);
_.forEach(groups, (el) => {
- renderedItems.push(
- {el[0].group}
- );
-
for (const item of el) {
renderedItems.push(
props.itemCallback(item)}
/>
@@ -51,6 +47,7 @@ export function SlashMenu(
* close due to trigger="hover".
*/
defaultOpened={true}
+ unstyled
trigger={"hover"}
closeDelay={10000000}>
diff --git a/packages/react/src/SlashMenu/components/SlashMenuItem.tsx b/packages/react/src/SlashMenu/components/SlashMenuItem.tsx
index 176889f51b..a5b9365691 100644
--- a/packages/react/src/SlashMenu/components/SlashMenuItem.tsx
+++ b/packages/react/src/SlashMenu/components/SlashMenuItem.tsx
@@ -70,10 +70,7 @@ export function SlashMenuItem(props: SlashMenuItemProps) {
}>
{/*Might need separate classes.*/}
-
- {props.name}
-
- {props.hint}
+ {props.name}
);
diff --git a/packages/react/src/SlashMenu/defaultReactSlashMenuItems.tsx b/packages/react/src/SlashMenu/defaultReactSlashMenuItems.tsx
index 740265e12d..d7c3820236 100644
--- a/packages/react/src/SlashMenu/defaultReactSlashMenuItems.tsx
+++ b/packages/react/src/SlashMenu/defaultReactSlashMenuItems.tsx
@@ -3,16 +3,17 @@ import {
DefaultBlockSchema,
defaultSlashMenuItems,
} from "@blocknote/core";
-import {
- RiH1,
- RiH2,
- RiH3,
- RiListOrdered,
- RiListUnordered,
- RiText,
-} from "react-icons/ri";
import { formatKeyboardShortcut } from "../utils";
import { ReactSlashMenuItem } from "./ReactSlashMenuItem";
+import iconsData from "../FormattingToolbar/components/FontIcons";
+const { heading, task, numList, bullet, checkbox, getIcon } = iconsData;
+
+const FullHeadingIcon = ({ order }: { order: number }) => (
+ <>
+ {heading()}
+ {getIcon(order)}
+ >
+);
const extraFields: Record<
string,
Omit<
@@ -20,42 +21,55 @@ const extraFields: Record<
keyof BaseSlashMenuItem
>
> = {
+ Task: {
+ group: "Basic blocks",
+ icon: task(),
+ markdownHint: "* task",
+ hint: "Used to display a task list",
+ shortcut: "",
+ },
+ Checklist: {
+ group: "Basic blocks",
+ icon: checkbox(),
+ markdownHint: "+ checklist",
+ hint: "Used to display a checkbox list",
+ shortcut: "",
+ },
+ "Bullet Point": {
+ group: "Basic blocks",
+ icon: bullet(),
+ markdownHint: "- bullet",
+ hint: "Used to display an unordered list",
+ shortcut: formatKeyboardShortcut("Mod-Alt-9"),
+ },
+ "Numbered List": {
+ group: "Basic blocks",
+ icon: numList(),
+ markdownHint: ". number",
+ hint: "Used to display a numbered list",
+ shortcut: formatKeyboardShortcut("Mod-Alt-7"),
+ },
Heading: {
group: "Headings",
- icon: ,
+ icon: ,
+ markdownHint: "# H1",
hint: "Used for a top-level heading",
shortcut: formatKeyboardShortcut("Mod-Alt-1"),
},
"Heading 2": {
group: "Headings",
- icon: ,
+ icon: ,
+ markdownHint: "## H2",
hint: "Used for key sections",
shortcut: formatKeyboardShortcut("Mod-Alt-2"),
},
"Heading 3": {
group: "Headings",
- icon: ,
+ icon: ,
+ markdownHint: "### H3",
hint: "Used for subsections and group headings",
shortcut: formatKeyboardShortcut("Mod-Alt-3"),
},
- "Numbered List": {
- group: "Basic blocks",
- icon: ,
- hint: "Used to display a numbered list",
- shortcut: formatKeyboardShortcut("Mod-Alt-7"),
- },
- "Bullet List": {
- group: "Basic blocks",
- icon: ,
- hint: "Used to display an unordered list",
- shortcut: formatKeyboardShortcut("Mod-Alt-9"),
- },
- Paragraph: {
- group: "Basic blocks",
- icon: ,
- hint: "Used for the body of your document",
- shortcut: formatKeyboardShortcut("Mod-Alt-0"),
- },
};
export const defaultReactSlashMenuItems = defaultSlashMenuItems.map(
@@ -67,6 +81,7 @@ export const defaultReactSlashMenuItems = defaultSlashMenuItems.map(
extraFields[item.name].group,
extraFields[item.name].icon,
extraFields[item.name].hint,
- extraFields[item.name].shortcut
+ extraFields[item.name].shortcut,
+ extraFields[item.name].markdownHint
)
);