Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add the format brush extension to the default rich text editor #5603

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ui/packages/editor/src/dev/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
ExtensionListKeymap,
ExtensionSearchAndReplace,
ExtensionClearFormat,
ExtensionFormatBrush,
} from "../index";

const content = useLocalStorage("content", "");
Expand Down Expand Up @@ -113,6 +114,7 @@ const editor = useEditor({
ExtensionListKeymap,
ExtensionSearchAndReplace,
ExtensionClearFormat,
ExtensionFormatBrush,
],
parseOptions: {
preserveWhitespace: true,
Expand Down
120 changes: 120 additions & 0 deletions ui/packages/editor/src/extensions/format-brush/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import ToolbarItem from "@/components/toolbar/ToolbarItem.vue";
import { i18n } from "@/locales";
import { CoreEditor, Extension, Plugin, PluginKey } from "@/tiptap";
import { markRaw } from "vue";
import BxsBrushAlt from "~icons/bxs/brush-alt";
import { getMarksByFirstTextNode, setMarks } from "./util";

declare module "@/tiptap" {
interface Commands<ReturnType> {
formatBrush: {
copyFormatBrush: () => ReturnType;
pasteFormatBrush: () => ReturnType;
};
}
}

export interface FormatBrushStore {
formatBrush: boolean;
formatBrushMarks: any[];
}

const formatBrush = Extension.create<any, FormatBrushStore>({
addOptions() {
return {
...this.parent?.(),
getToolbarItems({ editor }: { editor: CoreEditor }) {
const formatBrush =
editor.view.dom.classList.contains("format-brush-mode");
return {
priority: 25,
component: markRaw(ToolbarItem),
props: {
editor,
isActive: formatBrush,
icon: markRaw(BxsBrushAlt),
title: formatBrush
? i18n.global.t(
"editor.extensions.format_brush.toolbar_item.cancel"
)
: i18n.global.t(
"editor.extensions.format_brush.toolbar_item.title"
),
action: () => {
if (formatBrush) {
editor.commands.pasteFormatBrush();
} else {
editor.commands.copyFormatBrush();
}
},
},
};
},
};
},

addCommands() {
return {
copyFormatBrush:
() =>
({ state }) => {
const markRange = getMarksByFirstTextNode(state);
this.storage.formatBrushMarks = markRange;
this.storage.formatBrush = true;
this.editor.view.dom.classList.add("format-brush-mode");
return true;
},
pasteFormatBrush: () => () => {
this.storage.formatBrushMarks = [];
this.storage.formatBrush = false;
this.editor.view.dom.classList.remove("format-brush-mode");
return true;
},
};
},

addStorage() {
return {
formatBrush: false,
formatBrushMarks: [],
};
},

addProseMirrorPlugins() {
const storage = this.storage;
const editor = this.editor;
return [
new Plugin({
key: new PluginKey("formatBrushPlugin"),
props: {
handleDOMEvents: {
mouseup(view) {
if (!storage.formatBrush) {
return;
}
editor
.chain()
.command(({ tr }) => {
setMarks(view.state, storage.formatBrushMarks, tr);
return true;
})
.pasteFormatBrush()
.run();
},
},
},
}),
];
},

addKeyboardShortcuts() {
return {
"Shift-Mod-c": () => {
this.editor.commands.copyFormatBrush();
return true;
},
};
},
});

export default formatBrush;
119 changes: 119 additions & 0 deletions ui/packages/editor/src/extensions/format-brush/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import type { EditorState, MarkRange, Transaction } from "@/tiptap";
import { AddMarkStep, CellSelection, RemoveMarkStep } from "@/tiptap/pm";

/**
* get its marks through the first text node in the selector
*
* @param state editor state
* @returns the marks of the current first text node
*/
export const getMarksByFirstTextNode = (state: EditorState): MarkRange[] => {
const marks: MarkRange[] = [];
const { doc, selection } = state;
const { from, to, empty } = selection;
if (empty) {
return marks;
}

let flag = false;
doc.nodesBetween(from, to, (node, pos) => {
if (!node || node?.nodeSize === undefined) {
return;
}

if (node.isText && !flag) {
flag = true;
marks.push(
...node.marks.map((mark) => ({
from: pos,
to: pos + node.nodeSize,
mark,
}))
);
return false;
}
});
return marks;
};

/**
*
* Set marks for the text in the currently selected content. This method will first remove all marks
* from the currently selected text, and then add marks again.
*
* For CellSelection, it is necessary to iterate through ranges and set marks for each range.
*
* @param state editor state
* @param marks the marks to be set
* @param transaction transaction
*
* @returns transaction
*
* */
export const setMarks = (
state: EditorState,
marks: MarkRange[],
transaction?: Transaction
): Transaction => {
const { selection } = state;
const tr = transaction || state.tr;
const { from, to } = selection;

// When selection is CellSelection, iterate through ranges
if (selection instanceof CellSelection) {
selection.ranges.forEach((cellRange) => {
const range = {
from: cellRange.$from.pos,
to: cellRange.$to.pos,
};
setMarksByRange(tr, state, range, marks);
});
} else {
setMarksByRange(
tr,
state,
{
from,
to,
},
marks
);
}

return tr;
};

export const setMarksByRange = (
tr: Transaction,
state: EditorState,
range: {
from: number;
to: number;
},
marks: MarkRange[]
) => {
const { from, to } = range;
state.doc.nodesBetween(from, to, (node, pos) => {
if (!node || node?.nodeSize === undefined) {
return;
}

if (node.isText) {
// the range of the current text node
const range = {
from: Math.max(pos, from),
to: Math.min(pos + node.nodeSize, to),
};
// remove all marks of the current text node
node.marks.forEach((mark) => {
tr.step(new RemoveMarkStep(range.from, range.to, mark));
});
// add all marks of the current text node
marks.forEach((mark) => {
tr.step(new AddMarkStep(range.from, range.to, mark.mark));
});
}

return true;
});
};
3 changes: 3 additions & 0 deletions ui/packages/editor/src/extensions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import ExtensionNodeSelected from "./node-selected";
import ExtensionTrailingNode from "./trailing-node";
import ExtensionSearchAndReplace from "./search-and-replace";
import ExtensionClearFormat from "./clear-format";
import ExtensionFormatBrush from "./format-brush";

const allExtensions = [
ExtensionBlockquote,
Expand Down Expand Up @@ -102,6 +103,7 @@ const allExtensions = [
ExtensionTrailingNode,
ExtensionSearchAndReplace,
ExtensionClearFormat,
ExtensionFormatBrush,
];

export {
Expand Down Expand Up @@ -150,4 +152,5 @@ export {
ExtensionListKeymap,
ExtensionSearchAndReplace,
ExtensionClearFormat,
ExtensionFormatBrush,
};
4 changes: 4 additions & 0 deletions ui/packages/editor/src/locales/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ editor:
match_word: Match Whole Word
use_regex: Use Regular Expression
close: Close
format_brush:
toolbar_item:
title: Format Brush
cancel: Cancel Format Brush
components:
color_picker:
more_color: More
Expand Down
4 changes: 4 additions & 0 deletions ui/packages/editor/src/locales/zh-CN.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ editor:
match_word: 全字匹配
use_regex: 使用正则表达式
close: 关闭
format_brush:
toolbar_item:
title: 格式刷
cancel: 取消格式刷
components:
color_picker:
more_color: 更多颜色
Expand Down
9 changes: 9 additions & 0 deletions ui/packages/editor/src/styles/format-brush.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.halo-rich-text-editor {
.ProseMirror {
&.format-brush-mode {
cursor: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+PHBhdGggZD0iTTI0IDB2MjRIMFYwek0xMi41OTQgMjMuMjU4bC0uMDEyLjAwMmwtLjA3MS4wMzVsLS4wMi4wMDRsLS4wMTQtLjAwNGwtLjA3MS0uMDM2Yy0uMDEtLjAwMy0uMDE5IDAtLjAyNC4wMDZsLS4wMDQuMDFsLS4wMTcuNDI4bC4wMDUuMDJsLjAxLjAxM2wuMTA0LjA3NGwuMDE1LjAwNGwuMDEyLS4wMDRsLjEwNC0uMDc0bC4wMTItLjAxNmwuMDA0LS4wMTdsLS4wMTctLjQyN2MtLjAwMi0uMDEtLjAwOS0uMDE3LS4wMTYtLjAxOG0uMjY0LS4xMTNsLS4wMTQuMDAybC0uMTg0LjA5M2wtLjAxLjAxbC0uMDAzLjAxMWwuMDE4LjQzbC4wMDUuMDEybC4wMDguMDA4bC4yMDEuMDkyYy4wMTIuMDA0LjAyMyAwIC4wMjktLjAwOGwuMDA0LS4wMTRsLS4wMzQtLjYxNGMtLjAwMy0uMDEyLS4wMS0uMDItLjAyLS4wMjJtLS43MTUuMDAyYS4wMjMuMDIzIDAgMCAwLS4wMjcuMDA2bC0uMDA2LjAxNGwtLjAzNC42MTRjMCAuMDEyLjAwNy4wMi4wMTcuMDI0bC4wMTUtLjAwMmwuMjAxLS4wOTNsLjAxLS4wMDhsLjAwMy0uMDExbC4wMTgtLjQzbC0uMDAzLS4wMTJsLS4wMS0uMDF6Ii8+PHBhdGggZmlsbD0iY3VycmVudENvbG9yIiBkPSJNMTQuMzMgMi4zMDdhMyAzIDAgMCAxIDIuMTIgMy42NzRsLTEuMDM1IDMuODY0bDEuMzU4LjM2M2MxLjY1NC40NDQgMi41MSAyLjEyIDIuMjc4IDMuNjU2Yy0uMTEuNzI5LS4xNTQgMS40NDYtLjA2NSAxLjk2OGMuMTg1IDEuMDg0LjgwOCAyLjE0NiAxLjQ1OCAzLjA3M2wuMzU0LjQ5MkExIDEgMCAwIDEgMjAgMjFINS41Yy0uMzIgMC0uNjI3LS4xNTgtLjgxMy0uNDE4Yy0uNDg5LS42ODQtLjgzOC0xLjQ1OC0xLjEyNS0yLjI0NGMtLjUtMS4zNjQtLjkxNy0zLjI5My0uNTQ4LTUuNTAyYTExLjYzMyAxMS42MzMgMCAwIDEgMS40MjgtMy45OTRjLjY1NS0xLjEwOCAxLjkzMS0xLjQyIDIuOTkxLTEuMTM2bDIuMTg3LjU4NmwxLjAzNS0zLjg2NGEzIDMgMCAwIDEgMy42NzQtMi4xMjFaTTcuOTc4IDE1YTEgMSAwIDAgMC0uOTkzIDEuMDA3Yy4wMDcgMS4wMjQuMzI3IDIuMDA1LjY2OSAyLjc3bC4xMDIuMjIzaDIuMjc3YTguOTQ0IDguOTQ0IDAgMCAxLS4zOTItLjY5OWMtLjMxLS42MTctLjU4My0xLjM0OC0uNjQ0LTIuMDQ3bC0uMDEyLS4yNjFBMSAxIDAgMCAwIDcuOTc5IDE1Wm00LjYwOC0xMC4wNTRsLTEuMjk0IDQuODNhMSAxIDAgMCAxLTEuMjI1LjcwNmwtMy4xNTItLjg0NGMtLjM4My0uMTAzLS42NDYuMDQzLS43NTEuMjIxYTkuMTExIDkuMTExIDAgMCAwLS43MDMgMS40OTRsMTEuNTEgMy4wNTNjLjAyNS0uMjkzLjA2MS0uNTc4LjEwMS0uODQyYy4xMDUtLjY5LS4yOTctMS4yODQtLjgxOC0xLjQyNGwtMi4zMjMtLjYyMmExIDEgMCAwIDEtLjcwNy0xLjIyNWwxLjI5NC00LjgzYTEgMSAwIDAgMC0xLjkzMi0uNTE3Ii8+PC9nPjwvc3ZnPg==)
5 10,
text;
}
}
}
1 change: 1 addition & 0 deletions ui/packages/editor/src/styles/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
@import "./draggable.scss";
@import "./columns.scss";
@import "./search.scss";
@import "./format-brush.scss";
2 changes: 2 additions & 0 deletions ui/src/components/editor/DefaultEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
ExtensionListKeymap,
ExtensionSearchAndReplace,
ExtensionClearFormat,
ExtensionFormatBrush,
} from "@halo-dev/richtext-editor";
// ui custom extension
import {
Expand Down Expand Up @@ -398,6 +399,7 @@ onMounted(() => {
UiExtensionUpload,
ExtensionSearchAndReplace,
ExtensionClearFormat,
ExtensionFormatBrush,
],
parseOptions: {
preserveWhitespace: true,
Expand Down