From 6073c3eec58e55002f71b5a1e92e7f3cf5d1735b Mon Sep 17 00:00:00 2001 From: Arek Nawo Date: Wed, 6 Nov 2024 11:07:28 +0100 Subject: [PATCH 01/14] fix: Import only from a single Shiki bundle --- package-lock.json | 1 + .../defaultSupportedLanguages.ts | 56 +++++++++++++------ 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index 328d5f36a1..2b63c867df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "@blocknote/mantine": "^0.18.1", "@blocknote/react": "^0.18.1", "@blocknote/shadcn": "^0.18.1", + "@blocknote/xl-multi-column": "^0.18.1", "@headlessui/react": "^1.7.18", "@heroicons/react": "^2.1.4", "@mantine/core": "^7.10.1", diff --git a/packages/core/src/blocks/CodeBlockContent/defaultSupportedLanguages.ts b/packages/core/src/blocks/CodeBlockContent/defaultSupportedLanguages.ts index 36fa7e712e..f4b58f0414 100644 --- a/packages/core/src/blocks/CodeBlockContent/defaultSupportedLanguages.ts +++ b/packages/core/src/blocks/CodeBlockContent/defaultSupportedLanguages.ts @@ -1,4 +1,4 @@ -import { bundledLanguagesInfo } from "shiki/bundle/web"; +import { bundledLanguagesInfo } from "shiki"; export type SupportedLanguageConfig = { id: string; @@ -14,23 +14,43 @@ export const defaultSupportedLanguages: SupportedLanguageConfig[] = [ }, ...bundledLanguagesInfo .filter((lang) => { - return ![ - "angular-html", - "angular-ts", - "astro", - "blade", - "coffee", - "handlebars", - "html-derivative", - "http", - "imba", - "jinja", - "jison", - "json5", - "marko", - "mdc", - "stylus", - "ts-tags", + return [ + "c", + "cpp", + "css", + "glsl", + "graphql", + "haml", + "html", + "java", + "javascript", + "json", + "jsonc", + "jsonl", + "jsx", + "julia", + "less", + "markdown", + "mdx", + "php", + "postcss", + "pug", + "python", + "r", + "regexp", + "sass", + "scss", + "shellscript", + "sql", + "svelte", + "tsx", + "typescript", + "vue", + "vue-html", + "wasm", + "wgsl", + "xml", + "yaml", ].includes(lang.id); }) .map((lang) => ({ From 436909296b516a0bd86170024f47c7e46244d7aa Mon Sep 17 00:00:00 2001 From: Arek Nawo Date: Wed, 6 Nov 2024 11:12:54 +0100 Subject: [PATCH 02/14] fix: Select options styling on Chrome, Linux --- packages/core/src/editor/Block.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/core/src/editor/Block.css b/packages/core/src/editor/Block.css index 49adf9b533..707ee5ee5c 100644 --- a/packages/core/src/editor/Block.css +++ b/packages/core/src/editor/Block.css @@ -299,6 +299,9 @@ NESTED BLOCKS transition: opacity 0.3s; transition-delay: 1s; } +.bn-block-content[data-content-type="codeBlock"] > div > select > option { + color: black; +} .bn-block-content[data-content-type="codeBlock"]:hover > div > select, .bn-block-content[data-content-type="codeBlock"] > div > select:focus { opacity: 0.5; From 3a390a601a1979cd1b57bf752a1501a22430fc2e Mon Sep 17 00:00:00 2001 From: Arek Nawo Date: Wed, 6 Nov 2024 11:29:32 +0100 Subject: [PATCH 03/14] fix: Detect proper language ID when parsing from HTML --- .../CodeBlockContent/CodeBlockContent.ts | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/core/src/blocks/CodeBlockContent/CodeBlockContent.ts b/packages/core/src/blocks/CodeBlockContent/CodeBlockContent.ts index 139cd5fbf5..ce71bba1bc 100644 --- a/packages/core/src/blocks/CodeBlockContent/CodeBlockContent.ts +++ b/packages/core/src/blocks/CodeBlockContent/CodeBlockContent.ts @@ -47,11 +47,15 @@ const CodeBlockContent = createStronglyTypedTiptapNode({ }; }, addAttributes() { + const supportedLanguages = this.options + .supportedLanguages as SupportedLanguageConfig[]; + return { language: { default: this.options.defaultLanguage, parseHTML: (inputElement) => { let element = inputElement as HTMLElement | null; + let language: string | null = null; if ( element?.tagName === "DIV" && @@ -67,20 +71,26 @@ const CodeBlockContent = createStronglyTypedTiptapNode({ const dataLanguage = element?.getAttribute("data-language"); if (dataLanguage) { - return dataLanguage.toLowerCase(); + language = dataLanguage.toLowerCase(); + } else { + const classNames = [...(element?.className.split(" ") || [])]; + const languages = classNames + .filter((className) => className.startsWith("language-")) + .map((className) => className.replace("language-", "")); + const [classLanguage] = languages; + + language = classLanguage.toLowerCase(); } - const classNames = [...(element?.className.split(" ") || [])]; - const languages = classNames - .filter((className) => className.startsWith("language-")) - .map((className) => className.replace("language-", "")); - const [language] = languages; - if (!language) { return null; } - return language.toLowerCase(); + return ( + supportedLanguages.find(({ match }) => { + return match.includes(language); + })?.id || this.options.defaultLanguage + ); }, renderHTML: (attributes) => { return attributes.language && attributes.language !== "text" From 1e0003db8f4a079752ff4cb62af476e0222919c0 Mon Sep 17 00:00:00 2001 From: Arek Nawo Date: Wed, 6 Nov 2024 11:29:58 +0100 Subject: [PATCH 04/14] fix: Support "typescriptreact" alias for tsx --- .../src/blocks/CodeBlockContent/defaultSupportedLanguages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/blocks/CodeBlockContent/defaultSupportedLanguages.ts b/packages/core/src/blocks/CodeBlockContent/defaultSupportedLanguages.ts index f4b58f0414..b5da23b0cd 100644 --- a/packages/core/src/blocks/CodeBlockContent/defaultSupportedLanguages.ts +++ b/packages/core/src/blocks/CodeBlockContent/defaultSupportedLanguages.ts @@ -43,7 +43,6 @@ export const defaultSupportedLanguages: SupportedLanguageConfig[] = [ "shellscript", "sql", "svelte", - "tsx", "typescript", "vue", "vue-html", @@ -58,6 +57,7 @@ export const defaultSupportedLanguages: SupportedLanguageConfig[] = [ id: lang.id, name: lang.name, })), + { id: "tsx", name: "TSX", match: ["tsx", "typescriptreact"] }, { id: "haskell", name: "Haskell", From 31d0f87bb7d45c5b6ced86c0ef8d07aed14e5ae4 Mon Sep 17 00:00:00 2001 From: yousefed Date: Thu, 14 Nov 2024 13:22:02 +0100 Subject: [PATCH 05/14] minor code change --- .../src/api/clipboard/fromClipboard/handleVSCodePaste.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/src/api/clipboard/fromClipboard/handleVSCodePaste.ts b/packages/core/src/api/clipboard/fromClipboard/handleVSCodePaste.ts index 697a880dec..a9131c43cf 100644 --- a/packages/core/src/api/clipboard/fromClipboard/handleVSCodePaste.ts +++ b/packages/core/src/api/clipboard/fromClipboard/handleVSCodePaste.ts @@ -18,9 +18,6 @@ export async function handleVSCodePaste< } const text = event.clipboardData!.getData("text/plain"); - const vscode = event.clipboardData!.getData("vscode-editor-data"); - const vscodeData = vscode ? JSON.parse(vscode) : undefined; - const language = vscodeData?.mode; if (!text) { return false; @@ -28,10 +25,13 @@ export async function handleVSCodePaste< if (!schema.nodes.codeBlock) { view.pasteText(text); - return true; } + const vscode = event.clipboardData!.getData("vscode-editor-data"); + const vscodeData = vscode ? JSON.parse(vscode) : undefined; + const language = vscodeData?.mode; + if (!language) { return false; } From fc608ff799fddfcb5050366ec40597aff6f11bca Mon Sep 17 00:00:00 2001 From: yousefed Date: Thu, 14 Nov 2024 13:23:22 +0100 Subject: [PATCH 06/14] remove setting cursor position when inserting code block --- .../extensions/SuggestionMenu/getDefaultSlashMenuItems.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts index 1af3dae3fd..fb6fbad7ec 100644 --- a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts @@ -176,14 +176,9 @@ export function getDefaultSlashMenuItems< if (checkDefaultBlockTypeInSchema("codeBlock", editor)) { items.push({ onItemClick: () => { - const pos = editor._tiptapEditor.state.selection.from; - insertOrUpdateBlock(editor, { type: "codeBlock", }); - - // Move the cursor inside the code block - editor._tiptapEditor.commands.setTextSelection(pos); }, badge: formatKeyboardShortcut("Mod-Alt-c"), key: "code_block", From a462eae86abe9f921cbcb072bb8f186cc97172bc Mon Sep 17 00:00:00 2001 From: Arek Nawo Date: Mon, 25 Nov 2024 10:08:01 +0100 Subject: [PATCH 07/14] experiment: Visual Viewport API --- .../FormattingToolbarController.tsx | 2 +- .../src/hooks/useUIElementPositioning.ts | 65 ++++++++++++++----- playground/src/style.css | 10 +++ 3 files changed, 61 insertions(+), 16 deletions(-) diff --git a/packages/react/src/components/FormattingToolbar/FormattingToolbarController.tsx b/packages/react/src/components/FormattingToolbar/FormattingToolbarController.tsx index f4bab49b83..dafabde6e8 100644 --- a/packages/react/src/components/FormattingToolbar/FormattingToolbarController.tsx +++ b/packages/react/src/components/FormattingToolbar/FormattingToolbarController.tsx @@ -75,7 +75,7 @@ export const FormattingToolbarController = (props: { ); const { isMounted, ref, style, getFloatingProps } = useUIElementPositioning( - state?.show || false, + state?.show || true, state?.referencePos || null, 3000, { diff --git a/packages/react/src/hooks/useUIElementPositioning.ts b/packages/react/src/hooks/useUIElementPositioning.ts index 7c2f47d56e..8de3c9cb1b 100644 --- a/packages/react/src/hooks/useUIElementPositioning.ts +++ b/packages/react/src/hooks/useUIElementPositioning.ts @@ -5,7 +5,7 @@ import { useInteractions, useTransitionStyles, } from "@floating-ui/react"; -import { useEffect, useMemo } from "react"; +import { useEffect, useMemo, useState } from "react"; export function useUIElementPositioning( show: boolean, @@ -17,7 +17,7 @@ export function useUIElementPositioning( open: show, ...options, }); - + const [transform, setTransform] = useState("none"); const { isMounted, styles } = useTransitionStyles(context); // handle "escape" and other dismiss events, these will add some listeners to @@ -40,28 +40,63 @@ export function useUIElementPositioning( getBoundingClientRect: () => referencePos, }); }, [referencePos, refs]); + useEffect(() => { + const viewport = window.visualViewport!; + + function viewportHandler() { + const layoutViewport = document.body; + + // Since the bar is position: fixed we need to offset it by the visual + // viewport's offset from the layout viewport origin. + const offsetLeft = viewport.offsetLeft; + const offsetTop = + viewport.height - + layoutViewport.getBoundingClientRect().height + + viewport.offsetTop; + + // You could also do this by setting style.left and style.top if you + // use width: 100% instead. - return useMemo( - () => ({ + setTransform( + `translate(${offsetLeft}px, ${offsetTop}px) scale(${ + 1 / viewport.scale + })` + ); + } + window.visualViewport!.addEventListener("scroll", viewportHandler); + window.visualViewport!.addEventListener("resize", viewportHandler); + viewportHandler(); + }, []); + + return useMemo(() => { + return { isMounted, ref: refs.setFloating, style: { display: "flex", ...styles, ...floatingStyles, + position: "absolute", + transition: "none", + transform: transform, + maxWidth: "100vw", + overflowX: "auto", + bottom: 0, + top: "unset", zIndex: zIndex, + opacity: 1, }, getFloatingProps, getReferenceProps, - }), - [ - floatingStyles, - isMounted, - refs.setFloating, - styles, - zIndex, - getFloatingProps, - getReferenceProps, - ] - ); + }; + }, [ + floatingStyles, + isMounted, + refs.setFloating, + styles, + zIndex, + getFloatingProps, + getReferenceProps, + transform, + ]); } diff --git a/playground/src/style.css b/playground/src/style.css index de46ca3abe..7e716f2b47 100644 --- a/playground/src/style.css +++ b/playground/src/style.css @@ -13,3 +13,13 @@ body { .mantine-AppShell-navbar { background-color: #f7f7f5; } + +@media (max-width: 767px) { + .mantine-AppShell-navbar { + display: none !important; + } + + .mantine-AppShell-main { + padding: 0 !important; + } +} From 4e57e6883b72de46b49096be22f8ff7bdfb1539c Mon Sep 17 00:00:00 2001 From: Arek Nawo Date: Mon, 25 Nov 2024 15:43:56 +0100 Subject: [PATCH 08/14] experiment: Directly setting transform --- .../src/hooks/useUIElementPositioning.ts | 64 ++++++++++--------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/packages/react/src/hooks/useUIElementPositioning.ts b/packages/react/src/hooks/useUIElementPositioning.ts index 8de3c9cb1b..a80bfbe01f 100644 --- a/packages/react/src/hooks/useUIElementPositioning.ts +++ b/packages/react/src/hooks/useUIElementPositioning.ts @@ -5,7 +5,7 @@ import { useInteractions, useTransitionStyles, } from "@floating-ui/react"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; export function useUIElementPositioning( show: boolean, @@ -13,24 +13,26 @@ export function useUIElementPositioning( zIndex: number, options?: Partial ) { - const { refs, update, context, floatingStyles } = useFloating({ + /*const { refs, update, context, floatingStyles } = useFloating({ open: show, ...options, - }); - const [transform, setTransform] = useState("none"); - const { isMounted, styles } = useTransitionStyles(context); + });*/ + //const [transform, setTransform] = useState("none"); + const ref = useRef(null); + const viewportHandlerRef = useRef<() => void>(() => {}); + //const { isMounted, styles } = useTransitionStyles(context); // handle "escape" and other dismiss events, these will add some listeners to // getFloatingProps which need to be attached to the floating element - const dismiss = useDismiss(context); + //const dismiss = useDismiss(context); - const { getReferenceProps, getFloatingProps } = useInteractions([dismiss]); + //const { getReferenceProps, getFloatingProps } = useInteractions([dismiss]); - useEffect(() => { + /*useEffect(() => { update(); - }, [referencePos, update]); + }, [referencePos, update]);*/ - useEffect(() => { + /*useEffect(() => { // Will be null on initial render when used in UI component controllers. if (referencePos === null) { return; @@ -39,7 +41,7 @@ export function useUIElementPositioning( refs.setReference({ getBoundingClientRect: () => referencePos, }); - }, [referencePos, refs]); + }, [referencePos, refs]);*/ useEffect(() => { const viewport = window.visualViewport!; @@ -56,29 +58,33 @@ export function useUIElementPositioning( // You could also do this by setting style.left and style.top if you // use width: 100% instead. - - setTransform( - `translate(${offsetLeft}px, ${offsetTop}px) scale(${ + if (ref.current) { + ref.current.style.transform = `translate(${offsetLeft}px, ${offsetTop}px) scale(${ 1 / viewport.scale - })` - ); + })`; + } } window.visualViewport!.addEventListener("scroll", viewportHandler); window.visualViewport!.addEventListener("resize", viewportHandler); viewportHandler(); + viewportHandlerRef.current = viewportHandler; }, []); return useMemo(() => { return { - isMounted, - ref: refs.setFloating, + isMounted: true, + ref: (el) => { + ref.current = el; + viewportHandlerRef.current!(); + }, style: { display: "flex", - ...styles, - ...floatingStyles, + /*...styles, + ...floatingStyles,*/ position: "absolute", - transition: "none", - transform: transform, + transition: "transform 0.1s", + + //transform: transform, maxWidth: "100vw", overflowX: "auto", bottom: 0, @@ -86,17 +92,17 @@ export function useUIElementPositioning( zIndex: zIndex, opacity: 1, }, - getFloatingProps, - getReferenceProps, + getFloatingProps: () => ({}), + getReferenceProps: () => ({}), }; }, [ - floatingStyles, + /*floatingStyles, isMounted, refs.setFloating, - styles, + styles,*/ zIndex, - getFloatingProps, - getReferenceProps, - transform, + /*getFloatingProps, + getReferenceProps,*/ + //transform, ]); } From 3071f09157cbda11a68fbcd058c0facb6408363c Mon Sep 17 00:00:00 2001 From: Arek Nawo Date: Wed, 27 Nov 2024 18:37:49 +0100 Subject: [PATCH 09/14] feat: Experimental mobile formatting toolbar controller --- ...entalMobileFormattingToolbarController.tsx | 85 +++++++++++++++++++ .../FormattingToolbarController.tsx | 2 +- .../src/hooks/useUIElementPositioning.ts | 84 +++++------------- packages/react/src/index.ts | 1 + 4 files changed, 107 insertions(+), 65 deletions(-) create mode 100644 packages/react/src/components/FormattingToolbar/ExperimentalMobileFormattingToolbarController.tsx diff --git a/packages/react/src/components/FormattingToolbar/ExperimentalMobileFormattingToolbarController.tsx b/packages/react/src/components/FormattingToolbar/ExperimentalMobileFormattingToolbarController.tsx new file mode 100644 index 0000000000..edd782d415 --- /dev/null +++ b/packages/react/src/components/FormattingToolbar/ExperimentalMobileFormattingToolbarController.tsx @@ -0,0 +1,85 @@ +import { BlockSchema, InlineContentSchema, StyleSchema } from "@blocknote/core"; +import { UseFloatingOptions } from "@floating-ui/react"; +import { FC, CSSProperties, useMemo, useRef, useState, useEffect } from "react"; +import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js"; +import { useUIPluginState } from "../../hooks/useUIPluginState.js"; +import { FormattingToolbar } from "./FormattingToolbar.js"; +import { FormattingToolbarProps } from "./FormattingToolbarProps.js"; + +export const ExperimentalMobileFormattingToolbarController = (props: { + formattingToolbar?: FC; + floatingOptions?: Partial; +}) => { + const [transform, setTransform] = useState("none"); + const divRef = useRef(null); + const editor = useBlockNoteEditor< + BlockSchema, + InlineContentSchema, + StyleSchema + >(); + const state = useUIPluginState( + editor.formattingToolbar.onUpdate.bind(editor.formattingToolbar) + ); + const style = useMemo(() => { + return { + display: "flex", + position: "absolute", + maxWidth: "100vw", + overflowX: "auto", + bottom: 0, + top: "unset", + zIndex: 3000, + transform, + }; + }, [transform]); + + useEffect(() => { + const viewport = window.visualViewport!; + function viewportHandler() { + const layoutViewport = document.body; + const offsetLeft = viewport.offsetLeft; + const offsetTop = + viewport.height - + layoutViewport.getBoundingClientRect().height + + viewport.offsetTop; + + setTransform( + `translate(${offsetLeft}px, ${offsetTop}px) scale(${ + 1 / viewport.scale + })` + ); + } + window.visualViewport!.addEventListener("scroll", viewportHandler); + window.visualViewport!.addEventListener("resize", viewportHandler); + viewportHandler(); + + return () => { + window.visualViewport!.removeEventListener("scroll", viewportHandler); + window.visualViewport!.removeEventListener("resize", viewportHandler); + }; + }, []); + + if (!state) { + return null; + } + + if (!state.show && divRef.current) { + // The component is fading out. Use the previous state to render the toolbar with innerHTML, + // because otherwise the toolbar will quickly flickr (i.e.: show a different state) while fading out, + // which looks weird + return ( +
+ ); + } + + const Component = props.formattingToolbar || FormattingToolbar; + + return ( +
+ +
+ ); +}; diff --git a/packages/react/src/components/FormattingToolbar/FormattingToolbarController.tsx b/packages/react/src/components/FormattingToolbar/FormattingToolbarController.tsx index dafabde6e8..f4bab49b83 100644 --- a/packages/react/src/components/FormattingToolbar/FormattingToolbarController.tsx +++ b/packages/react/src/components/FormattingToolbar/FormattingToolbarController.tsx @@ -75,7 +75,7 @@ export const FormattingToolbarController = (props: { ); const { isMounted, ref, style, getFloatingProps } = useUIElementPositioning( - state?.show || true, + state?.show || false, state?.referencePos || null, 3000, { diff --git a/packages/react/src/hooks/useUIElementPositioning.ts b/packages/react/src/hooks/useUIElementPositioning.ts index a80bfbe01f..328e481eb9 100644 --- a/packages/react/src/hooks/useUIElementPositioning.ts +++ b/packages/react/src/hooks/useUIElementPositioning.ts @@ -5,7 +5,7 @@ import { useInteractions, useTransitionStyles, } from "@floating-ui/react"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useMemo } from "react"; export function useUIElementPositioning( show: boolean, @@ -13,96 +13,52 @@ export function useUIElementPositioning( zIndex: number, options?: Partial ) { - /*const { refs, update, context, floatingStyles } = useFloating({ + const { refs, update, context, floatingStyles } = useFloating({ open: show, ...options, - });*/ - //const [transform, setTransform] = useState("none"); - const ref = useRef(null); - const viewportHandlerRef = useRef<() => void>(() => {}); - //const { isMounted, styles } = useTransitionStyles(context); + }); + const { isMounted, styles } = useTransitionStyles(context); // handle "escape" and other dismiss events, these will add some listeners to // getFloatingProps which need to be attached to the floating element - //const dismiss = useDismiss(context); + const dismiss = useDismiss(context); - //const { getReferenceProps, getFloatingProps } = useInteractions([dismiss]); + const { getReferenceProps, getFloatingProps } = useInteractions([dismiss]); - /*useEffect(() => { + useEffect(() => { update(); - }, [referencePos, update]);*/ + }, [referencePos, update]); - /*useEffect(() => { + useEffect(() => { // Will be null on initial render when used in UI component controllers. if (referencePos === null) { return; } - refs.setReference({ getBoundingClientRect: () => referencePos, }); - }, [referencePos, refs]);*/ - useEffect(() => { - const viewport = window.visualViewport!; - - function viewportHandler() { - const layoutViewport = document.body; - - // Since the bar is position: fixed we need to offset it by the visual - // viewport's offset from the layout viewport origin. - const offsetLeft = viewport.offsetLeft; - const offsetTop = - viewport.height - - layoutViewport.getBoundingClientRect().height + - viewport.offsetTop; - - // You could also do this by setting style.left and style.top if you - // use width: 100% instead. - if (ref.current) { - ref.current.style.transform = `translate(${offsetLeft}px, ${offsetTop}px) scale(${ - 1 / viewport.scale - })`; - } - } - window.visualViewport!.addEventListener("scroll", viewportHandler); - window.visualViewport!.addEventListener("resize", viewportHandler); - viewportHandler(); - viewportHandlerRef.current = viewportHandler; - }, []); + }, [referencePos, refs]); return useMemo(() => { return { - isMounted: true, - ref: (el) => { - ref.current = el; - viewportHandlerRef.current!(); - }, + isMounted, + ref: refs.setFloating, style: { display: "flex", - /*...styles, - ...floatingStyles,*/ - position: "absolute", - transition: "transform 0.1s", - - //transform: transform, - maxWidth: "100vw", - overflowX: "auto", - bottom: 0, - top: "unset", + ...styles, + ...floatingStyles, zIndex: zIndex, - opacity: 1, }, - getFloatingProps: () => ({}), - getReferenceProps: () => ({}), + getFloatingProps, + getReferenceProps, }; }, [ - /*floatingStyles, + floatingStyles, isMounted, refs.setFloating, - styles,*/ + styles, zIndex, - /*getFloatingProps, - getReferenceProps,*/ - //transform, + getFloatingProps, + getReferenceProps, ]); } diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 713325abfa..1ae092b267 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -26,6 +26,7 @@ export * from "./components/FormattingToolbar/DefaultButtons/TextAlignButton.js" export * from "./components/FormattingToolbar/DefaultSelects/BlockTypeSelect.js"; export * from "./components/FormattingToolbar/FormattingToolbar.js"; export * from "./components/FormattingToolbar/FormattingToolbarController.js"; +export * from "./components/FormattingToolbar/ExperimentalMobileFormattingToolbarController.js"; export * from "./components/FormattingToolbar/FormattingToolbarProps.js"; export * from "./components/LinkToolbar/DefaultButtons/DeleteLinkButton.js"; From 1c0dd2d0151d1f7c64f8f45a6650d989872ed8c6 Mon Sep 17 00:00:00 2001 From: Arek Nawo Date: Wed, 27 Nov 2024 20:36:15 +0100 Subject: [PATCH 10/14] feat: Optimize formatting toolbar controller for mobile --- .../FormattingToolbar/FormattingToolbarController.tsx | 4 ++-- packages/react/src/hooks/useUIElementPositioning.ts | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/react/src/components/FormattingToolbar/FormattingToolbarController.tsx b/packages/react/src/components/FormattingToolbar/FormattingToolbarController.tsx index f4bab49b83..43937bc3f7 100644 --- a/packages/react/src/components/FormattingToolbar/FormattingToolbarController.tsx +++ b/packages/react/src/components/FormattingToolbar/FormattingToolbarController.tsx @@ -4,7 +4,7 @@ import { InlineContentSchema, StyleSchema, } from "@blocknote/core"; -import { UseFloatingOptions, flip, offset } from "@floating-ui/react"; +import { UseFloatingOptions, flip, offset, shift } from "@floating-ui/react"; import { FC, useMemo, useRef, useState } from "react"; import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js"; @@ -80,7 +80,7 @@ export const FormattingToolbarController = (props: { 3000, { placement, - middleware: [offset(10), flip()], + middleware: [offset(10), shift(), flip()], onOpenChange: (open, _event) => { // console.log("change", event); if (!open) { diff --git a/packages/react/src/hooks/useUIElementPositioning.ts b/packages/react/src/hooks/useUIElementPositioning.ts index 328e481eb9..9be3202651 100644 --- a/packages/react/src/hooks/useUIElementPositioning.ts +++ b/packages/react/src/hooks/useUIElementPositioning.ts @@ -45,6 +45,8 @@ export function useUIElementPositioning( ref: refs.setFloating, style: { display: "flex", + maxWidth: "100vw", + overflowX: "auto", ...styles, ...floatingStyles, zIndex: zIndex, From 6dfdc1fca0035cf7aef107f3c098e149a351ef2f Mon Sep 17 00:00:00 2001 From: Arek Nawo Date: Thu, 28 Nov 2024 13:09:15 +0100 Subject: [PATCH 11/14] fix: Move overflow-x to bn-toolbar --- packages/ariakit/src/style.css | 4 ++++ packages/mantine/src/style.css | 6 ++++++ packages/react/src/hooks/useUIElementPositioning.ts | 2 -- packages/shadcn/src/style.css | 4 ++++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/ariakit/src/style.css b/packages/ariakit/src/style.css index bf1dcd4ced..f966bee877 100644 --- a/packages/ariakit/src/style.css +++ b/packages/ariakit/src/style.css @@ -11,6 +11,10 @@ gap: 0.5rem; } +.bn-toolbar.bn-ak-toolbar { + overflow-x: auto; + max-width: 100vw; +} .bn-toolbar .bn-ak-button { width: unset; } diff --git a/packages/mantine/src/style.css b/packages/mantine/src/style.css index 55b5301fb0..9fe02136fa 100644 --- a/packages/mantine/src/style.css +++ b/packages/mantine/src/style.css @@ -134,6 +134,10 @@ overflow: auto; } +.bn-mantine .mantine-Button-root[aria-controls*="dropdown"] { + min-width: fit-content; +} + /* Toolbar styling */ .bn-mantine .bn-toolbar { background-color: var(--bn-colors-menu-background); @@ -144,6 +148,8 @@ gap: 2px; padding: 2px; width: fit-content; + overflow-x: auto; + max-width: 100vw; } .bn-mantine .bn-toolbar:empty { diff --git a/packages/react/src/hooks/useUIElementPositioning.ts b/packages/react/src/hooks/useUIElementPositioning.ts index 9be3202651..328e481eb9 100644 --- a/packages/react/src/hooks/useUIElementPositioning.ts +++ b/packages/react/src/hooks/useUIElementPositioning.ts @@ -45,8 +45,6 @@ export function useUIElementPositioning( ref: refs.setFloating, style: { display: "flex", - maxWidth: "100vw", - overflowX: "auto", ...styles, ...floatingStyles, zIndex: zIndex, diff --git a/packages/shadcn/src/style.css b/packages/shadcn/src/style.css index 102e80d8b9..f353cccd33 100644 --- a/packages/shadcn/src/style.css +++ b/packages/shadcn/src/style.css @@ -169,3 +169,7 @@ .bn-shadcn .bn-extend-button-add-remove-rows { cursor: row-resize; } +.bn-shadcn .bn-toolbar { + overflow-x: auto; + max-width: 100vw; +} From fc3b3a65f55066e18f6d9c4676eaa2edc76ce76b Mon Sep 17 00:00:00 2001 From: Arek Nawo Date: Sun, 1 Dec 2024 20:05:25 +0100 Subject: [PATCH 12/14] chore: Describe the experimental mobile formatting toolbar controller --- .../ExperimentalMobileFormattingToolbarController.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/react/src/components/FormattingToolbar/ExperimentalMobileFormattingToolbarController.tsx b/packages/react/src/components/FormattingToolbar/ExperimentalMobileFormattingToolbarController.tsx index edd782d415..b2093a9a4e 100644 --- a/packages/react/src/components/FormattingToolbar/ExperimentalMobileFormattingToolbarController.tsx +++ b/packages/react/src/components/FormattingToolbar/ExperimentalMobileFormattingToolbarController.tsx @@ -6,6 +6,12 @@ import { useUIPluginState } from "../../hooks/useUIPluginState.js"; import { FormattingToolbar } from "./FormattingToolbar.js"; import { FormattingToolbarProps } from "./FormattingToolbarProps.js"; +/** + * Experimental formatting toolbar controller for mobile devices. + * Uses Visual Viewport API to position the toolbar above the virtual keyboard. + * + * Currently marked experimental due to the flickering issue with positioning cause by the use of the API (and likely a delay in its updates). + */ export const ExperimentalMobileFormattingToolbarController = (props: { formattingToolbar?: FC; floatingOptions?: Partial; @@ -36,6 +42,7 @@ export const ExperimentalMobileFormattingToolbarController = (props: { useEffect(() => { const viewport = window.visualViewport!; function viewportHandler() { + // Calculate the offset necessary to set the toolbar above the virtual keyboard (using the offset info from the visualViewport) const layoutViewport = document.body; const offsetLeft = viewport.offsetLeft; const offsetTop = From f0838da8c80844181f8d925e4448c1a575679d9b Mon Sep 17 00:00:00 2001 From: Arek Nawo Date: Sun, 1 Dec 2024 20:05:27 +0100 Subject: [PATCH 13/14] chore: Add an example for the experimental mobile formatting toolbar controller --- .../.bnexample.json | 11 +++++ .../App.tsx | 41 +++++++++++++++++++ .../README.md | 10 +++++ .../index.html | 14 +++++++ .../main.tsx | 11 +++++ .../package.json | 37 +++++++++++++++++ .../style.css | 9 ++++ .../tsconfig.json | 36 ++++++++++++++++ .../vite.config.ts | 32 +++++++++++++++ playground/src/examples.gen.tsx | 21 ++++++++++ 10 files changed, 222 insertions(+) create mode 100644 examples/03-ui-components/14-experimental-mobile-formatting-toolbar/.bnexample.json create mode 100644 examples/03-ui-components/14-experimental-mobile-formatting-toolbar/App.tsx create mode 100644 examples/03-ui-components/14-experimental-mobile-formatting-toolbar/README.md create mode 100644 examples/03-ui-components/14-experimental-mobile-formatting-toolbar/index.html create mode 100644 examples/03-ui-components/14-experimental-mobile-formatting-toolbar/main.tsx create mode 100644 examples/03-ui-components/14-experimental-mobile-formatting-toolbar/package.json create mode 100644 examples/03-ui-components/14-experimental-mobile-formatting-toolbar/style.css create mode 100644 examples/03-ui-components/14-experimental-mobile-formatting-toolbar/tsconfig.json create mode 100644 examples/03-ui-components/14-experimental-mobile-formatting-toolbar/vite.config.ts diff --git a/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/.bnexample.json b/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/.bnexample.json new file mode 100644 index 0000000000..16f9aea065 --- /dev/null +++ b/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/.bnexample.json @@ -0,0 +1,11 @@ +{ + "playground": true, + "docs": true, + "author": "areknawo", + "tags": [ + "Intermediate", + "UI Components", + "Formatting Toolbar", + "Appearance & Styling" + ] +} diff --git a/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/App.tsx b/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/App.tsx new file mode 100644 index 0000000000..525330c0d0 --- /dev/null +++ b/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/App.tsx @@ -0,0 +1,41 @@ +import "@blocknote/core/fonts/inter.css"; +import { + ExperimentalMobileFormattingToolbarController, + useCreateBlockNote, +} from "@blocknote/react"; +import { BlockNoteView } from "@blocknote/mantine"; +import "@blocknote/mantine/style.css"; + +import "./style.css"; + +export default function App() { + // Creates a new editor instance. + const editor = useCreateBlockNote({ + initialContent: [ + { + type: "paragraph", + content: "Welcome to this demo!", + }, + { + type: "paragraph", + content: "Check out the static formatting toolbar above!", + }, + { + type: "paragraph", + }, + ], + }); + + // Renders the editor instance using a React component. + return ( + // Disables the default formatting toolbar and re-adds it without the + // `FormattingToolbarController` component. You may have seen + // `FormattingToolbarController` used in other examples, but we omit it here + // as we want to control the position and visibility ourselves. BlockNote + // also uses the `FormattingToolbarController` when displaying the + // Formatting Toolbar by default. + + + + ); +} diff --git a/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/README.md b/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/README.md new file mode 100644 index 0000000000..7998433781 --- /dev/null +++ b/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/README.md @@ -0,0 +1,10 @@ +# Experimental Mobile Formatting Toolbar + +This example shows how to use the experimental mobile formatting toolbar, which uses [Visual Viewport API](https://developer.mozilla.org/en-US/docs/Web/API/Visual_Viewport_API) to position the toolbar right above the virtual keyboard on mobile devices. + +Controller is currently marked **experimental** due to the flickering issue with positioning (caused by delays of the Visual Viewport API) + +**Relevant Docs:** + +- [Changing the Formatting Toolbar](/docs/ui-components/formatting-toolbar#changing-the-formatting-toolbar) +- [Editor Setup](/docs/editor-basics/setup) diff --git a/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/index.html b/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/index.html new file mode 100644 index 0000000000..6914836688 --- /dev/null +++ b/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/index.html @@ -0,0 +1,14 @@ + + + + + + Experimental Mobile Formatting Toolbar + + +
+ + + diff --git a/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/main.tsx b/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/main.tsx new file mode 100644 index 0000000000..f88b490fbd --- /dev/null +++ b/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/main.tsx @@ -0,0 +1,11 @@ +// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY +import React from "react"; +import { createRoot } from "react-dom/client"; +import App from "./App"; + +const root = createRoot(document.getElementById("root")!); +root.render( + + + +); diff --git a/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/package.json b/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/package.json new file mode 100644 index 0000000000..e6377e75b1 --- /dev/null +++ b/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/package.json @@ -0,0 +1,37 @@ +{ + "name": "@blocknote/example-experimental-mobile-formatting-toolbar", + "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY", + "private": true, + "version": "0.12.4", + "scripts": { + "start": "vite", + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "lint": "eslint . --max-warnings 0" + }, + "dependencies": { + "@blocknote/core": "latest", + "@blocknote/react": "latest", + "@blocknote/ariakit": "latest", + "@blocknote/mantine": "latest", + "@blocknote/shadcn": "latest", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.9", + "@vitejs/plugin-react": "^4.3.1", + "eslint": "^8.10.0", + "vite": "^5.3.4" + }, + "eslintConfig": { + "extends": [ + "../../../.eslintrc.js" + ] + }, + "eslintIgnore": [ + "dist" + ] +} \ No newline at end of file diff --git a/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/style.css b/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/style.css new file mode 100644 index 0000000000..839dd7baa0 --- /dev/null +++ b/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/style.css @@ -0,0 +1,9 @@ +.bn-container { + display: flex; + flex-direction: column-reverse; + gap: 8px; +} + +.bn-formatting-toolbar { + margin-inline: auto; +} \ No newline at end of file diff --git a/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/tsconfig.json b/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/tsconfig.json new file mode 100644 index 0000000000..1bd8ab3c57 --- /dev/null +++ b/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/tsconfig.json @@ -0,0 +1,36 @@ +{ + "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": [ + "DOM", + "DOM.Iterable", + "ESNext" + ], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "composite": true + }, + "include": [ + "." + ], + "__ADD_FOR_LOCAL_DEV_references": [ + { + "path": "../../../packages/core/" + }, + { + "path": "../../../packages/react/" + } + ] +} \ No newline at end of file diff --git a/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/vite.config.ts b/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/vite.config.ts new file mode 100644 index 0000000000..f62ab20bc2 --- /dev/null +++ b/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/vite.config.ts @@ -0,0 +1,32 @@ +// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY +import react from "@vitejs/plugin-react"; +import * as fs from "fs"; +import * as path from "path"; +import { defineConfig } from "vite"; +// import eslintPlugin from "vite-plugin-eslint"; +// https://vitejs.dev/config/ +export default defineConfig((conf) => ({ + plugins: [react()], + optimizeDeps: {}, + build: { + sourcemap: true, + }, + resolve: { + alias: + conf.command === "build" || + !fs.existsSync(path.resolve(__dirname, "../../packages/core/src")) + ? {} + : ({ + // Comment out the lines below to load a built version of blocknote + // or, keep as is to load live from sources with live reload working + "@blocknote/core": path.resolve( + __dirname, + "../../packages/core/src/" + ), + "@blocknote/react": path.resolve( + __dirname, + "../../packages/react/src/" + ), + } as any), + }, +})); diff --git a/playground/src/examples.gen.tsx b/playground/src/examples.gen.tsx index 67e6090d2e..c95bd0a46b 100644 --- a/playground/src/examples.gen.tsx +++ b/playground/src/examples.gen.tsx @@ -621,6 +621,27 @@ "slug": "ui-components" } }, + { + "projectSlug": "experimental-mobile-formatting-toolbar", + "fullSlug": "ui-components/experimental-mobile-formatting-toolbar", + "pathFromRoot": "examples/03-ui-components/14-experimental-mobile-formatting-toolbar", + "config": { + "playground": true, + "docs": true, + "author": "areknawo", + "tags": [ + "Intermediate", + "UI Components", + "Formatting Toolbar", + "Appearance & Styling" + ] + }, + "title": "Experimental Mobile Formatting Toolbar", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" + } + }, { "projectSlug": "link-toolbar-buttons", "fullSlug": "ui-components/link-toolbar-buttons", From 2af8966fce7c278a5bce97987745b37c7d4d01e1 Mon Sep 17 00:00:00 2001 From: Arek Nawo Date: Mon, 2 Dec 2024 11:55:28 +0100 Subject: [PATCH 14/14] fix: Experimental toolbar positioning --- .../14-experimental-mobile-formatting-toolbar/App.tsx | 3 ++- .../ExperimentalMobileFormattingToolbarController.tsx | 5 +---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/App.tsx b/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/App.tsx index 525330c0d0..62c82ddfe8 100644 --- a/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/App.tsx +++ b/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/App.tsx @@ -18,7 +18,8 @@ export default function App() { }, { type: "paragraph", - content: "Check out the static formatting toolbar above!", + content: + "Check out the experimental mobile formatting toolbar by selecting some text (best experienced on a mobile device).", }, { type: "paragraph", diff --git a/packages/react/src/components/FormattingToolbar/ExperimentalMobileFormattingToolbarController.tsx b/packages/react/src/components/FormattingToolbar/ExperimentalMobileFormattingToolbarController.tsx index b2093a9a4e..7110ecdf88 100644 --- a/packages/react/src/components/FormattingToolbar/ExperimentalMobileFormattingToolbarController.tsx +++ b/packages/react/src/components/FormattingToolbar/ExperimentalMobileFormattingToolbarController.tsx @@ -29,11 +29,8 @@ export const ExperimentalMobileFormattingToolbarController = (props: { const style = useMemo(() => { return { display: "flex", - position: "absolute", - maxWidth: "100vw", - overflowX: "auto", + position: "fixed", bottom: 0, - top: "unset", zIndex: 3000, transform, };