forked from github/plane
working tables with row/column selectors
This commit is contained in:
parent
b6f1cb71d8
commit
020e5fe922
@ -32,10 +32,6 @@
|
||||
"@tiptap/extension-color": "^2.1.11",
|
||||
"@tiptap/extension-image": "^2.1.7",
|
||||
"@tiptap/extension-link": "^2.1.7",
|
||||
"@tiptap/extension-table": "^2.1.6",
|
||||
"@tiptap/extension-table-cell": "^2.1.6",
|
||||
"@tiptap/extension-table-header": "^2.1.6",
|
||||
"@tiptap/extension-table-row": "^2.1.6",
|
||||
"@tiptap/extension-task-item": "^2.1.7",
|
||||
"@tiptap/extension-task-list": "^2.1.7",
|
||||
"@tiptap/extension-text-style": "^2.1.11",
|
||||
@ -56,7 +52,9 @@
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"tiptap-markdown": "^0.8.2",
|
||||
"use-debounce": "^9.0.4"
|
||||
"use-debounce": "^9.0.4",
|
||||
"@tiptap/prosemirror-tables": "^1.1.4",
|
||||
"jsx-dom-cjs": "^8.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^7.32.0",
|
||||
|
@ -2,6 +2,8 @@
|
||||
// import "./styles/tailwind.css";
|
||||
// import "./styles/editor.css";
|
||||
|
||||
export * from "./ui/extensions/table-new/Table";
|
||||
|
||||
// utils
|
||||
export * from "./lib/utils";
|
||||
export { startImageUpload } from "./ui/plugins/upload-image";
|
||||
|
@ -212,9 +212,9 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
|
||||
}
|
||||
}
|
||||
|
||||
.tableWrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
/* .tableWrapper { */
|
||||
/* overflow-x: auto; */
|
||||
/* } */
|
||||
|
||||
.resize-cursor {
|
||||
cursor: ew-resize;
|
||||
|
@ -13,7 +13,7 @@ export const EditorContainer = ({ editor, editorClassNames, children }: EditorCo
|
||||
onClick={() => {
|
||||
editor?.chain().focus().run();
|
||||
}}
|
||||
className={`cursor-text ${editorClassNames}`}
|
||||
className={`cursor-text editorContainer ${editorClassNames}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
@ -10,10 +10,10 @@ interface EditorContentProps {
|
||||
}
|
||||
|
||||
export const EditorContentWrapper = ({ editor, editorContentCustomClassNames = '', children }: EditorContentProps) => (
|
||||
<div className={`${editorContentCustomClassNames}`}>
|
||||
<div className={`contentEditor ${editorContentCustomClassNames}`}>
|
||||
{/* @ts-ignore */}
|
||||
<EditorContent editor={editor} />
|
||||
<TableMenu editor={editor} />
|
||||
{/* <TableMenu editor={editor} /> */}
|
||||
{editor?.isActive("image") && <ImageResizer editor={editor} />}
|
||||
{children}
|
||||
</div>
|
||||
|
@ -8,15 +8,19 @@ import TaskList from "@tiptap/extension-task-list";
|
||||
import { Markdown } from "tiptap-markdown";
|
||||
import Gapcursor from "@tiptap/extension-gapcursor";
|
||||
|
||||
import { CustomTableCell } from "./table/table-cell";
|
||||
import { Table } from "./table";
|
||||
import { TableHeader } from "./table/table-header";
|
||||
import { TableRow } from "@tiptap/extension-table-row";
|
||||
// import { CustomTableCell } from "./table/table-cell";
|
||||
// import { Table } from "./table";
|
||||
// import { TableHeader } from "./table/table-header";
|
||||
// import { TableRow } from "@tiptap/extension-table-row";
|
||||
|
||||
import ImageExtension from "./image";
|
||||
|
||||
import { DeleteImage } from "../../types/delete-image";
|
||||
import { isValidHttpUrl } from "../../lib/utils";
|
||||
import Table from "./table-new/Table";
|
||||
import TableHeader from "./table-new/TableHeader";
|
||||
import TableCell from "./table-new/TableCell";
|
||||
import TableRow from "./table-new/TableRow";
|
||||
|
||||
|
||||
export const CoreEditorExtensions = (
|
||||
@ -92,6 +96,10 @@ export const CoreEditorExtensions = (
|
||||
}),
|
||||
Table,
|
||||
TableHeader,
|
||||
CustomTableCell,
|
||||
TableCell,
|
||||
TableRow,
|
||||
// Table,
|
||||
// TableHeader,
|
||||
// CustomTableCell,
|
||||
// TableRow,
|
||||
];
|
||||
|
336
packages/editor/core/src/ui/extensions/table-new/Table/Table.ts
Normal file
336
packages/editor/core/src/ui/extensions/table-new/Table/Table.ts
Normal file
@ -0,0 +1,336 @@
|
||||
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 "./tableControls"
|
||||
import { TableView } from "./TableView"
|
||||
import { createTable } from "./utilities/createTable"
|
||||
import { deleteTableWhenAllCellsSelected } from "./utilities/deleteTableWhenAllCellsSelected"
|
||||
|
||||
/**
|
||||
* Extension based on:
|
||||
* - Tiptap TableExtension (https://github.com/ueberdosis/tiptap/blob/main/packages/extension-table/src/table.ts)
|
||||
*/
|
||||
|
||||
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> {
|
||||
/**
|
||||
* Table Role
|
||||
*/
|
||||
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 }) => {
|
||||
return addColumnBefore(state, dispatch)
|
||||
},
|
||||
addColumnAfter:
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
return addColumnAfter(state, dispatch)
|
||||
},
|
||||
deleteColumn:
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
return deleteColumn(state, dispatch)
|
||||
},
|
||||
addRowBefore:
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
return addRowBefore(state, dispatch)
|
||||
},
|
||||
addRowAfter:
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
return addRowAfter(state, dispatch)
|
||||
},
|
||||
deleteRow:
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
return deleteRow(state, dispatch)
|
||||
},
|
||||
deleteTable:
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
return deleteTable(state, dispatch)
|
||||
},
|
||||
mergeCells:
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
return mergeCells(state, dispatch)
|
||||
},
|
||||
splitCell:
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
return splitCell(state, dispatch)
|
||||
},
|
||||
toggleHeaderColumn:
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
return toggleHeader("column")(state, dispatch)
|
||||
},
|
||||
toggleHeaderRow:
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
return toggleHeader("row")(state, dispatch)
|
||||
},
|
||||
toggleHeaderCell:
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
return toggleHeaderCell(state, dispatch)
|
||||
},
|
||||
mergeOrSplit:
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
if (mergeCells(state, dispatch)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return splitCell(state, dispatch)
|
||||
},
|
||||
setCellAttribute:
|
||||
(name, value) =>
|
||||
({ state, dispatch }) => {
|
||||
return setCellAttr(name, value)(state, dispatch)
|
||||
},
|
||||
goToNextCell:
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
return goToNextCell(1)(state, dispatch)
|
||||
},
|
||||
goToPreviousCell:
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
return 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,500 @@
|
||||
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, Tippy } 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.insertColumnLeft,
|
||||
action: ({ editor }: { editor: Editor }) => editor.chain().focus().addColumnBefore().run()
|
||||
},
|
||||
{
|
||||
label: "Add Column After",
|
||||
icon: icons.insertColumnRight,
|
||||
action: ({ editor }: { editor: Editor }) => editor.chain().focus().addColumnAfter().run()
|
||||
},
|
||||
{
|
||||
label: "Pick Column Color",
|
||||
icon: icons.colorPicker,
|
||||
action: ({ editor, triggerButton, 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.insertRowTop,
|
||||
action: ({ editor }: { editor: Editor }) => editor.chain().focus().addRowBefore().run()
|
||||
},
|
||||
{
|
||||
label: "Add Row Below",
|
||||
icon: icons.insertRowBottom,
|
||||
action: ({ editor }: { editor: Editor }) => editor.chain().focus().addRowAfter().run()
|
||||
},
|
||||
{
|
||||
label: "Pick a Color",
|
||||
icon: icons.colorPicker,
|
||||
action: ({ editor, triggerButton, controlsContainer }: { editor: Editor, triggerButton: HTMLElement, controlsContainer: any }) => {
|
||||
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 = {
|
||||
"Fond par défault": "#ffffff",
|
||||
"Fond gris clair": "#e7f3f8",
|
||||
"Fond gris foncé": "#c7d2d7",
|
||||
"Fond bleu": "#e7f3f8",
|
||||
"Fond rouge": "#ffc4c7",
|
||||
"Fond jaune": "#fbf3db"
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
/**
|
||||
* DOM
|
||||
*/
|
||||
|
||||
// Controllers
|
||||
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 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)
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
const icons = {
|
||||
insertColumnLeft: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0H24V24H0z"/><path d="M20 3c.552 0 1 .448 1 1v16c0 .552-.448 1-1 1h-6c-.552 0-1-.448-1-1V4c0-.552.448-1 1-1h6zm-1 2h-4v14h4V5zM6 7c2.761 0 5 2.239 5 5s-2.239 5-5 5-5-2.239-5-5 2.239-5 5-5zm1 2H5v1.999L3 11v2l2-.001V15h2v-2.001L9 13v-2l-2-.001V9z"/></svg>`,
|
||||
insertColumnRight: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0H24V24H0z"/><path d="M10 3c.552 0 1 .448 1 1v16c0 .552-.448 1-1 1H4c-.552 0-1-.448-1-1V4c0-.552.448-1 1-1h6zM9 5H5v14h4V5zm9 2c2.761 0 5 2.239 5 5s-2.239 5-5 5-5-2.239-5-5 2.239-5 5-5zm1 2h-2v1.999L15 11v2l2-.001V15h2v-2.001L21 13v-2l-2-.001V9z"/></svg>`,
|
||||
insertRowTop: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0H24V24H0z"/><path d="M20 13c.552 0 1 .448 1 1v6c0 .552-.448 1-1 1H4c-.552 0-1-.448-1-1v-6c0-.552.448-1 1-1h16zm-1 2H5v4h14v-4zM12 1c2.761 0 5 2.239 5 5s-2.239 5-5 5-5-2.239-5-5 2.239-5 5-5zm1 2h-2v1.999L9 5v2l2-.001V9h2V6.999L15 7V5l-2-.001V3z"/></svg>`,
|
||||
insertRowBottom: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0H24V24H0z"/><path d="M12 13c2.761 0 5 2.239 5 5s-2.239 5-5 5-5-2.239-5-5 2.239-5 5-5zm1 2h-2v1.999L9 17v2l2-.001V21h2v-2.001L15 19v-2l-2-.001V15zm7-12c.552 0 1 .448 1 1v6c0 .552-.448 1-1 1H4c-.552 0-1-.448-1-1V4c0-.552.448-1 1-1h16zM5 5v4h14V5H5z"/></svg>`,
|
||||
colorPicker: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" style="fill: inherit;transform: ;msFilter:;"><path 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="none" 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="none" 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>`
|
||||
}
|
||||
|
||||
export default icons
|
@ -0,0 +1 @@
|
||||
export { default as default } from "./Table"
|
@ -0,0 +1,118 @@
|
||||
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
|
||||
if (hoveredTable) {
|
||||
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,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,47 @@
|
||||
import { Fragment, Node as ProsemirrorNode, Schema } from "@tiptap/pm/model"
|
||||
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react"
|
||||
|
||||
import { createCell } from "./createCell"
|
||||
import { getTableNodeTypes } from "./getTableNodeTypes"
|
||||
|
||||
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,41 @@
|
||||
import { findParentNodeClosestToPos, KeyboardShortcutCommand } from "@tiptap/core"
|
||||
|
||||
import { isCellSelection } from "./isCellSelection"
|
||||
|
||||
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) => {
|
||||
return 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
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
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
|
||||
]
|
||||
}
|
||||
})
|
@ -0,0 +1 @@
|
||||
export { default as default } from "./TableCell"
|
@ -0,0 +1,54 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
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 "./TableHeader"
|
@ -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
|
||||
]
|
||||
}
|
||||
})
|
@ -0,0 +1 @@
|
||||
export { default as default } from "./TableRow"
|
@ -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];
|
||||
},
|
||||
});
|
@ -1,7 +0,0 @@
|
||||
import { TableHeader as BaseTableHeader } from "@tiptap/extension-table-header";
|
||||
|
||||
const TableHeader = BaseTableHeader.extend({
|
||||
content: "paragraph",
|
||||
});
|
||||
|
||||
export { TableHeader };
|
@ -8,10 +8,10 @@ import TaskList from "@tiptap/extension-task-list";
|
||||
import { Markdown } from "tiptap-markdown";
|
||||
import Gapcursor from "@tiptap/extension-gapcursor";
|
||||
|
||||
import { CustomTableCell } from "../extensions/table/table-cell";
|
||||
import { Table } from "../extensions/table";
|
||||
import { TableHeader } from "../extensions/table/table-header";
|
||||
import { TableRow } from "@tiptap/extension-table-row";
|
||||
// import { CustomTableCell } from "../extensions/table/table-cell";
|
||||
// import { Table } from "../extensions/table";
|
||||
// import { TableHeader } from "../extensions/table/table-header";
|
||||
// import { TableRow } from "@tiptap/extension-table-row";
|
||||
|
||||
import ReadOnlyImageExtension from "../extensions/image/read-only-image";
|
||||
import { isValidHttpUrl } from "../../lib/utils";
|
||||
@ -85,8 +85,8 @@ export const CoreReadOnlyEditorExtensions = [
|
||||
html: true,
|
||||
transformCopiedText: true,
|
||||
}),
|
||||
Table,
|
||||
TableHeader,
|
||||
CustomTableCell,
|
||||
TableRow,
|
||||
// Table,
|
||||
// TableHeader,
|
||||
// CustomTableCell,
|
||||
// TableRow,
|
||||
];
|
||||
|
@ -14,8 +14,8 @@ import { LiteTextEditorWithRef, LiteReadOnlyEditorWithRef } from "@plane/lite-te
|
||||
import { timeAgo } from "helpers/date-time.helper";
|
||||
// types
|
||||
import type { IIssueComment } from "types";
|
||||
import fileService from "services/file.service";
|
||||
// services
|
||||
import fileService from "services/file.service";
|
||||
|
||||
type Props = {
|
||||
comment: IIssueComment;
|
||||
|
@ -6,6 +6,7 @@ import NProgress from "nprogress";
|
||||
// styles
|
||||
import "styles/globals.css";
|
||||
import "styles/editor.css";
|
||||
import "styles/tables.css";
|
||||
import "styles/command-pallette.css";
|
||||
import "styles/nprogress.css";
|
||||
import "styles/react-datepicker.css";
|
||||
|
200
web/styles/tables.css
Normal file
200
web/styles/tables.css
Normal file
@ -0,0 +1,200 @@
|
||||
.tableWrapper {
|
||||
overflow-x: auto;
|
||||
padding: 2px;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.tableWrapper table {
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
margin: 0;
|
||||
border: 1px solid rgb(var(--color-border-200));
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tableWrapper table td,
|
||||
.tableWrapper table th {
|
||||
min-width: 1em;
|
||||
border: 1px solid rgb(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: rgb(var(--color-primary-100));
|
||||
}
|
||||
|
||||
.tableWrapper table td:hover{
|
||||
background-color: rgba(var(--color-primary-300), 0.1);
|
||||
}
|
||||
|
||||
.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: rgb(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: rgb(var(--color-primary-100));
|
||||
border: 1px solid rgb(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 button:hover { */
|
||||
/* transform: scale(1.2, 1.2); */
|
||||
/* background-color: var(--color-n50); */
|
||||
/* } */
|
||||
|
||||
.tableWrapper .tableControls .tableToolbox,
|
||||
.tableWrapper .tableControls .tableColorPickerToolbox {
|
||||
padding: 0.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 200px;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.tableWrapper .tableControls .tableToolbox .toolboxItem,
|
||||
.tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem {
|
||||
background-color: rgb(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: var(--color-n100);
|
||||
}
|
||||
|
||||
.tableWrapper .tableControls .tableToolbox .toolboxItem .iconContainer,
|
||||
.tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem .iconContainer,
|
||||
.tableWrapper .tableControls .tableToolbox .toolboxItem .colorContainer,
|
||||
.tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem .colorContainer {
|
||||
border: 1px solid #e6e8f0;
|
||||
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: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.tableToolbox {
|
||||
background-color: rgb(var(--color-background-100));
|
||||
}
|
||||
|
||||
.tableWrapper .tableControls .tableToolbox .toolboxItem .label,
|
||||
.tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem .label {
|
||||
font-size: 0.95rem;
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
70
yarn.lock
70
yarn.lock
@ -2476,26 +2476,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-2.1.10.tgz#ec311395d16af15345b63d2dac2d459b9ad5fa9e"
|
||||
integrity sha512-KW63lZLPFIir5AIeh2I7UK6Tx1O3jetD7JIPUzEqp1I1BfJlHGHVQxV8VXAmJl0hTOzjQBsHW42PmBxSC97NUg==
|
||||
|
||||
"@tiptap/extension-table-cell@^2.1.6":
|
||||
version "2.1.10"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-table-cell/-/extension-table-cell-2.1.10.tgz#e594b55622435c43a95edf6f2adfaca402f5cbed"
|
||||
integrity sha512-NQOTKjPOTJrkI7VaR9wFF3UKB9N2THD8zJZJDcECKQxLR740udF6/6jWm1uwkTwdkBekVKHBMQvKKK9W1bOBiw==
|
||||
|
||||
"@tiptap/extension-table-header@^2.1.6":
|
||||
version "2.1.10"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-table-header/-/extension-table-header-2.1.10.tgz#6250676a26946e5b7186198a06990ea70f578a87"
|
||||
integrity sha512-NSC0Y10kXDvPGiJckJY/QU8VA7HHU0tI20Dj7/r1oD9itBWSnWP0zAOXzHVlQt9GpThhFNo2nu3fAaVQNfKoTg==
|
||||
|
||||
"@tiptap/extension-table-row@^2.1.6":
|
||||
version "2.1.10"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-table-row/-/extension-table-row-2.1.10.tgz#e7a1ca8342b623a400848b437c82d57680e551e3"
|
||||
integrity sha512-yMOnAaXE7vK7MwULuVUO8v6AYZu6wxTfHAWQe/FqPeMf9tG0HL6+gyt1audremw0xBFMGPx6v4t8vlqPXW9p2g==
|
||||
|
||||
"@tiptap/extension-table@^2.1.6":
|
||||
version "2.1.10"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-table/-/extension-table-2.1.10.tgz#5654426366b547631c647ffc5dacf040e65307e1"
|
||||
integrity sha512-fsf0c6qA+R6NzbFx+tm1l5POZsgadHjREsedvq5q1i8rCq1Gt1AK+lR7WQsaXlSeIRsWtg4RT0eUjAYNCmKkug==
|
||||
|
||||
"@tiptap/extension-task-item@^2.1.7":
|
||||
version "2.1.10"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-task-item/-/extension-task-item-2.1.10.tgz#8eb0d3e8b1234fa44205dd91619f3f1937ca3254"
|
||||
@ -2545,6 +2525,11 @@
|
||||
prosemirror-transform "^1.7.0"
|
||||
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":
|
||||
version "2.1.10"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/react/-/react-2.1.10.tgz#51cd96462e61f6fffa0ca4eb359d8d7d15ebf422"
|
||||
@ -2805,7 +2790,7 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@^18.0.17":
|
||||
"@types/react@*", "@types/react@18.0.15", "@types/react@18.0.28", "@types/react@18.2.0", "@types/react@^18.0.17", "@types/react@^18.2.5":
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.0.tgz#15cda145354accfc09a18d2f2305f9fc099ada21"
|
||||
integrity sha512-0FLj93y5USLHdnhIhABk83rm8XEGA7kH3cr+YUlvxoUGp1xNt/DINUMvqPxLyOQMzLmZe8i4RTHbvb8MC7NmrA==
|
||||
@ -2814,33 +2799,6 @@
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/react@18.0.15":
|
||||
version "18.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.15.tgz#d355644c26832dc27f3e6cbf0c4f4603fc4ab7fe"
|
||||
integrity sha512-iz3BtLuIYH1uWdsv6wXYdhozhqj20oD4/Hk2DNXIn1kFsmp9x8d9QB6FnPhfkbhd2PgEONt9Q1x/ebkwjfFLow==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/react@18.0.28":
|
||||
version "18.0.28"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.28.tgz#accaeb8b86f4908057ad629a26635fe641480065"
|
||||
integrity sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/react@^18.2.5":
|
||||
version "18.2.24"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.24.tgz#3c7d68c02e0205a472f04abe4a0c1df35d995c05"
|
||||
integrity sha512-Ee0Jt4sbJxMu1iDcetZEIKQr99J1Zfb6D4F3qfUWoR1JpInkY1Wdg4WwCyBjL257D0+jGqSl1twBjV8iCaC0Aw==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/reactcss@*":
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/reactcss/-/reactcss-1.2.6.tgz#133c1e7e896f2726370d1d5a26bf06a30a038bcc"
|
||||
@ -5761,6 +5719,13 @@ jsonpointer@^5.0.0:
|
||||
object.assign "^4.1.4"
|
||||
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:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.1.tgz#09c23b2be0611d26117ea2501c2c391a01f39eff"
|
||||
@ -7035,7 +7000,14 @@ prosemirror-menu@^1.2.1:
|
||||
prosemirror-history "^1.0.0"
|
||||
prosemirror-state "^1.0.0"
|
||||
|
||||
prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.18.1, prosemirror-model@^1.19.0, prosemirror-model@^1.8.1:
|
||||
prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.18.1, prosemirror-model@^1.8.1:
|
||||
version "1.18.1"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.18.1.tgz#1d5d6b6de7b983ee67a479dc607165fdef3935bd"
|
||||
integrity sha512-IxSVBKAEMjD7s3n8cgtwMlxAXZrC7Mlag7zYsAKDndAqnDScvSmp/UdnRTV/B33lTCVU3CCm7dyAn/rVVD0mcw==
|
||||
dependencies:
|
||||
orderedmap "^2.0.0"
|
||||
|
||||
prosemirror-model@^1.19.0:
|
||||
version "1.19.3"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.19.3.tgz#f0d55285487fefd962d0ac695f716f4ec6705006"
|
||||
integrity sha512-tgSnwN7BS7/UM0sSARcW+IQryx2vODKX4MI7xpqY2X+iaepJdKBPc7I4aACIsDV/LTaTjt12Z56MhDr9LsyuZQ==
|
||||
|
Loading…
Reference in New Issue
Block a user