mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
[feat]: Extended Tables Support (#2596)
* migrated table to new project structure * fixed range errors while deleting table nodes with no nodes below and removed console logs * fixed css for rendering table menu * removed old table menu * added support for read only editors as well * text-black removed * added design colors --------- Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
This commit is contained in:
parent
f0335751b3
commit
14ac885e55
@ -41,6 +41,8 @@
|
|||||||
"@tiptap/extension-task-list": "^2.1.7",
|
"@tiptap/extension-task-list": "^2.1.7",
|
||||||
"@tiptap/extension-text-style": "^2.1.11",
|
"@tiptap/extension-text-style": "^2.1.11",
|
||||||
"@tiptap/extension-underline": "^2.1.7",
|
"@tiptap/extension-underline": "^2.1.7",
|
||||||
|
"@tiptap/prosemirror-tables": "^1.1.4",
|
||||||
|
"jsx-dom-cjs": "^8.0.3",
|
||||||
"@tiptap/pm": "^2.1.7",
|
"@tiptap/pm": "^2.1.7",
|
||||||
"@tiptap/react": "^2.1.7",
|
"@tiptap/react": "^2.1.7",
|
||||||
"@tiptap/starter-kit": "^2.1.10",
|
"@tiptap/starter-kit": "^2.1.10",
|
||||||
|
@ -2,8 +2,11 @@
|
|||||||
// import "./styles/tailwind.css";
|
// import "./styles/tailwind.css";
|
||||||
// import "./styles/editor.css";
|
// import "./styles/editor.css";
|
||||||
|
|
||||||
|
export { isCellSelection } from "./ui/extensions/table/table/utilities/is-cell-selection";
|
||||||
|
|
||||||
// utils
|
// utils
|
||||||
export * from "./lib/utils";
|
export * from "./lib/utils";
|
||||||
|
export * from "./ui/extensions/table/table";
|
||||||
export { startImageUpload } from "./ui/plugins/upload-image";
|
export { startImageUpload } from "./ui/plugins/upload-image";
|
||||||
|
|
||||||
// components
|
// components
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { Editor, EditorContent } from "@tiptap/react";
|
import { Editor, EditorContent } from "@tiptap/react";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { ImageResizer } from "../extensions/image/image-resize";
|
import { ImageResizer } from "../extensions/image/image-resize";
|
||||||
import { TableMenu } from "../menus/table-menu";
|
|
||||||
|
|
||||||
interface EditorContentProps {
|
interface EditorContentProps {
|
||||||
editor: Editor | null;
|
editor: Editor | null;
|
||||||
@ -10,10 +9,8 @@ interface EditorContentProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const EditorContentWrapper = ({ editor, editorContentCustomClassNames = '', children }: EditorContentProps) => (
|
export const EditorContentWrapper = ({ editor, editorContentCustomClassNames = '', children }: EditorContentProps) => (
|
||||||
<div className={`${editorContentCustomClassNames}`}>
|
<div className={`contentEditor ${editorContentCustomClassNames}`}>
|
||||||
{/* @ts-ignore */}
|
|
||||||
<EditorContent editor={editor} />
|
<EditorContent editor={editor} />
|
||||||
{editor?.isEditable && <TableMenu editor={editor} />}
|
|
||||||
{(editor?.isActive("image") && editor?.isEditable) && <ImageResizer editor={editor} />}
|
{(editor?.isActive("image") && editor?.isEditable) && <ImageResizer editor={editor} />}
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,10 +8,10 @@ import TaskList from "@tiptap/extension-task-list";
|
|||||||
import { Markdown } from "tiptap-markdown";
|
import { Markdown } from "tiptap-markdown";
|
||||||
import Gapcursor from "@tiptap/extension-gapcursor";
|
import Gapcursor from "@tiptap/extension-gapcursor";
|
||||||
|
|
||||||
import { CustomTableCell } from "./table/table-cell";
|
import TableHeader from "./table/table-header/table-header";
|
||||||
import { Table } from "./table";
|
import Table from "./table/table";
|
||||||
import { TableHeader } from "./table/table-header";
|
import TableCell from "./table/table-cell/table-cell";
|
||||||
import { TableRow } from "@tiptap/extension-table-row";
|
import TableRow from "./table/table-row/table-row";
|
||||||
|
|
||||||
import ImageExtension from "./image";
|
import ImageExtension from "./image";
|
||||||
|
|
||||||
@ -95,7 +95,7 @@ export const CoreEditorExtensions = (
|
|||||||
}),
|
}),
|
||||||
Table,
|
Table,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
CustomTableCell,
|
TableCell,
|
||||||
TableRow,
|
TableRow,
|
||||||
Mentions(mentionConfig.mentionSuggestions, mentionConfig.mentionHighlights, false),
|
Mentions(mentionConfig.mentionSuggestions, mentionConfig.mentionHighlights, false),
|
||||||
];
|
];
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
import { Table as BaseTable } from "@tiptap/extension-table";
|
|
||||||
|
|
||||||
const Table = BaseTable.configure({
|
|
||||||
resizable: true,
|
|
||||||
cellMinWidth: 100,
|
|
||||||
allowTableNodeSelection: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export { Table };
|
|
@ -1,32 +0,0 @@
|
|||||||
import { TableCell } from "@tiptap/extension-table-cell";
|
|
||||||
|
|
||||||
export const CustomTableCell = TableCell.extend({
|
|
||||||
addAttributes() {
|
|
||||||
return {
|
|
||||||
...this.parent?.(),
|
|
||||||
isHeader: {
|
|
||||||
default: false,
|
|
||||||
parseHTML: (element) => {
|
|
||||||
isHeader: element.tagName === "TD";
|
|
||||||
},
|
|
||||||
renderHTML: (attributes) => {
|
|
||||||
tag: attributes.isHeader ? "th" : "td";
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
|
||||||
if (HTMLAttributes.isHeader) {
|
|
||||||
return [
|
|
||||||
"th",
|
|
||||||
{
|
|
||||||
...HTMLAttributes,
|
|
||||||
class: `relative ${HTMLAttributes.class}`,
|
|
||||||
},
|
|
||||||
["span", { class: "absolute top-0 right-0" }],
|
|
||||||
0,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return ["td", HTMLAttributes, 0];
|
|
||||||
},
|
|
||||||
});
|
|
@ -0,0 +1 @@
|
|||||||
|
export { default as default } from "./table-cell"
|
@ -0,0 +1,58 @@
|
|||||||
|
import { mergeAttributes, Node } from "@tiptap/core"
|
||||||
|
|
||||||
|
export interface TableCellOptions {
|
||||||
|
HTMLAttributes: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Node.create<TableCellOptions>({
|
||||||
|
name: "tableCell",
|
||||||
|
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
HTMLAttributes: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
content: "paragraph+",
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
colspan: {
|
||||||
|
default: 1
|
||||||
|
},
|
||||||
|
rowspan: {
|
||||||
|
default: 1
|
||||||
|
},
|
||||||
|
colwidth: {
|
||||||
|
default: null,
|
||||||
|
parseHTML: (element) => {
|
||||||
|
const colwidth = element.getAttribute("colwidth")
|
||||||
|
const value = colwidth ? [parseInt(colwidth, 10)] : null
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
default: "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
tableRole: "cell",
|
||||||
|
|
||||||
|
isolating: true,
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [{ tag: "td" }]
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ node, HTMLAttributes }) {
|
||||||
|
return [
|
||||||
|
"td",
|
||||||
|
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
||||||
|
style: `background-color: ${node.attrs.background}`
|
||||||
|
}),
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
@ -1,7 +0,0 @@
|
|||||||
import { TableHeader as BaseTableHeader } from "@tiptap/extension-table-header";
|
|
||||||
|
|
||||||
const TableHeader = BaseTableHeader.extend({
|
|
||||||
content: "paragraph",
|
|
||||||
});
|
|
||||||
|
|
||||||
export { TableHeader };
|
|
@ -0,0 +1 @@
|
|||||||
|
export { default as default } from "./table-header"
|
@ -0,0 +1,57 @@
|
|||||||
|
import { mergeAttributes, Node } from "@tiptap/core"
|
||||||
|
|
||||||
|
export interface TableHeaderOptions {
|
||||||
|
HTMLAttributes: Record<string, any>
|
||||||
|
}
|
||||||
|
export default Node.create<TableHeaderOptions>({
|
||||||
|
name: "tableHeader",
|
||||||
|
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
HTMLAttributes: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
content: "paragraph+",
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
colspan: {
|
||||||
|
default: 1
|
||||||
|
},
|
||||||
|
rowspan: {
|
||||||
|
default: 1
|
||||||
|
},
|
||||||
|
colwidth: {
|
||||||
|
default: null,
|
||||||
|
parseHTML: (element) => {
|
||||||
|
const colwidth = element.getAttribute("colwidth")
|
||||||
|
const value = colwidth ? [parseInt(colwidth, 10)] : null
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
default: "rgb(var(--color-primary-100))"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
tableRole: "header_cell",
|
||||||
|
|
||||||
|
isolating: true,
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [{ tag: "th" }]
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ node, HTMLAttributes }) {
|
||||||
|
return [
|
||||||
|
"th",
|
||||||
|
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
||||||
|
style: `background-color: ${node.attrs.background}`
|
||||||
|
}),
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
@ -0,0 +1 @@
|
|||||||
|
export { default as default } from "./table-row"
|
@ -0,0 +1,31 @@
|
|||||||
|
import { mergeAttributes, Node } from "@tiptap/core"
|
||||||
|
|
||||||
|
export interface TableRowOptions {
|
||||||
|
HTMLAttributes: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Node.create<TableRowOptions>({
|
||||||
|
name: "tableRow",
|
||||||
|
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
HTMLAttributes: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
content: "(tableCell | tableHeader)*",
|
||||||
|
|
||||||
|
tableRole: "row",
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [{ tag: "tr" }]
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return [
|
||||||
|
"tr",
|
||||||
|
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
55
packages/editor/core/src/ui/extensions/table/table/icons.ts
Normal file
55
packages/editor/core/src/ui/extensions/table/table/icons.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
const icons = {
|
||||||
|
colorPicker: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" style="transform: ;msFilter:;"><path fill="rgb(var(--color-text-300))" d="M20 14c-.092.064-2 2.083-2 3.5 0 1.494.949 2.448 2 2.5.906.044 2-.891 2-2.5 0-1.5-1.908-3.436-2-3.5zM9.586 20c.378.378.88.586 1.414.586s1.036-.208 1.414-.586l7-7-.707-.707L11 4.586 8.707 2.293 7.293 3.707 9.586 6 4 11.586c-.378.378-.586.88-.586 1.414s.208 1.036.586 1.414L9.586 20zM11 7.414 16.586 13H5.414L11 7.414z"></path></svg>`,
|
||||||
|
deleteColumn: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="#e53e3e" d="M0 0H24V24H0z"/><path d="M12 3c.552 0 1 .448 1 1v8c.835-.628 1.874-1 3-1 2.761 0 5 2.239 5 5s-2.239 5-5 5c-1.032 0-1.99-.313-2.787-.848L13 20c0 .552-.448 1-1 1H6c-.552 0-1-.448-1-1V4c0-.552.448-1 1-1h6zm-1 2H7v14h4V5zm8 10h-6v2h6v-2z"/></svg>`,
|
||||||
|
deleteRow: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="#e53e3e" d="M0 0H24V24H0z"/><path d="M20 5c.552 0 1 .448 1 1v6c0 .552-.448 1-1 1 .628.835 1 1.874 1 3 0 2.761-2.239 5-5 5s-5-2.239-5-5c0-1.126.372-2.165 1-3H4c-.552 0-1-.448-1-1V6c0-.552.448-1 1-1h16zm-7 10v2h6v-2h-6zm6-8H5v4h14V7z"/></svg>`,
|
||||||
|
insertLeftTableIcon: `<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
viewBox="0 -960 960 960"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M224.617-140.001q-30.307 0-51.307-21-21-21-21-51.308v-535.382q0-30.308 21-51.308t51.307-21H360q30.307 0 51.307 21 21 21 21 51.308v535.382q0 30.308-21 51.308t-51.307 21H224.617Zm375.383 0q-30.307 0-51.307-21-21-21-21-51.308v-535.382q0-30.308 21-51.308t51.307-21h135.383q30.307 0 51.307 21 21 21 21 51.308v535.382q0 30.308-21 51.308t-51.307 21H600Zm147.691-607.69q0-4.616-3.846-8.463-3.846-3.846-8.462-3.846H600q-4.616 0-8.462 3.846-3.847 3.847-3.847 8.463v535.382q0 4.616 3.847 8.463Q595.384-200 600-200h135.383q4.616 0 8.462-3.846 3.846-3.847 3.846-8.463v-535.382ZM587.691-200h160-160Z"
|
||||||
|
fill="rgb(var(--color-text-300))"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
insertRightTableIcon: `<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
viewBox="0 -960 960 960"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M600-140.001q-30.307 0-51.307-21-21-21-21-51.308v-535.382q0-30.308 21-51.308t51.307-21h135.383q30.307 0 51.307 21 21 21 21 51.308v535.382q0 30.308-21 51.308t-51.307 21H600Zm-375.383 0q-30.307 0-51.307-21-21-21-21-51.308v-535.382q0-30.308 21-51.308t51.307-21H360q30.307 0 51.307 21 21 21 21 51.308v535.382q0 30.308-21 51.308t-51.307 21H224.617Zm-12.308-607.69v535.382q0 4.616 3.846 8.463 3.846 3.846 8.462 3.846H360q4.616 0 8.462-3.846 3.847-3.847 3.847-8.463v-535.382q0-4.616-3.847-8.463Q364.616-760 360-760H224.617q-4.616 0-8.462 3.846-3.846 3.847-3.846 8.463Zm160 547.691h-160 160Z"
|
||||||
|
fill="rgb(var(--color-text-300))"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
insertTopTableIcon: `<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
viewBox="0 -960 960 960"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M212.309-527.693q-30.308 0-51.308-21t-21-51.307v-135.383q0-30.307 21-51.307 21-21 51.308-21h535.382q30.308 0 51.308 21t21 51.307V-600q0 30.307-21 51.307-21 21-51.308 21H212.309Zm0 375.383q-30.308 0-51.308-21t-21-51.307V-360q0-30.307 21-51.307 21-21 51.308-21h535.382q30.308 0 51.308 21t21 51.307v135.383q0 30.307-21 51.307-21 21-51.308 21H212.309Zm0-59.999h535.382q4.616 0 8.463-3.846 3.846-3.846 3.846-8.462V-360q0-4.616-3.846-8.462-3.847-3.847-8.463-3.847H212.309q-4.616 0-8.463 3.847Q200-364.616 200-360v135.383q0 4.616 3.846 8.462 3.847 3.846 8.463 3.846Zm-12.309-160v160-160Z"
|
||||||
|
fill="rgb(var(--color-text-300))"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
insertBottomTableIcon:`<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
viewBox="0 -960 960 960"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M212.309-152.31q-30.308 0-51.308-21t-21-51.307V-360q0-30.307 21-51.307 21-21 51.308-21h535.382q30.308 0 51.308 21t21 51.307v135.383q0 30.307-21 51.307-21 21-51.308 21H212.309Zm0-375.383q-30.308 0-51.308-21t-21-51.307v-135.383q0-30.307 21-51.307 21-21 51.308-21h535.382q30.308 0 51.308 21t21 51.307V-600q0 30.307-21 51.307-21 21-51.308 21H212.309Zm535.382-219.998H212.309q-4.616 0-8.463 3.846-3.846 3.846-3.846 8.462V-600q0 4.616 3.846 8.462 3.847 3.847 8.463 3.847h535.382q4.616 0 8.463-3.847Q760-595.384 760-600v-135.383q0-4.616-3.846-8.462-3.847-3.846-8.463-3.846ZM200-587.691v-160 160Z"
|
||||||
|
fill="rgb(var(--color-text-300))"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default icons;
|
@ -0,0 +1 @@
|
|||||||
|
export { default as default } from "./table"
|
@ -0,0 +1,117 @@
|
|||||||
|
import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
|
||||||
|
import { findParentNode } from "@tiptap/core";
|
||||||
|
import { DecorationSet, Decoration } from "@tiptap/pm/view";
|
||||||
|
|
||||||
|
const key = new PluginKey("tableControls");
|
||||||
|
|
||||||
|
export function tableControls() {
|
||||||
|
return new Plugin({
|
||||||
|
key,
|
||||||
|
state: {
|
||||||
|
init() {
|
||||||
|
return new TableControlsState();
|
||||||
|
},
|
||||||
|
apply(tr, prev) {
|
||||||
|
return prev.apply(tr);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
handleDOMEvents: {
|
||||||
|
mousemove: (view, event) => {
|
||||||
|
const pluginState = key.getState(view.state);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(event.target as HTMLElement).closest(".tableWrapper") &&
|
||||||
|
pluginState.values.hoveredTable
|
||||||
|
) {
|
||||||
|
return view.dispatch(
|
||||||
|
view.state.tr.setMeta(key, {
|
||||||
|
setHoveredTable: null,
|
||||||
|
setHoveredCell: null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pos = view.posAtCoords({
|
||||||
|
left: event.clientX,
|
||||||
|
top: event.clientY,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!pos) return;
|
||||||
|
|
||||||
|
const table = findParentNode((node) => node.type.name === "table")(
|
||||||
|
TextSelection.create(view.state.doc, pos.pos),
|
||||||
|
);
|
||||||
|
const cell = findParentNode(
|
||||||
|
(node) =>
|
||||||
|
node.type.name === "tableCell" ||
|
||||||
|
node.type.name === "tableHeader",
|
||||||
|
)(TextSelection.create(view.state.doc, pos.pos));
|
||||||
|
|
||||||
|
if (!table || !cell) return;
|
||||||
|
|
||||||
|
if (pluginState.values.hoveredCell?.pos !== cell.pos) {
|
||||||
|
return view.dispatch(
|
||||||
|
view.state.tr.setMeta(key, {
|
||||||
|
setHoveredTable: table,
|
||||||
|
setHoveredCell: cell,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
decorations: (state) => {
|
||||||
|
const pluginState = key.getState(state);
|
||||||
|
if (!pluginState) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { hoveredTable, hoveredCell } = pluginState.values;
|
||||||
|
const docSize = state.doc.content.size;
|
||||||
|
if (hoveredTable && hoveredCell && hoveredTable.pos < docSize && hoveredCell.pos < docSize) {
|
||||||
|
const decorations = [
|
||||||
|
Decoration.node(
|
||||||
|
hoveredTable.pos,
|
||||||
|
hoveredTable.pos + hoveredTable.node.nodeSize,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
hoveredTable,
|
||||||
|
hoveredCell,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
return DecorationSet.create(state.doc, decorations);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class TableControlsState {
|
||||||
|
values;
|
||||||
|
|
||||||
|
constructor(props = {}) {
|
||||||
|
this.values = {
|
||||||
|
hoveredTable: null,
|
||||||
|
hoveredCell: null,
|
||||||
|
...props,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(tr: any) {
|
||||||
|
const actions = tr.getMeta(key);
|
||||||
|
|
||||||
|
if (actions?.setHoveredTable !== undefined) {
|
||||||
|
this.values.hoveredTable = actions.setHoveredTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actions?.setHoveredCell !== undefined) {
|
||||||
|
this.values.hoveredCell = actions.setHoveredCell;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,530 @@
|
|||||||
|
import { h } from "jsx-dom-cjs";
|
||||||
|
import { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
||||||
|
import { Decoration, NodeView } from "@tiptap/pm/view";
|
||||||
|
import tippy, { Instance, Props } from "tippy.js";
|
||||||
|
|
||||||
|
import { Editor } from "@tiptap/core";
|
||||||
|
import {
|
||||||
|
CellSelection,
|
||||||
|
TableMap,
|
||||||
|
updateColumnsOnResize,
|
||||||
|
} from "@tiptap/prosemirror-tables";
|
||||||
|
|
||||||
|
import icons from "./icons";
|
||||||
|
|
||||||
|
export function updateColumns(
|
||||||
|
node: ProseMirrorNode,
|
||||||
|
colgroup: HTMLElement,
|
||||||
|
table: HTMLElement,
|
||||||
|
cellMinWidth: number,
|
||||||
|
overrideCol?: number,
|
||||||
|
overrideValue?: any,
|
||||||
|
) {
|
||||||
|
let totalWidth = 0;
|
||||||
|
let fixedWidth = true;
|
||||||
|
let nextDOM = colgroup.firstChild as HTMLElement;
|
||||||
|
const row = node.firstChild;
|
||||||
|
|
||||||
|
if (!row) return;
|
||||||
|
|
||||||
|
for (let i = 0, col = 0; i < row.childCount; i += 1) {
|
||||||
|
const { colspan, colwidth } = row.child(i).attrs;
|
||||||
|
|
||||||
|
for (let j = 0; j < colspan; j += 1, col += 1) {
|
||||||
|
const hasWidth =
|
||||||
|
overrideCol === col ? overrideValue : colwidth && colwidth[j];
|
||||||
|
const cssWidth = hasWidth ? `${hasWidth}px` : "";
|
||||||
|
|
||||||
|
totalWidth += hasWidth || cellMinWidth;
|
||||||
|
|
||||||
|
if (!hasWidth) {
|
||||||
|
fixedWidth = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nextDOM) {
|
||||||
|
colgroup.appendChild(document.createElement("col")).style.width =
|
||||||
|
cssWidth;
|
||||||
|
} else {
|
||||||
|
if (nextDOM.style.width !== cssWidth) {
|
||||||
|
nextDOM.style.width = cssWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextDOM = nextDOM.nextSibling as HTMLElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (nextDOM) {
|
||||||
|
const after = nextDOM.nextSibling;
|
||||||
|
|
||||||
|
nextDOM.parentNode?.removeChild(nextDOM);
|
||||||
|
nextDOM = after as HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fixedWidth) {
|
||||||
|
table.style.width = `${totalWidth}px`;
|
||||||
|
table.style.minWidth = "";
|
||||||
|
} else {
|
||||||
|
table.style.width = "";
|
||||||
|
table.style.minWidth = `${totalWidth}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultTippyOptions: Partial<Props> = {
|
||||||
|
allowHTML: true,
|
||||||
|
arrow: false,
|
||||||
|
trigger: "click",
|
||||||
|
animation: "scale-subtle",
|
||||||
|
theme: "light-border no-padding",
|
||||||
|
interactive: true,
|
||||||
|
hideOnClick: true,
|
||||||
|
placement: "right",
|
||||||
|
};
|
||||||
|
|
||||||
|
function setCellsBackgroundColor(editor: Editor, backgroundColor) {
|
||||||
|
return editor
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.updateAttributes("tableCell", {
|
||||||
|
background: backgroundColor,
|
||||||
|
})
|
||||||
|
.updateAttributes("tableHeader", {
|
||||||
|
background: backgroundColor,
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnsToolboxItems = [
|
||||||
|
{
|
||||||
|
label: "Add Column Before",
|
||||||
|
icon: icons.insertLeftTableIcon,
|
||||||
|
action: ({ editor }: { editor: Editor }) =>
|
||||||
|
editor.chain().focus().addColumnBefore().run(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Add Column After",
|
||||||
|
icon: icons.insertRightTableIcon,
|
||||||
|
action: ({ editor }: { editor: Editor }) =>
|
||||||
|
editor.chain().focus().addColumnAfter().run(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Pick Column Color",
|
||||||
|
icon: icons.colorPicker,
|
||||||
|
action: ({
|
||||||
|
editor,
|
||||||
|
triggerButton,
|
||||||
|
controlsContainer,
|
||||||
|
}: {
|
||||||
|
editor: Editor;
|
||||||
|
triggerButton: HTMLElement;
|
||||||
|
controlsContainer;
|
||||||
|
}) => {
|
||||||
|
createColorPickerToolbox({
|
||||||
|
triggerButton,
|
||||||
|
tippyOptions: {
|
||||||
|
appendTo: controlsContainer,
|
||||||
|
},
|
||||||
|
onSelectColor: (color) => setCellsBackgroundColor(editor, color),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Delete Column",
|
||||||
|
icon: icons.deleteColumn,
|
||||||
|
action: ({ editor }: { editor: Editor }) =>
|
||||||
|
editor.chain().focus().deleteColumn().run(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const rowsToolboxItems = [
|
||||||
|
{
|
||||||
|
label: "Add Row Above",
|
||||||
|
icon: icons.insertTopTableIcon,
|
||||||
|
action: ({ editor }: { editor: Editor }) =>
|
||||||
|
editor.chain().focus().addRowBefore().run(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Add Row Below",
|
||||||
|
icon: icons.insertBottomTableIcon,
|
||||||
|
action: ({ editor }: { editor: Editor }) =>
|
||||||
|
editor.chain().focus().addRowAfter().run(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Pick Row Color",
|
||||||
|
icon: icons.colorPicker,
|
||||||
|
action: ({
|
||||||
|
editor,
|
||||||
|
triggerButton,
|
||||||
|
controlsContainer,
|
||||||
|
}: {
|
||||||
|
editor: Editor;
|
||||||
|
triggerButton: HTMLButtonElement;
|
||||||
|
controlsContainer:
|
||||||
|
| Element
|
||||||
|
| "parent"
|
||||||
|
| ((ref: Element) => Element)
|
||||||
|
| undefined;
|
||||||
|
}) => {
|
||||||
|
createColorPickerToolbox({
|
||||||
|
triggerButton,
|
||||||
|
tippyOptions: {
|
||||||
|
appendTo: controlsContainer,
|
||||||
|
},
|
||||||
|
onSelectColor: (color) => setCellsBackgroundColor(editor, color),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Delete Row",
|
||||||
|
icon: icons.deleteRow,
|
||||||
|
action: ({ editor }: { editor: Editor }) =>
|
||||||
|
editor.chain().focus().deleteRow().run(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function createToolbox({
|
||||||
|
triggerButton,
|
||||||
|
items,
|
||||||
|
tippyOptions,
|
||||||
|
onClickItem,
|
||||||
|
}: {
|
||||||
|
triggerButton: HTMLElement;
|
||||||
|
items: { icon: string; label: string }[];
|
||||||
|
tippyOptions: any;
|
||||||
|
onClickItem: any;
|
||||||
|
}): Instance<Props> {
|
||||||
|
const toolbox = tippy(triggerButton, {
|
||||||
|
content: h(
|
||||||
|
"div",
|
||||||
|
{ className: "tableToolbox" },
|
||||||
|
items.map((item) =>
|
||||||
|
h(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
className: "toolboxItem",
|
||||||
|
onClick() {
|
||||||
|
onClickItem(item);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h("div", {
|
||||||
|
className: "iconContainer",
|
||||||
|
innerHTML: item.icon,
|
||||||
|
}),
|
||||||
|
h("div", { className: "label" }, item.label),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...tippyOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.isArray(toolbox) ? toolbox[0] : toolbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createColorPickerToolbox({
|
||||||
|
triggerButton,
|
||||||
|
tippyOptions,
|
||||||
|
onSelectColor = () => {},
|
||||||
|
}: {
|
||||||
|
triggerButton: HTMLElement;
|
||||||
|
tippyOptions: Partial<Props>;
|
||||||
|
onSelectColor?: (color: string) => void;
|
||||||
|
}) {
|
||||||
|
const items = {
|
||||||
|
Default: "rgb(var(--color-primary-100))",
|
||||||
|
Orange: "#FFE5D1",
|
||||||
|
Grey: "#F1F1F1",
|
||||||
|
Yellow: "#FEF3C7",
|
||||||
|
Green: "#DCFCE7",
|
||||||
|
Red: "#FFDDDD",
|
||||||
|
Blue: "#D9E4FF",
|
||||||
|
Pink: "#FFE8FA",
|
||||||
|
Purple: "#E8DAFB",
|
||||||
|
};
|
||||||
|
|
||||||
|
const colorPicker = tippy(triggerButton, {
|
||||||
|
...defaultTippyOptions,
|
||||||
|
content: h(
|
||||||
|
"div",
|
||||||
|
{ className: "tableColorPickerToolbox" },
|
||||||
|
Object.entries(items).map(([key, value]) =>
|
||||||
|
h(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
className: "toolboxItem",
|
||||||
|
onClick: () => {
|
||||||
|
onSelectColor(value);
|
||||||
|
colorPicker.hide();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h("div", {
|
||||||
|
className: "colorContainer",
|
||||||
|
style: {
|
||||||
|
backgroundColor: value,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
h(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
className: "label",
|
||||||
|
},
|
||||||
|
key,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onHidden: (instance) => {
|
||||||
|
instance.destroy();
|
||||||
|
},
|
||||||
|
showOnCreate: true,
|
||||||
|
...tippyOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
return colorPicker;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TableView implements NodeView {
|
||||||
|
node: ProseMirrorNode;
|
||||||
|
cellMinWidth: number;
|
||||||
|
decorations: Decoration[];
|
||||||
|
editor: Editor;
|
||||||
|
getPos: () => number;
|
||||||
|
hoveredCell;
|
||||||
|
map: TableMap;
|
||||||
|
root: HTMLElement;
|
||||||
|
table: HTMLElement;
|
||||||
|
colgroup: HTMLElement;
|
||||||
|
tbody: HTMLElement;
|
||||||
|
rowsControl?: HTMLElement;
|
||||||
|
columnsControl?: HTMLElement;
|
||||||
|
columnsToolbox?: Instance<Props>;
|
||||||
|
rowsToolbox?: Instance<Props>;
|
||||||
|
controls?: HTMLElement;
|
||||||
|
|
||||||
|
get dom() {
|
||||||
|
return this.root;
|
||||||
|
}
|
||||||
|
|
||||||
|
get contentDOM() {
|
||||||
|
return this.tbody;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
node: ProseMirrorNode,
|
||||||
|
cellMinWidth: number,
|
||||||
|
decorations: Decoration[],
|
||||||
|
editor: Editor,
|
||||||
|
getPos: () => number,
|
||||||
|
) {
|
||||||
|
this.node = node;
|
||||||
|
this.cellMinWidth = cellMinWidth;
|
||||||
|
this.decorations = decorations;
|
||||||
|
this.editor = editor;
|
||||||
|
this.getPos = getPos;
|
||||||
|
this.hoveredCell = null;
|
||||||
|
this.map = TableMap.get(node);
|
||||||
|
|
||||||
|
if (editor.isEditable) {
|
||||||
|
this.rowsControl = h(
|
||||||
|
"div",
|
||||||
|
{ className: "rowsControl" },
|
||||||
|
h("button", {
|
||||||
|
onClick: () => this.selectRow(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.columnsControl = h(
|
||||||
|
"div",
|
||||||
|
{ className: "columnsControl" },
|
||||||
|
h("button", {
|
||||||
|
onClick: () => this.selectColumn(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.controls = h(
|
||||||
|
"div",
|
||||||
|
{ className: "tableControls", contentEditable: "false" },
|
||||||
|
this.rowsControl,
|
||||||
|
this.columnsControl,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.columnsToolbox = createToolbox({
|
||||||
|
triggerButton: this.columnsControl.querySelector("button"),
|
||||||
|
items: columnsToolboxItems,
|
||||||
|
tippyOptions: {
|
||||||
|
...defaultTippyOptions,
|
||||||
|
appendTo: this.controls,
|
||||||
|
},
|
||||||
|
onClickItem: (item) => {
|
||||||
|
item.action({
|
||||||
|
editor: this.editor,
|
||||||
|
triggerButton: this.columnsControl?.firstElementChild,
|
||||||
|
controlsContainer: this.controls,
|
||||||
|
});
|
||||||
|
this.columnsToolbox?.hide();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.rowsToolbox = createToolbox({
|
||||||
|
triggerButton: this.rowsControl.firstElementChild,
|
||||||
|
items: rowsToolboxItems,
|
||||||
|
tippyOptions: {
|
||||||
|
...defaultTippyOptions,
|
||||||
|
appendTo: this.controls,
|
||||||
|
},
|
||||||
|
onClickItem: (item) => {
|
||||||
|
item.action({
|
||||||
|
editor: this.editor,
|
||||||
|
triggerButton: this.rowsControl?.firstElementChild,
|
||||||
|
controlsContainer: this.controls,
|
||||||
|
});
|
||||||
|
this.rowsToolbox?.hide();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table
|
||||||
|
|
||||||
|
this.colgroup = h(
|
||||||
|
"colgroup",
|
||||||
|
null,
|
||||||
|
Array.from({ length: this.map.width }, () => 1).map(() => h("col")),
|
||||||
|
);
|
||||||
|
this.tbody = h("tbody");
|
||||||
|
this.table = h("table", null, this.colgroup, this.tbody);
|
||||||
|
|
||||||
|
this.root = h(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
className: "tableWrapper controls--disabled",
|
||||||
|
},
|
||||||
|
this.controls,
|
||||||
|
this.table,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
update(node: ProseMirrorNode, decorations) {
|
||||||
|
if (node.type !== this.node.type) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.node = node;
|
||||||
|
this.decorations = decorations;
|
||||||
|
this.map = TableMap.get(this.node);
|
||||||
|
|
||||||
|
if (this.editor.isEditable) {
|
||||||
|
this.updateControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.colgroup.children.length !== this.map.width) {
|
||||||
|
const cols = Array.from({ length: this.map.width }, () => 1).map(() =>
|
||||||
|
h("col"),
|
||||||
|
);
|
||||||
|
this.colgroup.replaceChildren(...cols);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateColumnsOnResize(
|
||||||
|
this.node,
|
||||||
|
this.colgroup,
|
||||||
|
this.table,
|
||||||
|
this.cellMinWidth,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ignoreMutation() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateControls() {
|
||||||
|
const { hoveredTable: table, hoveredCell: cell } = Object.values(
|
||||||
|
this.decorations,
|
||||||
|
).reduce(
|
||||||
|
(acc, curr) => {
|
||||||
|
if (curr.spec.hoveredCell !== undefined) {
|
||||||
|
acc["hoveredCell"] = curr.spec.hoveredCell;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (curr.spec.hoveredTable !== undefined) {
|
||||||
|
acc["hoveredTable"] = curr.spec.hoveredTable;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, HTMLElement>,
|
||||||
|
) as any;
|
||||||
|
|
||||||
|
if (table === undefined || cell === undefined) {
|
||||||
|
return this.root.classList.add("controls--disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.root.classList.remove("controls--disabled");
|
||||||
|
this.hoveredCell = cell;
|
||||||
|
|
||||||
|
const cellDom = this.editor.view.nodeDOM(cell.pos) as HTMLElement;
|
||||||
|
|
||||||
|
const tableRect = this.table.getBoundingClientRect();
|
||||||
|
const cellRect = cellDom.getBoundingClientRect();
|
||||||
|
|
||||||
|
this.columnsControl.style.left = `${
|
||||||
|
cellRect.left - tableRect.left - this.table.parentElement!.scrollLeft
|
||||||
|
}px`;
|
||||||
|
this.columnsControl.style.width = `${cellRect.width}px`;
|
||||||
|
|
||||||
|
this.rowsControl.style.top = `${cellRect.top - tableRect.top}px`;
|
||||||
|
this.rowsControl.style.height = `${cellRect.height}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectColumn() {
|
||||||
|
if (!this.hoveredCell) return;
|
||||||
|
|
||||||
|
const colIndex = this.map.colCount(
|
||||||
|
this.hoveredCell.pos - (this.getPos() + 1),
|
||||||
|
);
|
||||||
|
const anchorCellPos = this.hoveredCell.pos;
|
||||||
|
const headCellPos =
|
||||||
|
this.map.map[colIndex + this.map.width * (this.map.height - 1)] +
|
||||||
|
(this.getPos() + 1);
|
||||||
|
|
||||||
|
const cellSelection = CellSelection.create(
|
||||||
|
this.editor.view.state.doc,
|
||||||
|
anchorCellPos,
|
||||||
|
headCellPos,
|
||||||
|
);
|
||||||
|
this.editor.view.dispatch(
|
||||||
|
// @ts-ignore
|
||||||
|
this.editor.state.tr.setSelection(cellSelection),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectRow() {
|
||||||
|
if (!this.hoveredCell) return;
|
||||||
|
|
||||||
|
const anchorCellPos = this.hoveredCell.pos;
|
||||||
|
const anchorCellIndex = this.map.map.indexOf(
|
||||||
|
anchorCellPos - (this.getPos() + 1),
|
||||||
|
);
|
||||||
|
const headCellPos =
|
||||||
|
this.map.map[anchorCellIndex + (this.map.width - 1)] +
|
||||||
|
(this.getPos() + 1);
|
||||||
|
|
||||||
|
const cellSelection = CellSelection.create(
|
||||||
|
this.editor.state.doc,
|
||||||
|
anchorCellPos,
|
||||||
|
headCellPos,
|
||||||
|
);
|
||||||
|
this.editor.view.dispatch(
|
||||||
|
// @ts-ignore
|
||||||
|
this.editor.view.state.tr.setSelection(cellSelection),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
298
packages/editor/core/src/ui/extensions/table/table/table.ts
Normal file
298
packages/editor/core/src/ui/extensions/table/table/table.ts
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
import { TextSelection } from "@tiptap/pm/state"
|
||||||
|
|
||||||
|
import { callOrReturn, getExtensionField, mergeAttributes, Node, ParentConfig } from "@tiptap/core"
|
||||||
|
import {
|
||||||
|
addColumnAfter,
|
||||||
|
addColumnBefore,
|
||||||
|
addRowAfter,
|
||||||
|
addRowBefore,
|
||||||
|
CellSelection,
|
||||||
|
columnResizing,
|
||||||
|
deleteColumn,
|
||||||
|
deleteRow,
|
||||||
|
deleteTable,
|
||||||
|
fixTables,
|
||||||
|
goToNextCell,
|
||||||
|
mergeCells,
|
||||||
|
setCellAttr,
|
||||||
|
splitCell,
|
||||||
|
tableEditing,
|
||||||
|
toggleHeader,
|
||||||
|
toggleHeaderCell
|
||||||
|
} from "@tiptap/prosemirror-tables"
|
||||||
|
|
||||||
|
import { tableControls } from "./table-controls"
|
||||||
|
import { TableView } from "./table-view"
|
||||||
|
import { createTable } from "./utilities/create-table"
|
||||||
|
import { deleteTableWhenAllCellsSelected } from "./utilities/delete-table-when-all-cells-selected"
|
||||||
|
|
||||||
|
export interface TableOptions {
|
||||||
|
HTMLAttributes: Record<string, any>
|
||||||
|
resizable: boolean
|
||||||
|
handleWidth: number
|
||||||
|
cellMinWidth: number
|
||||||
|
lastColumnResizable: boolean
|
||||||
|
allowTableNodeSelection: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "@tiptap/core" {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
table: {
|
||||||
|
insertTable: (options?: {
|
||||||
|
rows?: number
|
||||||
|
cols?: number
|
||||||
|
withHeaderRow?: boolean
|
||||||
|
}) => ReturnType
|
||||||
|
addColumnBefore: () => ReturnType
|
||||||
|
addColumnAfter: () => ReturnType
|
||||||
|
deleteColumn: () => ReturnType
|
||||||
|
addRowBefore: () => ReturnType
|
||||||
|
addRowAfter: () => ReturnType
|
||||||
|
deleteRow: () => ReturnType
|
||||||
|
deleteTable: () => ReturnType
|
||||||
|
mergeCells: () => ReturnType
|
||||||
|
splitCell: () => ReturnType
|
||||||
|
toggleHeaderColumn: () => ReturnType
|
||||||
|
toggleHeaderRow: () => ReturnType
|
||||||
|
toggleHeaderCell: () => ReturnType
|
||||||
|
mergeOrSplit: () => ReturnType
|
||||||
|
setCellAttribute: (name: string, value: any) => ReturnType
|
||||||
|
goToNextCell: () => ReturnType
|
||||||
|
goToPreviousCell: () => ReturnType
|
||||||
|
fixTables: () => ReturnType
|
||||||
|
setCellSelection: (position: {
|
||||||
|
anchorCell: number
|
||||||
|
headCell?: number
|
||||||
|
}) => ReturnType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NodeConfig<Options, Storage> {
|
||||||
|
tableRole?:
|
||||||
|
| string
|
||||||
|
| ((this: {
|
||||||
|
name: string
|
||||||
|
options: Options
|
||||||
|
storage: Storage
|
||||||
|
parent: ParentConfig<NodeConfig<Options>>["tableRole"]
|
||||||
|
}) => string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Node.create({
|
||||||
|
name: "table",
|
||||||
|
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
HTMLAttributes: {},
|
||||||
|
resizable: true,
|
||||||
|
handleWidth: 5,
|
||||||
|
cellMinWidth: 100,
|
||||||
|
lastColumnResizable: true,
|
||||||
|
allowTableNodeSelection: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
content: "tableRow+",
|
||||||
|
|
||||||
|
tableRole: "table",
|
||||||
|
|
||||||
|
isolating: true,
|
||||||
|
|
||||||
|
group: "block",
|
||||||
|
|
||||||
|
allowGapCursor: false,
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [{ tag: "table" }]
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return [
|
||||||
|
"table",
|
||||||
|
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
||||||
|
["tbody", 0]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
insertTable:
|
||||||
|
({ rows = 3, cols = 3, withHeaderRow = true} = {}) =>
|
||||||
|
({ tr, dispatch, editor }) => {
|
||||||
|
const node = createTable(
|
||||||
|
editor.schema,
|
||||||
|
rows,
|
||||||
|
cols,
|
||||||
|
withHeaderRow
|
||||||
|
)
|
||||||
|
|
||||||
|
if (dispatch) {
|
||||||
|
const offset = tr.selection.anchor + 1
|
||||||
|
|
||||||
|
tr.replaceSelectionWith(node)
|
||||||
|
.scrollIntoView()
|
||||||
|
.setSelection(
|
||||||
|
TextSelection.near(tr.doc.resolve(offset))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
addColumnBefore:
|
||||||
|
() =>
|
||||||
|
({ state, dispatch }) => addColumnBefore(state, dispatch),
|
||||||
|
addColumnAfter:
|
||||||
|
() =>
|
||||||
|
({ state, dispatch }) => addColumnAfter(state, dispatch),
|
||||||
|
deleteColumn:
|
||||||
|
() =>
|
||||||
|
({ state, dispatch }) => deleteColumn(state, dispatch),
|
||||||
|
addRowBefore:
|
||||||
|
() =>
|
||||||
|
({ state, dispatch }) => addRowBefore(state, dispatch),
|
||||||
|
addRowAfter:
|
||||||
|
() =>
|
||||||
|
({ state, dispatch }) => addRowAfter(state, dispatch),
|
||||||
|
deleteRow:
|
||||||
|
() =>
|
||||||
|
({ state, dispatch }) => deleteRow(state, dispatch),
|
||||||
|
deleteTable:
|
||||||
|
() =>
|
||||||
|
({ state, dispatch }) => deleteTable(state, dispatch),
|
||||||
|
mergeCells:
|
||||||
|
() =>
|
||||||
|
({ state, dispatch }) => mergeCells(state, dispatch),
|
||||||
|
splitCell:
|
||||||
|
() =>
|
||||||
|
({ state, dispatch }) => splitCell(state, dispatch),
|
||||||
|
toggleHeaderColumn:
|
||||||
|
() =>
|
||||||
|
({ state, dispatch }) => toggleHeader("column")(state, dispatch),
|
||||||
|
toggleHeaderRow:
|
||||||
|
() =>
|
||||||
|
({ state, dispatch }) => toggleHeader("row")(state, dispatch),
|
||||||
|
toggleHeaderCell:
|
||||||
|
() =>
|
||||||
|
({ state, dispatch }) => toggleHeaderCell(state, dispatch),
|
||||||
|
mergeOrSplit:
|
||||||
|
() =>
|
||||||
|
({ state, dispatch }) => {
|
||||||
|
if (mergeCells(state, dispatch)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return splitCell(state, dispatch)
|
||||||
|
},
|
||||||
|
setCellAttribute:
|
||||||
|
(name, value) =>
|
||||||
|
({ state, dispatch }) => setCellAttr(name, value)(state, dispatch),
|
||||||
|
goToNextCell:
|
||||||
|
() =>
|
||||||
|
({ state, dispatch }) => goToNextCell(1)(state, dispatch),
|
||||||
|
goToPreviousCell:
|
||||||
|
() =>
|
||||||
|
({ state, dispatch }) => goToNextCell(-1)(state, dispatch),
|
||||||
|
fixTables:
|
||||||
|
() =>
|
||||||
|
({ state, dispatch }) => {
|
||||||
|
if (dispatch) {
|
||||||
|
fixTables(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
setCellSelection:
|
||||||
|
(position) =>
|
||||||
|
({ tr, dispatch }) => {
|
||||||
|
if (dispatch) {
|
||||||
|
const selection = CellSelection.create(
|
||||||
|
tr.doc,
|
||||||
|
position.anchorCell,
|
||||||
|
position.headCell
|
||||||
|
)
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
tr.setSelection(selection)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addKeyboardShortcuts() {
|
||||||
|
return {
|
||||||
|
Tab: () => {
|
||||||
|
if (this.editor.commands.goToNextCell()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.editor.can().addRowAfter()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.editor.chain().addRowAfter().goToNextCell().run()
|
||||||
|
},
|
||||||
|
"Shift-Tab": () => this.editor.commands.goToPreviousCell(),
|
||||||
|
Backspace: deleteTableWhenAllCellsSelected,
|
||||||
|
"Mod-Backspace": deleteTableWhenAllCellsSelected,
|
||||||
|
Delete: deleteTableWhenAllCellsSelected,
|
||||||
|
"Mod-Delete": deleteTableWhenAllCellsSelected
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addNodeView() {
|
||||||
|
return ({ editor, getPos, node, decorations }) => {
|
||||||
|
const { cellMinWidth } = this.options
|
||||||
|
|
||||||
|
return new TableView(
|
||||||
|
node,
|
||||||
|
cellMinWidth,
|
||||||
|
decorations,
|
||||||
|
editor,
|
||||||
|
getPos as () => number
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
const isResizable = this.options.resizable && this.editor.isEditable
|
||||||
|
|
||||||
|
const plugins = [
|
||||||
|
tableEditing({
|
||||||
|
allowTableNodeSelection: this.options.allowTableNodeSelection
|
||||||
|
}),
|
||||||
|
tableControls()
|
||||||
|
]
|
||||||
|
|
||||||
|
if (isResizable) {
|
||||||
|
plugins.unshift(
|
||||||
|
columnResizing({
|
||||||
|
handleWidth: this.options.handleWidth,
|
||||||
|
cellMinWidth: this.options.cellMinWidth,
|
||||||
|
// View: TableView,
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
lastColumnResizable: this.options.lastColumnResizable
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugins
|
||||||
|
},
|
||||||
|
|
||||||
|
extendNodeSchema(extension) {
|
||||||
|
const context = {
|
||||||
|
name: extension.name,
|
||||||
|
options: extension.options,
|
||||||
|
storage: extension.storage
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tableRole: callOrReturn(
|
||||||
|
getExtensionField(extension, "tableRole", context)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
@ -0,0 +1,12 @@
|
|||||||
|
import { Fragment, Node as ProsemirrorNode, NodeType } from "prosemirror-model"
|
||||||
|
|
||||||
|
export function createCell(
|
||||||
|
cellType: NodeType,
|
||||||
|
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>
|
||||||
|
): ProsemirrorNode | null | undefined {
|
||||||
|
if (cellContent) {
|
||||||
|
return cellType.createChecked(null, cellContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cellType.createAndFill()
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
import { Fragment, Node as ProsemirrorNode, Schema } from "@tiptap/pm/model"
|
||||||
|
|
||||||
|
import { createCell } from "./create-cell"
|
||||||
|
import { getTableNodeTypes } from "./get-table-node-types"
|
||||||
|
|
||||||
|
export function createTable(
|
||||||
|
schema: Schema,
|
||||||
|
rowsCount: number,
|
||||||
|
colsCount: number,
|
||||||
|
withHeaderRow: boolean,
|
||||||
|
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>
|
||||||
|
): ProsemirrorNode {
|
||||||
|
const types = getTableNodeTypes(schema)
|
||||||
|
const headerCells: ProsemirrorNode[] = []
|
||||||
|
const cells: ProsemirrorNode[] = []
|
||||||
|
|
||||||
|
for (let index = 0; index < colsCount; index += 1) {
|
||||||
|
const cell = createCell(types.cell, cellContent)
|
||||||
|
|
||||||
|
if (cell) {
|
||||||
|
cells.push(cell)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (withHeaderRow) {
|
||||||
|
const headerCell = createCell(types.header_cell, cellContent)
|
||||||
|
|
||||||
|
if (headerCell) {
|
||||||
|
headerCells.push(headerCell)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows: ProsemirrorNode[] = []
|
||||||
|
|
||||||
|
for (let index = 0; index < rowsCount; index += 1) {
|
||||||
|
rows.push(
|
||||||
|
types.row.createChecked(
|
||||||
|
null,
|
||||||
|
withHeaderRow && index === 0 ? headerCells : cells
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.table.createChecked(null, rows)
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
import { findParentNodeClosestToPos, KeyboardShortcutCommand } from "@tiptap/core"
|
||||||
|
|
||||||
|
import { isCellSelection } from "./is-cell-selection"
|
||||||
|
|
||||||
|
export const deleteTableWhenAllCellsSelected: KeyboardShortcutCommand = ({
|
||||||
|
editor
|
||||||
|
}) => {
|
||||||
|
const { selection } = editor.state
|
||||||
|
|
||||||
|
if (!isCellSelection(selection)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let cellCount = 0
|
||||||
|
const table = findParentNodeClosestToPos(
|
||||||
|
selection.ranges[0].$from,
|
||||||
|
(node) => node.type.name === "table"
|
||||||
|
)
|
||||||
|
|
||||||
|
table?.node.descendants((node) => {
|
||||||
|
if (node.type.name === "table") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (["tableCell", "tableHeader"].includes(node.type.name)) {
|
||||||
|
cellCount += 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const allCellsSelected = cellCount === selection.ranges.length
|
||||||
|
|
||||||
|
if (!allCellsSelected) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.commands.deleteTable()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
import { NodeType, Schema } from "prosemirror-model"
|
||||||
|
|
||||||
|
export function getTableNodeTypes(schema: Schema): { [key: string]: NodeType } {
|
||||||
|
if (schema.cached.tableNodeTypes) {
|
||||||
|
return schema.cached.tableNodeTypes
|
||||||
|
}
|
||||||
|
|
||||||
|
const roles: { [key: string]: NodeType } = {}
|
||||||
|
|
||||||
|
Object.keys(schema.nodes).forEach((type) => {
|
||||||
|
const nodeType = schema.nodes[type]
|
||||||
|
|
||||||
|
if (nodeType.spec.tableRole) {
|
||||||
|
roles[nodeType.spec.tableRole] = nodeType
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
schema.cached.tableNodeTypes = roles
|
||||||
|
|
||||||
|
return roles
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
import { CellSelection } from "@tiptap/prosemirror-tables"
|
||||||
|
|
||||||
|
export function isCellSelection(value: unknown): value is CellSelection {
|
||||||
|
return value instanceof CellSelection
|
||||||
|
}
|
@ -9,7 +9,6 @@ export interface CustomMentionOptions extends MentionOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const CustomMention = Mention.extend<CustomMentionOptions>({
|
export const CustomMention = Mention.extend<CustomMentionOptions>({
|
||||||
|
|
||||||
addAttributes() {
|
addAttributes() {
|
||||||
return {
|
return {
|
||||||
id: {
|
id: {
|
||||||
@ -54,6 +53,3 @@ export const CustomMention = Mention.extend<CustomMentionOptions>({
|
|||||||
return ['mention-component', mergeAttributes(HTMLAttributes)]
|
return ['mention-component', mergeAttributes(HTMLAttributes)]
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,120 +0,0 @@
|
|||||||
import { useState, useEffect } from "react";
|
|
||||||
import { Rows, Columns, ToggleRight } from "lucide-react";
|
|
||||||
import InsertLeftTableIcon from "./InsertLeftTableIcon";
|
|
||||||
import InsertRightTableIcon from "./InsertRightTableIcon";
|
|
||||||
import InsertTopTableIcon from "./InsertTopTableIcon";
|
|
||||||
import InsertBottomTableIcon from "./InsertBottomTableIcon";
|
|
||||||
import { cn, findTableAncestor } from "../../../lib/utils";
|
|
||||||
import { Tooltip } from "./tooltip";
|
|
||||||
|
|
||||||
interface TableMenuItem {
|
|
||||||
command: () => void;
|
|
||||||
icon: any;
|
|
||||||
key: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const TableMenu = ({ editor }: { editor: any }) => {
|
|
||||||
const [tableLocation, setTableLocation] = useState({ bottom: 0, left: 0 });
|
|
||||||
const isOpen = editor?.isActive("table");
|
|
||||||
|
|
||||||
const items: TableMenuItem[] = [
|
|
||||||
{
|
|
||||||
command: () => editor.chain().focus().addColumnBefore().run(),
|
|
||||||
icon: InsertLeftTableIcon,
|
|
||||||
key: "insert-column-left",
|
|
||||||
name: "Insert 1 column left",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: () => editor.chain().focus().addColumnAfter().run(),
|
|
||||||
icon: InsertRightTableIcon,
|
|
||||||
key: "insert-column-right",
|
|
||||||
name: "Insert 1 column right",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: () => editor.chain().focus().addRowBefore().run(),
|
|
||||||
icon: InsertTopTableIcon,
|
|
||||||
key: "insert-row-above",
|
|
||||||
name: "Insert 1 row above",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: () => editor.chain().focus().addRowAfter().run(),
|
|
||||||
icon: InsertBottomTableIcon,
|
|
||||||
key: "insert-row-below",
|
|
||||||
name: "Insert 1 row below",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: () => editor.chain().focus().deleteColumn().run(),
|
|
||||||
icon: Columns,
|
|
||||||
key: "delete-column",
|
|
||||||
name: "Delete column",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: () => editor.chain().focus().deleteRow().run(),
|
|
||||||
icon: Rows,
|
|
||||||
key: "delete-row",
|
|
||||||
name: "Delete row",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: () => editor.chain().focus().toggleHeaderRow().run(),
|
|
||||||
icon: ToggleRight,
|
|
||||||
key: "toggle-header-row",
|
|
||||||
name: "Toggle header row",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!window) return;
|
|
||||||
|
|
||||||
const handleWindowClick = () => {
|
|
||||||
const selection: any = window?.getSelection();
|
|
||||||
|
|
||||||
if (selection.rangeCount !== 0) {
|
|
||||||
const range = selection.getRangeAt(0);
|
|
||||||
const tableNode = findTableAncestor(range.startContainer);
|
|
||||||
|
|
||||||
if (tableNode) {
|
|
||||||
const tableRect = tableNode.getBoundingClientRect();
|
|
||||||
const tableCenter = tableRect.left + tableRect.width / 2;
|
|
||||||
const menuWidth = 45;
|
|
||||||
const menuLeft = tableCenter - menuWidth / 2;
|
|
||||||
const tableBottom = tableRect.bottom;
|
|
||||||
|
|
||||||
setTableLocation({ bottom: tableBottom, left: menuLeft });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("click", handleWindowClick);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("click", handleWindowClick);
|
|
||||||
};
|
|
||||||
}, [tableLocation, editor]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section
|
|
||||||
className={`absolute z-20 left-1/2 -translate-x-1/2 overflow-hidden rounded border border-custom-border-300 bg-custom-background-100 shadow-custom-shadow-sm p-1 ${
|
|
||||||
isOpen ? "block" : "hidden"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{items.map((item, index) => (
|
|
||||||
<Tooltip key={index} tooltipContent={item.name}>
|
|
||||||
<button
|
|
||||||
onClick={item.command}
|
|
||||||
className="p-1.5 text-custom-text-200 hover:bg-text-custom-text-100 hover:bg-custom-background-80 active:bg-custom-background-80 rounded"
|
|
||||||
title={item.name}
|
|
||||||
>
|
|
||||||
<item.icon
|
|
||||||
className={cn("h-4 w-4 text-lg", {
|
|
||||||
"text-red-600": item.key.includes("delete"),
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
))}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
@ -8,10 +8,10 @@ import TaskList from "@tiptap/extension-task-list";
|
|||||||
import { Markdown } from "tiptap-markdown";
|
import { Markdown } from "tiptap-markdown";
|
||||||
import Gapcursor from "@tiptap/extension-gapcursor";
|
import Gapcursor from "@tiptap/extension-gapcursor";
|
||||||
|
|
||||||
import { CustomTableCell } from "../extensions/table/table-cell";
|
import TableHeader from "../extensions/table/table-header/table-header";
|
||||||
import { Table } from "../extensions/table";
|
import Table from "../extensions/table/table";
|
||||||
import { TableHeader } from "../extensions/table/table-header";
|
import TableCell from "../extensions/table/table-cell/table-cell";
|
||||||
import { TableRow } from "@tiptap/extension-table-row";
|
import TableRow from "../extensions/table/table-row/table-row";
|
||||||
|
|
||||||
import ReadOnlyImageExtension from "../extensions/image/read-only-image";
|
import ReadOnlyImageExtension from "../extensions/image/read-only-image";
|
||||||
import { isValidHttpUrl } from "../../lib/utils";
|
import { isValidHttpUrl } from "../../lib/utils";
|
||||||
@ -91,7 +91,7 @@ export const CoreReadOnlyEditorExtensions = (
|
|||||||
}),
|
}),
|
||||||
Table,
|
Table,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
CustomTableCell,
|
TableCell,
|
||||||
TableRow,
|
TableRow,
|
||||||
Mentions(mentionConfig.mentionSuggestions, mentionConfig.mentionHighlights, true),
|
Mentions(mentionConfig.mentionSuggestions, mentionConfig.mentionHighlights, true),
|
||||||
];
|
];
|
||||||
|
@ -1,4 +1,11 @@
|
|||||||
import { useState, useEffect, useCallback, ReactNode, useRef, useLayoutEffect } from "react";
|
import {
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useCallback,
|
||||||
|
ReactNode,
|
||||||
|
useRef,
|
||||||
|
useLayoutEffect,
|
||||||
|
} from "react";
|
||||||
import { Editor, Range, Extension } from "@tiptap/core";
|
import { Editor, Range, Extension } from "@tiptap/core";
|
||||||
import Suggestion from "@tiptap/suggestion";
|
import Suggestion from "@tiptap/suggestion";
|
||||||
import { ReactRenderer } from "@tiptap/react";
|
import { ReactRenderer } from "@tiptap/react";
|
||||||
@ -18,7 +25,18 @@ import {
|
|||||||
Table,
|
Table,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { UploadImage } from "../";
|
import { UploadImage } from "../";
|
||||||
import { cn, insertTableCommand, toggleBlockquote, toggleBulletList, toggleOrderedList, toggleTaskList, insertImageCommand, toggleHeadingOne, toggleHeadingTwo, toggleHeadingThree } from "@plane/editor-core";
|
import {
|
||||||
|
cn,
|
||||||
|
insertTableCommand,
|
||||||
|
toggleBlockquote,
|
||||||
|
toggleBulletList,
|
||||||
|
toggleOrderedList,
|
||||||
|
toggleTaskList,
|
||||||
|
insertImageCommand,
|
||||||
|
toggleHeadingOne,
|
||||||
|
toggleHeadingTwo,
|
||||||
|
toggleHeadingThree,
|
||||||
|
} from "@plane/editor-core";
|
||||||
|
|
||||||
interface CommandItemProps {
|
interface CommandItemProps {
|
||||||
title: string;
|
title: string;
|
||||||
@ -37,7 +55,15 @@ const Command = Extension.create({
|
|||||||
return {
|
return {
|
||||||
suggestion: {
|
suggestion: {
|
||||||
char: "/",
|
char: "/",
|
||||||
command: ({ editor, range, props }: { editor: Editor; range: Range; props: any }) => {
|
command: ({
|
||||||
|
editor,
|
||||||
|
range,
|
||||||
|
props,
|
||||||
|
}: {
|
||||||
|
editor: Editor;
|
||||||
|
range: Range;
|
||||||
|
props: any;
|
||||||
|
}) => {
|
||||||
props.command({ editor, range });
|
props.command({ editor, range });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -59,7 +85,9 @@ const Command = Extension.create({
|
|||||||
const getSuggestionItems =
|
const getSuggestionItems =
|
||||||
(
|
(
|
||||||
uploadFile: UploadImage,
|
uploadFile: UploadImage,
|
||||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
|
setIsSubmitting?: (
|
||||||
|
isSubmitting: "submitting" | "submitted" | "saved",
|
||||||
|
) => void,
|
||||||
) =>
|
) =>
|
||||||
({ query }: { query: string }) =>
|
({ query }: { query: string }) =>
|
||||||
[
|
[
|
||||||
@ -69,7 +97,12 @@ const getSuggestionItems =
|
|||||||
searchTerms: ["p", "paragraph"],
|
searchTerms: ["p", "paragraph"],
|
||||||
icon: <Text size={18} />,
|
icon: <Text size={18} />,
|
||||||
command: ({ editor, range }: CommandProps) => {
|
command: ({ editor, range }: CommandProps) => {
|
||||||
editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run();
|
editor
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.deleteRange(range)
|
||||||
|
.toggleNode("paragraph", "paragraph")
|
||||||
|
.run();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -105,7 +138,7 @@ const getSuggestionItems =
|
|||||||
searchTerms: ["todo", "task", "list", "check", "checkbox"],
|
searchTerms: ["todo", "task", "list", "check", "checkbox"],
|
||||||
icon: <CheckSquare size={18} />,
|
icon: <CheckSquare size={18} />,
|
||||||
command: ({ editor, range }: CommandProps) => {
|
command: ({ editor, range }: CommandProps) => {
|
||||||
toggleTaskList(editor, range)
|
toggleTaskList(editor, range);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -141,7 +174,7 @@ const getSuggestionItems =
|
|||||||
searchTerms: ["ordered"],
|
searchTerms: ["ordered"],
|
||||||
icon: <ListOrdered size={18} />,
|
icon: <ListOrdered size={18} />,
|
||||||
command: ({ editor, range }: CommandProps) => {
|
command: ({ editor, range }: CommandProps) => {
|
||||||
toggleOrderedList(editor, range)
|
toggleOrderedList(editor, range);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -150,7 +183,7 @@ const getSuggestionItems =
|
|||||||
searchTerms: ["blockquote"],
|
searchTerms: ["blockquote"],
|
||||||
icon: <TextQuote size={18} />,
|
icon: <TextQuote size={18} />,
|
||||||
command: ({ editor, range }: CommandProps) =>
|
command: ({ editor, range }: CommandProps) =>
|
||||||
toggleBlockquote(editor, range)
|
toggleBlockquote(editor, range),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Code",
|
title: "Code",
|
||||||
@ -175,7 +208,8 @@ const getSuggestionItems =
|
|||||||
return (
|
return (
|
||||||
item.title.toLowerCase().includes(search) ||
|
item.title.toLowerCase().includes(search) ||
|
||||||
item.description.toLowerCase().includes(search) ||
|
item.description.toLowerCase().includes(search) ||
|
||||||
(item.searchTerms && item.searchTerms.some((term: string) => term.includes(search)))
|
(item.searchTerms &&
|
||||||
|
item.searchTerms.some((term: string) => term.includes(search)))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@ -213,7 +247,7 @@ const CommandList = ({
|
|||||||
command(item);
|
command(item);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[command, items]
|
[command, items],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -266,11 +300,17 @@ const CommandList = ({
|
|||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
`flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm text-custom-text-200 hover:bg-custom-primary-100/5 hover:text-custom-text-100`,
|
`flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm text-custom-text-200 hover:bg-custom-primary-100/5 hover:text-custom-text-100`,
|
||||||
{ "bg-custom-primary-100/5 text-custom-text-100": index === selectedIndex }
|
{
|
||||||
|
"bg-custom-primary-100/5 text-custom-text-100":
|
||||||
|
index === selectedIndex,
|
||||||
|
},
|
||||||
)}
|
)}
|
||||||
key={index}
|
key={index}
|
||||||
onClick={() => selectItem(index)}
|
onClick={() => selectItem(index)}
|
||||||
>
|
>
|
||||||
|
<div className="flex h-10 w-10 items-center justify-center rounded-md border border-custom-border-300 bg-custom-background-100">
|
||||||
|
{item.icon}
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">{item.title}</p>
|
<p className="font-medium">{item.title}</p>
|
||||||
<p className="text-xs text-custom-text-300">{item.description}</p>
|
<p className="text-xs text-custom-text-300">{item.description}</p>
|
||||||
@ -331,7 +371,9 @@ const renderItems = () => {
|
|||||||
|
|
||||||
export const SlashCommand = (
|
export const SlashCommand = (
|
||||||
uploadFile: UploadImage,
|
uploadFile: UploadImage,
|
||||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
|
setIsSubmitting?: (
|
||||||
|
isSubmitting: "submitting" | "submitted" | "saved",
|
||||||
|
) => void,
|
||||||
) =>
|
) =>
|
||||||
Command.configure({
|
Command.configure({
|
||||||
suggestion: {
|
suggestion: {
|
||||||
|
@ -1,10 +1,18 @@
|
|||||||
import { BubbleMenu, BubbleMenuProps } from "@tiptap/react";
|
import { BubbleMenu, BubbleMenuProps, isNodeSelection } from "@tiptap/react";
|
||||||
import { FC, useState } from "react";
|
import { FC, useEffect, useState } from "react";
|
||||||
import { BoldIcon } from "lucide-react";
|
import { BoldIcon } from "lucide-react";
|
||||||
|
|
||||||
import { NodeSelector } from "./node-selector";
|
import { NodeSelector } from "./node-selector";
|
||||||
import { LinkSelector } from "./link-selector";
|
import { LinkSelector } from "./link-selector";
|
||||||
import { BoldItem, cn, CodeItem, ItalicItem, StrikeThroughItem, UnderLineItem } from "@plane/editor-core";
|
import {
|
||||||
|
BoldItem,
|
||||||
|
cn,
|
||||||
|
CodeItem,
|
||||||
|
isCellSelection,
|
||||||
|
ItalicItem,
|
||||||
|
StrikeThroughItem,
|
||||||
|
UnderLineItem,
|
||||||
|
} from "@plane/editor-core";
|
||||||
|
|
||||||
export interface BubbleMenuItem {
|
export interface BubbleMenuItem {
|
||||||
name: string;
|
name: string;
|
||||||
@ -26,14 +34,35 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
|||||||
|
|
||||||
const bubbleMenuProps: EditorBubbleMenuProps = {
|
const bubbleMenuProps: EditorBubbleMenuProps = {
|
||||||
...props,
|
...props,
|
||||||
shouldShow: ({ editor }) => {
|
shouldShow: ({ view, state, editor }) => {
|
||||||
if (!editor.isEditable) {
|
const { selection } = state;
|
||||||
|
|
||||||
|
const { empty } = selection;
|
||||||
|
const hasEditorFocus = view.hasFocus();
|
||||||
|
|
||||||
|
// if (typeof window !== "undefined") {
|
||||||
|
// const selection: any = window?.getSelection();
|
||||||
|
// if (selection.rangeCount !== 0) {
|
||||||
|
// const range = selection.getRangeAt(0);
|
||||||
|
// if (findTableAncestor(range.startContainer)) {
|
||||||
|
// console.log("table");
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (
|
||||||
|
!hasEditorFocus ||
|
||||||
|
empty ||
|
||||||
|
!editor.isEditable ||
|
||||||
|
editor.isActive("image") ||
|
||||||
|
isNodeSelection(selection) ||
|
||||||
|
isCellSelection(selection) ||
|
||||||
|
isSelecting
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (editor.isActive("image")) {
|
return true;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return editor.view.state.selection.content().size > 0;
|
|
||||||
},
|
},
|
||||||
tippyOptions: {
|
tippyOptions: {
|
||||||
moveTransition: "transform 0.15s ease-out",
|
moveTransition: "transform 0.15s ease-out",
|
||||||
@ -47,11 +76,41 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
|||||||
const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false);
|
const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false);
|
||||||
const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false);
|
const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false);
|
||||||
|
|
||||||
|
const [isSelecting, setIsSelecting] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
function handleMouseDown() {
|
||||||
|
function handleMouseMove() {
|
||||||
|
if (!props.editor.state.selection.empty) {
|
||||||
|
setIsSelecting(true);
|
||||||
|
document.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseUp() {
|
||||||
|
setIsSelecting(false);
|
||||||
|
|
||||||
|
document.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
document.removeEventListener("mouseup", handleMouseUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("mousemove", handleMouseMove);
|
||||||
|
document.addEventListener("mouseup", handleMouseUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("mousedown", handleMouseDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleMouseDown);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BubbleMenu
|
<BubbleMenu
|
||||||
{...bubbleMenuProps}
|
{...bubbleMenuProps}
|
||||||
className="flex w-fit divide-x divide-custom-border-300 rounded border border-custom-border-300 bg-custom-background-100 shadow-xl"
|
className="flex w-fit divide-x divide-custom-border-300 rounded border border-custom-border-300 bg-custom-background-100 shadow-xl"
|
||||||
>
|
>
|
||||||
|
{isSelecting ? null : (
|
||||||
|
<>
|
||||||
{!props.editor.isActive("table") && (
|
{!props.editor.isActive("table") && (
|
||||||
<NodeSelector
|
<NodeSelector
|
||||||
editor={props.editor!}
|
editor={props.editor!}
|
||||||
@ -79,8 +138,9 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"p-2 text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5 transition-colors",
|
"p-2 text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5 transition-colors",
|
||||||
{
|
{
|
||||||
"text-custom-text-100 bg-custom-primary-100/5": item.isActive(),
|
"text-custom-text-100 bg-custom-primary-100/5":
|
||||||
}
|
item.isActive(),
|
||||||
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<item.icon
|
<item.icon
|
||||||
@ -91,6 +151,8 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</BubbleMenu>
|
</BubbleMenu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -4,6 +4,8 @@ import { ThemeProvider } from "next-themes";
|
|||||||
// styles
|
// styles
|
||||||
import "styles/globals.css";
|
import "styles/globals.css";
|
||||||
import "styles/editor.css";
|
import "styles/editor.css";
|
||||||
|
import "styles/table.css";
|
||||||
|
|
||||||
// contexts
|
// contexts
|
||||||
import { ToastContextProvider } from "contexts/toast.context";
|
import { ToastContextProvider } from "contexts/toast.context";
|
||||||
// mobx store provider
|
// mobx store provider
|
||||||
|
194
space/styles/table.css
Normal file
194
space/styles/table.css
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
.tableWrapper {
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 2px;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
table-layout: fixed;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
border: 1px solid rgba(var(--color-border-200));
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper table td,
|
||||||
|
.tableWrapper table th {
|
||||||
|
min-width: 1em;
|
||||||
|
border: 1px solid rgba(var(--color-border-200));
|
||||||
|
padding: 10px 15px;
|
||||||
|
vertical-align: top;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper table td > *,
|
||||||
|
.tableWrapper table th > * {
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0.25rem 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper table td.has-focus,
|
||||||
|
.tableWrapper table th.has-focus {
|
||||||
|
box-shadow: rgba(var(--color-primary-300), 0.1) 0px 0px 0px 2px inset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper table th {
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
background-color: rgba(var(--color-primary-100));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper table th * {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper table .selectedCell:after {
|
||||||
|
z-index: 2;
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(var(--color-primary-300), 0.1);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper table .column-resize-handle {
|
||||||
|
position: absolute;
|
||||||
|
right: -2px;
|
||||||
|
top: 0;
|
||||||
|
bottom: -2px;
|
||||||
|
width: 4px;
|
||||||
|
z-index: 99;
|
||||||
|
background-color: rgba(var(--color-primary-400));
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper .tableControls {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper .tableControls .columnsControl,
|
||||||
|
.tableWrapper .tableControls .rowsControl {
|
||||||
|
transition: opacity ease-in 100ms;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 99;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper .tableControls .columnsControl {
|
||||||
|
height: 20px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper .tableControls .columnsControl > button {
|
||||||
|
color: white;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M4.5 10.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S6 12.825 6 12s-.675-1.5-1.5-1.5zm15 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S21 12.825 21 12s-.675-1.5-1.5-1.5zm-7.5 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E");
|
||||||
|
width: 30px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper .tableControls .rowsControl {
|
||||||
|
width: 20px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper .tableControls .rowsControl > button {
|
||||||
|
color: white;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M12 3c-.825 0-1.5.675-1.5 1.5S11.175 6 12 6s1.5-.675 1.5-1.5S12.825 3 12 3zm0 15c-.825 0-1.5.675-1.5 1.5S11.175 21 12 21s1.5-.675 1.5-1.5S12.825 18 12 18zm0-7.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E");
|
||||||
|
height: 30px;
|
||||||
|
width: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper .tableControls button {
|
||||||
|
background-color: rgba(var(--color-primary-100));
|
||||||
|
border: 1px solid rgba(var(--color-border-200));
|
||||||
|
border-radius: 2px;
|
||||||
|
background-size: 1.25rem;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
transition: transform ease-out 100ms, background-color ease-out 100ms;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: #000 0px 2px 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper .tableControls .tableToolbox,
|
||||||
|
.tableWrapper .tableControls .tableColorPickerToolbox {
|
||||||
|
border: 1px solid rgba(var(--color-border-300));
|
||||||
|
background-color: rgba(var(--color-background-100));
|
||||||
|
padding: 0.25rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 200px;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper .tableControls .tableToolbox .toolboxItem,
|
||||||
|
.tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem {
|
||||||
|
background-color: rgba(var(--color-background-100));
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
border: none;
|
||||||
|
padding: 0.1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper .tableControls .tableToolbox .toolboxItem:hover,
|
||||||
|
.tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem:hover {
|
||||||
|
background-color: rgba(var(--color-background-100), 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper .tableControls .tableToolbox .toolboxItem .iconContainer,
|
||||||
|
.tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem .iconContainer,
|
||||||
|
.tableWrapper .tableControls .tableToolbox .toolboxItem .colorContainer,
|
||||||
|
.tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem .colorContainer {
|
||||||
|
border: 1px solid rgba(var(--color-border-300));
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1.75rem;
|
||||||
|
height: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper .tableControls .tableToolbox .toolboxItem .iconContainer svg,
|
||||||
|
.tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem .iconContainer svg,
|
||||||
|
.tableWrapper .tableControls .tableToolbox .toolboxItem .colorContainer svg,
|
||||||
|
.tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem .colorContainer svg {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableToolbox {
|
||||||
|
background-color: rgba(var(--color-background-100));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper .tableControls .tableToolbox .toolboxItem .label,
|
||||||
|
.tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem .label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: rgba(var(--color-text-300));
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-cursor .tableWrapper .tableControls .rowsControl,
|
||||||
|
.tableWrapper.controls--disabled .tableControls .rowsControl,
|
||||||
|
.resize-cursor .tableWrapper .tableControls .columnsControl,
|
||||||
|
.tableWrapper.controls--disabled .tableControls .columnsControl {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
@ -139,8 +139,6 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) =
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span>{errors.name ? errors.name.message : null}</span>
|
<span>{errors.name ? errors.name.message : null}</span>
|
||||||
|
|
||||||
<span className="">
|
|
||||||
<RichTextEditor
|
<RichTextEditor
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
@ -153,8 +151,6 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) =
|
|||||||
mentionSuggestions={editorSuggestions.mentionSuggestions}
|
mentionSuggestions={editorSuggestions.mentionSuggestions}
|
||||||
mentionHighlights={editorSuggestions.mentionHighlights}
|
mentionHighlights={editorSuggestions.mentionHighlights}
|
||||||
/>
|
/>
|
||||||
</span>
|
|
||||||
|
|
||||||
<IssueReaction
|
<IssueReaction
|
||||||
issueReactions={issueReactions}
|
issueReactions={issueReactions}
|
||||||
user={user}
|
user={user}
|
||||||
|
@ -8,6 +8,7 @@ import NProgress from "nprogress";
|
|||||||
// styles
|
// styles
|
||||||
import "styles/globals.css";
|
import "styles/globals.css";
|
||||||
import "styles/editor.css";
|
import "styles/editor.css";
|
||||||
|
import "styles/table.css";
|
||||||
import "styles/command-pallette.css";
|
import "styles/command-pallette.css";
|
||||||
import "styles/nprogress.css";
|
import "styles/nprogress.css";
|
||||||
import "styles/react-datepicker.css";
|
import "styles/react-datepicker.css";
|
||||||
|
194
web/styles/table.css
Normal file
194
web/styles/table.css
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
.tableWrapper {
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 2px;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
table-layout: fixed;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
border: 1px solid rgba(var(--color-border-200));
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper table td,
|
||||||
|
.tableWrapper table th {
|
||||||
|
min-width: 1em;
|
||||||
|
border: 1px solid rgba(var(--color-border-200));
|
||||||
|
padding: 10px 15px;
|
||||||
|
vertical-align: top;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper table td > *,
|
||||||
|
.tableWrapper table th > * {
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0.25rem 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper table td.has-focus,
|
||||||
|
.tableWrapper table th.has-focus {
|
||||||
|
box-shadow: rgba(var(--color-primary-300), 0.1) 0px 0px 0px 2px inset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper table th {
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
background-color: rgba(var(--color-primary-100));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper table th * {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper table .selectedCell:after {
|
||||||
|
z-index: 2;
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(var(--color-primary-300), 0.1);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper table .column-resize-handle {
|
||||||
|
position: absolute;
|
||||||
|
right: -2px;
|
||||||
|
top: 0;
|
||||||
|
bottom: -2px;
|
||||||
|
width: 4px;
|
||||||
|
z-index: 99;
|
||||||
|
background-color: rgba(var(--color-primary-400));
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper .tableControls {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper .tableControls .columnsControl,
|
||||||
|
.tableWrapper .tableControls .rowsControl {
|
||||||
|
transition: opacity ease-in 100ms;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 99;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper .tableControls .columnsControl {
|
||||||
|
height: 20px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper .tableControls .columnsControl > button {
|
||||||
|
color: white;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M4.5 10.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S6 12.825 6 12s-.675-1.5-1.5-1.5zm15 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S21 12.825 21 12s-.675-1.5-1.5-1.5zm-7.5 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E");
|
||||||
|
width: 30px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper .tableControls .rowsControl {
|
||||||
|
width: 20px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper .tableControls .rowsControl > button {
|
||||||
|
color: white;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M12 3c-.825 0-1.5.675-1.5 1.5S11.175 6 12 6s1.5-.675 1.5-1.5S12.825 3 12 3zm0 15c-.825 0-1.5.675-1.5 1.5S11.175 21 12 21s1.5-.675 1.5-1.5S12.825 18 12 18zm0-7.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E");
|
||||||
|
height: 30px;
|
||||||
|
width: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper .tableControls button {
|
||||||
|
background-color: rgba(var(--color-primary-100));
|
||||||
|
border: 1px solid rgba(var(--color-border-200));
|
||||||
|
border-radius: 2px;
|
||||||
|
background-size: 1.25rem;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
transition: transform ease-out 100ms, background-color ease-out 100ms;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: #000 0px 2px 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper .tableControls .tableToolbox,
|
||||||
|
.tableWrapper .tableControls .tableColorPickerToolbox {
|
||||||
|
border: 1px solid rgba(var(--color-border-300));
|
||||||
|
background-color: rgba(var(--color-background-100));
|
||||||
|
padding: 0.25rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 200px;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper .tableControls .tableToolbox .toolboxItem,
|
||||||
|
.tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem {
|
||||||
|
background-color: rgba(var(--color-background-100));
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
border: none;
|
||||||
|
padding: 0.1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper .tableControls .tableToolbox .toolboxItem:hover,
|
||||||
|
.tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem:hover {
|
||||||
|
background-color: rgba(var(--color-background-100), 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper .tableControls .tableToolbox .toolboxItem .iconContainer,
|
||||||
|
.tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem .iconContainer,
|
||||||
|
.tableWrapper .tableControls .tableToolbox .toolboxItem .colorContainer,
|
||||||
|
.tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem .colorContainer {
|
||||||
|
border: 1px solid rgba(var(--color-border-300));
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1.75rem;
|
||||||
|
height: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper .tableControls .tableToolbox .toolboxItem .iconContainer svg,
|
||||||
|
.tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem .iconContainer svg,
|
||||||
|
.tableWrapper .tableControls .tableToolbox .toolboxItem .colorContainer svg,
|
||||||
|
.tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem .colorContainer svg {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableToolbox {
|
||||||
|
background-color: rgba(var(--color-background-100));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper .tableControls .tableToolbox .toolboxItem .label,
|
||||||
|
.tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem .label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: rgba(var(--color-text-300));
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-cursor .tableWrapper .tableControls .rowsControl,
|
||||||
|
.tableWrapper.controls--disabled .tableControls .rowsControl,
|
||||||
|
.resize-cursor .tableWrapper .tableControls .columnsControl,
|
||||||
|
.tableWrapper.controls--disabled .tableControls .columnsControl {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
12
yarn.lock
12
yarn.lock
@ -2554,6 +2554,11 @@
|
|||||||
prosemirror-transform "^1.7.0"
|
prosemirror-transform "^1.7.0"
|
||||||
prosemirror-view "^1.28.2"
|
prosemirror-view "^1.28.2"
|
||||||
|
|
||||||
|
"@tiptap/prosemirror-tables@^1.1.4":
|
||||||
|
version "1.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tiptap/prosemirror-tables/-/prosemirror-tables-1.1.4.tgz#e123978f13c9b5f980066ba660ec5df857755916"
|
||||||
|
integrity sha512-O2XnDhZV7xTHSFxMMl8Ei3UVeCxuMlbGYZ+J2QG8CzkK8mxDpBa66kFr5DdyAhvdi1ptpcH9u7/GMwItQpN4sA==
|
||||||
|
|
||||||
"@tiptap/react@^2.1.7":
|
"@tiptap/react@^2.1.7":
|
||||||
version "2.1.12"
|
version "2.1.12"
|
||||||
resolved "https://registry.yarnpkg.com/@tiptap/react/-/react-2.1.12.tgz#23566c7992b9642137171b282335e646922ae559"
|
resolved "https://registry.yarnpkg.com/@tiptap/react/-/react-2.1.12.tgz#23566c7992b9642137171b282335e646922ae559"
|
||||||
@ -5879,6 +5884,13 @@ jsonpointer@^5.0.0:
|
|||||||
object.assign "^4.1.4"
|
object.assign "^4.1.4"
|
||||||
object.values "^1.1.6"
|
object.values "^1.1.6"
|
||||||
|
|
||||||
|
jsx-dom-cjs@^8.0.3:
|
||||||
|
version "8.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/jsx-dom-cjs/-/jsx-dom-cjs-8.0.7.tgz#098c54680ebf5bb6f6d12cdea5cde3799c172212"
|
||||||
|
integrity sha512-dQWnuQ+bTm7o72ZlJU4glzeMX8KLxx5U+ZwmEAzVP1+roL7BSM0MrkWdHjdsuNgmxobZCJ+qgiot9EgbJPOoEg==
|
||||||
|
dependencies:
|
||||||
|
csstype "^3.1.2"
|
||||||
|
|
||||||
keycode@^2.2.0:
|
keycode@^2.2.0:
|
||||||
version "2.2.1"
|
version "2.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.1.tgz#09c23b2be0611d26117ea2501c2c391a01f39eff"
|
resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.1.tgz#09c23b2be0611d26117ea2501c2c391a01f39eff"
|
||||||
|
Loading…
Reference in New Issue
Block a user