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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"playground": true,
"docs": true,
"author": "areknawo",
"tags": [
"Intermediate",
"UI Components",
"Formatting Toolbar",
"Appearance & Styling"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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 experimental mobile formatting toolbar by selecting some text (best experienced on a mobile device).",
},
{
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.
<BlockNoteView editor={editor} formattingToolbar={false}>
<ExperimentalMobileFormattingToolbarController />
</BlockNoteView>
);
}
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When is the flickering visible? I don't think I saw it when playing around in the preview

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you test on mobile? When you scroll / down a large document the updating of the position is a bit delayed


**Relevant Docs:**

- [Changing the Formatting Toolbar](/docs/ui-components/formatting-toolbar#changing-the-formatting-toolbar)
- [Editor Setup](/docs/editor-basics/setup)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<html lang="en">
<head>
<script>
<!-- AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY -->
</script>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Experimental Mobile Formatting Toolbar</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -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(
<React.StrictMode>
<App />
</React.StrictMode>
);
Original file line number Diff line number Diff line change
@@ -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"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.bn-container {
display: flex;
flex-direction: column-reverse;
gap: 8px;
}

.bn-formatting-toolbar {
margin-inline: auto;
}
Original file line number Diff line number Diff line change
@@ -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/"
}
]
}
Original file line number Diff line number Diff line change
@@ -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),
},
}));
4 changes: 4 additions & 0 deletions packages/ariakit/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
6 changes: 6 additions & 0 deletions packages/mantine/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -144,6 +148,8 @@
gap: 2px;
padding: 2px;
width: fit-content;
overflow-x: auto;
max-width: 100vw;
}

.bn-mantine .bn-toolbar:empty {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
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";

/**
* 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<FormattingToolbarProps>;
floatingOptions?: Partial<UseFloatingOptions>;
}) => {
const [transform, setTransform] = useState<string>("none");
const divRef = useRef<HTMLDivElement>(null);
const editor = useBlockNoteEditor<
BlockSchema,
InlineContentSchema,
StyleSchema
>();
const state = useUIPluginState(
editor.formattingToolbar.onUpdate.bind(editor.formattingToolbar)
);
const style = useMemo<CSSProperties>(() => {
return {
display: "flex",
position: "fixed",
bottom: 0,
zIndex: 3000,
transform,
};
}, [transform]);

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 =
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 (
<div
ref={divRef}
style={style}
dangerouslySetInnerHTML={{ __html: divRef.current.innerHTML }}></div>
);
}

const Component = props.formattingToolbar || FormattingToolbar;

return (
<div ref={divRef} style={style}>
<Component />
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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) {
Expand Down
Loading