editor: choose virtualization mode between blocks and pages

This commit is contained in:
Ammar Ahmed
2026-08-25 18:53:17 +05:00
parent 6a9bdce999
commit 33074112fb
7 changed files with 593 additions and 535 deletions

View File

@@ -39,6 +39,7 @@ import {
getChangedNodes,
LinkAttributes,
profiler,
type VirtualizationMode,
type Selection
} from "@notesnook/editor";
import { installProfilerGlobals, setProfiledEditor } from "./profiling";
@@ -117,7 +118,7 @@ type TipTapProps = {
dayFormat: DayFormat;
markdownShortcuts: boolean;
fontLigatures: boolean;
virtualization: boolean;
virtualization: VirtualizationMode;
};
function countCharacters(text: string) {
@@ -292,7 +293,7 @@ function TipTap(props: TipTapProps) {
onFocus,
onCreate: async ({ editor }) => {
setProfiledEditor(editor as Editor);
profiler.setContext("virtualization", !!virtualization);
profiler.setContext("virtualization", virtualization);
profiler.setContext("noteId", id);
profiler.setContext("topLevelBlocks", editor.state.doc.childCount);
profiler.setContext("characters", editor.state.doc.textContent.length);

View File

@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { SettingsGroup } from "./types";
import { VirtualizationMode } from "@notesnook/editor";
import {
editorConfig,
onEditorConfigChange,
@@ -165,10 +166,18 @@ export const EditorSettings: SettingsGroup[] = [
useSettingStore.subscribe((c) => c.editorVirtualization, listener),
components: [
{
type: "toggle",
isToggled: () => useSettingStore.getState().editorVirtualization,
toggle: () =>
useSettingStore.getState().toggleEditorVirtualization()
type: "dropdown",
options: [
{ value: "off", title: strings.editorVirtualizationOff() },
{ value: "blocks", title: strings.editorVirtualizationBlocks() },
{ value: "pages", title: strings.editorVirtualizationPages() }
],
selectedOption: () =>
useSettingStore.getState().editorVirtualization,
onSelectionChanged: (value) =>
useSettingStore
.getState()
.setEditorVirtualization(value as VirtualizationMode)
}
]
}

View File

@@ -25,6 +25,7 @@ import Config from "../utils/config";
import BaseStore from "./index";
import { TimeFormat, DayFormat, WeekFormat, EVENTS } from "@notesnook/core";
import { Profile, TrashCleanupInterval } from "@notesnook/core";
import { VirtualizationMode, toVirtualizationMode } from "@notesnook/editor";
import { showToast } from "../utils/toast";
import { ConfirmDialog } from "../dialogs/confirm";
import * as openpgp from "openpgp";
@@ -57,7 +58,9 @@ class SettingStore extends BaseStore<SettingStore> {
doubleSpacedParagraphs = Config.get("doubleSpacedLines", true);
markdownShortcuts = Config.get("markdownShortcuts", false);
fontLigatures = Config.get("fontLigatures", false);
editorVirtualization = Config.get("editorVirtualization", false);
editorVirtualization: VirtualizationMode = toVirtualizationMode(
Config.get<VirtualizationMode | boolean>("editorVirtualization", "off")
);
notificationsSettings = Config.get("notifications", { reminder: true });
isFullOfflineMode = Config.get("fullOfflineMode", false);
serverUrls: Partial<Record<HostId, string>> = Config.get("serverUrls", {});
@@ -251,11 +254,9 @@ class SettingStore extends BaseStore<SettingStore> {
Config.set("fontLigatures", !fontLigatures);
};
toggleEditorVirtualization = (toggleState?: boolean) => {
const editorVirtualization = this.get().editorVirtualization;
const next = toggleState ?? !editorVirtualization;
this.set((state) => (state.editorVirtualization = next));
Config.set("editorVirtualization", next);
setEditorVirtualization = (mode: VirtualizationMode) => {
this.set((state) => (state.editorVirtualization = mode));
Config.set("editorVirtualization", mode);
};
togglePrivacyMode = async () => {

View File

@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import {
EditorOptions,
Node as TiptapNode,
extensions as TiptapCoreExtensions,
getHTMLFromFragment
} from "@tiptap/core";
@@ -84,6 +85,7 @@ import { Callout } from "./extensions/callout/index.js";
import BlockId from "./extensions/block-id/index.js";
import { Virtualization } from "./extensions/virtualization/index.js";
import { EditorProfiler } from "./extensions/profiler/index.js";
import { Page, Paging } from "./extensions/paging/index.js";
import { useEditorSearchStore } from "./toolbar/stores/search-store.js";
import { DiffHighlighter } from "./extensions/diff-highlighter/index.js";
import { getChangedNodes } from "./utils/prosemirror.js";
@@ -139,7 +141,7 @@ export type TiptapOptions = EditorOptions &
isMobile?: boolean;
doubleSpacedLines?: boolean;
enableFontLigatures?: boolean;
virtualization?: boolean;
virtualization?: VirtualizationMode | boolean;
} & {
placeholder: string;
};
@@ -192,8 +194,9 @@ const useTiptap = (
};
}, [closeAllPopups]);
const defaultOptions = useMemo<Partial<EditorOptions>>(
() => ({
const defaultOptions = useMemo<Partial<EditorOptions>>(() => {
const mode = toVirtualizationMode(virtualization);
return {
enableCoreExtensions: false,
editorProps: {
...editorProps,
@@ -237,6 +240,7 @@ const useTiptap = (
doubleSpaced: doubleSpacedLines
}),
StarterKit.configure({
document: false,
code: false,
codeBlock: false,
listItem: false,
@@ -276,7 +280,10 @@ const useTiptap = (
}
}),
BlockId,
Virtualization.configure({ enabled: !!virtualization }),
PagedDocument,
Page,
Virtualization.configure({ enabled: mode === "blocks" }),
Paging.configure({ enabled: mode === "pages" }),
EditorProfiler,
Blockquote,
CharacterCount,
@@ -417,23 +424,22 @@ const useTiptap = (
},
injectCSS: false,
parseOptions: { preserveWhitespace: true }
}),
[
isMobile,
previewAttachment,
downloadAttachment,
openAttachmentPicker,
getAttachmentData,
onBeforeCreate,
openLink,
dateFormat,
timeFormat,
editorProps,
copyToClipboard,
createInternalLink,
virtualization
]
);
};
}, [
isMobile,
previewAttachment,
downloadAttachment,
openAttachmentPicker,
getAttachmentData,
onBeforeCreate,
openLink,
dateFormat,
timeFormat,
editorProps,
copyToClipboard,
createInternalLink,
virtualization
]);
const editor = useEditor(
{
@@ -446,6 +452,26 @@ const useTiptap = (
return editor;
};
/**
* Pages are optional in the schema so an unpaged document stays valid: nothing
* has to be migrated, and turning paging off simply stops producing them.
*/
const PagedDocument = TiptapNode.create({
name: "doc",
topNode: true,
content: "(page | block)+"
});
export type VirtualizationMode = "off" | "blocks" | "pages";
export function toVirtualizationMode(
value: VirtualizationMode | boolean | undefined
): VirtualizationMode {
if (value === true) return "blocks";
if (!value) return "off";
return value;
}
function hasStyle(element: HTMLElement | string) {
const style = (element as HTMLElement).getAttribute("style");
if (!style || style === "font-family: inherit;") return false;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2479,6 +2479,9 @@ Use this if changes from other devices are not appearing on this device. This wi
editorVirtualization: () => t`Editor paging (experimental)`,
editorVirtualizationDesc: () =>
t`Only render the part of a note that is currently on screen. Makes very large notes much faster to open, scroll and type in. While this is on, your browser's find (Ctrl+F) and printing will only cover the visible part of a note — use the editor's own search instead. If you face any issues, please turn it off.`,
editorVirtualizationOff: () => t`Off`,
editorVirtualizationBlocks: () => t`Blocks`,
editorVirtualizationPages: () => t`Pages`,
expandSidebar: () => t`Expand sidebar`,
viewAllLimits: () => `View all limits`,
freePlan: () => t`Free plan`,