mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-16 11:47:54 +01:00
editor: add support for adding/removing internal links
This commit is contained in:
40
packages/editor/package-lock.json
generated
40
packages/editor/package-lock.json
generated
@@ -41,6 +41,7 @@
|
||||
"detect-indent": "^7.0.0",
|
||||
"entities": "^4.5.0",
|
||||
"katex": "0.16.4",
|
||||
"linkifyjs": "^4.1.3",
|
||||
"nanoid": "^4.0.1",
|
||||
"prism-themes": "^1.9.0",
|
||||
"prosemirror-codemark": "^0.4.2",
|
||||
@@ -973,6 +974,22 @@
|
||||
"@styled-system/css": "^5.1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@theme-ui/color-modes": {
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@theme-ui/color-modes/-/color-modes-0.16.1.tgz",
|
||||
"integrity": "sha512-G2YoNEMwZroRS0DcftUG+E/8WM5/Osf8TRrQLLK+L43HJ4BmaWuBmVeyoNOaPBDlAuqMBx2203VRgoPmUaMqOg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@theme-ui/core": "^0.16.1",
|
||||
"@theme-ui/css": "^0.16.1",
|
||||
"deepmerge": "^4.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"react": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@theme-ui/components": {
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@theme-ui/components/-/components-0.16.1.tgz",
|
||||
@@ -1018,6 +1035,22 @@
|
||||
"@emotion/react": "^11.11.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@theme-ui/theme-provider": {
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@theme-ui/theme-provider/-/theme-provider-0.16.1.tgz",
|
||||
"integrity": "sha512-+/3BJYLIOC2DwTS76cqNhigRQJJ+qOT845DYF7t3TaG2fXDfgh16/DGZSnVjGOGc9dYE3C/ZFAYcVDVwO94Guw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@theme-ui/color-modes": "^0.16.1",
|
||||
"@theme-ui/core": "^0.16.1",
|
||||
"@theme-ui/css": "^0.16.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"react": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/core": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.1.12.tgz",
|
||||
@@ -2508,8 +2541,7 @@
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||
},
|
||||
"node_modules/json-parse-even-better-errors": {
|
||||
"version": "2.3.1",
|
||||
@@ -2573,7 +2605,6 @@
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
@@ -3149,7 +3180,6 @@
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
||||
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
@@ -3170,7 +3200,6 @@
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
||||
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.0"
|
||||
@@ -3302,7 +3331,6 @@
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
|
||||
"integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"detect-indent": "^7.0.0",
|
||||
"entities": "^4.5.0",
|
||||
"katex": "0.16.4",
|
||||
"linkifyjs": "^4.1.3",
|
||||
"nanoid": "^4.0.1",
|
||||
"prism-themes": "^1.9.0",
|
||||
"prosemirror-codemark": "^0.4.2",
|
||||
@@ -48,11 +49,11 @@
|
||||
"unfurl.js": "^6.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"react-modal": "3.16.1",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"zustand": "4.4.7",
|
||||
"@emotion/react": "11.11.1",
|
||||
"@mdi/js": "^7.2.96",
|
||||
"@mdi/react": "^1.6.1",
|
||||
"@theme-ui/components": "^0.16.1",
|
||||
"@theme-ui/core": "^0.16.1",
|
||||
"@types/katex": "^0.14.0",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@types/react": "^18.2.39",
|
||||
@@ -64,26 +65,26 @@
|
||||
"happy-dom": "^8.9.0",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"prosemirror-test-builder": "^1.1.0",
|
||||
"@theme-ui/components": "^0.16.1",
|
||||
"@theme-ui/core": "^0.16.1",
|
||||
"@emotion/react": "11.11.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-modal": "3.16.1",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"vitest": "^0.29.2",
|
||||
"web-vitals": "^2.1.4"
|
||||
"web-vitals": "^2.1.4",
|
||||
"zustand": "4.4.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"framer-motion": ">=10",
|
||||
"@emotion/react": ">=11",
|
||||
"@mdi/js": ">=7.2.96",
|
||||
"@mdi/react": ">=1.6.1",
|
||||
"@theme-ui/components": ">=0.16.0",
|
||||
"@theme-ui/core": ">=0.16.0",
|
||||
"tinycolor2": ">=1.6",
|
||||
"zustand": ">=4",
|
||||
"react-modal": ">=3",
|
||||
"framer-motion": ">=10",
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
"react-dom": ">=18",
|
||||
"react-modal": ">=3",
|
||||
"tinycolor2": ">=1.6",
|
||||
"zustand": ">=4"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
|
||||
@@ -28,3 +28,24 @@ index 38c9884..4a9e10c 100644
|
||||
};
|
||||
|
||||
const liftListItem = typeOrName => ({ state, dispatch }) => {
|
||||
diff --git a/node_modules/@tiptap/core/dist/packages/core/src/Editor.d.ts b/node_modules/@tiptap/core/dist/packages/core/src/Editor.d.ts
|
||||
index cb0ede6..8a19ff2 100644
|
||||
--- a/node_modules/@tiptap/core/dist/packages/core/src/Editor.d.ts
|
||||
+++ b/node_modules/@tiptap/core/dist/packages/core/src/Editor.d.ts
|
||||
@@ -6,6 +6,7 @@ import { ExtensionManager } from './ExtensionManager.js';
|
||||
import * as extensions from './extensions/index.js';
|
||||
import { CanCommands, ChainedCommands, EditorEvents, EditorOptions, JSONContent, SingleCommands, TextSerializer } from './types.js';
|
||||
export { extensions };
|
||||
+export interface EditorStorage extends Record<string, any> { }
|
||||
export interface HTMLElement {
|
||||
editor?: Editor;
|
||||
}
|
||||
@@ -22,7 +23,7 @@ export declare class Editor extends EventEmitter<EditorEvents> {
|
||||
/**
|
||||
* Returns the editor storage.
|
||||
*/
|
||||
- get storage(): Record<string, any>;
|
||||
+ get storage(): EditorStorage;
|
||||
/**
|
||||
* An object of all registered commands.
|
||||
*/
|
||||
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Node, mergeAttributes, findChildren, Editor } from "@tiptap/core";
|
||||
import { Node, mergeAttributes, findChildren } from "@tiptap/core";
|
||||
import { Attribute } from "@tiptap/core";
|
||||
import { createSelectionBasedNodeView } from "../react";
|
||||
import { AttachmentComponent } from "./component";
|
||||
@@ -27,19 +27,13 @@ export type AttachmentType = "image" | "file" | "camera";
|
||||
export interface AttachmentOptions {
|
||||
types: string[];
|
||||
HTMLAttributes: Record<string, unknown>;
|
||||
onDownloadAttachment: (editor: Editor, attachment: Attachment) => boolean;
|
||||
onOpenAttachmentPicker: (editor: Editor, type: AttachmentType) => boolean;
|
||||
onPreviewAttachment: (editor: Editor, attachment: Attachment) => boolean;
|
||||
}
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
attachment: {
|
||||
openAttachmentPicker: (type: AttachmentType) => ReturnType;
|
||||
insertAttachment: (attachment: Attachment) => ReturnType;
|
||||
removeAttachment: () => ReturnType;
|
||||
downloadAttachment: (attachment: Attachment) => ReturnType;
|
||||
previewAttachment: (options: Attachment) => ReturnType;
|
||||
updateAttachment: (
|
||||
attachment: Partial<Attachment>,
|
||||
options: {
|
||||
@@ -62,10 +56,7 @@ export const AttachmentNode = Node.create<AttachmentOptions>({
|
||||
addOptions() {
|
||||
return {
|
||||
types: [this.name],
|
||||
HTMLAttributes: {},
|
||||
onDownloadAttachment: () => false,
|
||||
onOpenAttachmentPicker: () => false,
|
||||
onPreviewAttachment: () => false
|
||||
HTMLAttributes: {}
|
||||
};
|
||||
},
|
||||
|
||||
@@ -139,16 +130,6 @@ export const AttachmentNode = Node.create<AttachmentOptions>({
|
||||
({ commands }) => {
|
||||
return commands.deleteSelection();
|
||||
},
|
||||
downloadAttachment:
|
||||
(attachment) =>
|
||||
({ editor }) => {
|
||||
return this.options.onDownloadAttachment(editor, attachment);
|
||||
},
|
||||
openAttachmentPicker:
|
||||
(type: AttachmentType) =>
|
||||
({ editor }) => {
|
||||
return this.options.onOpenAttachmentPicker(editor, type);
|
||||
},
|
||||
updateAttachment:
|
||||
(attachment, options) =>
|
||||
({ state, tr, dispatch }) => {
|
||||
@@ -174,20 +155,14 @@ export const AttachmentNode = Node.create<AttachmentOptions>({
|
||||
tr.setMeta("addToHistory", false);
|
||||
if (dispatch) dispatch(tr);
|
||||
return true;
|
||||
},
|
||||
|
||||
previewAttachment:
|
||||
(attachment) =>
|
||||
({ editor }) => {
|
||||
if (!this.options.onPreviewAttachment) return false;
|
||||
return this.options.onPreviewAttachment(editor, attachment);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
"Mod-Shift-A": () => this.editor.commands.openAttachmentPicker("file")
|
||||
"Mod-Shift-A": () =>
|
||||
this.editor.storage.openAttachmentPicker?.("file") || true
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -17,23 +17,35 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Node, NodeWithPos } from "@tiptap/core";
|
||||
import { Extension, NodeWithPos } from "@tiptap/core";
|
||||
import { Plugin } from "@tiptap/pm/state";
|
||||
import { nanoid } from "nanoid";
|
||||
import { getChangedNodes } from "../../utils/prosemirror";
|
||||
|
||||
const types: { [name: string]: boolean } = {
|
||||
heading: true,
|
||||
paragraph: true
|
||||
};
|
||||
const BLOCK_ID_TYPES = [
|
||||
"paragraph",
|
||||
"heading",
|
||||
"blockquote",
|
||||
"bulletList",
|
||||
"orderedList",
|
||||
"taskItem",
|
||||
"taskList",
|
||||
"table",
|
||||
"codeblock",
|
||||
"image",
|
||||
"outlineList",
|
||||
"mathBlock",
|
||||
"webclip",
|
||||
"embed"
|
||||
];
|
||||
|
||||
export const BlockId = Node.create({
|
||||
export const BlockId = Extension.create({
|
||||
name: "blockId",
|
||||
|
||||
addGlobalAttributes() {
|
||||
return [
|
||||
{
|
||||
types: Object.keys(types),
|
||||
types: BLOCK_ID_TYPES,
|
||||
attributes: {
|
||||
blockId: {
|
||||
default: null,
|
||||
@@ -52,44 +64,37 @@ export const BlockId = Node.create({
|
||||
}
|
||||
];
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
appendTransaction: (transactions, oldState, newState) => {
|
||||
// no changes
|
||||
if (newState.doc === oldState.doc) {
|
||||
return;
|
||||
}
|
||||
const tr = newState.tr;
|
||||
const isDocChanged = transactions.some((tr) => tr.docChanged);
|
||||
if (!isDocChanged) return null;
|
||||
|
||||
const blockIds = new Set<string>();
|
||||
const blocksWithoutBlockId: NodeWithPos[] = [];
|
||||
|
||||
for (const tr of transactions) {
|
||||
blocksWithoutBlockId.push(
|
||||
...getChangedNodes(tr, {
|
||||
descend: false,
|
||||
predicate: (n) => {
|
||||
const shouldInclude =
|
||||
n.isBlock &&
|
||||
(!n.attrs.blockId || blockIds.has(n.attrs.blockId));
|
||||
|
||||
if (n.attrs.blockId) blockIds.add(n.attrs.blockId);
|
||||
return shouldInclude;
|
||||
}
|
||||
})
|
||||
);
|
||||
newState.tr.doc.forEach((n, offset) => {
|
||||
if (
|
||||
n.isBlock &&
|
||||
BLOCK_ID_TYPES.includes(n.type.name) &&
|
||||
!n.attrs.blockId
|
||||
)
|
||||
blocksWithoutBlockId.push({ node: n, pos: offset });
|
||||
});
|
||||
if (blocksWithoutBlockId.length > 0) {
|
||||
console.log(blocksWithoutBlockId);
|
||||
const { tr } = newState;
|
||||
for (const { node, pos } of blocksWithoutBlockId) {
|
||||
const id = nanoid(8);
|
||||
tr.setNodeMarkup(pos, undefined, {
|
||||
...node.attrs,
|
||||
blockId: id
|
||||
});
|
||||
}
|
||||
return tr;
|
||||
}
|
||||
|
||||
console.log(blocksWithoutBlockId);
|
||||
for (const { node, pos } of blocksWithoutBlockId) {
|
||||
const id = nanoid(8);
|
||||
tr.setNodeMarkup(pos, undefined, {
|
||||
...node.attrs,
|
||||
blockId: id
|
||||
});
|
||||
}
|
||||
return tr;
|
||||
return null;
|
||||
}
|
||||
})
|
||||
];
|
||||
@@ -1,5 +1,24 @@
|
||||
import { BlockId } from "./blockid";
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
export * from "./blockid";
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { BlockId } from "./block-id";
|
||||
|
||||
export * from "./block-id";
|
||||
|
||||
export default BlockId;
|
||||
|
||||
@@ -27,36 +27,9 @@ import { clipboardTextParser } from "./clipboard-text-parser";
|
||||
import { clipboardTextSerializer } from "./clipboard-text-serializer";
|
||||
import { EditorView } from "prosemirror-view";
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
clipboard: {
|
||||
copyToClipboard: (text: string) => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export type ClipboardOptions = {
|
||||
copyToClipboard: (text: string) => void;
|
||||
};
|
||||
|
||||
export const Clipboard = Extension.create({
|
||||
name: "clipboard",
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
copyToClipboard: () => {}
|
||||
};
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
copyToClipboard: (text: string) => (props) => {
|
||||
this.options.copyToClipboard(text);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
|
||||
@@ -163,7 +163,7 @@ export function CodeblockComponent(
|
||||
bg: "transparent"
|
||||
}}
|
||||
onClick={() => {
|
||||
editor.commands.copyToClipboard(node.textContent);
|
||||
editor.storage.copyToClipboard?.(node.textContent);
|
||||
start();
|
||||
}}
|
||||
title="Copy to clipboard"
|
||||
|
||||
@@ -66,7 +66,7 @@ export function ImageComponent(
|
||||
if (!align) align = textDirection ? "right" : "left";
|
||||
|
||||
const downloadOptions = useToolbarStore((store) => store.downloadOptions);
|
||||
const isReadonly = !editor.current?.isEditable;
|
||||
const isReadonly = !editor.isEditable;
|
||||
const isSVG = !!mime && mime.includes("/svg");
|
||||
|
||||
const { height, width } = clampSize(
|
||||
@@ -79,7 +79,7 @@ export function ImageComponent(
|
||||
if (!inView) return;
|
||||
if (src || !hash || bloburl) return;
|
||||
(async function () {
|
||||
const data = await editor.current?.storage
|
||||
const data = await editor.storage
|
||||
.getAttachmentData?.(node.attrs)
|
||||
.catch(() => null);
|
||||
if (typeof data !== "string" || !data) return; // TODO: show error
|
||||
@@ -271,7 +271,7 @@ export function ImageComponent(
|
||||
onDoubleClick={() => {
|
||||
const { hash, filename, mime, size } = node.attrs;
|
||||
if (!!hash && !!filename && !!mime && !!size)
|
||||
editor.current?.commands.previewAttachment({
|
||||
editor.storage.previewAttachment?.({
|
||||
type: "image",
|
||||
hash,
|
||||
filename,
|
||||
|
||||
@@ -199,7 +199,8 @@ export const ImageNode = Node.create<ImageOptions>({
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
"Mod-Shift-I": () => this.editor.commands.openAttachmentPicker("image")
|
||||
"Mod-Shift-I": () =>
|
||||
this.editor.storage.openAttachmentPicker?.("image") || true
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
167
packages/editor/src/extensions/link/helpers/autolink.ts
Normal file
167
packages/editor/src/extensions/link/helpers/autolink.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
combineTransactionSteps,
|
||||
findChildrenInRange,
|
||||
getChangedRanges,
|
||||
getMarksBetween,
|
||||
NodeWithPos
|
||||
} from "@tiptap/core";
|
||||
import { MarkType } from "@tiptap/pm/model";
|
||||
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
import { find } from "linkifyjs";
|
||||
|
||||
type AutolinkOptions = {
|
||||
type: MarkType;
|
||||
validate?: (url: string) => boolean;
|
||||
};
|
||||
|
||||
export function autolink(options: AutolinkOptions): Plugin {
|
||||
return new Plugin({
|
||||
key: new PluginKey("autolink"),
|
||||
appendTransaction: (transactions, oldState, newState) => {
|
||||
const docChanges =
|
||||
transactions.some((transaction) => transaction.docChanged) &&
|
||||
!oldState.doc.eq(newState.doc);
|
||||
const preventAutolink = transactions.some((transaction) =>
|
||||
transaction.getMeta("preventAutolink")
|
||||
);
|
||||
|
||||
if (!docChanges || preventAutolink) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { tr } = newState;
|
||||
const transform = combineTransactionSteps(oldState.doc, [
|
||||
...transactions
|
||||
]);
|
||||
const changes = getChangedRanges(transform);
|
||||
|
||||
changes.forEach(({ newRange }) => {
|
||||
// Now let’s see if we can add new links.
|
||||
const nodesInChangedRanges = findChildrenInRange(
|
||||
newState.doc,
|
||||
newRange,
|
||||
(node) => node.isTextblock
|
||||
);
|
||||
|
||||
let textBlock: NodeWithPos | undefined;
|
||||
let textBeforeWhitespace: string | undefined;
|
||||
|
||||
if (nodesInChangedRanges.length > 1) {
|
||||
// Grab the first node within the changed ranges (ex. the first of two paragraphs when hitting enter).
|
||||
textBlock = nodesInChangedRanges[0];
|
||||
textBeforeWhitespace = newState.doc.textBetween(
|
||||
textBlock.pos,
|
||||
textBlock.pos + textBlock.node.nodeSize,
|
||||
undefined,
|
||||
" "
|
||||
);
|
||||
} else if (
|
||||
nodesInChangedRanges.length &&
|
||||
// We want to make sure to include the block seperator argument to treat hard breaks like spaces.
|
||||
newState.doc
|
||||
.textBetween(newRange.from, newRange.to, " ", " ")
|
||||
.endsWith(" ")
|
||||
) {
|
||||
textBlock = nodesInChangedRanges[0];
|
||||
textBeforeWhitespace = newState.doc.textBetween(
|
||||
textBlock.pos,
|
||||
newRange.to,
|
||||
undefined,
|
||||
" "
|
||||
);
|
||||
}
|
||||
|
||||
if (textBlock && textBeforeWhitespace) {
|
||||
const wordsBeforeWhitespace = textBeforeWhitespace
|
||||
.split(" ")
|
||||
.filter((s) => s !== "");
|
||||
|
||||
if (wordsBeforeWhitespace.length <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lastWordBeforeSpace =
|
||||
wordsBeforeWhitespace[wordsBeforeWhitespace.length - 1];
|
||||
const lastWordAndBlockOffset =
|
||||
textBlock.pos +
|
||||
textBeforeWhitespace.lastIndexOf(lastWordBeforeSpace);
|
||||
|
||||
if (!lastWordBeforeSpace) {
|
||||
return false;
|
||||
}
|
||||
|
||||
find(lastWordBeforeSpace)
|
||||
.filter((link) => link.isLink)
|
||||
// Calculate link position.
|
||||
.map((link) => ({
|
||||
...link,
|
||||
from: lastWordAndBlockOffset + link.start + 1,
|
||||
to: lastWordAndBlockOffset + link.end + 1
|
||||
}))
|
||||
// ignore link inside code mark
|
||||
.filter((link) => {
|
||||
if (!newState.schema.marks.code) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !newState.doc.rangeHasMark(
|
||||
link.from,
|
||||
link.to,
|
||||
newState.schema.marks.code
|
||||
);
|
||||
})
|
||||
// validate link
|
||||
.filter((link) => {
|
||||
if (options.validate) {
|
||||
return options.validate(link.value);
|
||||
}
|
||||
return true;
|
||||
})
|
||||
// Add link mark.
|
||||
.forEach((link) => {
|
||||
if (
|
||||
getMarksBetween(link.from, link.to, newState.doc).some(
|
||||
(item) => item.mark.type === options.type
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
tr.addMark(
|
||||
link.from,
|
||||
link.to,
|
||||
options.type.create({
|
||||
href: link.href
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (!tr.steps.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
return tr;
|
||||
}
|
||||
});
|
||||
}
|
||||
61
packages/editor/src/extensions/link/helpers/clickHandler.ts
Normal file
61
packages/editor/src/extensions/link/helpers/clickHandler.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { getAttributes } from "@tiptap/core";
|
||||
import { MarkType } from "@tiptap/pm/model";
|
||||
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
|
||||
type ClickHandlerOptions = {
|
||||
type: MarkType;
|
||||
};
|
||||
|
||||
export function clickHandler(options: ClickHandlerOptions): Plugin {
|
||||
return new Plugin({
|
||||
key: new PluginKey("handleClickLink"),
|
||||
props: {
|
||||
handleClick: (view, pos, event) => {
|
||||
if (event.button !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const eventTarget = event.target as HTMLElement;
|
||||
|
||||
if (eventTarget.nodeName !== "A") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const attrs = getAttributes(view.state, options.type.name);
|
||||
const link = event.target as HTMLLinkElement;
|
||||
|
||||
const href = link?.href ?? attrs.href;
|
||||
const target = link?.target ?? attrs.target;
|
||||
|
||||
if (link && href) {
|
||||
if (view.editable) {
|
||||
window.open(href, target);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
65
packages/editor/src/extensions/link/helpers/pasteHandler.ts
Normal file
65
packages/editor/src/extensions/link/helpers/pasteHandler.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { MarkType } from "@tiptap/pm/model";
|
||||
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
import { find } from "linkifyjs";
|
||||
|
||||
type PasteHandlerOptions = {
|
||||
editor: Editor;
|
||||
type: MarkType;
|
||||
};
|
||||
|
||||
export function pasteHandler(options: PasteHandlerOptions): Plugin {
|
||||
return new Plugin({
|
||||
key: new PluginKey("handlePasteLink"),
|
||||
props: {
|
||||
handlePaste: (view, event, slice) => {
|
||||
const { state } = view;
|
||||
const { selection } = state;
|
||||
const { empty } = selection;
|
||||
|
||||
if (empty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let textContent = "";
|
||||
|
||||
slice.content.forEach((node) => {
|
||||
textContent += node.textContent;
|
||||
});
|
||||
|
||||
const link = find(textContent).find(
|
||||
(item) => item.isLink && item.value === textContent
|
||||
);
|
||||
|
||||
if (!textContent || !link) {
|
||||
return false;
|
||||
}
|
||||
|
||||
options.editor.commands.setMark(options.type, {
|
||||
href: link.href
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -16,13 +16,177 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { markInputRule, markPasteRule } from "@tiptap/core";
|
||||
import TiptapLink from "@tiptap/extension-link";
|
||||
|
||||
import {
|
||||
Command,
|
||||
Mark,
|
||||
getMarkType,
|
||||
isMarkActive,
|
||||
markInputRule,
|
||||
markPasteRule,
|
||||
mergeAttributes
|
||||
} from "@tiptap/core";
|
||||
import { Plugin, TextSelection } from "@tiptap/pm/state";
|
||||
import { find, registerCustomProtocol, reset } from "linkifyjs";
|
||||
|
||||
import { autolink } from "./helpers/autolink";
|
||||
import { clickHandler } from "./helpers/clickHandler";
|
||||
import { pasteHandler } from "./helpers/pasteHandler";
|
||||
import { ImageNode } from "../image";
|
||||
import { selectionToOffset } from "../../utils/prosemirror";
|
||||
|
||||
export interface LinkProtocolOptions {
|
||||
scheme: string;
|
||||
optionalSlashes?: boolean;
|
||||
}
|
||||
|
||||
export interface LinkOptions {
|
||||
/**
|
||||
* If enabled, it adds links as you type.
|
||||
*/
|
||||
autolink: boolean;
|
||||
/**
|
||||
* An array of custom protocols to be registered with linkifyjs.
|
||||
*/
|
||||
protocols: Array<LinkProtocolOptions | string>;
|
||||
/**
|
||||
* If enabled, links will be opened on click.
|
||||
*/
|
||||
openOnClick: boolean;
|
||||
/**
|
||||
* Adds a link to the current selection if the pasted content only contains an url.
|
||||
*/
|
||||
linkOnPaste: boolean;
|
||||
/**
|
||||
* A list of HTML attributes to be rendered.
|
||||
*/
|
||||
HTMLAttributes: Record<string, any>;
|
||||
/**
|
||||
* A validation function that modifies link verification for the auto linker.
|
||||
* @param url - The url to be validated.
|
||||
* @returns - True if the url is valid, false otherwise.
|
||||
*/
|
||||
validate?: (url: string) => boolean;
|
||||
}
|
||||
|
||||
export type LinkAttributes = {
|
||||
href: string;
|
||||
target?: string | null;
|
||||
rel?: string | null;
|
||||
class?: string | null;
|
||||
title?: string | null;
|
||||
};
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
link: {
|
||||
/**
|
||||
* Set a link mark
|
||||
*/
|
||||
setLink: (attributes: LinkAttributes) => ReturnType;
|
||||
/**
|
||||
* Toggle a link mark
|
||||
*/
|
||||
toggleLink: (attributes: LinkAttributes) => ReturnType;
|
||||
/**
|
||||
* Unset a link mark
|
||||
*/
|
||||
unsetLink: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const linkRegex = /(?:__|[*#])|\[(.*?)\]\(.*?\)/gm;
|
||||
const regExp = /(?:__|[*#])|\[.*?\]\((.*?)\)/gm;
|
||||
export const Link = Mark.create<LinkOptions>({
|
||||
name: "link",
|
||||
|
||||
priority: 1000,
|
||||
|
||||
keepOnSplit: false,
|
||||
|
||||
onCreate() {
|
||||
this.options.protocols.forEach((protocol) => {
|
||||
if (typeof protocol === "string") {
|
||||
registerCustomProtocol(protocol);
|
||||
return;
|
||||
}
|
||||
registerCustomProtocol(protocol.scheme, protocol.optionalSlashes);
|
||||
});
|
||||
},
|
||||
|
||||
onDestroy() {
|
||||
reset();
|
||||
},
|
||||
|
||||
inclusive() {
|
||||
return this.options.autolink;
|
||||
},
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
openOnClick: true,
|
||||
linkOnPaste: true,
|
||||
autolink: true,
|
||||
protocols: [],
|
||||
HTMLAttributes: {
|
||||
target: "_blank",
|
||||
rel: "noopener noreferrer nofollow",
|
||||
class: null,
|
||||
title: null
|
||||
},
|
||||
validate: undefined
|
||||
};
|
||||
},
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
href: {
|
||||
default: null
|
||||
},
|
||||
target: {
|
||||
default: this.options.HTMLAttributes.target
|
||||
},
|
||||
rel: {
|
||||
default: this.options.HTMLAttributes.rel
|
||||
},
|
||||
class: {
|
||||
default: this.options.HTMLAttributes.class
|
||||
},
|
||||
title: {
|
||||
default: this.options.HTMLAttributes.title
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [{ tag: 'a[href]:not([href *= "javascript:" i])' }];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return [
|
||||
"a",
|
||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
||||
0
|
||||
];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setLink: (attributes) => insertLink(attributes, false),
|
||||
|
||||
toggleLink: (attributes) => insertLink(attributes, true),
|
||||
|
||||
unsetLink:
|
||||
() =>
|
||||
({ chain }) => {
|
||||
return chain()
|
||||
.unsetMark(this.name, { extendEmptyMarkRange: true })
|
||||
.setMeta("preventAutolink", true)
|
||||
.run();
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
export const Link = TiptapLink.extend({
|
||||
addInputRules() {
|
||||
return [
|
||||
...(this.parent?.() || []),
|
||||
@@ -37,6 +201,7 @@ export const Link = TiptapLink.extend({
|
||||
})
|
||||
];
|
||||
},
|
||||
|
||||
addPasteRules() {
|
||||
return [
|
||||
...(this.parent?.() || []),
|
||||
@@ -48,7 +213,151 @@ export const Link = TiptapLink.extend({
|
||||
href: regExp.exec(match[0])?.[1]
|
||||
};
|
||||
}
|
||||
}),
|
||||
markPasteRule({
|
||||
find: (text) =>
|
||||
find(text)
|
||||
.filter((link) => {
|
||||
if (this.options.validate) {
|
||||
return this.options.validate(link.value);
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.filter((link) => link.isLink)
|
||||
.map((link) => ({
|
||||
text: link.value,
|
||||
index: link.start,
|
||||
data: link
|
||||
})),
|
||||
type: this.type,
|
||||
getAttributes: (match, pasteEvent) => {
|
||||
const html = pasteEvent?.clipboardData?.getData("text/html");
|
||||
const hrefRegex = /href="([^"]*)"/;
|
||||
|
||||
const existingLink = html?.match(hrefRegex);
|
||||
|
||||
if (existingLink) {
|
||||
return {
|
||||
href: existingLink[1]
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
href: match.data?.href
|
||||
};
|
||||
}
|
||||
})
|
||||
];
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
const plugins: Plugin[] = [];
|
||||
|
||||
if (this.options.autolink) {
|
||||
plugins.push(
|
||||
autolink({
|
||||
type: this.type,
|
||||
validate: this.options.validate
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (this.options.openOnClick) {
|
||||
plugins.push(
|
||||
clickHandler({
|
||||
type: this.type
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (this.options.linkOnPaste) {
|
||||
plugins.push(
|
||||
pasteHandler({
|
||||
editor: this.editor,
|
||||
type: this.type
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}
|
||||
});
|
||||
|
||||
const insertLink: (attributes: LinkAttributes, toggle?: boolean) => Command =
|
||||
(attributes, toggle) =>
|
||||
({ chain, editor }) => {
|
||||
let commandChain = chain();
|
||||
|
||||
const offset = selectionToOffset(editor.state);
|
||||
if (!offset) return false;
|
||||
const { from, to, node } = offset;
|
||||
|
||||
const isSelection = !editor.state.selection.empty;
|
||||
|
||||
const isEditing = isMarkActive(editor.state, Link.name);
|
||||
const isImage = node?.type.name === ImageNode.name;
|
||||
|
||||
if (isEditing && node) {
|
||||
if (!isImage) {
|
||||
const markType = getMarkType(Link.name, editor.schema);
|
||||
commandChain = commandChain.command(({ tr }) => {
|
||||
tr.removeMark(from, to, markType);
|
||||
tr.insertText(
|
||||
attributes.title || node.textContent,
|
||||
tr.mapping.map(from),
|
||||
tr.mapping.map(to)
|
||||
);
|
||||
tr.setSelection(
|
||||
TextSelection.create(
|
||||
tr.doc,
|
||||
tr.mapping.map(from),
|
||||
tr.mapping.map(to)
|
||||
)
|
||||
);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
return commandChain
|
||||
.extendMarkRange("link")
|
||||
.setMark(Link.name, attributes)
|
||||
.command(({ tr }) => {
|
||||
tr.setSelection(
|
||||
TextSelection.create(
|
||||
tr.doc,
|
||||
tr.mapping.map(editor.state.selection.from),
|
||||
tr.mapping.map(editor.state.selection.to)
|
||||
)
|
||||
);
|
||||
return true;
|
||||
})
|
||||
.focus(undefined, { scrollIntoView: true })
|
||||
.run();
|
||||
}
|
||||
|
||||
commandChain = toggle
|
||||
? commandChain.toggleMark(Link.name, attributes, {
|
||||
extendEmptyMarkRange: true
|
||||
})
|
||||
: commandChain.extendMarkRange(Link.name).setMark(Link.name, attributes);
|
||||
|
||||
if (!isImage)
|
||||
commandChain = commandChain.insertContent(
|
||||
attributes.title || attributes.href
|
||||
);
|
||||
|
||||
if (!isSelection && !isImage) {
|
||||
commandChain = commandChain.command(({ tr }) => {
|
||||
tr.insertText(" ", tr.mapping.map(to));
|
||||
tr.removeMark(
|
||||
tr.mapping.map(to) - 1,
|
||||
tr.mapping.map(to),
|
||||
getMarkType(Link.name, editor.schema)
|
||||
);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
return commandChain.focus().setMeta("preventAutolink", true).run();
|
||||
};
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export * from "./open-link";
|
||||
export { OpenLink as default } from "./open-link";
|
||||
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Extension } from "@tiptap/core";
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
openlink: {
|
||||
/**
|
||||
* Open a link in browser
|
||||
*/
|
||||
openLink: (url: string) => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export type OpenLinkOptions = {
|
||||
onOpenLink: (url: string) => boolean;
|
||||
};
|
||||
|
||||
export const OpenLink = Extension.create<OpenLinkOptions>({
|
||||
name: "openlink",
|
||||
addOptions() {
|
||||
return {
|
||||
onOpenLink: () => false
|
||||
};
|
||||
},
|
||||
addCommands() {
|
||||
return {
|
||||
openLink: (url: string) => () => {
|
||||
return this.options.onOpenLink(url);
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -50,16 +50,13 @@ export class ReactNodeView<P extends ReactNodeViewProps> implements NodeView {
|
||||
contentDOM: HTMLElement | undefined;
|
||||
node: PMNode;
|
||||
isDragging = false;
|
||||
portalProviderAPI: PortalProviderAPI;
|
||||
|
||||
constructor(
|
||||
node: PMNode,
|
||||
protected readonly editor: Editor,
|
||||
protected readonly getPos: GetPosNode,
|
||||
protected readonly portalProviderAPI: PortalProviderAPI,
|
||||
protected readonly options: ReactNodeViewOptions<P>
|
||||
) {
|
||||
this.portalProviderAPI = editor.storage
|
||||
.portalProviderAPI as PortalProviderAPI;
|
||||
this.node = node;
|
||||
}
|
||||
|
||||
@@ -482,11 +479,18 @@ export function createNodeView<TProps extends ReactNodeViewProps>(
|
||||
) {
|
||||
return ({ node, getPos, editor }: NodeViewRendererProps) => {
|
||||
const _getPos = () => (typeof getPos === "boolean" ? -1 : getPos());
|
||||
if (!editor.storage.portalProviderAPI) return {};
|
||||
|
||||
return new ReactNodeView<TProps>(node, editor as Editor, _getPos, {
|
||||
...options,
|
||||
component
|
||||
}).init();
|
||||
return new ReactNodeView<TProps>(
|
||||
node,
|
||||
editor as Editor,
|
||||
_getPos,
|
||||
editor.storage.portalProviderAPI,
|
||||
{
|
||||
...options,
|
||||
component
|
||||
}
|
||||
).init();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -18,12 +18,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { PropsWithChildren, useContext } from "react";
|
||||
import {
|
||||
createPortal,
|
||||
unstable_renderSubtreeIntoContainer,
|
||||
unmountComponentAtNode
|
||||
} from "react-dom";
|
||||
import { createPortal } from "react-dom";
|
||||
import { EventDispatcher } from "./event-dispatcher";
|
||||
import { Root, createRoot } from "react-dom/client";
|
||||
|
||||
export type BasePortalProviderProps = PropsWithChildren<unknown>;
|
||||
|
||||
@@ -33,12 +30,8 @@ export type PortalRendererState = {
|
||||
portals: Portals;
|
||||
};
|
||||
|
||||
type MountedPortal = {
|
||||
children: () => React.ReactChild | null;
|
||||
};
|
||||
|
||||
export class PortalProviderAPI extends EventDispatcher {
|
||||
portals: Map<HTMLElement, MountedPortal> = new Map();
|
||||
portals: Map<HTMLElement, Root> = new Map();
|
||||
context?: PortalRenderer;
|
||||
|
||||
constructor() {
|
||||
@@ -56,17 +49,17 @@ export class PortalProviderAPI extends EventDispatcher {
|
||||
) {
|
||||
if (!this.context) return;
|
||||
|
||||
this.portals.set(container, {
|
||||
children
|
||||
});
|
||||
const wrappedChildren = children() as JSX.Element;
|
||||
// const wrappedChildren = children() as JSX.Element;
|
||||
|
||||
unstable_renderSubtreeIntoContainer(
|
||||
this.context,
|
||||
wrappedChildren,
|
||||
container,
|
||||
callback
|
||||
);
|
||||
// unstable_renderSubtreeIntoContainer(
|
||||
// this.context,
|
||||
// wrappedChildren,
|
||||
// container,
|
||||
// callback
|
||||
// );
|
||||
const root = this.portals.get(container) || createRoot(container);
|
||||
root.render(children());
|
||||
this.portals.set(container, root);
|
||||
}
|
||||
|
||||
// TODO: until https://product-fabric.atlassian.net/browse/ED-5013
|
||||
@@ -75,6 +68,7 @@ export class PortalProviderAPI extends EventDispatcher {
|
||||
forceUpdate() {}
|
||||
|
||||
remove(container: HTMLElement) {
|
||||
const root = this.portals.get(container);
|
||||
this.portals.delete(container);
|
||||
|
||||
// There is a race condition that can happen caused by Prosemirror vs React,
|
||||
@@ -84,7 +78,8 @@ export class PortalProviderAPI extends EventDispatcher {
|
||||
// Both Prosemirror and React remove the elements asynchronously, and in edge
|
||||
// cases Prosemirror beats React
|
||||
try {
|
||||
unmountComponentAtNode(container);
|
||||
root?.unmount();
|
||||
// unmountComponentAtNode(container);
|
||||
} catch (error) {
|
||||
// IGNORE console.error(error);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
import { ReactNodeView } from "./react-node-view";
|
||||
import { Editor, NodeViewRendererProps } from "@tiptap/core";
|
||||
import { EmotionThemeProvider } from "@notesnook/theme";
|
||||
import { PortalProviderAPI } from "./react-portal-provider";
|
||||
|
||||
/**
|
||||
* A ReactNodeView that handles React components sensitive
|
||||
@@ -72,9 +73,10 @@ export class SelectionBasedNodeView<
|
||||
node: PMNode,
|
||||
editor: Editor,
|
||||
getPos: GetPosNode,
|
||||
portalProviderAPI: PortalProviderAPI,
|
||||
options: ReactNodeViewOptions<P>
|
||||
) {
|
||||
super(node, editor, getPos, options);
|
||||
super(node, editor, getPos, portalProviderAPI, options);
|
||||
|
||||
this.updatePos();
|
||||
|
||||
@@ -277,9 +279,16 @@ export function createSelectionBasedNodeView<
|
||||
) {
|
||||
return ({ node, getPos, editor }: NodeViewRendererProps) => {
|
||||
const _getPos = () => (typeof getPos === "boolean" ? -1 : getPos());
|
||||
return new SelectionBasedNodeView(node, editor as Editor, _getPos, {
|
||||
...options,
|
||||
component
|
||||
}).init();
|
||||
if (!editor.storage.portalProviderAPI) return {};
|
||||
return new SelectionBasedNodeView(
|
||||
node,
|
||||
editor as Editor,
|
||||
_getPos,
|
||||
editor.storage.portalProviderAPI,
|
||||
{
|
||||
...options,
|
||||
component
|
||||
}
|
||||
).init();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -82,6 +82,8 @@ export function TableComponent(props: ReactNodeViewProps) {
|
||||
}
|
||||
|
||||
export function TableNodeView(editor: TiptapEditor) {
|
||||
if (!editor.storage.portalProviderAPI) return;
|
||||
const api = editor.storage.portalProviderAPI;
|
||||
class TableNode
|
||||
extends ReactNodeView<ReactNodeViewProps<unknown>>
|
||||
implements NodeView
|
||||
@@ -91,6 +93,7 @@ export function TableNodeView(editor: TiptapEditor) {
|
||||
node,
|
||||
editor,
|
||||
() => 0, // todo
|
||||
api,
|
||||
{
|
||||
component: TableComponent,
|
||||
shouldUpdate: (prev, next) => {
|
||||
@@ -161,9 +164,9 @@ function TableRowToolbar(props: TableToolbarProps) {
|
||||
}
|
||||
}
|
||||
|
||||
editor.current?.on("selectionUpdate", onSelectionUpdate);
|
||||
editor.on("selectionUpdate", onSelectionUpdate);
|
||||
return () => {
|
||||
editor.current?.off("selectionUpdate", onSelectionUpdate);
|
||||
editor.off("selectionUpdate", onSelectionUpdate);
|
||||
};
|
||||
}, [textDirection]);
|
||||
|
||||
@@ -236,9 +239,9 @@ function TableColumnToolbar(props: TableToolbarProps) {
|
||||
columnToolsRef.current.style.top = `${pos.top}px`;
|
||||
}
|
||||
|
||||
editor.current?.on("selectionUpdate", onSelectionUpdate);
|
||||
editor.on("selectionUpdate", onSelectionUpdate);
|
||||
return () => {
|
||||
editor.current?.off("selectionUpdate", onSelectionUpdate);
|
||||
editor.off("selectionUpdate", onSelectionUpdate);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -36,16 +36,16 @@ export function TaskItemComponent(
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
if (!editor.isEditable || !editor.current) return false;
|
||||
if (!editor.isEditable || !editor) return false;
|
||||
|
||||
const { empty, from, to } = editor.current.state.selection;
|
||||
const { empty, from, to } = editor.state.selection;
|
||||
const selectedTaskItems = findChildrenInRange(
|
||||
editor.current.state.doc,
|
||||
editor.state.doc,
|
||||
{ from, to },
|
||||
(node) => node.type.name === TaskItemNode.name
|
||||
);
|
||||
if (!empty && selectedTaskItems.findIndex((a) => a.node === node) > -1) {
|
||||
editor.current.commands.command(({ tr }) => {
|
||||
editor.commands.command(({ tr }) => {
|
||||
for (const { pos } of selectedTaskItems) {
|
||||
tr.setNodeMarkup(pos, null, { checked: !checked });
|
||||
}
|
||||
@@ -152,12 +152,12 @@ export function TaskItemComponent(
|
||||
cursor: "pointer"
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!editor.current) return;
|
||||
if (!editor) return;
|
||||
const pos = getPos();
|
||||
|
||||
// we need to get a fresh instance of the task list instead
|
||||
// of using the one we got via props.
|
||||
const node = editor.current.state.doc.nodeAt(pos);
|
||||
const node = editor.state.doc.nodeAt(pos);
|
||||
if (!node) return;
|
||||
|
||||
editor.commands.command(({ tr }) => {
|
||||
|
||||
@@ -97,7 +97,7 @@ export function TaskListComponent(
|
||||
}}
|
||||
onClick={() => {
|
||||
const parentPos = getPos();
|
||||
editor.current?.commands.command(({ tr }) => {
|
||||
editor.commands.command(({ tr }) => {
|
||||
const node = tr.doc.nodeAt(parentPos);
|
||||
if (!node) return false;
|
||||
toggleChildren(tr, node, !checked, parentPos);
|
||||
@@ -125,8 +125,8 @@ export function TaskListComponent(
|
||||
onChange={(e) => {
|
||||
e.target.value = replaceDateTime(
|
||||
e.target.value,
|
||||
editor.current?.storage.dateFormat,
|
||||
editor.current?.storage.timeFormat
|
||||
editor.storage.dateFormat,
|
||||
editor.storage.timeFormat
|
||||
);
|
||||
updateAttributes(
|
||||
{ title: e.target.value },
|
||||
@@ -146,7 +146,7 @@ export function TaskListComponent(
|
||||
}}
|
||||
onClick={() => {
|
||||
const parentPos = getPos();
|
||||
editor.current?.commands.command(({ tr }) => {
|
||||
editor.commands.command(({ tr }) => {
|
||||
const node = tr.doc.nodeAt(parentPos);
|
||||
if (!node) return false;
|
||||
const toggleState = !node.attrs.readonly;
|
||||
@@ -177,7 +177,7 @@ export function TaskListComponent(
|
||||
}}
|
||||
onClick={() => {
|
||||
const pos = getPos();
|
||||
editor.current
|
||||
editor
|
||||
?.chain()
|
||||
.focus()
|
||||
.command(({ tr }) => {
|
||||
@@ -197,7 +197,7 @@ export function TaskListComponent(
|
||||
onClick={() => {
|
||||
const pos = getPos();
|
||||
|
||||
editor.current
|
||||
editor
|
||||
?.chain()
|
||||
.focus()
|
||||
.command(({ tr }) => {
|
||||
|
||||
@@ -37,15 +37,7 @@ export const useEditor = (
|
||||
options: Partial<EditorOptions> = {},
|
||||
deps: DependencyList = []
|
||||
) => {
|
||||
const editor = useMemo<Editor>(() => {
|
||||
const instance = new Editor(options);
|
||||
if (instance && typeof instance.current === "undefined") {
|
||||
Object.defineProperty(instance, "current", {
|
||||
get: () => editorRef.current
|
||||
});
|
||||
}
|
||||
return instance;
|
||||
}, []);
|
||||
const editor = useMemo<Editor>(() => new Editor(options), []);
|
||||
const forceUpdate = useForceUpdate();
|
||||
const editorRef = useRef<TiptapEditor>(editor);
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import CharacterCount from "@tiptap/extension-character-count";
|
||||
import { Code } from "@tiptap/extension-code";
|
||||
import Color from "@tiptap/extension-color";
|
||||
import HorizontalRule from "@tiptap/extension-horizontal-rule";
|
||||
import { Link } from "./extensions/link";
|
||||
import { Link, LinkAttributes } from "./extensions/link";
|
||||
import Placeholder from "@tiptap/extension-placeholder";
|
||||
import Subscript from "@tiptap/extension-subscript";
|
||||
import Superscript from "@tiptap/extension-superscript";
|
||||
@@ -42,7 +42,8 @@ import "./extensions";
|
||||
import {
|
||||
Attachment,
|
||||
AttachmentNode,
|
||||
AttachmentOptions
|
||||
AttachmentOptions,
|
||||
AttachmentType
|
||||
} from "./extensions/attachment";
|
||||
import BulletList from "./extensions/bullet-list";
|
||||
import { CodeBlock } from "./extensions/code-block";
|
||||
@@ -57,13 +58,13 @@ import { KeepInView } from "./extensions/keep-in-view";
|
||||
import { KeyMap } from "./extensions/key-map";
|
||||
import { ListItem } from "./extensions/list-item";
|
||||
import { MathBlock, MathInline } from "./extensions/math";
|
||||
import { OpenLink, OpenLinkOptions } from "./extensions/open-link";
|
||||
import OrderedList from "./extensions/ordered-list";
|
||||
import { OutlineList } from "./extensions/outline-list";
|
||||
import { OutlineListItem } from "./extensions/outline-list-item";
|
||||
import { Paragraph } from "./extensions/paragraph";
|
||||
import {
|
||||
NodeViewSelectionNotifier,
|
||||
PortalProviderAPI,
|
||||
usePortalProvider
|
||||
} from "./extensions/react";
|
||||
import { SearchReplace } from "./extensions/search-replace";
|
||||
@@ -79,7 +80,7 @@ import Toolbar from "./toolbar";
|
||||
import { useToolbarStore } from "./toolbar/stores/toolbar-store";
|
||||
import { DownloadOptions } from "./utils/downloader";
|
||||
import { Heading } from "./extensions/heading";
|
||||
import Clipboard, { ClipboardOptions } from "./extensions/clipboard";
|
||||
import Clipboard from "./extensions/clipboard";
|
||||
import Blockquote from "./extensions/blockquote";
|
||||
import { Quirks } from "./extensions/quirks";
|
||||
import { LIST_NODE_TYPES } from "./utils/node-types";
|
||||
@@ -88,6 +89,25 @@ import CheckListItem from "./extensions/check-list-item";
|
||||
import { Callout } from "./extensions/callout";
|
||||
import BlockId from "./extensions/block-id";
|
||||
|
||||
interface TiptapStorage {
|
||||
portalProviderAPI?: PortalProviderAPI;
|
||||
dateFormat?: DateTimeOptions["dateFormat"];
|
||||
timeFormat?: DateTimeOptions["timeFormat"];
|
||||
openLink?: (url: string) => void;
|
||||
downloadAttachment?: (attachment: Attachment) => void;
|
||||
openAttachmentPicker?: (type: AttachmentType) => void;
|
||||
previewAttachment?: (attachment: Attachment) => void;
|
||||
copyToClipboard?: (text: string, html?: string) => void;
|
||||
createInternalLink?: (
|
||||
attributes?: LinkAttributes
|
||||
) => Promise<LinkAttributes | undefined>;
|
||||
getAttachmentData: (attachment: Attachment) => Promise<string | undefined>;
|
||||
}
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface EditorStorage extends TiptapStorage {}
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var keyboardShown: boolean;
|
||||
@@ -106,16 +126,13 @@ const CoreExtensions = Object.entries(TiptapCoreExtensions)
|
||||
.map(([, extension]) => extension);
|
||||
|
||||
export type TiptapOptions = EditorOptions &
|
||||
Omit<AttachmentOptions, "HTMLAttributes"> &
|
||||
Omit<WebClipOptions, "HTMLAttributes"> &
|
||||
Omit<ImageOptions, "HTMLAttributes"> &
|
||||
DateTimeOptions &
|
||||
ClipboardOptions &
|
||||
OpenLinkOptions & {
|
||||
Omit<TiptapStorage, "portalProviderAPI"> & {
|
||||
downloadOptions?: DownloadOptions;
|
||||
isMobile?: boolean;
|
||||
doubleSpacedLines?: boolean;
|
||||
getAttachmentData: (attachment: Attachment) => Promise<string | undefined>;
|
||||
};
|
||||
|
||||
const useTiptap = (
|
||||
@@ -123,18 +140,20 @@ const useTiptap = (
|
||||
deps: React.DependencyList = []
|
||||
) => {
|
||||
const {
|
||||
doubleSpacedLines = true,
|
||||
isMobile,
|
||||
onDownloadAttachment,
|
||||
onOpenAttachmentPicker,
|
||||
onPreviewAttachment,
|
||||
onOpenLink,
|
||||
getAttachmentData,
|
||||
downloadAttachment,
|
||||
openAttachmentPicker,
|
||||
previewAttachment,
|
||||
openLink,
|
||||
onBeforeCreate,
|
||||
downloadOptions,
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
copyToClipboard,
|
||||
createInternalLink,
|
||||
|
||||
doubleSpacedLines = true,
|
||||
isMobile,
|
||||
downloadOptions,
|
||||
editorProps,
|
||||
...restOptions
|
||||
} = options;
|
||||
@@ -245,9 +264,7 @@ const useTiptap = (
|
||||
allowTableNodeSelection: true,
|
||||
cellMinWidth: 50
|
||||
}),
|
||||
Clipboard.configure({
|
||||
copyToClipboard
|
||||
}),
|
||||
Clipboard,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
@@ -262,15 +279,9 @@ const useTiptap = (
|
||||
Placeholder.configure({
|
||||
placeholder: "Start writing your note..."
|
||||
}),
|
||||
OpenLink.configure({
|
||||
onOpenLink
|
||||
}),
|
||||
ImageNode.configure({ allowBase64: true }),
|
||||
EmbedNode,
|
||||
AttachmentNode.configure({
|
||||
onDownloadAttachment,
|
||||
onOpenAttachmentPicker,
|
||||
onPreviewAttachment,
|
||||
types: [AttachmentNode.name, ImageNode.name, WebClipNode.name]
|
||||
}),
|
||||
OutlineListItem,
|
||||
@@ -333,24 +344,33 @@ const useTiptap = (
|
||||
editor.storage.portalProviderAPI = PortalProviderAPI;
|
||||
editor.storage.dateFormat = dateFormat;
|
||||
editor.storage.timeFormat = timeFormat;
|
||||
|
||||
editor.storage.openLink = openLink;
|
||||
editor.storage.downloadAttachment = downloadAttachment;
|
||||
editor.storage.openAttachmentPicker = openAttachmentPicker;
|
||||
editor.storage.previewAttachment = previewAttachment;
|
||||
editor.storage.copyToClipboard = copyToClipboard;
|
||||
editor.storage.createInternalLink = createInternalLink;
|
||||
editor.storage.getAttachmentData = getAttachmentData;
|
||||
|
||||
if (onBeforeCreate) onBeforeCreate({ editor });
|
||||
},
|
||||
injectCSS: false,
|
||||
parseOptions: { preserveWhitespace: true }
|
||||
}),
|
||||
[
|
||||
onPreviewAttachment,
|
||||
onDownloadAttachment,
|
||||
onOpenAttachmentPicker,
|
||||
previewAttachment,
|
||||
downloadAttachment,
|
||||
openAttachmentPicker,
|
||||
getAttachmentData,
|
||||
PortalProviderAPI,
|
||||
onBeforeCreate,
|
||||
onOpenLink,
|
||||
openLink,
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
editorProps,
|
||||
copyToClipboard
|
||||
copyToClipboard,
|
||||
createInternalLink
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ export type ToolButtonProps = ButtonProps & {
|
||||
icon: IconNames;
|
||||
iconColor?: SchemeColors;
|
||||
iconSize?: keyof Theme["iconSizes"] | number;
|
||||
toggled: boolean;
|
||||
toggled?: boolean;
|
||||
buttonRef?: React.RefObject<HTMLButtonElement>;
|
||||
variant?: ToolButtonVariant;
|
||||
};
|
||||
|
||||
@@ -68,14 +68,14 @@ export function HoverPopupHandler(props: FloatingMenuProps) {
|
||||
hoverTimeoutId.current = setTimeout(
|
||||
() => {
|
||||
const PopupHandler = handlers.find((h) => h.isActive(element));
|
||||
if (!PopupHandler || !editor.current) return;
|
||||
if (!PopupHandler || !editor) return;
|
||||
|
||||
const { popup: Popup } = PopupHandler;
|
||||
const pos = editor.current.view.posAtDOM(element, 0);
|
||||
|
||||
const pos = editor.view.posAtDOM(element, 0);
|
||||
if (pos < 0) return;
|
||||
|
||||
const node = editor.current.view.state.doc.nodeAt(pos);
|
||||
|
||||
const node = editor.view.state.doc.nodeAt(pos);
|
||||
if (!node) return;
|
||||
|
||||
const hidePopup = showPopup({
|
||||
@@ -95,7 +95,8 @@ export function HoverPopupHandler(props: FloatingMenuProps) {
|
||||
target: element,
|
||||
align: "center",
|
||||
location: "top",
|
||||
isTargetAbsolute: true
|
||||
isTargetAbsolute: true,
|
||||
yOffset: -30
|
||||
}
|
||||
});
|
||||
activePopup.current = { element, hide: hidePopup };
|
||||
|
||||
@@ -121,7 +121,8 @@ import {
|
||||
mdiCheckboxMultipleBlankOutline,
|
||||
mdiCheckboxMultipleMarked,
|
||||
mdiFormatFloatLeft,
|
||||
mdiMessageOutline
|
||||
mdiMessageOutline,
|
||||
mdiVectorLink
|
||||
} from "@mdi/js";
|
||||
|
||||
export const Icons = {
|
||||
@@ -148,6 +149,7 @@ export const Icons = {
|
||||
bulletList: mdiFormatListBulleted,
|
||||
highlight: mdiFormatColorHighlight,
|
||||
textColor: mdiFormatColorText,
|
||||
noteLink: mdiVectorLink,
|
||||
link: mdiLinkPlus,
|
||||
linkRemove: mdiLinkOff,
|
||||
openLink: mdiOpenInNew,
|
||||
|
||||
@@ -48,16 +48,10 @@ export function CellProperties(props: CellPropertiesProps) {
|
||||
expanded={true}
|
||||
color={attributes.backgroundColor}
|
||||
onChange={(color) =>
|
||||
editor.current?.commands.setCellAttribute(
|
||||
"backgroundColor",
|
||||
color
|
||||
)
|
||||
editor.commands.setCellAttribute("backgroundColor", color)
|
||||
}
|
||||
onClear={() =>
|
||||
editor.current?.commands.setCellAttribute(
|
||||
"backgroundColor",
|
||||
undefined
|
||||
)
|
||||
editor.commands.setCellAttribute("backgroundColor", undefined)
|
||||
}
|
||||
/>
|
||||
</Tab>
|
||||
@@ -72,11 +66,9 @@ export function CellProperties(props: CellPropertiesProps) {
|
||||
expanded={true}
|
||||
color={attributes.color}
|
||||
onChange={(color) =>
|
||||
editor.current?.commands.setCellAttribute("color", color)
|
||||
}
|
||||
onClear={() =>
|
||||
editor.current?.commands.setCellAttribute("color", undefined)
|
||||
editor.commands.setCellAttribute("color", color)
|
||||
}
|
||||
onClear={() => editor.commands.setCellAttribute("color", undefined)}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
@@ -94,13 +86,10 @@ export function CellProperties(props: CellPropertiesProps) {
|
||||
expanded={true}
|
||||
color={attributes.borderColor}
|
||||
onChange={(color) =>
|
||||
editor.current?.commands.setCellAttribute("borderColor", color)
|
||||
editor.commands.setCellAttribute("borderColor", color)
|
||||
}
|
||||
onClear={() =>
|
||||
editor.current?.commands.setCellAttribute(
|
||||
"borderColor",
|
||||
undefined
|
||||
)
|
||||
editor.commands.setCellAttribute("borderColor", undefined)
|
||||
}
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
@@ -47,7 +47,7 @@ export function ImageProperties(props: ImagePropertiesProps) {
|
||||
sx: { mr: 1 }
|
||||
}}
|
||||
onChange={(e) => {
|
||||
editor.current?.commands.setImageSize({
|
||||
editor.commands.setImageSize({
|
||||
width: e.target.valueAsNumber,
|
||||
height: aspectRatio
|
||||
? e.target.valueAsNumber / aspectRatio
|
||||
@@ -60,7 +60,7 @@ export function ImageProperties(props: ImagePropertiesProps) {
|
||||
type="number"
|
||||
value={height || 0}
|
||||
onChange={(e) => {
|
||||
editor.current?.commands.setImageSize({
|
||||
editor.commands.setImageSize({
|
||||
width: aspectRatio
|
||||
? e.target.valueAsNumber * aspectRatio
|
||||
: e.target.valueAsNumber,
|
||||
|
||||
@@ -28,9 +28,16 @@ export type LinkPopupProps = {
|
||||
isEditing?: boolean;
|
||||
onDone: (link: LinkDefinition) => void;
|
||||
onClose: () => void;
|
||||
isImageActive?: boolean;
|
||||
};
|
||||
export function LinkPopup(props: LinkPopupProps) {
|
||||
const { link: _link, isEditing = false, onDone, onClose } = props;
|
||||
const {
|
||||
link: _link = { title: "", href: "" },
|
||||
isEditing = false,
|
||||
onDone,
|
||||
onClose,
|
||||
isImageActive
|
||||
} = props;
|
||||
const link = useRefValue(_link);
|
||||
|
||||
return (
|
||||
@@ -46,14 +53,17 @@ export function LinkPopup(props: LinkPopupProps) {
|
||||
}}
|
||||
>
|
||||
<Flex sx={{ p: 1, flexDirection: "column", width: ["auto", 250] }}>
|
||||
{!link.current?.isImage && (
|
||||
{!isImageActive && (
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Link text"
|
||||
defaultValue={link.current?.text}
|
||||
defaultValue={link.current?.title}
|
||||
sx={{ mb: 1 }}
|
||||
onChange={(e) =>
|
||||
(link.current = { ...link.current, text: e.target.value })
|
||||
(link.current = {
|
||||
...link.current,
|
||||
title: e.target.value
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -40,7 +40,7 @@ export function SearchReplacePopup(props: SearchReplacePopupProps) {
|
||||
|
||||
const search = useCallback(
|
||||
(term: string) => {
|
||||
editor.current?.commands.search(term, {
|
||||
editor.commands.search(term, {
|
||||
matchCase,
|
||||
enableRegex,
|
||||
matchWholeWord
|
||||
@@ -121,7 +121,7 @@ export function SearchReplacePopup(props: SearchReplacePopupProps) {
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
editor.current?.commands.moveToNextResult();
|
||||
editor.commands.moveToNextResult();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -219,7 +219,7 @@ export function SearchReplacePopup(props: SearchReplacePopupProps) {
|
||||
title="Previous match"
|
||||
id="previousMatch"
|
||||
icon="previousMatch"
|
||||
onClick={() => editor.current?.commands.moveToPreviousResult()}
|
||||
onClick={() => editor.commands.moveToPreviousResult()}
|
||||
sx={{ mr: 0 }}
|
||||
iconSize={"big"}
|
||||
/>
|
||||
@@ -228,7 +228,7 @@ export function SearchReplacePopup(props: SearchReplacePopupProps) {
|
||||
title="Next match"
|
||||
id="nextMatch"
|
||||
icon="nextMatch"
|
||||
onClick={() => editor.current?.commands.moveToNextResult()}
|
||||
onClick={() => editor.commands.moveToNextResult()}
|
||||
sx={{ mr: 0 }}
|
||||
iconSize={"big"}
|
||||
/>
|
||||
@@ -237,7 +237,7 @@ export function SearchReplacePopup(props: SearchReplacePopupProps) {
|
||||
title="Close"
|
||||
id="close"
|
||||
icon="close"
|
||||
onClick={() => editor.current?.chain().focus().endSearch().run()}
|
||||
onClick={() => editor.chain().focus().endSearch().run()}
|
||||
sx={{ mr: 0 }}
|
||||
iconSize={"big"}
|
||||
/>
|
||||
@@ -249,9 +249,7 @@ export function SearchReplacePopup(props: SearchReplacePopupProps) {
|
||||
title="Replace"
|
||||
id="replace"
|
||||
icon="replaceOne"
|
||||
onClick={() =>
|
||||
editor.current?.commands.replace(replaceText.current)
|
||||
}
|
||||
onClick={() => editor.commands.replace(replaceText.current)}
|
||||
sx={{ mr: 0 }}
|
||||
iconSize={18}
|
||||
/>
|
||||
@@ -260,9 +258,7 @@ export function SearchReplacePopup(props: SearchReplacePopupProps) {
|
||||
title="Replace all"
|
||||
id="replaceAll"
|
||||
icon="replaceAll"
|
||||
onClick={() =>
|
||||
editor.current?.commands.replaceAll(replaceText.current)
|
||||
}
|
||||
onClick={() => editor.commands.replaceAll(replaceText.current)}
|
||||
sx={{ mr: 0 }}
|
||||
iconSize={18}
|
||||
/>
|
||||
|
||||
@@ -41,6 +41,10 @@ const tools: Record<ToolId, ToolDefinition> = {
|
||||
icon: "strikethrough",
|
||||
title: "Strikethrough"
|
||||
},
|
||||
addInternalLink: {
|
||||
icon: "noteLink",
|
||||
title: "Add bi-directional note link"
|
||||
},
|
||||
addLink: {
|
||||
icon: "link",
|
||||
title: "Link"
|
||||
@@ -413,7 +417,7 @@ const defaultPresets: Record<"default" | "minimal", ToolbarDefinition> = {
|
||||
["fontSize"],
|
||||
["headings", "fontFamily"],
|
||||
["checkList", "numberedList", "bulletList"],
|
||||
["addLink"],
|
||||
["addLink", "addInternalLink"],
|
||||
["alignment", "textDirection"],
|
||||
["clearformatting"]
|
||||
],
|
||||
|
||||
@@ -58,11 +58,11 @@ export function Toolbar(props: ToolbarProps) {
|
||||
const toolbarTools = useMemo(
|
||||
() =>
|
||||
isMobile
|
||||
? editor?.current?.isEditable
|
||||
? editor.isEditable
|
||||
? [...MOBILE_STATIC_TOOLBAR_GROUPS, ...tools]
|
||||
: READONLY_MOBILE_STATIC_TOOLBAR_GROUPS
|
||||
: [...STATIC_TOOLBAR_GROUPS, ...tools],
|
||||
[tools, isMobile]
|
||||
[tools, editor.isEditable, isMobile]
|
||||
);
|
||||
|
||||
const setToolbarLocation = useToolbarStore(
|
||||
|
||||
@@ -35,11 +35,7 @@ function AlignmentTool(props: AlignmentToolProps) {
|
||||
<ToolButton
|
||||
{...toolProps}
|
||||
onClick={() => {
|
||||
editor.current
|
||||
?.chain()
|
||||
.focus()
|
||||
.setTextAlign(alignmentRef.current)
|
||||
.run();
|
||||
editor?.chain().focus().setTextAlign(alignmentRef.current).run();
|
||||
}}
|
||||
disabled={editor.isActive(CodeBlock.name)}
|
||||
toggled={false}
|
||||
|
||||
@@ -56,7 +56,7 @@ export function DownloadAttachment(props: ToolProps) {
|
||||
findSelectedNode(editor, "image");
|
||||
|
||||
const attachment = (attachmentNode?.attrs || {}) as Attachment;
|
||||
editor.current?.chain().focus().downloadAttachment(attachment).run();
|
||||
editor.storage.downloadAttachment?.(attachment);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -81,7 +81,7 @@ export function PreviewAttachment(props: ToolProps) {
|
||||
findSelectedNode(editor, "image");
|
||||
|
||||
const attachment = (attachmentNode?.attrs || {}) as Attachment;
|
||||
editor.current?.commands.previewAttachment(attachment);
|
||||
editor.storage.previewAttachment?.(attachment);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -93,7 +93,7 @@ export function RemoveAttachment(props: ToolProps) {
|
||||
<ToolButton
|
||||
{...props}
|
||||
toggled={false}
|
||||
onClick={() => editor.current?.chain().focus().removeAttachment().run()}
|
||||
onClick={() => editor.chain().focus().removeAttachment().run()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ const horizontalRule = (editor: Editor): MenuItem => ({
|
||||
title: "Horizontal rule",
|
||||
icon: Icons.horizontalRule,
|
||||
isChecked: editor?.isActive("horizontalRule"),
|
||||
onClick: () => editor.current?.chain().focus().setHorizontalRule().run()
|
||||
onClick: () => editor.chain().focus().setHorizontalRule().run()
|
||||
});
|
||||
|
||||
const codeblock = (editor: Editor): MenuItem => ({
|
||||
@@ -116,7 +116,7 @@ const codeblock = (editor: Editor): MenuItem => ({
|
||||
title: "Code block",
|
||||
icon: Icons.codeblock,
|
||||
isChecked: editor?.isActive("codeBlock"),
|
||||
onClick: () => editor.current?.chain().focus().toggleCodeBlock().run(),
|
||||
onClick: () => editor.chain().focus().toggleCodeBlock().run(),
|
||||
modifier: "Mod-Shift-C"
|
||||
});
|
||||
|
||||
@@ -126,7 +126,7 @@ const blockquote = (editor: Editor): MenuItem => ({
|
||||
title: "Quote",
|
||||
icon: Icons.blockquote,
|
||||
isChecked: editor?.isActive("blockQuote"),
|
||||
onClick: () => editor.current?.chain().focus().toggleBlockquote().run(),
|
||||
onClick: () => editor.chain().focus().toggleBlockquote().run(),
|
||||
modifier: "Mod-Shift-B"
|
||||
});
|
||||
|
||||
@@ -136,7 +136,7 @@ const mathblock = (editor: Editor): MenuItem => ({
|
||||
title: "Math & formulas",
|
||||
icon: Icons.mathBlock,
|
||||
isChecked: editor?.isActive("mathBlock"),
|
||||
onClick: () => editor.current?.chain().focus().insertMathBlock().run(),
|
||||
onClick: () => editor.chain().focus().insertMathBlock().run(),
|
||||
modifier: "Mod-Shift-M"
|
||||
});
|
||||
|
||||
@@ -183,8 +183,7 @@ const image = (editor: Editor, isMobile: boolean): MenuItem => ({
|
||||
type: "button",
|
||||
title: "Upload from disk",
|
||||
icon: Icons.upload,
|
||||
onClick: () =>
|
||||
editor.current?.chain().focus().openAttachmentPicker("image").run(),
|
||||
onClick: () => editor.storage.openAttachmentPicker?.("image"),
|
||||
modifier: "Mod-Shift-I"
|
||||
},
|
||||
{
|
||||
@@ -193,8 +192,7 @@ const image = (editor: Editor, isMobile: boolean): MenuItem => ({
|
||||
title: "Take a photo using camera",
|
||||
icon: Icons.camera,
|
||||
isHidden: !isMobile,
|
||||
onClick: () =>
|
||||
editor.current?.chain().focus().openAttachmentPicker("camera").run()
|
||||
onClick: () => editor.storage.openAttachmentPicker?.("camera")
|
||||
},
|
||||
isMobile ? uploadImageFromURLMobile(editor) : uploadImageFromURL(editor)
|
||||
]
|
||||
@@ -215,7 +213,7 @@ const table = (editor: Editor): MenuItem => ({
|
||||
component: (props) => (
|
||||
<TablePopup
|
||||
onInsertTable={(size) => {
|
||||
editor.current
|
||||
editor
|
||||
?.chain()
|
||||
.focus()
|
||||
.insertTable({
|
||||
@@ -249,7 +247,7 @@ const embedMobile = (editor: Editor): MenuItem => ({
|
||||
title="Insert embed"
|
||||
onClose={(embed) => {
|
||||
if (!embed) return onClick?.();
|
||||
editor.current?.chain().insertEmbed(embed).run();
|
||||
editor.chain().insertEmbed(embed).run();
|
||||
onClick?.();
|
||||
}}
|
||||
/>
|
||||
@@ -273,7 +271,7 @@ const embedDesktop = (editor: Editor): MenuItem => ({
|
||||
title="Insert embed"
|
||||
onClose={(embed) => {
|
||||
if (!embed) return hide();
|
||||
editor.current?.chain().insertEmbed(embed).run();
|
||||
editor.chain().insertEmbed(embed).run();
|
||||
hide();
|
||||
}}
|
||||
/>
|
||||
@@ -288,8 +286,7 @@ const attachment = (editor: Editor): MenuItem => ({
|
||||
title: "Attachment",
|
||||
icon: Icons.attachment,
|
||||
isChecked: editor?.isActive("attachment"),
|
||||
onClick: () =>
|
||||
editor.current?.chain().focus().openAttachmentPicker("file").run(),
|
||||
onClick: () => editor.storage.openAttachmentPicker?.("file"),
|
||||
modifier: "Mod-Shift-A"
|
||||
});
|
||||
|
||||
@@ -299,7 +296,7 @@ const tasklist = (editor: Editor): MenuItem => ({
|
||||
title: "Task list",
|
||||
icon: Icons.checkbox,
|
||||
isChecked: editor?.isActive("taskList"),
|
||||
onClick: () => editor.current?.chain().focus().toggleTaskList().run(),
|
||||
onClick: () => editor.chain().focus().toggleTaskList().run(),
|
||||
modifier: "Mod-Shift-T"
|
||||
});
|
||||
|
||||
@@ -309,7 +306,7 @@ const outlinelist = (editor: Editor): MenuItem => ({
|
||||
title: "Outline list",
|
||||
icon: Icons.outlineList,
|
||||
isChecked: editor?.isActive("outlineList"),
|
||||
onClick: () => editor.current?.chain().focus().toggleOutlineList().run(),
|
||||
onClick: () => editor.chain().focus().toggleOutlineList().run(),
|
||||
modifier: "Mod-Shift-O"
|
||||
});
|
||||
|
||||
|
||||
@@ -135,8 +135,8 @@ export function Highlight(props: ToolProps) {
|
||||
title={"Background color"}
|
||||
onColorChange={(color) =>
|
||||
color
|
||||
? editor.current?.chain().setHighlight(color).run()
|
||||
: editor.current?.chain().unsetHighlight().run()
|
||||
? editor.chain().focus().setHighlight(color).run()
|
||||
: editor.chain().focus().unsetHighlight().run()
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -152,8 +152,8 @@ export function TextColor(props: ToolProps) {
|
||||
title="Text color"
|
||||
onColorChange={(color) =>
|
||||
color
|
||||
? editor.current?.chain().setColor(color).run()
|
||||
: editor.current?.chain().unsetColor().run()
|
||||
? editor.chain().focus().setColor(color).run()
|
||||
: editor.chain().focus().unsetColor().run()
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -54,11 +54,7 @@ export function EmbedAlignLeft(props: ToolProps) {
|
||||
{...props}
|
||||
toggled={false}
|
||||
onClick={() =>
|
||||
editor.current
|
||||
?.chain()
|
||||
.focus()
|
||||
.setEmbedAlignment({ align: "left" })
|
||||
.run()
|
||||
editor?.chain().focus().setEmbedAlignment({ align: "left" }).run()
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -71,11 +67,7 @@ export function EmbedAlignRight(props: ToolProps) {
|
||||
{...props}
|
||||
toggled={false}
|
||||
onClick={() =>
|
||||
editor.current
|
||||
?.chain()
|
||||
.focus()
|
||||
.setEmbedAlignment({ align: "right" })
|
||||
.run()
|
||||
editor?.chain().focus().setEmbedAlignment({ align: "right" }).run()
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -88,11 +80,7 @@ export function EmbedAlignCenter(props: ToolProps) {
|
||||
{...props}
|
||||
toggled={false}
|
||||
onClick={() =>
|
||||
editor.current
|
||||
?.chain()
|
||||
.focus()
|
||||
.setEmbedAlignment({ align: "center" })
|
||||
.run()
|
||||
editor?.chain().focus().setEmbedAlignment({ align: "center" }).run()
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -136,14 +124,14 @@ export function EmbedProperties(props: ToolProps) {
|
||||
title="Embed properties"
|
||||
onClose={(newEmbed) => {
|
||||
if (!newEmbed) {
|
||||
editor.current?.commands.setEmbedSize(embed);
|
||||
editor.commands.setEmbedSize(embed);
|
||||
} else if (newEmbed.src !== embed.src)
|
||||
editor.current?.commands.setEmbedSource(newEmbed.src);
|
||||
editor.commands.setEmbedSource(newEmbed.src);
|
||||
|
||||
setIsOpen(false);
|
||||
}}
|
||||
embed={embed}
|
||||
onSizeChanged={(size) => editor.current?.commands.setEmbedSize(size)}
|
||||
onSizeChanged={(size) => editor.commands.setEmbedSize(size)}
|
||||
/>
|
||||
</ResponsivePresenter>
|
||||
</>
|
||||
|
||||
@@ -49,25 +49,13 @@ export function FontSize(props: ToolProps) {
|
||||
title="Font size"
|
||||
disabled={editor.isActive(CodeBlock.name)}
|
||||
onDecrease={() =>
|
||||
editor.current
|
||||
?.chain()
|
||||
.focus()
|
||||
.setFontSize(`${decreaseFontSize()}px`)
|
||||
.run()
|
||||
editor?.chain().focus().setFontSize(`${decreaseFontSize()}px`).run()
|
||||
}
|
||||
onIncrease={() => {
|
||||
editor.current
|
||||
?.chain()
|
||||
.focus()
|
||||
.setFontSize(`${increaseFontSize()}px`)
|
||||
.run();
|
||||
editor?.chain().focus().setFontSize(`${increaseFontSize()}px`).run();
|
||||
}}
|
||||
onReset={() =>
|
||||
editor.current
|
||||
?.chain()
|
||||
.focus()
|
||||
.setFontSize(`${defaultFontSize}px`)
|
||||
.run()
|
||||
editor?.chain().focus().setFontSize(`${defaultFontSize}px`).run()
|
||||
}
|
||||
value={fontSize || `${defaultFontSize}px`}
|
||||
/>
|
||||
@@ -108,8 +96,7 @@ function toMenuItems(editor: Editor, currentFontFamily: string): MenuItem[] {
|
||||
type: "button",
|
||||
title: font.title,
|
||||
isChecked: font.id === currentFontFamily,
|
||||
onClick: () =>
|
||||
editor.current?.chain().focus().setFontFamily(font.id).run(),
|
||||
onClick: () => editor.chain().focus().setFontFamily(font.id).run(),
|
||||
styles: {
|
||||
title: {
|
||||
fontFamily: font.font
|
||||
|
||||
@@ -66,7 +66,7 @@ function toMenuItems(
|
||||
isChecked: level === currentHeadingLevel,
|
||||
modifier: `Mod-Alt-${level}`,
|
||||
onClick: () =>
|
||||
editor.current
|
||||
editor
|
||||
?.chain()
|
||||
.focus()
|
||||
.updateAttributes("textStyle", { fontSize: null, fontStyle: null })
|
||||
@@ -79,7 +79,7 @@ function toMenuItems(
|
||||
title: "Paragraph",
|
||||
isChecked: !currentHeadingLevel,
|
||||
modifier: `Mod-Alt-0`,
|
||||
onClick: () => editor.current?.chain().focus().setParagraph().run()
|
||||
onClick: () => editor.chain().focus().setParagraph().run()
|
||||
};
|
||||
return [paragraph, ...menuItems];
|
||||
}
|
||||
|
||||
@@ -66,11 +66,7 @@ export function ImageAlignLeft(props: ToolProps) {
|
||||
{...props}
|
||||
toggled={!align || align === "left"}
|
||||
onClick={() =>
|
||||
editor.current
|
||||
?.chain()
|
||||
.focus()
|
||||
.setImageAlignment({ align: "left" })
|
||||
.run()
|
||||
editor?.chain().focus().setImageAlignment({ align: "left" }).run()
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -88,11 +84,7 @@ export function ImageAlignRight(props: ToolProps) {
|
||||
{...props}
|
||||
toggled={align === "right"}
|
||||
onClick={() =>
|
||||
editor.current
|
||||
?.chain()
|
||||
.focus()
|
||||
.setImageAlignment({ align: "right" })
|
||||
.run()
|
||||
editor?.chain().focus().setImageAlignment({ align: "right" }).run()
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -110,11 +102,7 @@ export function ImageAlignCenter(props: ToolProps) {
|
||||
{...props}
|
||||
toggled={align === "center"}
|
||||
onClick={() =>
|
||||
editor.current
|
||||
?.chain()
|
||||
.focus()
|
||||
.setImageAlignment({ align: "center" })
|
||||
.run()
|
||||
editor?.chain().focus().setImageAlignment({ align: "center" }).run()
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -84,6 +84,7 @@ import {
|
||||
} from "./embed";
|
||||
import {
|
||||
AddLink,
|
||||
AddInternalLink,
|
||||
EditLink,
|
||||
RemoveLink,
|
||||
LinkSettings,
|
||||
@@ -108,6 +109,7 @@ const tools = {
|
||||
subscript: Subscript,
|
||||
superscript: Superscript,
|
||||
clearformatting: ClearFormatting,
|
||||
addInternalLink: AddInternalLink,
|
||||
addLink: AddLink,
|
||||
editLink: EditLink,
|
||||
removeLink: RemoveLink,
|
||||
|
||||
@@ -28,7 +28,7 @@ export function Italic(props: ToolProps) {
|
||||
<ToolButton
|
||||
{...props}
|
||||
toggled={editor.isActive("italic")}
|
||||
onClick={() => editor.current?.chain().focus().toggleItalic().run()}
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -39,7 +39,7 @@ export function Strikethrough(props: ToolProps) {
|
||||
<ToolButton
|
||||
{...props}
|
||||
toggled={editor.isActive("strike")}
|
||||
onClick={() => editor.current?.chain().focus().toggleStrike().run()}
|
||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -50,7 +50,7 @@ export function Underline(props: ToolProps) {
|
||||
<ToolButton
|
||||
{...props}
|
||||
toggled={editor.isActive("underline")}
|
||||
onClick={() => editor.current?.chain().focus().toggleUnderline().run()}
|
||||
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -63,7 +63,7 @@ export function Code(props: ToolProps) {
|
||||
{...props}
|
||||
toggled={editor.isActive("code")}
|
||||
disabled={editor.isActive(CodeBlock.name)}
|
||||
onClick={() => editor.current?.chain().focus().toggleCode().run()}
|
||||
onClick={() => editor.chain().focus().toggleCode().run()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -75,7 +75,7 @@ export function Bold(props: ToolProps) {
|
||||
<ToolButton
|
||||
{...props}
|
||||
toggled={editor.isActive("bold")}
|
||||
onClick={() => editor.current?.chain().focus().toggleBold().run()}
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -87,7 +87,7 @@ export function Subscript(props: ToolProps) {
|
||||
{...props}
|
||||
toggled={editor.isActive("subscript")}
|
||||
disabled={editor.isActive(CodeBlock.name)}
|
||||
onClick={() => editor.current?.chain().focus().toggleSubscript().run()}
|
||||
onClick={() => editor.chain().focus().toggleSubscript().run()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -99,7 +99,7 @@ export function Superscript(props: ToolProps) {
|
||||
{...props}
|
||||
toggled={editor.isActive("superscript")}
|
||||
disabled={editor.isActive(CodeBlock.name)}
|
||||
onClick={() => editor.current?.chain().focus().toggleSuperscript().run()}
|
||||
onClick={() => editor.chain().focus().toggleSuperscript().run()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -111,7 +111,7 @@ export function ClearFormatting(props: ToolProps) {
|
||||
{...props}
|
||||
toggled={false}
|
||||
onClick={() =>
|
||||
editor.current
|
||||
editor
|
||||
?.chain()
|
||||
.focus()
|
||||
.clearNodes()
|
||||
@@ -131,7 +131,7 @@ export function CodeRemove(props: ToolProps) {
|
||||
<ToolButton
|
||||
{...props}
|
||||
toggled={false}
|
||||
onClick={() => editor.current?.chain().focus().unsetMark("code").run()}
|
||||
onClick={() => editor.chain().focus().unsetMark("code").run()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -142,7 +142,7 @@ export function Math(props: ToolProps) {
|
||||
<ToolButton
|
||||
{...props}
|
||||
toggled={false}
|
||||
onClick={() => editor.current?.chain().focus().insertMathInline().run()}
|
||||
onClick={() => editor.chain().focus().insertMathInline().run()}
|
||||
disabled={editor.isActive(CodeBlock.name)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -19,16 +19,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { ToolProps } from "../types";
|
||||
import { ToolButton } from "../components/tool-button";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { ResponsivePresenter } from "../../components/responsive";
|
||||
import { LinkPopup } from "../popups/link-popup";
|
||||
import { useToolbarLocation } from "../stores/toolbar-store";
|
||||
import { MoreTools } from "../components/more-tools";
|
||||
import { useRefValue } from "../../hooks/use-ref-value";
|
||||
import { findMark, selectionToOffset } from "../../utils/prosemirror";
|
||||
import { TextSelection } from "prosemirror-state";
|
||||
import { Flex, Link } from "@theme-ui/components";
|
||||
import { ImageNode } from "../../extensions/image";
|
||||
import { Link as LinkNode } from "../../extensions/link";
|
||||
import { getMarkAttributes } from "@tiptap/core";
|
||||
|
||||
export function LinkSettings(props: ToolProps) {
|
||||
const { editor } = props;
|
||||
@@ -53,49 +54,42 @@ export function LinkSettings(props: ToolProps) {
|
||||
export function AddLink(props: ToolProps) {
|
||||
const { editor } = props;
|
||||
|
||||
const isActive = props.editor.isActive("link");
|
||||
|
||||
const onDone = useCallback(
|
||||
(link: LinkDefinition) => {
|
||||
const { href, text, isImage } = link;
|
||||
if (!href) return;
|
||||
|
||||
let commandChain = editor.current?.chain().focus();
|
||||
if (!commandChain) return;
|
||||
|
||||
const isSelection = !editor.current?.state.selection.empty;
|
||||
|
||||
commandChain = commandChain
|
||||
.extendMarkRange("link")
|
||||
.toggleLink({ href, target: "_blank" });
|
||||
if (!isImage) commandChain = commandChain.insertContent(text || href);
|
||||
|
||||
commandChain = commandChain.focus();
|
||||
|
||||
if (!isSelection && !isImage)
|
||||
commandChain = commandChain.unsetMark("link").insertContent(" ");
|
||||
|
||||
commandChain.run();
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
const isActive = editor.isActive("link");
|
||||
|
||||
if (isActive) return <EditLink {...props} icon={"linkEdit"} />;
|
||||
return (
|
||||
<LinkTool
|
||||
{...props}
|
||||
onDone={onDone}
|
||||
onDone={(attributes) => editor.commands.toggleLink(attributes)}
|
||||
onClick={() => {
|
||||
if (!editor.current) return;
|
||||
const { state } = editor.current;
|
||||
const { from, to } = state.selection;
|
||||
if (!editor) return;
|
||||
const selectedText = editor.state.doc.textBetween(
|
||||
editor.state.selection.from,
|
||||
editor.state.selection.to
|
||||
);
|
||||
return { title: selectedText, href: "" };
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const isImage = state.doc.nodeAt(from)?.type.name === ImageNode.name;
|
||||
if (isImage) return { isImage };
|
||||
export function AddInternalLink(props: ToolProps) {
|
||||
const { editor } = props;
|
||||
const isActive = editor.isActive(LinkNode.name);
|
||||
|
||||
const selectedText = state.doc.textBetween(from, to);
|
||||
return { text: selectedText };
|
||||
if (isActive) return null;
|
||||
return (
|
||||
<ToolButton
|
||||
{...props}
|
||||
onClick={async () => {
|
||||
const link = await editor.storage.createInternalLink?.();
|
||||
if (!link) return;
|
||||
|
||||
const selectedText = editor.state.doc.textBetween(
|
||||
editor.state.selection.from,
|
||||
editor.state.selection.to
|
||||
);
|
||||
editor.commands.setLink({ ...link, title: selectedText || link.title });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -106,66 +100,32 @@ export function EditLink(props: ToolProps) {
|
||||
const selectedNode = useRefValue(
|
||||
_selectedNode || selectionToOffset(editor.state)
|
||||
);
|
||||
const { node } = _selectedNode || {};
|
||||
const link = node ? findMark(node, LinkNode.name) : null;
|
||||
const attrs = link?.attrs || getMarkAttributes(editor.state, LinkNode.name);
|
||||
|
||||
const onDone = useCallback(
|
||||
(link: LinkDefinition) => {
|
||||
if (!selectedNode.current) return;
|
||||
if (attrs && isInternalLink(attrs.href))
|
||||
return (
|
||||
<ToolButton
|
||||
{...props}
|
||||
onClick={async () => {
|
||||
const link = await editor.storage.createInternalLink?.();
|
||||
if (!link) return;
|
||||
const { from, to } = editor.state.selection;
|
||||
if (selectedNode.current)
|
||||
editor.commands.setTextSelection(selectedNode.current);
|
||||
editor.commands.setLink(link);
|
||||
if (selectedNode.current)
|
||||
editor.commands.setTextSelection({ from, to });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const { href, text, isImage } = link;
|
||||
const { from, node, to } = selectedNode.current;
|
||||
if (!href || !editor.current || !node) return;
|
||||
|
||||
const mark = findMark(node, "link");
|
||||
if (!mark) return;
|
||||
|
||||
const selection = editor.current.state.selection;
|
||||
|
||||
let commandChain = editor.current.chain();
|
||||
|
||||
if (!isImage) {
|
||||
commandChain = commandChain.command(({ tr }) => {
|
||||
tr.removeMark(from, to, mark.type);
|
||||
tr.insertText(
|
||||
text || node.textContent,
|
||||
tr.mapping.map(from),
|
||||
tr.mapping.map(to)
|
||||
);
|
||||
tr.setSelection(
|
||||
TextSelection.create(
|
||||
tr.doc,
|
||||
tr.mapping.map(from),
|
||||
tr.mapping.map(to)
|
||||
)
|
||||
);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
commandChain
|
||||
.extendMarkRange("link")
|
||||
.toggleLink({ href, target: "_blank" })
|
||||
.command(({ tr }) => {
|
||||
tr.setSelection(
|
||||
TextSelection.create(
|
||||
tr.doc,
|
||||
tr.mapping.map(selection.from),
|
||||
tr.mapping.map(selection.to)
|
||||
)
|
||||
);
|
||||
return true;
|
||||
})
|
||||
.focus(undefined, { scrollIntoView: true })
|
||||
.run();
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
if (!editor.current?.isEditable) return null;
|
||||
return (
|
||||
<LinkTool
|
||||
{...props}
|
||||
isEditing
|
||||
onDone={onDone}
|
||||
onDone={(attributes) => editor.commands.setLink(attributes)}
|
||||
onClick={() => {
|
||||
if (!selectedNode.current) return;
|
||||
|
||||
@@ -177,9 +137,8 @@ export function EditLink(props: ToolProps) {
|
||||
|
||||
if (!mark) return;
|
||||
return {
|
||||
text: selectedText,
|
||||
href: mark.attrs.href,
|
||||
isImage: node.type.name === ImageNode.name
|
||||
title: selectedText,
|
||||
href: mark.attrs.href
|
||||
};
|
||||
}}
|
||||
/>
|
||||
@@ -188,18 +147,18 @@ export function EditLink(props: ToolProps) {
|
||||
|
||||
export function RemoveLink(props: ToolProps) {
|
||||
const { editor, selectedNode } = props;
|
||||
if (!editor.current?.isEditable) return null;
|
||||
if (!editor.isEditable) return null;
|
||||
return (
|
||||
<ToolButton
|
||||
{...props}
|
||||
toggled={false}
|
||||
onClick={() => {
|
||||
if (selectedNode)
|
||||
editor.current?.commands.setTextSelection({
|
||||
editor.commands.setTextSelection({
|
||||
from: selectedNode.from,
|
||||
to: selectedNode.to
|
||||
});
|
||||
editor.current?.chain().focus().unsetLink().run();
|
||||
editor.chain().focus().unsetLink().run();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -221,7 +180,7 @@ export function OpenLink(props: ToolProps) {
|
||||
href={href}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
editor.commands.openLink(href);
|
||||
editor.storage.openLink?.(href);
|
||||
}}
|
||||
target="_blank"
|
||||
variant="body"
|
||||
@@ -244,7 +203,7 @@ export function OpenLink(props: ToolProps) {
|
||||
{...props}
|
||||
toggled={false}
|
||||
onClick={() => {
|
||||
editor.commands.openLink(href);
|
||||
editor.storage.openLink?.(href);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
@@ -266,16 +225,20 @@ export function CopyLink(props: ToolProps) {
|
||||
{...props}
|
||||
toggled={false}
|
||||
onClick={() => {
|
||||
editor.commands.copyToClipboard(href);
|
||||
editor.storage.copyToClipboard?.(
|
||||
href,
|
||||
`<a href="${href}">${
|
||||
selectedNode.current?.node?.textContent || link?.attrs.title
|
||||
}</a>`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export type LinkDefinition = {
|
||||
href?: string;
|
||||
text?: string;
|
||||
isImage?: boolean;
|
||||
href: string;
|
||||
title?: string;
|
||||
};
|
||||
type LinkToolProps = ToolProps & {
|
||||
isEditing?: boolean;
|
||||
@@ -288,6 +251,7 @@ function LinkTool(props: LinkToolProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const [linkDefinition, setLinkDefinition] = useState<LinkDefinition>();
|
||||
const isImageActive = editor.isActive(ImageNode.name);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -317,13 +281,14 @@ function LinkTool(props: LinkToolProps) {
|
||||
items={[]}
|
||||
onClose={() => {
|
||||
setIsOpen(false);
|
||||
editor.current?.commands.focus();
|
||||
editor.commands.focus();
|
||||
}}
|
||||
focusOnRender={false}
|
||||
>
|
||||
<LinkPopup
|
||||
link={linkDefinition}
|
||||
isEditing={isEditing}
|
||||
isImageActive={isImageActive}
|
||||
onClose={() => setIsOpen(false)}
|
||||
onDone={(link) => {
|
||||
onDone(link);
|
||||
@@ -334,3 +299,7 @@ function LinkTool(props: LinkToolProps) {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function isInternalLink(href: string) {
|
||||
return href.startsWith("nn://");
|
||||
}
|
||||
|
||||
@@ -90,10 +90,10 @@ function _ListTool<TListStyleTypes extends string>(
|
||||
variant={"menuitem"}
|
||||
sx={{ width: 80 }}
|
||||
onClick={() => {
|
||||
let chain = editor.current?.chain().focus();
|
||||
if (!chain || !editor.current) return;
|
||||
let chain = editor.chain().focus();
|
||||
if (!chain || !editor) return;
|
||||
|
||||
if (!isListActive(editor.current)) {
|
||||
if (!isListActive(editor)) {
|
||||
if (type === "bulletList") chain = chain.toggleBulletList();
|
||||
else chain = chain.toggleOrderedList();
|
||||
}
|
||||
@@ -120,7 +120,7 @@ export function NumberedList(props: ToolProps) {
|
||||
const { editor } = props;
|
||||
|
||||
const onClick = useCallback(
|
||||
() => editor.current?.chain().focus().toggleOrderedList().run(),
|
||||
() => editor.chain().focus().toggleOrderedList().run(),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
@@ -154,7 +154,7 @@ export function NumberedList(props: ToolProps) {
|
||||
export function BulletList(props: ToolProps) {
|
||||
const { editor } = props;
|
||||
const onClick = useCallback(
|
||||
() => editor.current?.chain().focus().toggleBulletList().run(),
|
||||
() => editor.chain().focus().toggleBulletList().run(),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
@@ -197,9 +197,7 @@ export function Indent(props: ToolProps) {
|
||||
<ToolButton
|
||||
{...toolProps}
|
||||
toggled={false}
|
||||
onClick={() =>
|
||||
editor.current?.chain().focus().sinkListItem(listItemType).run()
|
||||
}
|
||||
onClick={() => editor.chain().focus().sinkListItem(listItemType).run()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -215,9 +213,7 @@ export function Outdent(props: ToolProps) {
|
||||
<ToolButton
|
||||
{...toolProps}
|
||||
toggled={false}
|
||||
onClick={() =>
|
||||
editor.current?.chain().focus().liftListItem(listItemType).run()
|
||||
}
|
||||
onClick={() => editor.chain().focus().liftListItem(listItemType).run()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -228,10 +228,10 @@ export function CellBackgroundColor(props: ToolProps) {
|
||||
<ColorTool
|
||||
{...props}
|
||||
cacheKey="cellBackgroundColor"
|
||||
activeColor={editor.current?.getAttributes("tableCell").backgroundColor}
|
||||
activeColor={editor.getAttributes("tableCell").backgroundColor}
|
||||
title={"Cell background color"}
|
||||
onColorChange={(color) =>
|
||||
editor.current?.chain().setCellAttribute("backgroundColor", color).run()
|
||||
editor.chain().setCellAttribute("backgroundColor", color).run()
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -244,10 +244,10 @@ export function CellTextColor(props: ToolProps) {
|
||||
<ColorTool
|
||||
{...props}
|
||||
cacheKey="cellTextColor"
|
||||
activeColor={editor.current?.getAttributes("tableCell").color}
|
||||
activeColor={editor.getAttributes("tableCell").color}
|
||||
title={"Cell text color"}
|
||||
onColorChange={(color) =>
|
||||
editor.current?.chain().focus().setCellAttribute("color", color).run()
|
||||
editor.chain().focus().setCellAttribute("color", color).run()
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -260,14 +260,10 @@ export function CellBorderColor(props: ToolProps) {
|
||||
<ColorTool
|
||||
{...props}
|
||||
cacheKey="cellBorderColor"
|
||||
activeColor={editor.current?.getAttributes("tableCell").borderColor}
|
||||
activeColor={editor.getAttributes("tableCell").borderColor}
|
||||
title={"Cell border color"}
|
||||
onColorChange={(color) =>
|
||||
editor.current
|
||||
?.chain()
|
||||
.focus()
|
||||
.setCellAttribute("borderColor", color)
|
||||
.run()
|
||||
editor?.chain().focus().setCellAttribute("borderColor", color).run()
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -295,20 +291,12 @@ export function CellBorderWidth(props: ToolProps) {
|
||||
<Counter
|
||||
title="cell border width"
|
||||
onDecrease={() =>
|
||||
editor.current?.commands.setCellAttribute(
|
||||
"borderWidth",
|
||||
decreaseBorderWidth()
|
||||
)
|
||||
editor.commands.setCellAttribute("borderWidth", decreaseBorderWidth())
|
||||
}
|
||||
onIncrease={() =>
|
||||
editor.current?.commands.setCellAttribute(
|
||||
"borderWidth",
|
||||
increaseBorderWidth()
|
||||
)
|
||||
}
|
||||
onReset={() =>
|
||||
editor.current?.commands.setCellAttribute("borderWidth", 1)
|
||||
editor.commands.setCellAttribute("borderWidth", increaseBorderWidth())
|
||||
}
|
||||
onReset={() => editor.commands.setCellAttribute("borderWidth", 1)}
|
||||
value={borderWidth + "px"}
|
||||
/>
|
||||
);
|
||||
@@ -316,12 +304,12 @@ export function CellBorderWidth(props: ToolProps) {
|
||||
|
||||
const insertColumnLeft = (editor: Editor): MenuButtonItem => ({
|
||||
...toolToMenuButton(getToolDefinition("insertColumnLeft")),
|
||||
onClick: () => editor.current?.chain().focus().addColumnBefore().run()
|
||||
onClick: () => editor.chain().focus().addColumnBefore().run()
|
||||
});
|
||||
|
||||
const insertColumnRight = (editor: Editor): MenuButtonItem => ({
|
||||
...toolToMenuButton(getToolDefinition("insertColumnRight")),
|
||||
onClick: () => editor.current?.chain().focus().addColumnAfter().run()
|
||||
onClick: () => editor.chain().focus().addColumnAfter().run()
|
||||
});
|
||||
|
||||
const moveColumnLeft = (editor: Editor): MenuButtonItem => ({
|
||||
@@ -336,27 +324,27 @@ const moveColumnRight = (editor: Editor): MenuButtonItem => ({
|
||||
|
||||
const deleteColumn = (editor: Editor): MenuButtonItem => ({
|
||||
...toolToMenuButton(getToolDefinition("deleteColumn")),
|
||||
onClick: () => editor.current?.chain().focus().deleteColumn().run()
|
||||
onClick: () => editor.chain().focus().deleteColumn().run()
|
||||
});
|
||||
|
||||
const splitCells = (editor: Editor): MenuButtonItem => ({
|
||||
...toolToMenuButton(getToolDefinition("splitCells")),
|
||||
onClick: () => editor.current?.chain().focus().splitCell().run()
|
||||
onClick: () => editor.chain().focus().splitCell().run()
|
||||
});
|
||||
|
||||
const mergeCells = (editor: Editor): MenuButtonItem => ({
|
||||
...toolToMenuButton(getToolDefinition("mergeCells")),
|
||||
onClick: () => editor.current?.chain().focus().mergeCells().run()
|
||||
onClick: () => editor.chain().focus().mergeCells().run()
|
||||
});
|
||||
|
||||
const insertRowAbove = (editor: Editor): MenuButtonItem => ({
|
||||
...toolToMenuButton(getToolDefinition("insertRowAbove")),
|
||||
onClick: () => editor.current?.chain().focus().addRowBefore().run()
|
||||
onClick: () => editor.chain().focus().addRowBefore().run()
|
||||
});
|
||||
|
||||
const insertRowBelow = (editor: Editor): MenuButtonItem => ({
|
||||
...toolToMenuButton(getToolDefinition("insertRowBelow")),
|
||||
onClick: () => editor.current?.chain().focus().addRowAfter().run()
|
||||
onClick: () => editor.chain().focus().addRowAfter().run()
|
||||
});
|
||||
|
||||
const moveRowUp = (editor: Editor): MenuButtonItem => ({
|
||||
@@ -370,12 +358,12 @@ const moveRowDown = (editor: Editor): MenuButtonItem => ({
|
||||
|
||||
const deleteRow = (editor: Editor): MenuButtonItem => ({
|
||||
...toolToMenuButton(getToolDefinition("deleteRow")),
|
||||
onClick: () => editor.current?.chain().focus().deleteRow().run()
|
||||
onClick: () => editor.chain().focus().deleteRow().run()
|
||||
});
|
||||
|
||||
const deleteTable = (editor: Editor): MenuButtonItem => ({
|
||||
...toolToMenuButton(getToolDefinition("deleteTable")),
|
||||
onClick: () => editor.current?.chain().focus().deleteTable().run()
|
||||
onClick: () => editor.chain().focus().deleteTable().run()
|
||||
});
|
||||
|
||||
const cellProperties = (editor: Editor): MenuButtonItem => ({
|
||||
|
||||
@@ -38,11 +38,7 @@ function TextDirectionTool(props: TextDirectionToolProps) {
|
||||
<ToolButton
|
||||
{...toolProps}
|
||||
onClick={() =>
|
||||
editor.current
|
||||
?.chain()
|
||||
.focus()
|
||||
.setTextDirection(directionRef.current)
|
||||
.run()
|
||||
editor?.chain().focus().setTextDirection(directionRef.current).run()
|
||||
}
|
||||
disabled={editor.isActive(CodeBlock.name)}
|
||||
toggled={false}
|
||||
|
||||
@@ -49,14 +49,14 @@ export function WebClipFullScreen(props: ToolProps) {
|
||||
const offset = selectionToOffset(editor.state);
|
||||
if (!offset) return;
|
||||
|
||||
const dom = editor.current?.view.nodeDOM(offset.from);
|
||||
const dom = editor.view.nodeDOM(offset.from);
|
||||
if (!dom || !(dom instanceof HTMLElement)) return;
|
||||
|
||||
const iframe = dom.querySelector("iframe");
|
||||
if (!iframe) return;
|
||||
|
||||
iframe.requestFullscreen();
|
||||
editor.current?.commands.updateAttributes("webclip", {
|
||||
editor.commands.updateAttributes("webclip", {
|
||||
fullscreen: true
|
||||
});
|
||||
}}
|
||||
@@ -74,7 +74,7 @@ export function WebClipOpenExternal(props: ToolProps) {
|
||||
const offset = selectionToOffset(editor.state);
|
||||
if (!offset) return;
|
||||
|
||||
const dom = editor.current?.view.nodeDOM(offset.from);
|
||||
const dom = editor.view.nodeDOM(offset.from);
|
||||
if (!dom || !(dom instanceof HTMLElement)) return;
|
||||
|
||||
const iframe = dom.querySelector("iframe");
|
||||
@@ -86,7 +86,7 @@ export function WebClipOpenExternal(props: ToolProps) {
|
||||
{ type: "text/html" }
|
||||
)
|
||||
);
|
||||
editor.current?.commands.openLink(url);
|
||||
editor.storage.openLink?.(url);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -101,7 +101,7 @@ export function WebClipOpenSource(props: ToolProps) {
|
||||
onClick={async () => {
|
||||
const node = findSelectedNode(editor, "webclip");
|
||||
if (!node) return;
|
||||
editor.current?.commands.openLink(node.attrs.src);
|
||||
editor.storage.openLink?.(node.attrs.src);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -24,12 +24,6 @@ export type PermissionRequestEvent = CustomEvent<{ id: keyof UnionCommands }>;
|
||||
|
||||
export class Editor extends TiptapEditor {
|
||||
private mutex: Mutex = new Mutex();
|
||||
/**
|
||||
* Use this to get the latest instance of the editor.
|
||||
* This is required to reduce unnecessary rerenders of
|
||||
* toolbar elements.
|
||||
*/
|
||||
current?: TiptapEditor;
|
||||
|
||||
/**
|
||||
* Request permission before executing a command to make sure user
|
||||
@@ -45,7 +39,7 @@ export class Editor extends TiptapEditor {
|
||||
|
||||
if (!window.dispatchEvent(event)) return undefined;
|
||||
|
||||
return this.current;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,8 +48,6 @@ export class Editor extends TiptapEditor {
|
||||
* you are getting `RangeError: Applying a mismatched transaction` errors.
|
||||
*/
|
||||
threadsafe(callback: (editor: TiptapEditor) => void) {
|
||||
return this.mutex.runExclusive(() =>
|
||||
this.current ? callback(this.current) : void 0
|
||||
);
|
||||
return this.mutex.runExclusive(() => (this ? callback(this) : void 0));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,14 +129,12 @@ export function findMark(
|
||||
export function selectionToOffset(
|
||||
state: EditorState
|
||||
): NodeWithOffset | undefined {
|
||||
const { from, $from } = state.selection;
|
||||
const { from, $from, to, $to } = state.selection;
|
||||
const node = state.doc.nodeAt(from);
|
||||
if (!node) return;
|
||||
|
||||
return {
|
||||
node,
|
||||
node: node || undefined,
|
||||
from: from - $from.textOffset,
|
||||
to: from - $from.textOffset + node.nodeSize
|
||||
to: node ? from - $from.textOffset + node.nodeSize : to - $to.textOffset
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,22 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ProseMirror a[href^="nn://"]::before {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
content: "";
|
||||
background-size: 14px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
|
||||
background-color: var(--accent);
|
||||
mask: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGZpbGw9ImN1cnJlbnRDb2xvciIgZD0iTTMgMWMtMS4xMSAwLTIgLjg5LTIgMnYxMWMwIDEuMTEuODkgMiAyIDJoMTFjMS4xMSAwIDItLjg5IDItMnYtM2gtMnYzSDNWM2gxMXYyaDJWM2MwLTEuMTEtLjg5LTItMi0yTTkgN2MtMS4xMSAwLTIgLjg5LTIgMnYzaDJWOWgxMXYxMUg5di0ySDd2MmMwIDEuMTEuODkgMiAyIDJoMTFjMS4xMSAwIDItLjg5IDItMlY5YzAtMS4xMS0uODktMi0yLTJ6Ii8+PC9zdmc+)
|
||||
no-repeat 50% 50%;
|
||||
mask-size: cover;
|
||||
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.ProseMirror a:hover {
|
||||
filter: brightness(70%);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user