feat: add support for single spaced paragraphs

This commit is contained in:
thecodrr
2022-08-04 09:29:32 +05:00
parent 8979ed632a
commit 369b8b7abb
4 changed files with 195 additions and 1 deletions

View File

@@ -0,0 +1,62 @@
import { Extension, TextSerializer } from "@tiptap/core";
import { Plugin, PluginKey } from "prosemirror-state";
import { Node as ProseMirrorNode, Schema, Slice } from "prosemirror-model";
export const ClipboardTextSerializer = Extension.create({
name: "clipboardTextSerializer",
addProseMirrorPlugins() {
return [
new Plugin({
key: new PluginKey("clipboardTextSerializer"),
props: {
clipboardTextSerializer: (content) => {
const {
editor: { schema },
} = this;
return getTextBetween(content, schema);
},
},
}),
];
},
});
function getTextBetween(slice: Slice, schema: Schema): string {
const range = { from: 0, to: slice.size };
const { from, to } = range;
const separator = (node: ProseMirrorNode) =>
node.attrs.spacing === "single" ? "\n" : "\n\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(node);
separated = true;
}
if (parent) {
text += textSerializer({
node,
pos,
parent,
index,
range,
});
}
} else if (node.isText) {
text += node?.text?.slice(Math.max(from, pos) - pos, to - pos); // eslint-disable-line
separated = false;
} else if (node.isBlock && !separated) {
text += separator(node);
separated = true;
}
});
return text;
}

View File

@@ -0,0 +1 @@
export * from "./paragraph";

View File

@@ -0,0 +1,109 @@
import { mergeAttributes, Node } from "@tiptap/core";
import { HardBreak } from "@tiptap/extension-hard-break";
export interface ParagraphOptions {
HTMLAttributes: Record<string, any>;
doubleSpaced: boolean;
}
declare module "@tiptap/core" {
interface Commands<ReturnType> {
paragraph: {
/**
* Toggle a paragraph
*/
setParagraph: () => ReturnType;
};
}
}
export const Paragraph = Node.create<ParagraphOptions>({
name: "paragraph",
priority: 1000,
addOptions() {
return {
HTMLAttributes: {},
doubleSpaced: true,
};
},
group: "block",
content: "inline*",
// addAttributes() {
// return {
// spacing: {
// keepOnSplit: false,
// default: getSpacing(this.options.doubleSpaced),
// parseHTML: (element) => element.dataset.spacing,
// renderHTML: (attributes) => {
// if (!attributes.spacing) return;
// return {
// "data-spacing": attributes.spacing,
// };
// },
// },
// };
// },
parseHTML() {
return [{ tag: "p" }];
},
renderHTML({ HTMLAttributes }) {
return [
"p",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
0,
];
},
addCommands() {
return {
setParagraph:
() =>
({ commands }) => {
return commands.setNode(this.name);
},
};
},
addKeyboardShortcuts() {
return {
Enter: ({ editor }) => {
if (this.options.doubleSpaced) return false;
// if (this.options.doubleSpaced) return false;
const { state } = editor;
const { selection } = state;
const { $from, empty } = selection;
if (!empty || $from.parent.type !== this.type) {
return false;
}
const endsWithNewline = $from.nodeBefore?.type.name === HardBreak.name;
if (endsWithNewline) {
return this.editor.commands.command(({ tr }) => {
const { from } = tr.selection;
tr.delete(from - 1, from);
return false;
});
}
return this.editor.commands.setHardBreak();
},
"Mod-Alt-0": () => this.editor.commands.setParagraph(),
};
},
});
function getSpacing(doubleSpaced: boolean): "single" | "double" {
return doubleSpaced ? "double" : "single";
}

View File

@@ -43,9 +43,19 @@ import { SelectionPersist } from "./extensions/selection-persist";
import { Table } from "./extensions/table";
import { useToolbarStore } from "./toolbar/stores/toolbar-store";
import { useEditor } from "./hooks/use-editor";
import { EditorOptions } from "@tiptap/core";
import {
EditorOptions,
extensions as TiptapCoreExtensions,
} from "@tiptap/core";
import { usePermissionHandler } from "./hooks/use-permission-handler";
import { Highlight } from "./extensions/highlight";
import { Paragraph } from "./extensions/paragraph";
import { ClipboardTextSerializer } from "./extensions/clipboard-text-serializer";
const CoreExtensions = Object.entries(TiptapCoreExtensions)
// we will implement our own customized clipboard serializer
.filter(([name]) => name !== "ClipboardTextSerializer")
.map(([, extension]) => extension);
EditorView.prototype.updateState = function updateState(state) {
if (!(this as any).docView) return; // This prevents the matchesNode error on hot reloads
@@ -57,6 +67,7 @@ type TiptapOptions = EditorOptions &
theme: Theme;
isMobile?: boolean;
isKeyboardOpen?: boolean;
doubleSpacedLines?: boolean;
};
const useTiptap = (
@@ -65,6 +76,7 @@ const useTiptap = (
) => {
const {
theme,
doubleSpacedLines = true,
isMobile,
isKeyboardOpen,
onDownloadAttachment,
@@ -85,16 +97,26 @@ const useTiptap = (
const defaultOptions = useMemo<Partial<EditorOptions>>(
() => ({
enableCoreExtensions: false,
extensions: [
...CoreExtensions,
ClipboardTextSerializer,
NodeViewSelectionNotifier,
SearchReplace,
TextStyle,
Paragraph.configure({
doubleSpaced: doubleSpacedLines,
}),
StarterKit.configure({
dropcursor: false,
codeBlock: false,
listItem: false,
orderedList: false,
bulletList: false,
paragraph: false,
hardBreak: {
keepMarks: true,
},
history: {
depth: 200,
newGroupDelay: 1000,