mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-16 19:57:52 +01:00
editor: refactor clipboard extension
This commit is contained in:
committed by
Abdullah Atta
parent
0cfa7ce774
commit
a3d3886c51
11
packages/editor/package-lock.json
generated
11
packages/editor/package-lock.json
generated
@@ -38,7 +38,6 @@
|
|||||||
"@tiptap/extension-underline": "2.0.3",
|
"@tiptap/extension-underline": "2.0.3",
|
||||||
"@tiptap/pm": "2.0.3",
|
"@tiptap/pm": "2.0.3",
|
||||||
"@tiptap/starter-kit": "2.0.3",
|
"@tiptap/starter-kit": "2.0.3",
|
||||||
"clipboard-polyfill": "4.0.0",
|
|
||||||
"detect-indent": "^7.0.0",
|
"detect-indent": "^7.0.0",
|
||||||
"entities": "^4.5.0",
|
"entities": "^4.5.0",
|
||||||
"katex": "0.16.4",
|
"katex": "0.16.4",
|
||||||
@@ -1910,11 +1909,6 @@
|
|||||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/clipboard-polyfill": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/clipboard-polyfill/-/clipboard-polyfill-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-U4KPNJqAYuyOtixCZZUyWTcj+wlI66j07g5ggMRE2DR1VFu/3ZWXkjLAslmme8i065gBSCUblHET7DKQ2Xg3RA=="
|
|
||||||
},
|
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "1.9.3",
|
"version": "1.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||||
@@ -5098,11 +5092,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"clipboard-polyfill": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/clipboard-polyfill/-/clipboard-polyfill-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-U4KPNJqAYuyOtixCZZUyWTcj+wlI66j07g5ggMRE2DR1VFu/3ZWXkjLAslmme8i065gBSCUblHET7DKQ2Xg3RA=="
|
|
||||||
},
|
|
||||||
"color-convert": {
|
"color-convert": {
|
||||||
"version": "1.9.3",
|
"version": "1.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||||
|
|||||||
@@ -33,7 +33,6 @@
|
|||||||
"@tiptap/extension-underline": "2.0.3",
|
"@tiptap/extension-underline": "2.0.3",
|
||||||
"@tiptap/pm": "2.0.3",
|
"@tiptap/pm": "2.0.3",
|
||||||
"@tiptap/starter-kit": "2.0.3",
|
"@tiptap/starter-kit": "2.0.3",
|
||||||
"clipboard-polyfill": "4.0.0",
|
|
||||||
"detect-indent": "^7.0.0",
|
"detect-indent": "^7.0.0",
|
||||||
"entities": "^4.5.0",
|
"entities": "^4.5.0",
|
||||||
"katex": "0.16.4",
|
"katex": "0.16.4",
|
||||||
|
|||||||
@@ -1,152 +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, TextSerializer } from "@tiptap/core";
|
|
||||||
import { Plugin, PluginKey } from "prosemirror-state";
|
|
||||||
import { Fragment, Schema, Slice } from "prosemirror-model";
|
|
||||||
import { ListItem } from "../list-item";
|
|
||||||
import { LIST_NODE_TYPES } from "../../utils/node-types";
|
|
||||||
import {
|
|
||||||
DOMSerializer,
|
|
||||||
DOMParser as ProsemirrorDOMParser
|
|
||||||
} from "@tiptap/pm/model";
|
|
||||||
import { convertTextToHTML } from "../../utils/html";
|
|
||||||
|
|
||||||
export class ClipboardDOMSerializer extends DOMSerializer {
|
|
||||||
static fromSchema(schema: Schema): ClipboardDOMSerializer {
|
|
||||||
return (
|
|
||||||
schema.cached.domSerializer2 ||
|
|
||||||
(schema.cached.domSerializer2 = new ClipboardDOMSerializer(
|
|
||||||
this.nodesFromSchema(schema),
|
|
||||||
this.marksFromSchema(schema)
|
|
||||||
))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
serializeFragment(
|
|
||||||
fragment: Fragment,
|
|
||||||
options?: { document?: Document | undefined } | undefined,
|
|
||||||
target?: HTMLElement | DocumentFragment | undefined
|
|
||||||
): HTMLElement | DocumentFragment {
|
|
||||||
const dom = super.serializeFragment(fragment, options, target);
|
|
||||||
for (const p of dom.querySelectorAll("li > p")) {
|
|
||||||
if (p.parentElement && p.parentElement.childElementCount > 1) continue;
|
|
||||||
p.parentElement?.append(...p.childNodes);
|
|
||||||
p.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const p of dom.querySelectorAll('p[data-spacing="single"]')) {
|
|
||||||
if (!p.previousElementSibling || p.previousElementSibling.tagName !== "P")
|
|
||||||
continue;
|
|
||||||
if (p.previousElementSibling.childNodes.length > 0)
|
|
||||||
p.previousElementSibling.appendChild(document.createElement("br"));
|
|
||||||
p.previousElementSibling.append(...p.childNodes);
|
|
||||||
p.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
return dom;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ClipboardTextSerializer = Extension.create({
|
|
||||||
name: "clipboardTextSerializer",
|
|
||||||
|
|
||||||
addProseMirrorPlugins() {
|
|
||||||
return [
|
|
||||||
new Plugin({
|
|
||||||
key: new PluginKey("clipboardTextSerializer"),
|
|
||||||
props: {
|
|
||||||
transformCopied,
|
|
||||||
clipboardSerializer: ClipboardDOMSerializer.fromSchema(
|
|
||||||
this.editor.view.state.schema
|
|
||||||
),
|
|
||||||
clipboardTextParser: (text) => {
|
|
||||||
const doc = new DOMParser().parseFromString(
|
|
||||||
convertTextToHTML(text),
|
|
||||||
"text/html"
|
|
||||||
);
|
|
||||||
return ProsemirrorDOMParser.fromSchema(
|
|
||||||
this.editor.view.state.schema
|
|
||||||
).parseSlice(doc, { preserveWhitespace: "full" });
|
|
||||||
},
|
|
||||||
clipboardTextSerializer: (content, view) => {
|
|
||||||
return getTextBetween(content, view.state.schema);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export function transformCopied(slice: Slice) {
|
|
||||||
// when copying a single list item, we shouldn't retain the
|
|
||||||
// list formatting but copy it as a paragraph.
|
|
||||||
const maybeList = slice.content.firstChild;
|
|
||||||
if (
|
|
||||||
maybeList &&
|
|
||||||
LIST_NODE_TYPES.includes(maybeList.type.name) &&
|
|
||||||
maybeList.childCount === 1 &&
|
|
||||||
maybeList.firstChild
|
|
||||||
) {
|
|
||||||
return transformCopied(new Slice(maybeList.firstChild.content, 0, 0));
|
|
||||||
}
|
|
||||||
return slice;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTextBetween(slice: Slice, schema: Schema): string {
|
|
||||||
const range = { from: 0, to: slice.size };
|
|
||||||
const separator = "\n";
|
|
||||||
let text = "";
|
|
||||||
let separated = true;
|
|
||||||
|
|
||||||
slice.content.nodesBetween(0, slice.size, (node, pos, parent, index) => {
|
|
||||||
const textSerializer = schema.nodes[node.type.name]?.spec
|
|
||||||
.toText as TextSerializer;
|
|
||||||
|
|
||||||
if (textSerializer) {
|
|
||||||
if (node.isBlock && !separated) {
|
|
||||||
text += separator;
|
|
||||||
separated = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parent) {
|
|
||||||
text += textSerializer({
|
|
||||||
node,
|
|
||||||
pos,
|
|
||||||
parent,
|
|
||||||
index,
|
|
||||||
range
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (node.isText) {
|
|
||||||
text += node?.text;
|
|
||||||
separated = false;
|
|
||||||
} else if (node.isBlock && !!text) {
|
|
||||||
// we don't want double spaced list items when pasting
|
|
||||||
if (index === 0 && parent?.type.name === ListItem.name) return;
|
|
||||||
|
|
||||||
text += separator;
|
|
||||||
if (node.attrs.spacing === "double" && node.childCount > 0)
|
|
||||||
text += separator;
|
|
||||||
separated = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
@@ -17,11 +17,32 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { encodeNonAsciiHTML } from "entities";
|
import {
|
||||||
|
DOMParser as ProsemirrorDOMParser,
|
||||||
|
ParseOptions
|
||||||
|
} from "@tiptap/pm/model";
|
||||||
|
import { Schema, Slice } from "prosemirror-model";
|
||||||
|
|
||||||
export function convertBrToParagraph(html: string) {
|
export class ClipboardDOMParser extends ProsemirrorDOMParser {
|
||||||
const doc = new DOMParser().parseFromString(html, "text/html");
|
static fromSchema(schema: Schema): ClipboardDOMParser {
|
||||||
for (const br of doc.querySelectorAll("br")) {
|
return (
|
||||||
|
(schema.cached.clipboardDomParser as ClipboardDOMParser) ||
|
||||||
|
(schema.cached.clipboardDomParser = new ClipboardDOMParser(
|
||||||
|
schema,
|
||||||
|
(ProsemirrorDOMParser as any).schemaRules(schema)
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
parseSlice(dom: Node, options?: ParseOptions | undefined): Slice {
|
||||||
|
convertBrToSingleSpacedParagraphs(dom);
|
||||||
|
return super.parseSlice(dom, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertBrToSingleSpacedParagraphs(dom: Node) {
|
||||||
|
if (!(dom instanceof HTMLElement)) return;
|
||||||
|
for (const br of dom.querySelectorAll("br")) {
|
||||||
let paragraph = br.closest("p");
|
let paragraph = br.closest("p");
|
||||||
|
|
||||||
// if no paragraph is found over the br, we add one.
|
// if no paragraph is found over the br, we add one.
|
||||||
@@ -37,14 +58,13 @@ export function convertBrToParagraph(html: string) {
|
|||||||
if (paragraph) {
|
if (paragraph) {
|
||||||
splitOn(paragraph, br);
|
splitOn(paragraph, br);
|
||||||
const children = Array.from(paragraph.childNodes.values());
|
const children = Array.from(paragraph.childNodes.values());
|
||||||
const newParagraph = doc.createElement("p");
|
const newParagraph = document.createElement("p");
|
||||||
newParagraph.dataset.spacing = "single";
|
newParagraph.dataset.spacing = "single";
|
||||||
newParagraph.append(...children.slice(children.indexOf(br) + 1));
|
newParagraph.append(...children.slice(children.indexOf(br) + 1));
|
||||||
paragraph.insertAdjacentElement("afterend", newParagraph);
|
paragraph.insertAdjacentElement("afterend", newParagraph);
|
||||||
br.remove();
|
br.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return doc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function splitOn(bound: Element, cutElement: Element) {
|
function splitOn(bound: Element, cutElement: Element) {
|
||||||
@@ -63,25 +83,3 @@ function splitOn(bound: Element, cutElement: Element) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertTextToHTML(src: string) {
|
|
||||||
return src
|
|
||||||
.split(/[\r\n]/)
|
|
||||||
.map((line) =>
|
|
||||||
line
|
|
||||||
? `<p data-spacing="single">${encodeLine(line)}</p>`
|
|
||||||
: `<p data-spacing="single"></p>`
|
|
||||||
)
|
|
||||||
.join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
function encodeLine(line: string) {
|
|
||||||
line = encodeNonAsciiHTML(line);
|
|
||||||
line = line.replace(/(^ +)|( {2,})/g, (sub, ...args) => {
|
|
||||||
const [starting, inline] = args;
|
|
||||||
if (starting) return " ".repeat(starting.length);
|
|
||||||
if (inline) return " ".repeat(inline.length);
|
|
||||||
return sub;
|
|
||||||
});
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
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 { Fragment, Schema } from "prosemirror-model";
|
||||||
|
import { DOMSerializer } from "@tiptap/pm/model";
|
||||||
|
|
||||||
|
export class ClipboardDOMSerializer extends DOMSerializer {
|
||||||
|
static fromSchema(schema: Schema): ClipboardDOMSerializer {
|
||||||
|
return (
|
||||||
|
schema.cached.clipboardDomSerializer ||
|
||||||
|
(schema.cached.clipboardDomSerializer = new ClipboardDOMSerializer(
|
||||||
|
this.nodesFromSchema(schema),
|
||||||
|
this.marksFromSchema(schema)
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
serializeFragment(
|
||||||
|
fragment: Fragment,
|
||||||
|
options?: { document?: Document | undefined } | undefined,
|
||||||
|
target?: HTMLElement | DocumentFragment | undefined
|
||||||
|
): HTMLElement | DocumentFragment {
|
||||||
|
const dom = super.serializeFragment(fragment, options, target);
|
||||||
|
for (const p of dom.querySelectorAll("li > p")) {
|
||||||
|
if (p.parentElement && p.parentElement.childElementCount > 1) continue;
|
||||||
|
p.parentElement?.append(...p.childNodes);
|
||||||
|
p.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const p of dom.querySelectorAll('p[data-spacing="single"]')) {
|
||||||
|
if (!p.previousElementSibling || p.previousElementSibling.tagName !== "P")
|
||||||
|
continue;
|
||||||
|
if (p.previousElementSibling.childNodes.length > 0)
|
||||||
|
p.previousElementSibling.appendChild(document.createElement("br"));
|
||||||
|
p.previousElementSibling.append(...p.childNodes);
|
||||||
|
p.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
return dom;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
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 { ResolvedPos, Slice } from "@tiptap/pm/model";
|
||||||
|
import { encodeNonAsciiHTML } from "entities";
|
||||||
|
import { ClipboardDOMParser } from "./clipboard-dom-parser";
|
||||||
|
import { EditorView } from "@tiptap/pm/view";
|
||||||
|
|
||||||
|
export function clipboardTextParser(
|
||||||
|
text: string,
|
||||||
|
_$context: ResolvedPos,
|
||||||
|
_plain: boolean,
|
||||||
|
view: EditorView
|
||||||
|
): Slice {
|
||||||
|
const doc = new DOMParser().parseFromString(
|
||||||
|
convertTextToHTML(text),
|
||||||
|
"text/html"
|
||||||
|
);
|
||||||
|
return ClipboardDOMParser.fromSchema(view.state.schema).parseSlice(doc, {
|
||||||
|
preserveWhitespace: "full"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertTextToHTML(src: string) {
|
||||||
|
return src
|
||||||
|
.split(/[\r\n]/)
|
||||||
|
.map((line) =>
|
||||||
|
line
|
||||||
|
? `<p data-spacing="single">${encodeLine(line)}</p>`
|
||||||
|
: `<p data-spacing="single"></p>`
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeLine(line: string) {
|
||||||
|
line = encodeNonAsciiHTML(line);
|
||||||
|
line = line.replace(/(^ +)|( {2,})/g, (sub, ...args) => {
|
||||||
|
const [starting, inline] = args;
|
||||||
|
if (starting) return " ".repeat(starting.length);
|
||||||
|
if (inline) return " ".repeat(inline.length);
|
||||||
|
return sub;
|
||||||
|
});
|
||||||
|
return line;
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
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 { TextSerializer } from "@tiptap/core";
|
||||||
|
import { Schema, Slice } from "prosemirror-model";
|
||||||
|
import { ListItem } from "../list-item";
|
||||||
|
import { EditorView } from "@tiptap/pm/view";
|
||||||
|
|
||||||
|
export function clipboardTextSerializer(content: Slice, view: EditorView) {
|
||||||
|
return getTextBetween(content, view.state.schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTextBetween(slice: Slice, schema: Schema): string {
|
||||||
|
const range = { from: 0, to: slice.size };
|
||||||
|
const separator = "\n";
|
||||||
|
let text = "";
|
||||||
|
let separated = true;
|
||||||
|
|
||||||
|
slice.content.nodesBetween(0, slice.size, (node, pos, parent, index) => {
|
||||||
|
const textSerializer = schema.nodes[node.type.name]?.spec
|
||||||
|
.toText as TextSerializer;
|
||||||
|
|
||||||
|
if (textSerializer) {
|
||||||
|
if (node.isBlock && !separated) {
|
||||||
|
text += separator;
|
||||||
|
separated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent) {
|
||||||
|
text += textSerializer({
|
||||||
|
node,
|
||||||
|
pos,
|
||||||
|
parent,
|
||||||
|
index,
|
||||||
|
range
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (node.isText) {
|
||||||
|
text += node?.text;
|
||||||
|
separated = false;
|
||||||
|
} else if (node.isBlock && !!text) {
|
||||||
|
// we don't want double spaced list items when pasting
|
||||||
|
if (index === 0 && parent?.type.name === ListItem.name) return;
|
||||||
|
|
||||||
|
text += separator;
|
||||||
|
if (node.attrs.spacing === "double" && node.childCount > 0)
|
||||||
|
text += separator;
|
||||||
|
separated = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
@@ -16,8 +16,15 @@ GNU General Public License for more details.
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Extension } from "@tiptap/core";
|
import { Extension } from "@tiptap/core";
|
||||||
import { writeText } from "clipboard-polyfill";
|
import { Plugin, PluginKey } from "prosemirror-state";
|
||||||
|
import { Slice } from "prosemirror-model";
|
||||||
|
import { LIST_NODE_TYPES } from "../../utils/node-types";
|
||||||
|
import { ClipboardDOMParser } from "./clipboard-dom-parser";
|
||||||
|
import { ClipboardDOMSerializer } from "./clipboard-dom-serializer";
|
||||||
|
import { clipboardTextParser } from "./clipboard-text-parser";
|
||||||
|
import { clipboardTextSerializer } from "./clipboard-text-serializer";
|
||||||
|
|
||||||
declare module "@tiptap/core" {
|
declare module "@tiptap/core" {
|
||||||
interface Commands<ReturnType> {
|
interface Commands<ReturnType> {
|
||||||
@@ -31,14 +38,15 @@ export type ClipboardOptions = {
|
|||||||
copyToClipboard: (text: string) => void;
|
copyToClipboard: (text: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Clipboard = Extension.create<ClipboardOptions>({
|
export const Clipboard = Extension.create({
|
||||||
|
name: "clipboard",
|
||||||
|
|
||||||
addOptions() {
|
addOptions() {
|
||||||
return {
|
return {
|
||||||
copyToClipboard: (text) => {
|
copyToClipboard: () => {}
|
||||||
writeText(text);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
addCommands() {
|
addCommands() {
|
||||||
return {
|
return {
|
||||||
copyToClipboard: (text: string) => (props) => {
|
copyToClipboard: (text: string) => (props) => {
|
||||||
@@ -46,5 +54,39 @@ export const Clipboard = Extension.create<ClipboardOptions>({
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
return [
|
||||||
|
new Plugin({
|
||||||
|
key: new PluginKey("clipboard"),
|
||||||
|
props: {
|
||||||
|
clipboardParser: ClipboardDOMParser.fromSchema(
|
||||||
|
this.editor.view.state.schema
|
||||||
|
),
|
||||||
|
clipboardSerializer: ClipboardDOMSerializer.fromSchema(
|
||||||
|
this.editor.view.state.schema
|
||||||
|
),
|
||||||
|
transformCopied,
|
||||||
|
clipboardTextParser,
|
||||||
|
clipboardTextSerializer
|
||||||
|
}
|
||||||
|
})
|
||||||
|
];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function transformCopied(slice: Slice) {
|
||||||
|
// when copying a single list item, we shouldn't retain the
|
||||||
|
// list formatting but copy it as a paragraph.
|
||||||
|
const maybeList = slice.content.firstChild;
|
||||||
|
if (
|
||||||
|
maybeList &&
|
||||||
|
LIST_NODE_TYPES.includes(maybeList.type.name) &&
|
||||||
|
maybeList.childCount === 1 &&
|
||||||
|
maybeList.firstChild
|
||||||
|
) {
|
||||||
|
return transformCopied(new Slice(maybeList.firstChild.content, 0, 0));
|
||||||
|
}
|
||||||
|
return slice;
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ GNU General Public License for more details.
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import { Clipboard } from "./clipboard";
|
|
||||||
|
|
||||||
export * from "./clipboard";
|
export * from "./clipboard";
|
||||||
|
export { Clipboard as default } from "./clipboard";
|
||||||
export default Clipboard;
|
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`convert br tags to paragraphs (
|
||||||
|
<html><body>
|
||||||
|
<!--StartFragment-->A troll, they call me, but I have no wish<br>
|
||||||
|
to be associated with those dolls<br>
|
||||||
|
<br>
|
||||||
|
We lack religion, purpose, politics,<br>
|
||||||
|
and yet, we somehow manage to get by.<br>
|
||||||
|
</body>
|
||||||
|
</html>) 1`] = `
|
||||||
|
"<!--StartFragment-->A troll, they call me, but I have no wish<br>
|
||||||
|
to be associated with those dolls<br>
|
||||||
|
<br>
|
||||||
|
We lack religion, purpose, politics,<br>
|
||||||
|
and yet, we somehow manage to get by.<br>"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`convert br tags to paragraphs (<html><body>
|
||||||
|
<!--StartFragment--><p dir="auto">When I try to paste something (e.g. email content) to a note, the styling is kept, which is good, but the newlines are removed.<br>
|
||||||
|
Also when I share the selection to Notesnook via the share functionality from Android, I have the same issue.</p>
|
||||||
|
<hr>
|
||||||
|
<p dir="auto"><strong>Device information:</strong><br>
|
||||||
|
App version: 2.3.0<br>
|
||||||
|
Platform: android<br>
|
||||||
|
Model: OnePlus-CPH2409-31<br>
|
||||||
|
Pro: true<br>
|
||||||
|
Logged in: yes</p><!--EndFragment-->
|
||||||
|
</body>
|
||||||
|
</html>) 1`] = `
|
||||||
|
"<!--StartFragment--><p dir=\\"auto\\">When I try to paste something (e.g. email content) to a note, the styling is kept, which is good, but the newlines are removed.<br>
|
||||||
|
Also when I share the selection to Notesnook via the share functionality from Android, I have the same issue.</p>
|
||||||
|
<hr>
|
||||||
|
<p dir=\\"auto\\"><strong>Device information:</strong><br>
|
||||||
|
App version: 2.3.0<br>
|
||||||
|
Platform: android<br>
|
||||||
|
Model: OnePlus-CPH2409-31<br>
|
||||||
|
Pro: true<br>
|
||||||
|
Logged in: yes</p><!--EndFragment-->"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`convert br tags to paragraphs (<html><body>
|
||||||
|
<!--StartFragment--><span class="css-901oao css-16my406 r-poiln3 r-bcqeeo r-qvutc0">Why switch from Gmail?
|
||||||
|
|
||||||
|
Not sacrificing features for more privacy, prefer using one app, in many public groups and channels (Telegram)
|
||||||
|
|
||||||
|
LibreOffice Slow & buggy
|
||||||
|
|
||||||
|
Switched to Brave for the better Android app, more private out of the box & unsure if uBlock Origin closes gap</span><!--EndFragment-->
|
||||||
|
</body>
|
||||||
|
</html>) 1`] = `
|
||||||
|
"<!--StartFragment--><span class=\\"css-901oao css-16my406 r-poiln3 r-bcqeeo r-qvutc0\\">Why switch from Gmail?
|
||||||
|
|
||||||
|
Not sacrificing features for more privacy, prefer using one app, in many public groups and channels (Telegram)
|
||||||
|
|
||||||
|
LibreOffice Slow & buggy
|
||||||
|
|
||||||
|
Switched to Brave for the better Android app, more private out of the box & unsure if uBlock Origin closes gap</span><!--EndFragment-->"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`convert br tags to paragraphs (<p><br/></p>) 1`] = `"<p><br></p>"`;
|
||||||
|
|
||||||
|
exports[`convert br tags to paragraphs (<p>line <em>1<br>line</em> 2</p>) 1`] = `"<p>line <em>1<br>line</em> 2</p>"`;
|
||||||
|
|
||||||
|
exports[`convert br tags to paragraphs (<p>line <span><em>1<br data-some="hello">line</em></span> 2</p>) 1`] = `"<p>line <span><em>1<br data-some=\\"hello\\">line</em></span> 2</p>"`;
|
||||||
|
|
||||||
|
exports[`convert br tags to paragraphs (<p>line <span><em>1<br>line</em></span> 2</p>) 1`] = `"<p>line <span><em>1<br>line</em></span> 2</p>"`;
|
||||||
|
|
||||||
|
exports[`convert br tags to paragraphs (<p>line 1<br>line 2</p>) 1`] = `"<p>line 1<br>line 2</p>"`;
|
||||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { test } from "vitest";
|
import { test } from "vitest";
|
||||||
import { convertBrToParagraph } from "../html";
|
import { convertBrToSingleSpacedParagraphs } from "../clipboard-dom-parser";
|
||||||
|
|
||||||
const cases = [
|
const cases = [
|
||||||
[`<p>line 1<br>line 2</p>`],
|
[`<p>line 1<br>line 2</p>`],
|
||||||
@@ -68,8 +68,8 @@ and yet, we somehow manage to get by.<br>
|
|||||||
for (const testCase of cases) {
|
for (const testCase of cases) {
|
||||||
const [html, expected] = testCase;
|
const [html, expected] = testCase;
|
||||||
test(`convert br tags to paragraphs (${testCase})`, (t) => {
|
test(`convert br tags to paragraphs (${testCase})`, (t) => {
|
||||||
t.expect(
|
const element = new DOMParser().parseFromString(html, "text/html");
|
||||||
convertBrToParagraph(html).body.innerHTML.trim()
|
convertBrToSingleSpacedParagraphs(element);
|
||||||
).toMatchSnapshot();
|
t.expect(element.body.innerHTML.trim()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -21,12 +21,10 @@ import { test } from "vitest";
|
|||||||
import { createEditor, h } from "../../../../test-utils";
|
import { createEditor, h } from "../../../../test-utils";
|
||||||
import OrderedList from "../../ordered-list";
|
import OrderedList from "../../ordered-list";
|
||||||
import { ListItem } from "../../list-item";
|
import { ListItem } from "../../list-item";
|
||||||
import {
|
import { transformCopied } from "../index";
|
||||||
getTextBetween,
|
|
||||||
transformCopied,
|
|
||||||
ClipboardDOMSerializer
|
|
||||||
} from "../index";
|
|
||||||
import { Paragraph } from "../../paragraph";
|
import { Paragraph } from "../../paragraph";
|
||||||
|
import { ClipboardDOMSerializer } from "../clipboard-dom-serializer";
|
||||||
|
import { clipboardTextSerializer } from "../clipboard-text-serializer";
|
||||||
|
|
||||||
test("copied list items shouldn't contain extra newlines", (t) => {
|
test("copied list items shouldn't contain extra newlines", (t) => {
|
||||||
const { editor } = createEditor({
|
const { editor } = createEditor({
|
||||||
@@ -59,9 +57,9 @@ test("copied list items shouldn't contain extra newlines", (t) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
t.expect(
|
t.expect(
|
||||||
getTextBetween(
|
clipboardTextSerializer(
|
||||||
editor.state.doc.slice(0, editor.state.doc.nodeSize - 2),
|
editor.state.doc.slice(0, editor.state.doc.nodeSize - 2),
|
||||||
editor.schema
|
editor.view
|
||||||
)
|
)
|
||||||
).toBe(`This is line: number 1.
|
).toBe(`This is line: number 1.
|
||||||
And this is line number 2.
|
And this is line number 2.
|
||||||
@@ -194,9 +192,9 @@ for (const testCase of paragraphTestCases) {
|
|||||||
).toBe(testCase.expectedHtml);
|
).toBe(testCase.expectedHtml);
|
||||||
|
|
||||||
t.expect(
|
t.expect(
|
||||||
getTextBetween(
|
clipboardTextSerializer(
|
||||||
editor.state.doc.slice(0, editor.state.doc.nodeSize - 2),
|
editor.state.doc.slice(0, editor.state.doc.nodeSize - 2),
|
||||||
editor.schema
|
editor.view
|
||||||
)
|
)
|
||||||
).toBe(testCase.expectedText);
|
).toBe(testCase.expectedText);
|
||||||
});
|
});
|
||||||
File diff suppressed because one or more lines are too long
@@ -122,7 +122,10 @@ test("pasting code from vscode should automatically create a syntax highlighted
|
|||||||
(clipboardEvent as unknown as any)["clipboardData"] = {
|
(clipboardEvent as unknown as any)["clipboardData"] = {
|
||||||
getData: (type: string) =>
|
getData: (type: string) =>
|
||||||
type === "text/plain"
|
type === "text/plain"
|
||||||
? "function hello() { }"
|
? `function hello()
|
||||||
|
{
|
||||||
|
const world = "hello";
|
||||||
|
}`
|
||||||
: type === "vscode-editor-data"
|
: type === "vscode-editor-data"
|
||||||
? JSON.stringify({ mode: "javascript" })
|
? JSON.stringify({ mode: "javascript" })
|
||||||
: undefined
|
: undefined
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ import { useEffect, useMemo } from "react";
|
|||||||
import "./extensions";
|
import "./extensions";
|
||||||
import { AttachmentNode, AttachmentOptions } from "./extensions/attachment";
|
import { AttachmentNode, AttachmentOptions } from "./extensions/attachment";
|
||||||
import BulletList from "./extensions/bullet-list";
|
import BulletList from "./extensions/bullet-list";
|
||||||
import { ClipboardTextSerializer } from "./extensions/clipboard-text-serializer";
|
|
||||||
import { CodeBlock } from "./extensions/code-block";
|
import { CodeBlock } from "./extensions/code-block";
|
||||||
import { Codemark } from "./extensions/code-mark";
|
import { Codemark } from "./extensions/code-mark";
|
||||||
import { DateTime, DateTimeOptions } from "./extensions/date-time";
|
import { DateTime, DateTimeOptions } from "./extensions/date-time";
|
||||||
@@ -76,7 +75,6 @@ import { useToolbarStore } from "./toolbar/stores/toolbar-store";
|
|||||||
import { DownloadOptions } from "./utils/downloader";
|
import { DownloadOptions } from "./utils/downloader";
|
||||||
import { Heading } from "./extensions/heading";
|
import { Heading } from "./extensions/heading";
|
||||||
import Clipboard, { ClipboardOptions } from "./extensions/clipboard";
|
import Clipboard, { ClipboardOptions } from "./extensions/clipboard";
|
||||||
import { convertBrToParagraph } from "./utils/html";
|
|
||||||
import Blockquote from "./extensions/blockquote";
|
import Blockquote from "./extensions/blockquote";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -147,14 +145,10 @@ const useTiptap = (
|
|||||||
() => ({
|
() => ({
|
||||||
enableCoreExtensions: false,
|
enableCoreExtensions: false,
|
||||||
editorProps: {
|
editorProps: {
|
||||||
...editorProps,
|
...editorProps
|
||||||
transformPastedHTML(html) {
|
|
||||||
return convertBrToParagraph(html).documentElement.outerHTML;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
extensions: [
|
extensions: [
|
||||||
...CoreExtensions,
|
...CoreExtensions,
|
||||||
ClipboardTextSerializer,
|
|
||||||
NodeViewSelectionNotifier,
|
NodeViewSelectionNotifier,
|
||||||
SearchReplace,
|
SearchReplace,
|
||||||
TextStyle.extend({
|
TextStyle.extend({
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
||||||
|
|
||||||
exports[`convert br tags to paragraphs (
|
|
||||||
<html><body>
|
|
||||||
<!--StartFragment-->A troll, they call me, but I have no wish<br>
|
|
||||||
to be associated with those dolls<br>
|
|
||||||
<br>
|
|
||||||
We lack religion, purpose, politics,<br>
|
|
||||||
and yet, we somehow manage to get by.<br>
|
|
||||||
</body>
|
|
||||||
</html>) 1`] = `"<p><!--StartFragment-->A troll, they call me, but I have no wish</p><p data-spacing=\\"single\\">to be associated with those dolls</p><p data-spacing=\\"single\\"></p><p data-spacing=\\"single\\">We lack religion, purpose, politics,</p><p data-spacing=\\"single\\">and yet, we somehow manage to get by.</p><p data-spacing=\\"single\\"></p><p data-spacing=\\"single\\"> </p>"`;
|
|
||||||
|
|
||||||
exports[`convert br tags to paragraphs (<html><body>
|
|
||||||
<!--StartFragment--><p dir="auto">When I try to paste something (e.g. email content) to a note, the styling is kept, which is good, but the newlines are removed.<br>
|
|
||||||
Also when I share the selection to Notesnook via the share functionality from Android, I have the same issue.</p>
|
|
||||||
<hr>
|
|
||||||
<p dir="auto"><strong>Device information:</strong><br>
|
|
||||||
App version: 2.3.0<br>
|
|
||||||
Platform: android<br>
|
|
||||||
Model: OnePlus-CPH2409-31<br>
|
|
||||||
Pro: true<br>
|
|
||||||
Logged in: yes</p><!--EndFragment-->
|
|
||||||
</body>
|
|
||||||
</html>) 1`] = `"<!--StartFragment--><p dir=\\"auto\\">When I try to paste something (e.g. email content) to a note, the styling is kept, which is good, but the newlines are removed.</p><p data-spacing=\\"single\\"> Also when I share the selection to Notesnook via the share functionality from Android, I have the same issue.</p> <hr> <p dir=\\"auto\\"><strong>Device information:</strong></p><p data-spacing=\\"single\\"> App version: 2.3.0</p><p data-spacing=\\"single\\"> Platform: android</p><p data-spacing=\\"single\\"> Model: OnePlus-CPH2409-31</p><p data-spacing=\\"single\\"> Pro: true</p><p data-spacing=\\"single\\"> Logged in: yes</p><!--EndFragment-->"`;
|
|
||||||
|
|
||||||
exports[`convert br tags to paragraphs (<html><body>
|
|
||||||
<!--StartFragment--><span class="css-901oao css-16my406 r-poiln3 r-bcqeeo r-qvutc0">Why switch from Gmail?
|
|
||||||
|
|
||||||
Not sacrificing features for more privacy, prefer using one app, in many public groups and channels (Telegram)
|
|
||||||
|
|
||||||
LibreOffice Slow & buggy
|
|
||||||
|
|
||||||
Switched to Brave for the better Android app, more private out of the box & unsure if uBlock Origin closes gap</span><!--EndFragment-->
|
|
||||||
</body>
|
|
||||||
</html>) 1`] = `"<!--StartFragment--><span class=\\"css-901oao css-16my406 r-poiln3 r-bcqeeo r-qvutc0\\"><p>Why switch from Gmail? </p><p data-spacing=\\"single\\"></p><p data-spacing=\\"single\\"> Not sacrificing features for more privacy, prefer using one app, in many public groups and channels (Telegram)</p><p data-spacing=\\"single\\"> </p><p data-spacing=\\"single\\"> LibreOffice Slow & buggy</p><p data-spacing=\\"single\\"> </p><p data-spacing=\\"single\\"> Switched to Brave for the better Android app, more private out of the box & unsure if uBlock Origin closes gap</p></span><!--EndFragment-->"`;
|
|
||||||
|
|
||||||
exports[`convert br tags to paragraphs (<p><br/></p>) 1`] = `"<p><br></p>"`;
|
|
||||||
|
|
||||||
exports[`convert br tags to paragraphs (<p>line <em>1<br>line</em> 2</p>) 1`] = `"<p>line <em>1</em></p><p data-spacing=\\"single\\"><em>line</em> 2</p>"`;
|
|
||||||
|
|
||||||
exports[`convert br tags to paragraphs (<p>line <span><em>1<br data-some="hello">line</em></span> 2</p>) 1`] = `"<p>line <span><em>1</em></span></p><p data-spacing=\\"single\\"><span><em>line</em></span> 2</p>"`;
|
|
||||||
|
|
||||||
exports[`convert br tags to paragraphs (<p>line <span><em>1<br>line</em></span> 2</p>) 1`] = `"<p>line <span><em>1</em></span></p><p data-spacing=\\"single\\"><span><em>line</em></span> 2</p>"`;
|
|
||||||
|
|
||||||
exports[`convert br tags to paragraphs (<p>line 1<br>line 2</p>) 1`] = `"<p>line 1</p><p data-spacing=\\"single\\">line 2</p>"`;
|
|
||||||
Reference in New Issue
Block a user