mirror of
https://github.com/docmost/docmost.git
synced 2026-05-16 22:41:30 +08:00
feat: columns
This commit is contained in:
@@ -25,3 +25,4 @@ export * from "./lib/heading/heading";
|
||||
export * from "./lib/unique-id";
|
||||
export * from "./lib/shared-storage";
|
||||
export * from "./lib/recreate-transform";
|
||||
export * from "./lib/columns";
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { Node, mergeAttributes, findParentNode } from "@tiptap/core";
|
||||
import { TextSelection } from "prosemirror-state";
|
||||
|
||||
export interface ColumnOptions {
|
||||
HTMLAttributes: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface ColumnAttributes {
|
||||
width?: number | null;
|
||||
}
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
column: {
|
||||
setColumnWidth: (width: number | null) => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const Column = Node.create<ColumnOptions>({
|
||||
name: "column",
|
||||
group: "block",
|
||||
content: "block+",
|
||||
defining: true,
|
||||
isolating: true,
|
||||
selectable: false,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {},
|
||||
};
|
||||
},
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
width: {
|
||||
default: null,
|
||||
parseHTML: (element) => {
|
||||
const value = element.getAttribute("data-width");
|
||||
return value ? parseFloat(value) : null;
|
||||
},
|
||||
renderHTML: (attributes: ColumnAttributes) => {
|
||||
if (!attributes.width) return {};
|
||||
return {
|
||||
"data-width": attributes.width,
|
||||
style: `flex: ${attributes.width}`,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: `div[data-type="${this.name}"]`,
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return [
|
||||
"div",
|
||||
mergeAttributes(
|
||||
{ "data-type": this.name },
|
||||
this.options.HTMLAttributes,
|
||||
HTMLAttributes,
|
||||
),
|
||||
0,
|
||||
];
|
||||
},
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
const jumpToColumn = (direction: 1 | -1) => () => {
|
||||
const { state, dispatch } = this.editor.view;
|
||||
|
||||
const columns = findParentNode(
|
||||
(node) => node.type.name === "columns",
|
||||
)(state.selection);
|
||||
if (!columns) return false;
|
||||
|
||||
const column = findParentNode(
|
||||
(node) => node.type.name === "column",
|
||||
)(state.selection);
|
||||
if (!column) return false;
|
||||
|
||||
let currentIndex = -1;
|
||||
columns.node.forEach((_child, offset, index) => {
|
||||
if (columns.pos + 1 + offset === column.pos) {
|
||||
currentIndex = index;
|
||||
}
|
||||
});
|
||||
|
||||
const targetIndex = currentIndex + direction;
|
||||
if (targetIndex < 0 || targetIndex >= columns.node.childCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let offset = 0;
|
||||
for (let j = 0; j < targetIndex; j++) {
|
||||
offset += columns.node.child(j).nodeSize;
|
||||
}
|
||||
|
||||
const targetPos = columns.pos + 1 + offset + 1 + 1;
|
||||
if (dispatch) {
|
||||
dispatch(
|
||||
state.tr.setSelection(TextSelection.create(state.doc, targetPos)),
|
||||
);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
return {
|
||||
Tab: jumpToColumn(1),
|
||||
"Shift-Tab": jumpToColumn(-1),
|
||||
};
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setColumnWidth:
|
||||
(width) =>
|
||||
({ commands }) =>
|
||||
commands.updateAttributes("column", { width }),
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,178 @@
|
||||
import { Node, mergeAttributes, findParentNode } from "@tiptap/core";
|
||||
import { Fragment, Node as PMNode } from "prosemirror-model";
|
||||
|
||||
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) =>
|
||||
({ commands }) => {
|
||||
const layout = attributes?.layout || "two_equal";
|
||||
const count = columnCountFromLayout(layout);
|
||||
const columns = Array.from({ length: count }, () => ({
|
||||
type: "column",
|
||||
content: [{ type: "paragraph" }],
|
||||
}));
|
||||
|
||||
return commands.insertContent({
|
||||
type: this.name,
|
||||
attrs: attributes,
|
||||
content: columns,
|
||||
});
|
||||
},
|
||||
|
||||
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 }),
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
export { Columns } from "./columns";
|
||||
export type { ColumnsOptions, ColumnsAttributes, ColumnsLayout, WidthMode } from "./columns";
|
||||
export { Column } from "./column";
|
||||
export type { ColumnOptions, ColumnAttributes } from "./column";
|
||||
Reference in New Issue
Block a user