mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 06:23:06 +08:00
ef87210b3d
* feat: new image menu * switch to resizable side handles * use pixels * refactor excalidraw and drawio menu * support image resize undo * video resize * callout menu refresh * refresh table menus * fix color scheme * fix: patch @tiptap/core ResizableNodeView to prevent resize sticking after mouseup * feat: columns * notes callout * focus on first column * capture tab key in column * fix print * hide columns menu when some nodes are focused * fix print * fix columns * selective placeholder * fix blockquote * quote * fix callout in columns
197 lines
5.6 KiB
TypeScript
197 lines
5.6 KiB
TypeScript
import { Node, mergeAttributes, findParentNode } from "@tiptap/core";
|
|
import { Fragment, Node as PMNode } from "prosemirror-model";
|
|
import { TextSelection } from "prosemirror-state";
|
|
|
|
export type ColumnsLayout =
|
|
| "two_equal"
|
|
| "two_left_sidebar"
|
|
| "two_right_sidebar"
|
|
| "three_equal"
|
|
| "three_left_wide"
|
|
| "three_right_wide"
|
|
| "three_with_sidebars"
|
|
| "four_equal"
|
|
| "five_equal";
|
|
|
|
export interface ColumnsOptions {
|
|
HTMLAttributes: Record<string, any>;
|
|
}
|
|
|
|
export type WidthMode = "normal" | "wide";
|
|
|
|
export interface ColumnsAttributes {
|
|
layout?: ColumnsLayout;
|
|
widthMode?: WidthMode;
|
|
}
|
|
|
|
declare module "@tiptap/core" {
|
|
interface Commands<ReturnType> {
|
|
columns: {
|
|
insertColumns: (attributes?: ColumnsAttributes) => ReturnType;
|
|
setColumnsWidthMode: (widthMode: WidthMode) => ReturnType;
|
|
setColumnCount: (count: number) => ReturnType;
|
|
setColumnsLayout: (layout: ColumnsLayout) => ReturnType;
|
|
};
|
|
}
|
|
}
|
|
|
|
function columnCountFromLayout(layout: string): number {
|
|
if (layout.startsWith("five")) return 5;
|
|
if (layout.startsWith("four")) return 4;
|
|
if (layout.startsWith("three")) return 3;
|
|
return 2;
|
|
}
|
|
|
|
function defaultLayoutForCount(count: number): ColumnsLayout {
|
|
if (count === 3) return "three_equal";
|
|
if (count === 4) return "four_equal";
|
|
if (count === 5) return "five_equal";
|
|
return "two_equal";
|
|
}
|
|
|
|
export const Columns = Node.create<ColumnsOptions>({
|
|
name: "columns",
|
|
group: "block",
|
|
content: "column+",
|
|
defining: true,
|
|
isolating: true,
|
|
|
|
addOptions() {
|
|
return {
|
|
HTMLAttributes: {},
|
|
};
|
|
},
|
|
|
|
addAttributes() {
|
|
return {
|
|
layout: {
|
|
default: "two_equal",
|
|
parseHTML: (element) => element.getAttribute("data-layout"),
|
|
renderHTML: (attributes: ColumnsAttributes) => ({
|
|
"data-layout": attributes.layout,
|
|
}),
|
|
},
|
|
widthMode: {
|
|
default: "normal",
|
|
parseHTML: (element) =>
|
|
element.getAttribute("data-width-mode") || "normal",
|
|
renderHTML: (attributes: ColumnsAttributes) => {
|
|
if (!attributes.widthMode || attributes.widthMode === "normal")
|
|
return {};
|
|
return { "data-width-mode": attributes.widthMode };
|
|
},
|
|
},
|
|
};
|
|
},
|
|
|
|
parseHTML() {
|
|
return [
|
|
{
|
|
tag: `div[data-type="${this.name}"]`,
|
|
},
|
|
];
|
|
},
|
|
|
|
renderHTML({ HTMLAttributes }) {
|
|
return [
|
|
"div",
|
|
mergeAttributes(
|
|
{ "data-type": this.name },
|
|
this.options.HTMLAttributes,
|
|
HTMLAttributes,
|
|
),
|
|
0,
|
|
];
|
|
},
|
|
|
|
addCommands() {
|
|
return {
|
|
insertColumns:
|
|
(attributes) =>
|
|
({ tr, state, dispatch }) => {
|
|
const layout = attributes?.layout || "two_equal";
|
|
const count = columnCountFromLayout(layout);
|
|
|
|
const columnType = state.schema.nodes.column;
|
|
const paraType = state.schema.nodes.paragraph;
|
|
const children = Array.from({ length: count }, () =>
|
|
columnType.create(null, paraType.create()),
|
|
);
|
|
const columnsNode = this.type.create(
|
|
attributes,
|
|
Fragment.from(children),
|
|
);
|
|
|
|
const stepsBefore = tr.steps.length;
|
|
tr.replaceSelectionWith(columnsNode);
|
|
|
|
if (tr.steps.length > stepsBefore) {
|
|
const stepMap = tr.steps[tr.steps.length - 1].getMap();
|
|
let insertStart = 0;
|
|
stepMap.forEach((_from, _to, newFrom) => {
|
|
insertStart = newFrom;
|
|
});
|
|
tr.setSelection(
|
|
TextSelection.near(tr.doc.resolve(insertStart + 1), 1),
|
|
);
|
|
}
|
|
|
|
if (dispatch) dispatch(tr);
|
|
return true;
|
|
},
|
|
|
|
setColumnsWidthMode:
|
|
(widthMode) =>
|
|
({ commands }) =>
|
|
commands.updateAttributes("columns", { widthMode }),
|
|
|
|
setColumnCount:
|
|
(count: number) =>
|
|
({ tr, state }) => {
|
|
const predicate = (node: PMNode) => node.type.name === "columns";
|
|
const parent = findParentNode(predicate)(state.selection);
|
|
if (!parent) return false;
|
|
|
|
const { node: columnsNode, pos: parentPos } = parent;
|
|
const currentCount = columnsNode.childCount;
|
|
if (count === currentCount || count < 2 || count > 5) return false;
|
|
|
|
const columnType = state.schema.nodes.column;
|
|
const paraType = state.schema.nodes.paragraph;
|
|
const newChildren: PMNode[] = [];
|
|
|
|
if (count > currentCount) {
|
|
for (let i = 0; i < currentCount; i++) {
|
|
newChildren.push(columnsNode.child(i));
|
|
}
|
|
for (let i = currentCount; i < count; i++) {
|
|
newChildren.push(columnType.create(null, paraType.create()));
|
|
}
|
|
} else {
|
|
for (let i = 0; i < count - 1; i++) {
|
|
newChildren.push(columnsNode.child(i));
|
|
}
|
|
let mergedContent = columnsNode.child(count - 1).content;
|
|
for (let j = count; j < currentCount; j++) {
|
|
mergedContent = mergedContent.append(columnsNode.child(j).content);
|
|
}
|
|
newChildren.push(columnType.create(null, mergedContent));
|
|
}
|
|
|
|
const newLayout = defaultLayoutForCount(count);
|
|
const newNode = columnsNode.type.create(
|
|
{ ...columnsNode.attrs, layout: newLayout },
|
|
Fragment.from(newChildren),
|
|
);
|
|
tr.replaceWith(parentPos, parentPos + columnsNode.nodeSize, newNode);
|
|
return true;
|
|
},
|
|
|
|
setColumnsLayout:
|
|
(layout) =>
|
|
({ commands }) =>
|
|
commands.updateAttributes("columns", { layout }),
|
|
};
|
|
},
|
|
});
|