mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-16 11:47:54 +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/pm": "2.0.3",
|
||||
"@tiptap/starter-kit": "2.0.3",
|
||||
"clipboard-polyfill": "4.0.0",
|
||||
"detect-indent": "^7.0.0",
|
||||
"entities": "^4.5.0",
|
||||
"katex": "0.16.4",
|
||||
@@ -1910,11 +1909,6 @@
|
||||
"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": {
|
||||
"version": "1.9.3",
|
||||
"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": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
"@tiptap/extension-underline": "2.0.3",
|
||||
"@tiptap/pm": "2.0.3",
|
||||
"@tiptap/starter-kit": "2.0.3",
|
||||
"clipboard-polyfill": "4.0.0",
|
||||
"detect-indent": "^7.0.0",
|
||||
"entities": "^4.5.0",
|
||||
"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/>.
|
||||
*/
|
||||
|
||||
import { encodeNonAsciiHTML } from "entities";
|
||||
import {
|
||||
DOMParser as ProsemirrorDOMParser,
|
||||
ParseOptions
|
||||
} from "@tiptap/pm/model";
|
||||
import { Schema, Slice } from "prosemirror-model";
|
||||
|
||||
export function convertBrToParagraph(html: string) {
|
||||
const doc = new DOMParser().parseFromString(html, "text/html");
|
||||
for (const br of doc.querySelectorAll("br")) {
|
||||
export class ClipboardDOMParser extends ProsemirrorDOMParser {
|
||||
static fromSchema(schema: Schema): ClipboardDOMParser {
|
||||
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");
|
||||
|
||||
// if no paragraph is found over the br, we add one.
|
||||
@@ -37,14 +58,13 @@ export function convertBrToParagraph(html: string) {
|
||||
if (paragraph) {
|
||||
splitOn(paragraph, br);
|
||||
const children = Array.from(paragraph.childNodes.values());
|
||||
const newParagraph = doc.createElement("p");
|
||||
const newParagraph = document.createElement("p");
|
||||
newParagraph.dataset.spacing = "single";
|
||||
newParagraph.append(...children.slice(children.indexOf(br) + 1));
|
||||
paragraph.insertAdjacentElement("afterend", newParagraph);
|
||||
br.remove();
|
||||
}
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
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
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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" {
|
||||
interface Commands<ReturnType> {
|
||||
@@ -31,14 +38,15 @@ export type ClipboardOptions = {
|
||||
copyToClipboard: (text: string) => void;
|
||||
};
|
||||
|
||||
export const Clipboard = Extension.create<ClipboardOptions>({
|
||||
export const Clipboard = Extension.create({
|
||||
name: "clipboard",
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
copyToClipboard: (text) => {
|
||||
writeText(text);
|
||||
}
|
||||
copyToClipboard: () => {}
|
||||
};
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
copyToClipboard: (text: string) => (props) => {
|
||||
@@ -46,5 +54,39 @@ export const Clipboard = Extension.create<ClipboardOptions>({
|
||||
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
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { Clipboard } from "./clipboard";
|
||||
|
||||
export * from "./clipboard";
|
||||
|
||||
export default Clipboard;
|
||||
export { Clipboard as default } from "./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 { convertBrToParagraph } from "../html";
|
||||
import { convertBrToSingleSpacedParagraphs } from "../clipboard-dom-parser";
|
||||
|
||||
const cases = [
|
||||
[`<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) {
|
||||
const [html, expected] = testCase;
|
||||
test(`convert br tags to paragraphs (${testCase})`, (t) => {
|
||||
t.expect(
|
||||
convertBrToParagraph(html).body.innerHTML.trim()
|
||||
).toMatchSnapshot();
|
||||
const element = new DOMParser().parseFromString(html, "text/html");
|
||||
convertBrToSingleSpacedParagraphs(element);
|
||||
t.expect(element.body.innerHTML.trim()).toMatchSnapshot();
|
||||
});
|
||||
}
|
||||
@@ -21,12 +21,10 @@ import { test } from "vitest";
|
||||
import { createEditor, h } from "../../../../test-utils";
|
||||
import OrderedList from "../../ordered-list";
|
||||
import { ListItem } from "../../list-item";
|
||||
import {
|
||||
getTextBetween,
|
||||
transformCopied,
|
||||
ClipboardDOMSerializer
|
||||
} from "../index";
|
||||
import { transformCopied } from "../index";
|
||||
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) => {
|
||||
const { editor } = createEditor({
|
||||
@@ -59,9 +57,9 @@ test("copied list items shouldn't contain extra newlines", (t) => {
|
||||
);
|
||||
|
||||
t.expect(
|
||||
getTextBetween(
|
||||
clipboardTextSerializer(
|
||||
editor.state.doc.slice(0, editor.state.doc.nodeSize - 2),
|
||||
editor.schema
|
||||
editor.view
|
||||
)
|
||||
).toBe(`This is line: number 1.
|
||||
And this is line number 2.
|
||||
@@ -194,9 +192,9 @@ for (const testCase of paragraphTestCases) {
|
||||
).toBe(testCase.expectedHtml);
|
||||
|
||||
t.expect(
|
||||
getTextBetween(
|
||||
clipboardTextSerializer(
|
||||
editor.state.doc.slice(0, editor.state.doc.nodeSize - 2),
|
||||
editor.schema
|
||||
editor.view
|
||||
)
|
||||
).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"] = {
|
||||
getData: (type: string) =>
|
||||
type === "text/plain"
|
||||
? "function hello() { }"
|
||||
? `function hello()
|
||||
{
|
||||
const world = "hello";
|
||||
}`
|
||||
: type === "vscode-editor-data"
|
||||
? JSON.stringify({ mode: "javascript" })
|
||||
: undefined
|
||||
|
||||
@@ -40,7 +40,6 @@ import { useEffect, useMemo } from "react";
|
||||
import "./extensions";
|
||||
import { AttachmentNode, AttachmentOptions } from "./extensions/attachment";
|
||||
import BulletList from "./extensions/bullet-list";
|
||||
import { ClipboardTextSerializer } from "./extensions/clipboard-text-serializer";
|
||||
import { CodeBlock } from "./extensions/code-block";
|
||||
import { Codemark } from "./extensions/code-mark";
|
||||
import { DateTime, DateTimeOptions } from "./extensions/date-time";
|
||||
@@ -76,7 +75,6 @@ import { useToolbarStore } from "./toolbar/stores/toolbar-store";
|
||||
import { DownloadOptions } from "./utils/downloader";
|
||||
import { Heading } from "./extensions/heading";
|
||||
import Clipboard, { ClipboardOptions } from "./extensions/clipboard";
|
||||
import { convertBrToParagraph } from "./utils/html";
|
||||
import Blockquote from "./extensions/blockquote";
|
||||
|
||||
declare global {
|
||||
@@ -147,14 +145,10 @@ const useTiptap = (
|
||||
() => ({
|
||||
enableCoreExtensions: false,
|
||||
editorProps: {
|
||||
...editorProps,
|
||||
transformPastedHTML(html) {
|
||||
return convertBrToParagraph(html).documentElement.outerHTML;
|
||||
}
|
||||
...editorProps
|
||||
},
|
||||
extensions: [
|
||||
...CoreExtensions,
|
||||
ClipboardTextSerializer,
|
||||
NodeViewSelectionNotifier,
|
||||
SearchReplace,
|
||||
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