From ea196543022adaa52b2ef6823a3baf2b9e245538 Mon Sep 17 00:00:00 2001 From: Devansh Khetan <56186839+Devanshk9@users.noreply.github.com> Date: Tue, 11 Nov 2025 17:54:20 +0530 Subject: [PATCH 01/15] feat: new table --- components.d.ts | 2 + package.json | 9 +- src/components/TextEditor/TextEditor.vue | 597 +++++++++--------- src/components/TextEditor/commands.js | 3 +- .../TextEditor/components/TableBorderMenu.vue | 222 +++++++ .../TextEditor/composables/useTableMenu.ts | 122 ++++ .../tables/table-border-menu-plugin.ts | 224 +++++++ .../extensions/tables/table-cell-extension.ts | 56 ++ .../extensions/tables/table-extension.ts | 60 ++ .../tables/table-header-extension.ts | 56 ++ .../extensions/tables/table-menu-extension.ts | 52 ++ .../extensions/tables/table-row-extension.ts | 56 ++ .../extensions/tables/table-styles.css | 185 ++++++ .../extensions/tables/use-table-menu.ts | 122 ++++ 14 files changed, 1467 insertions(+), 299 deletions(-) create mode 100644 src/components/TextEditor/components/TableBorderMenu.vue create mode 100644 src/components/TextEditor/composables/useTableMenu.ts create mode 100644 src/components/TextEditor/extensions/tables/table-border-menu-plugin.ts create mode 100644 src/components/TextEditor/extensions/tables/table-cell-extension.ts create mode 100644 src/components/TextEditor/extensions/tables/table-extension.ts create mode 100644 src/components/TextEditor/extensions/tables/table-header-extension.ts create mode 100644 src/components/TextEditor/extensions/tables/table-menu-extension.ts create mode 100644 src/components/TextEditor/extensions/tables/table-row-extension.ts create mode 100644 src/components/TextEditor/extensions/tables/table-styles.css create mode 100644 src/components/TextEditor/extensions/tables/use-table-menu.ts diff --git a/components.d.ts b/components.d.ts index d97d9e59f..34e0eba33 100644 --- a/components.d.ts +++ b/components.d.ts @@ -164,6 +164,8 @@ declare module 'vue' { TabButtons: typeof import('./src/components/TabButtons/TabButtons.vue')['default'] 'TabButtons.story': typeof import('./src/components/TabButtons/TabButtons.story.vue')['default'] Table2: typeof import('./src/components/TextEditor/icons/table-2.vue')['default'] + TableActionMenu: typeof import('./src/components/TextEditor/extensions/table/TableActionMenu.vue')['default'] + TableCellActionHandle: typeof import('./src/components/TextEditor/extensions/table/TableCellActionHandle.vue')['default'] TabList: typeof import('./src/components/Tabs/TabList.vue')['default'] TabPanel: typeof import('./src/components/Tabs/TabPanel.vue')['default'] Tabs: typeof import('./src/components/Tabs/Tabs.vue')['default'] diff --git a/package.json b/package.json index 458f01c1d..df6d81a77 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@tailwindcss/line-clamp": "^0.4.4", "@tailwindcss/typography": "^0.5.16", "@tiptap/core": "^2.26.1", + "@tiptap/extension-bubble-menu": "^3.10.5", "@tiptap/extension-code": "^2.26.1", "@tiptap/extension-code-block": "^2.26.1", "@tiptap/extension-code-block-lowlight": "^2.26.1", @@ -61,6 +62,7 @@ "@tiptap/suggestion": "^2.26.1", "@tiptap/vue-3": "^2.26.1", "@vueuse/core": "^10.4.1", + "date-fns": "^4.1.0", "dayjs": "^1.11.13", "dompurify": "^3.2.6", "echarts": "^5.6.0", @@ -73,16 +75,17 @@ "marked": "^15.0.12", "ora": "5.4.1", "prettier": "^3.3.2", + "prosemirror-state": "^1.4.4", + "prosemirror-tables": "^1.8.1", "radix-vue": "^1.5.3", "reka-ui": "^2.5.0", + "slugify": "^1.6.6", "socket.io-client": "^4.5.1", "tippy.js": "^6.3.7", "typescript": "^5.0.2", "unplugin-auto-import": "^19.3.0", "unplugin-icons": "^22.1.0", - "unplugin-vue-components": "^28.4.1", - "date-fns": "^4.1.0", - "slugify": "^1.6.6" + "unplugin-vue-components": "^28.4.1" }, "peerDependencies": { "vue": ">=3.5.0", diff --git a/src/components/TextEditor/TextEditor.vue b/src/components/TextEditor/TextEditor.vue index f687c167e..4047042a4 100644 --- a/src/components/TextEditor/TextEditor.vue +++ b/src/components/TextEditor/TextEditor.vue @@ -1,38 +1,57 @@ diff --git a/src/components/TextEditor/commands.js b/src/components/TextEditor/commands.js index c90907dc4..152521469 100644 --- a/src/components/TextEditor/commands.js +++ b/src/components/TextEditor/commands.js @@ -146,7 +146,8 @@ export default { FontColor: { label: 'Font Color', icon: FontColor, - isActive: (editor) => editor.getAttributes('textStyle')?.color || editor.isActive('highlight'), + isActive: (editor) => + editor.getAttributes('textStyle')?.color || editor.isActive('highlight'), component: defineAsyncComponent(() => import('./FontColor.vue')), }, Blockquote: { diff --git a/src/components/TextEditor/components/TableBorderMenu.vue b/src/components/TextEditor/components/TableBorderMenu.vue new file mode 100644 index 000000000..9c0cd42b5 --- /dev/null +++ b/src/components/TextEditor/components/TableBorderMenu.vue @@ -0,0 +1,222 @@ + + + + + diff --git a/src/components/TextEditor/composables/useTableMenu.ts b/src/components/TextEditor/composables/useTableMenu.ts new file mode 100644 index 000000000..b4f8b6ca9 --- /dev/null +++ b/src/components/TextEditor/composables/useTableMenu.ts @@ -0,0 +1,122 @@ +import { ref, computed, onMounted, onBeforeUnmount, type Ref } from 'vue' +import type { Editor } from '@tiptap/vue-3' + +export interface TableCellInfo { + element: HTMLElement | null + rowIndex: number + colIndex: number + isFirstRow: boolean +} + +export interface TableBorderMenuPosition { + top: number + left: number +} + +export function useTableMenu(editor: Ref) { + const showTableBorderMenu = ref(false) + const tableBorderAxis = ref<'row' | 'column' | null>(null) + const tableBorderMenuPos = ref({ top: 0, left: 0 }) + const tableCellInfo = ref(null) + + const onBorderClick = (e: Event) => { + const { axis, position, cellInfo } = (e as CustomEvent).detail + tableBorderAxis.value = axis + tableBorderMenuPos.value = position + tableCellInfo.value = cellInfo + showTableBorderMenu.value = true + } + + const closeMenu = (e: MouseEvent) => { + const target = e.target as HTMLElement + if ( + !target.closest('.table-border-menu') && + !target.closest('.table-row-handle-overlay') && + !target.closest('.table-col-handle-overlay') + ) { + showTableBorderMenu.value = false + } + } + + const addRowBefore = () => { + editor.value?.chain().focus().addRowBefore().run() + showTableBorderMenu.value = false + } + + const addRowAfter = () => { + editor.value?.chain().focus().addRowAfter().run() + showTableBorderMenu.value = false + } + + const deleteRow = () => { + editor.value?.chain().focus().deleteRow().run() + showTableBorderMenu.value = false + } + + const addColumnBefore = () => { + editor.value?.chain().focus().addColumnBefore().run() + showTableBorderMenu.value = false + } + + const addColumnAfter = () => { + editor.value?.chain().focus().addColumnAfter().run() + showTableBorderMenu.value = false + } + + const deleteColumn = () => { + editor.value?.chain().focus().deleteColumn().run() + showTableBorderMenu.value = false + } + + const mergeCells = () => { + editor.value?.chain().focus().mergeCells().run() + showTableBorderMenu.value = false + } + + const toggleHeader = () => { + editor.value?.chain().focus().toggleHeaderRow().run() + showTableBorderMenu.value = false + } + + const setBackgroundColor = (color: string | null) => { + editor.value?.chain().focus().setCellAttribute('backgroundColor', color).run() + showTableBorderMenu.value = false + } + + const setBorderColor = (color: string | null) => { + editor.value?.chain().focus().setCellAttribute('borderColor', color).run() + showTableBorderMenu.value = false + } + + const canMergeCells = computed(() => { + return editor.value?.can().mergeCells() ?? false + }) + + onMounted(() => { + window.addEventListener('table-border-click', onBorderClick) + document.addEventListener('click', closeMenu) + }) + + onBeforeUnmount(() => { + window.removeEventListener('table-border-click', onBorderClick) + document.removeEventListener('click', closeMenu) + }) + + return { + showTableBorderMenu, + tableBorderAxis, + tableBorderMenuPos, + tableCellInfo, + canMergeCells, + addRowBefore, + addRowAfter, + deleteRow, + addColumnBefore, + addColumnAfter, + deleteColumn, + mergeCells, + toggleHeader, + setBackgroundColor, + setBorderColor, + } +} diff --git a/src/components/TextEditor/extensions/tables/table-border-menu-plugin.ts b/src/components/TextEditor/extensions/tables/table-border-menu-plugin.ts new file mode 100644 index 000000000..5feec1852 --- /dev/null +++ b/src/components/TextEditor/extensions/tables/table-border-menu-plugin.ts @@ -0,0 +1,224 @@ +import { Plugin, PluginKey } from '@tiptap/pm/state' +import { Editor } from '@tiptap/core' + +export const tableBorderMenuPluginKey = new PluginKey('tableBorderMenu') + +export function tableBorderMenuPlugin(editor: Editor) { + let currentRowHandle: HTMLElement | null = null + let currentColHandle: HTMLElement | null = null + let hideTimeout: NodeJS.Timeout | null = null + + const clearHandles = () => { + if (hideTimeout) clearTimeout(hideTimeout) + hideTimeout = setTimeout(() => { + currentRowHandle?.remove() + currentColHandle?.remove() + currentRowHandle = null + currentColHandle = null + }, 100) + } + + const cancelClear = () => { + if (hideTimeout) { + clearTimeout(hideTimeout) + hideTimeout = null + } + } + + return new Plugin({ + key: tableBorderMenuPluginKey, + props: { + handleDOMEvents: { + mousemove(view, event) { + const target = event.target as HTMLElement + if (target.closest('.table-row-handle-overlay') || target.closest('.table-col-handle-overlay')) { + cancelClear() + return false + } + const cell = target.closest('td, th') + if (!cell || !cell.closest('.ProseMirror table')) { + clearHandles() + return false + } + else{ + cancelClear() + } + const row = cell.closest('tr')! + const table = cell.closest('table')! + const rowIndex = Array.from(table.querySelectorAll('tr')).indexOf(row as HTMLTableRowElement) + const colIndex = Array.from(row.querySelectorAll('td, th')).indexOf(cell as HTMLTableCellElement) + + const editorElement = view.dom.parentElement! + const editorRect = editorElement.getBoundingClientRect() + + if (!currentRowHandle || currentRowHandle.getAttribute('data-row-id') !== String(rowIndex)) { + currentRowHandle?.remove() + + currentRowHandle = document.createElement('div') + currentRowHandle.className = 'table-row-handle-overlay' + currentRowHandle.textContent = '⋮⋮' + currentRowHandle.setAttribute('data-row-id', String(rowIndex)) + + const rowRect = row.getBoundingClientRect() + + currentRowHandle.style.cssText = ` + position: absolute; + left: ${rowRect.left - editorRect.left - 30}px; + top: ${rowRect.top - editorRect.top + (rowRect.height / 2) - 10}px; + height: 20px; + width: 24px; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + font-weight: bold; + color: var(--surface-gray-5); + cursor: pointer; + z-index: 10; + user-select: none; + background-color: white; + border: 1px solid var(--outline-gray-3); + border-radius: 4px; + box-shadow: 0 1px 2px rgba(0,0,0,0.05); + ` + + currentRowHandle.addEventListener('mouseenter', function() { + this.style.backgroundColor = 'var(--outline-gray-1)' + this.style.color = 'var(--outline-gray-4)' + cancelClear() + }) + + currentRowHandle.addEventListener('mouseleave', function() { + this.style.backgroundColor = 'white' + this.style.color = 'var(--surface-ink-8)' + clearHandles() + }) + + currentRowHandle.addEventListener('click', (e) => { + e.preventDefault() + e.stopPropagation() + + const cellEl = row.querySelector('td, th') + if (!cellEl) return + const cellRect = cellEl.getBoundingClientRect() + const editorRect = editorElement.getBoundingClientRect() + const menuHeight = 40; + const gap = 8; + + window.dispatchEvent(new CustomEvent('table-border-click', { + bubbles: true, + detail: { + axis: 'row', + position: { + top: cellRect.top - editorRect.top - menuHeight - gap, + left: cellRect.left - editorRect.left, + }, + cellInfo: { + element: cellEl, + rowIndex, + colIndex: 0, + }, + }, + })) + }) + + editorElement.appendChild(currentRowHandle) + } + + // COLUMN HANDLE (only on first row) + if (rowIndex === 0) { + if (!currentColHandle || currentColHandle.getAttribute('data-col-id') !== String(colIndex)) { + currentColHandle?.remove() + + currentColHandle = document.createElement('div') + currentColHandle.className = 'table-col-handle-overlay' + currentColHandle.textContent = '⋮⋮' + currentColHandle.setAttribute('data-col-id', String(colIndex)) + + const cellRect = cell.getBoundingClientRect() + + currentColHandle.style.cssText = ` + position: absolute; + left: ${cellRect.left - editorRect.left + (cellRect.width / 2) - 10}px; + top: ${cellRect.top - editorRect.top - 30}px; + height: 20px; + width: 24px; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + font-weight: bold; + color: var(--surface-gray-5); + cursor: pointer; + z-index: 1000; + user-select: none; + background-color: white; + border: 1px solid var(--outline-gray-3); + border-radius: 4px; + box-shadow: 0 1px 2px rgba(0,0,0,0.05); + ` + + currentColHandle.addEventListener('mouseenter', function() { + this.style.backgroundColor = 'var(--outline-gray-1)' + this.style.color = 'var(--outline-gray-4)' + cancelClear() + }) + + currentColHandle.addEventListener('mouseleave', function() { + this.style.backgroundColor = 'white' + this.style.color = 'var(--surface-ink-8)' + clearHandles() + }) + + currentColHandle.addEventListener('click', (e) => { + e.preventDefault() + e.stopPropagation() + + const cellRect = cell.getBoundingClientRect() + const editorRect = editorElement.getBoundingClientRect() + const menuHeight = 40; // px, adjust if your menu is taller/shorter + const gap = 8; // px, gap between menu and cell + + window.dispatchEvent(new CustomEvent('table-border-click', { + bubbles: true, + detail: { + axis: 'column', + position: { + // Appear above the clicked cell, not overlapping + top: cellRect.top - editorRect.top - menuHeight - gap, + left: cellRect.left - editorRect.left, + }, + cellInfo: { + element: cell, + rowIndex, + colIndex, + }, + }, + })) + }) + + editorElement.appendChild(currentColHandle) + } + } else { + if (currentColHandle) { + currentColHandle.remove() + currentColHandle = null + } + } + + return false + }, + }, + }, + view() { + return { + destroy() { + currentRowHandle?.remove() + currentColHandle?.remove() + currentRowHandle = null + currentColHandle = null + }, + } + }, + }) +} diff --git a/src/components/TextEditor/extensions/tables/table-cell-extension.ts b/src/components/TextEditor/extensions/tables/table-cell-extension.ts new file mode 100644 index 000000000..43d01dc25 --- /dev/null +++ b/src/components/TextEditor/extensions/tables/table-cell-extension.ts @@ -0,0 +1,56 @@ +import TableCell from '@tiptap/extension-table-cell' + +export const TableCellExtension = TableCell.extend({ + addAttributes() { + return { + ...this.parent?.(), + backgroundColor: { + default: null, + renderHTML: (attributes) => { + if (!attributes.backgroundColor) { + return {} + } + return { + class: `${attributes.backgroundColor}` + } + }, + parseHTML: (element) => { + return element.style.backgroundColor.replace(/['"]+/g, '') + }, + }, + borderColor: { + default: null, + renderHTML: (attributes) => { + if (!attributes.borderColor) { + return {} + } + + return { + class: `${attributes.borderColor}-border`, + } + }, + parseHTML: (element) => { + return element.style.borderColor.replace(/['"]+/g, '') + }, + }, + borderWidth: { + default: null, + renderHTML: (attributes) => { + if (!attributes.borderWidth) { + return {} + } + return { + class: `border-${attributes.borderWidth}`, + } + }, + parseHTML: (element) => { + return element.style.borderWidth.replace(/['"]+/g, '') + }, + }, + } + }, +}) + + +export default TableCellExtension + diff --git a/src/components/TextEditor/extensions/tables/table-extension.ts b/src/components/TextEditor/extensions/tables/table-extension.ts new file mode 100644 index 000000000..d260f8f10 --- /dev/null +++ b/src/components/TextEditor/extensions/tables/table-extension.ts @@ -0,0 +1,60 @@ +import Table from '@tiptap/extension-table' +import { tableCellMenuPlugin } from './table-menu-extension'; +import { tableBorderMenuPlugin } from './table-border-menu-plugin'; +import TableRow from '@tiptap/extension-table-row' +import TableHeader from '@tiptap/extension-table-header' +import TableCell from '@tiptap/extension-table-cell' + +export const TableExtension = Table.extend({ + TableRow, + TableHeader, + TableCell, + addAttributes() { + return { + backgroundColor: { + default: null, + renderHTML: (attributes) => { + if (!attributes.backgroundColor) { + return {} + } + + return { + class: `${attributes.backgroundColor}`, + } + }, + }, + borderColor: { + default: null, + renderHTML: (attributes) => { + if (!attributes.borderColor) { + return {} + } + + return { + class: `${attributes.borderColor}!`, + } + }, + }, + borderWidth: { + default: null, + renderHTML: (attributes) => { + if (!attributes.borderWidth) { + return {} + } + return { + class: `border-${attributes.borderWidth}`, + } + }, + }, + } + }, + addProseMirrorPlugins() { + return [ + ...(this.parent?.() ?? []), + tableCellMenuPlugin(this.editor), + tableBorderMenuPlugin(this.editor), + ] + }, +}) + +export default TableExtension diff --git a/src/components/TextEditor/extensions/tables/table-header-extension.ts b/src/components/TextEditor/extensions/tables/table-header-extension.ts new file mode 100644 index 000000000..68a9abcb7 --- /dev/null +++ b/src/components/TextEditor/extensions/tables/table-header-extension.ts @@ -0,0 +1,56 @@ +import TableHeader from '@tiptap/extension-table-header' + +export const TableHeaderExtension = TableHeader.extend({ + addAttributes() { + return { + ...this.parent?.(), + backgroundColor: { + default: null, + renderHTML: (attributes) => { + if (!attributes.backgroundColor) { + return {} + } + + return { + class: `${attributes.backgroundColor}` + } + }, + parseHTML: (element) => { + return element.style.backgroundColor.replace(/['"]+/g, '') + }, + }, + borderColor: { + default: null, + renderHTML: (attributes) => { + if (!attributes.borderColor) { + return {} + } + + return { + class: `${attributes.borderColor}-border`, + } + }, + parseHTML: (element) => { + return element.style.borderColor.replace(/['"]+/g, '') + }, + }, + borderWidth: { + default: null, + renderHTML: (attributes) => { + if (!attributes.borderWidth) { + return {} + } + return { + class: `border-${attributes.borderWidth}`, + } + }, + parseHTML: (element) => { + return element.style.borderWidth.replace(/['"]+/g, '') + }, + }, + } + }, +}) + + +export default TableHeaderExtension diff --git a/src/components/TextEditor/extensions/tables/table-menu-extension.ts b/src/components/TextEditor/extensions/tables/table-menu-extension.ts new file mode 100644 index 000000000..3fdedc546 --- /dev/null +++ b/src/components/TextEditor/extensions/tables/table-menu-extension.ts @@ -0,0 +1,52 @@ +import { Plugin, PluginKey } from '@tiptap/pm/state' +import { Editor } from '@tiptap/core' + +export const tableCellMenuPluginKey = new PluginKey('tableCellMenu') + +export function tableCellMenuPlugin(editor: Editor) { + return new Plugin({ + key: tableCellMenuPluginKey, + props: { + handleClick(view, pos, event) { + const target = event.target as HTMLElement + const cell = target.closest('td, th') + + if (!cell) return false + const cellPos = view.posAtDOM(cell, 0) + if (cellPos === null || cellPos === undefined) return false + + const $pos = view.state.doc.resolve(cellPos) + const table = cell.closest('table') + if (!table) return false + + const rows = Array.from(table.querySelectorAll('tr')) + const row = cell.closest('tr') + const rowIndex = rows.indexOf(row as HTMLTableRowElement) + const cells = Array.from((row as HTMLTableRowElement).querySelectorAll('td, th')) + const colIndex = cells.indexOf(cell as HTMLTableCellElement) + + const editorElement = editor.options.element as HTMLElement + const editorRect = editorElement.getBoundingClientRect() + const cellRect = cell.getBoundingClientRect() + const customEvent = new CustomEvent('table-cell-click', { + bubbles: true, + detail: { + element: cell, + pos: cellPos, + rowIndex, + colIndex, + isFirstRow: rowIndex === 0, + isFirstCol: colIndex === 0, + position: { + top: cellRect.bottom - editorRect.top + 5, + left: cellRect.left - editorRect.left, + }, + }, + }) + + editorElement.dispatchEvent(customEvent) + return false + }, + }, + }) +} \ No newline at end of file diff --git a/src/components/TextEditor/extensions/tables/table-row-extension.ts b/src/components/TextEditor/extensions/tables/table-row-extension.ts new file mode 100644 index 000000000..c4b72dbc2 --- /dev/null +++ b/src/components/TextEditor/extensions/tables/table-row-extension.ts @@ -0,0 +1,56 @@ +import TableRow from '@tiptap/extension-table-row' + +export const TableRowExtension = TableRow.extend({ + addAttributes() { + return { + ...this.parent?.(), + backgroundColor: { + default: null, + renderHTML: (attributes) => { + if (!attributes.backgroundColor) { + return {} + } + + return { + class: `${attributes.backgroundColor}` + } + }, + parseHTML: (element) => { + return element.style.backgroundColor.replace(/['"]+/g, '') + }, + }, + borderColor: { + default: null, + renderHTML: (attributes) => { + if (!attributes.borderColor) { + return {} + } + return { + class: `${attributes.borderColor}!`, + } + }, + parseHTML: (element) => { + return element.style.borderColor.replace(/['"]+/g, '') + }, + }, + borderWidth: { + default: null, + renderHTML: (attributes) => { + if (!attributes.borderWidth) { + return {} + } + console.log(attributes.borderWidth) + return { + class: `border-${attributes.borderWidth}`, + } + }, + parseHTML: (element) => { + return element.style.borderWidth.replace(/['"]+/g, '') + }, + }, + } + }, +}) + + +export default TableRowExtension diff --git a/src/components/TextEditor/extensions/tables/table-styles.css b/src/components/TextEditor/extensions/tables/table-styles.css new file mode 100644 index 000000000..6127b8b88 --- /dev/null +++ b/src/components/TextEditor/extensions/tables/table-styles.css @@ -0,0 +1,185 @@ +/* Table styles */ +.prose table p { + margin: 0; +} + +/* Table cells need relative positioning */ +.ProseMirror table td, +.ProseMirror table th { + position: relative; +} + +/* Row handle overlay - positioned absolutely outside table */ +.table-row-handle-overlay { + pointer-events: auto; +} + +.table-row-handle-overlay:hover { + background-color: #f3f4f6 !important; + border-radius: 4px !important; + color: #3b82f6 !important; +} + +/* Prosemirror specific table styles */ +.ProseMirror table .selectedCell:after { + z-index: 2; + position: absolute; + content: ''; + left: 0; + right: 0; + top: 0; + bottom: 0; + pointer-events: none; + background: theme('colors.blue.200'); + opacity: 0.3; + overflow-x: auto; + display: block; + -webkit-overflow-scrolling: touch; + max-width: 100%; +} + +.ProseMirror table .column-resize-handle { + position: absolute; + right: -1px; + top: 0; + bottom: -2px; + width: 2px; + background-color: theme('colors.blue.200'); + pointer-events: none; +} + +:root { + --table-bg-red: rgb(220, 38, 38); + --table-bg-orange: rgb(234, 88, 12); + --table-bg-yellow: rgb(202, 138, 4); + --table-bg-green: rgb(22, 163, 74); + --table-bg-teal: rgb(13, 148, 136); + --table-bg-cyan: rgb(6, 182, 212); + --table-bg-blue: rgb(37, 99, 235); + --table-bg-purple: rgb(147, 51, 234); + --table-bg-pink: rgb(219, 39, 119); + --table-bg-gray: rgb(107, 114, 128); + --table-border-red: rgb(220, 38, 38); + --table-border-orange: rgb(234, 88, 12); + --table-border-yellow: rgb(202, 138, 4); + --table-border-green: rgb(22, 163, 74); + --table-border-teal: rgb(13, 148, 136); + --table-border-cyan: rgb(6, 182, 212); + --table-border-blue: rgb(37, 99, 235); + --table-border-purple: rgb(147, 51, 234); + --table-border-pink: rgb(219, 39, 119); + --table-border-gray: rgb(107, 114, 128); +} + +.dark { + --table-bg-red: rgb(239, 68, 68); + --table-bg-orange: rgb(251, 146, 60); + --table-bg-yellow: rgb(250, 204, 21); + --table-bg-green: rgb(74, 222, 128); + --table-bg-teal: rgb(45, 212, 191); + --table-bg-cyan: rgb(34, 211, 238); + --table-bg-blue: rgb(96, 165, 250); + --table-bg-purple: rgb(192, 132, 252); + --table-bg-pink: rgb(244, 114, 182); + --table-bg-gray: rgb(156, 163, 175); + + --table-border-red: rgb(239, 68, 68); + --table-border-orange: rgb(251, 146, 60); + --table-border-yellow: rgb(250, 204, 21); + --table-border-green: rgb(74, 222, 128); + --table-border-teal: rgb(45, 212, 191); + --table-border-cyan: rgb(34, 211, 238); + --table-border-blue: rgb(96, 165, 250); + --table-border-purple: rgb(192, 132, 252); + --table-border-pink: rgb(244, 114, 182); + --table-border-gray: rgb(156, 163, 175); +} + +.writer-table-handle { + pointer-events: auto; + user-select: none; +} +.writer-table-handle:hover { + background: rgba(59, 130, 246, 0.08); + border-color: #93c5fd; +} + +.ProseMirror table th, +.ProseMirror table td { + position: relative; +} + +.ProseMirror table td.red, +.ProseMirror table th.red { background-color: var(--table-bg-red) ; } +.ProseMirror table td.orange, +.ProseMirror table th.orange { background-color: var(--table-bg-orange); } +.ProseMirror table td.amber, +.ProseMirror table th.amber { background-color: var(--table-bg-yellow) ; } +.ProseMirror table td.yellow, +.ProseMirror table th.yellow { background-color: var(--table-bg-yellow) ; } +.ProseMirror table td.lime, +.ProseMirror table th.lime { background-color: var(--table-bg-green) ; } +.ProseMirror table td.green, +.ProseMirror table th.green { background-color: var(--table-bg-green) ; } +.ProseMirror table td.emerald, +.ProseMirror table th.emerald { background-color: var(--table-bg-teal) ; } +.ProseMirror table td.teal, +.ProseMirror table th.teal { background-color: var(--table-bg-teal) ; } +.ProseMirror table td.cyan, +.ProseMirror table th.cyan { background-color: var(--table-bg-cyan) ; } +.ProseMirror table td.sky, +.ProseMirror table th.sky { background-color: var(--table-bg-cyan) ; } +.ProseMirror table td.blue, +.ProseMirror table th.blue { background-color: var(--table-bg-blue) ; } +.ProseMirror table td.indigo, +.ProseMirror table th.indigo { background-color: var(--table-bg-blue) ; } +.ProseMirror table td.violet, +.ProseMirror table th.violet { background-color: var(--table-bg-purple) ; } +.ProseMirror table td.purple, +.ProseMirror table th.purple { background-color: var(--table-bg-purple) ; } +.ProseMirror table td.fuchsia, +.ProseMirror table th.fuchsia { background-color: var(--table-bg-pink) ; } +.ProseMirror table td.pink, +.ProseMirror table th.pink { background-color: var(--table-bg-pink) ; } +.ProseMirror table td.rose, +.ProseMirror table th.rose { background-color: var(--table-bg-pink) ; } +.ProseMirror table td.gray, +.ProseMirror table th.gray { background-color: var(--table-bg-gray) ; } + +.ProseMirror table td.red-border, +.ProseMirror table th.red-border { border: 2px solid var(--table-border-red) ; } +.ProseMirror table td.orange-border, +.ProseMirror table th.orange-border { border: 2px solid var(--table-border-orange) ; } +.ProseMirror table td.amber-border, +.ProseMirror table th.amber-border { border: 2px solid var(--table-border-yellow) ; } +.ProseMirror table td.yellow-border, +.ProseMirror table th.yellow-border { border: 2px solid var(--table-border-yellow) ; } +.ProseMirror table td.lime-border, +.ProseMirror table th.lime-border { border: 2px solid var(--table-border-green) ; } +.ProseMirror table td.green-border, +.ProseMirror table th.green-border { border: 2px solid var(--table-border-green) ; } +.ProseMirror table td.emerald-border, +.ProseMirror table th.emerald-border { border: 2px solid var(--table-border-teal) ; } +.ProseMirror table td.teal-border, +.ProseMirror table th.teal-border { border: 2px solid var(--table-border-teal) ; } +.ProseMirror table td.cyan-border, +.ProseMirror table th.cyan-border { border: 2px solid var(--table-border-cyan) ; } +.ProseMirror table td.sky-border, +.ProseMirror table th.sky-border { border: 2px solid var(--table-border-cyan) ; } +.ProseMirror table td.blue-border, +.ProseMirror table th.blue-border { border: 2px solid var(--table-border-blue) ; } +.ProseMirror table td.indigo-border, +.ProseMirror table th.indigo-border { border: 2px solid var(--table-border-blue) ; } +.ProseMirror table td.violet-border, +.ProseMirror table th.violet-border { border: 2px solid var(--table-border-purple) ; } +.ProseMirror table td.purple-border, +.ProseMirror table th.purple-border { border: 2px solid var(--table-border-purple) ; } +.ProseMirror table td.fuchsia-border, +.ProseMirror table th.fuchsia-border { border: 2px solid var(--table-border-pink) ; } +.ProseMirror table td.pink-border, +.ProseMirror table th.pink-border { border: 2px solid var(--table-border-pink) ; } +.ProseMirror table td.rose-border, +.ProseMirror table th.rose-border { border: 2px solid var(--table-border-pink) ; } +.ProseMirror table td.gray-border, +.ProseMirror table th.gray-border { border: 2px solid var(--table-border-gray) ; } + diff --git a/src/components/TextEditor/extensions/tables/use-table-menu.ts b/src/components/TextEditor/extensions/tables/use-table-menu.ts new file mode 100644 index 000000000..b4f8b6ca9 --- /dev/null +++ b/src/components/TextEditor/extensions/tables/use-table-menu.ts @@ -0,0 +1,122 @@ +import { ref, computed, onMounted, onBeforeUnmount, type Ref } from 'vue' +import type { Editor } from '@tiptap/vue-3' + +export interface TableCellInfo { + element: HTMLElement | null + rowIndex: number + colIndex: number + isFirstRow: boolean +} + +export interface TableBorderMenuPosition { + top: number + left: number +} + +export function useTableMenu(editor: Ref) { + const showTableBorderMenu = ref(false) + const tableBorderAxis = ref<'row' | 'column' | null>(null) + const tableBorderMenuPos = ref({ top: 0, left: 0 }) + const tableCellInfo = ref(null) + + const onBorderClick = (e: Event) => { + const { axis, position, cellInfo } = (e as CustomEvent).detail + tableBorderAxis.value = axis + tableBorderMenuPos.value = position + tableCellInfo.value = cellInfo + showTableBorderMenu.value = true + } + + const closeMenu = (e: MouseEvent) => { + const target = e.target as HTMLElement + if ( + !target.closest('.table-border-menu') && + !target.closest('.table-row-handle-overlay') && + !target.closest('.table-col-handle-overlay') + ) { + showTableBorderMenu.value = false + } + } + + const addRowBefore = () => { + editor.value?.chain().focus().addRowBefore().run() + showTableBorderMenu.value = false + } + + const addRowAfter = () => { + editor.value?.chain().focus().addRowAfter().run() + showTableBorderMenu.value = false + } + + const deleteRow = () => { + editor.value?.chain().focus().deleteRow().run() + showTableBorderMenu.value = false + } + + const addColumnBefore = () => { + editor.value?.chain().focus().addColumnBefore().run() + showTableBorderMenu.value = false + } + + const addColumnAfter = () => { + editor.value?.chain().focus().addColumnAfter().run() + showTableBorderMenu.value = false + } + + const deleteColumn = () => { + editor.value?.chain().focus().deleteColumn().run() + showTableBorderMenu.value = false + } + + const mergeCells = () => { + editor.value?.chain().focus().mergeCells().run() + showTableBorderMenu.value = false + } + + const toggleHeader = () => { + editor.value?.chain().focus().toggleHeaderRow().run() + showTableBorderMenu.value = false + } + + const setBackgroundColor = (color: string | null) => { + editor.value?.chain().focus().setCellAttribute('backgroundColor', color).run() + showTableBorderMenu.value = false + } + + const setBorderColor = (color: string | null) => { + editor.value?.chain().focus().setCellAttribute('borderColor', color).run() + showTableBorderMenu.value = false + } + + const canMergeCells = computed(() => { + return editor.value?.can().mergeCells() ?? false + }) + + onMounted(() => { + window.addEventListener('table-border-click', onBorderClick) + document.addEventListener('click', closeMenu) + }) + + onBeforeUnmount(() => { + window.removeEventListener('table-border-click', onBorderClick) + document.removeEventListener('click', closeMenu) + }) + + return { + showTableBorderMenu, + tableBorderAxis, + tableBorderMenuPos, + tableCellInfo, + canMergeCells, + addRowBefore, + addRowAfter, + deleteRow, + addColumnBefore, + addColumnAfter, + deleteColumn, + mergeCells, + toggleHeader, + setBackgroundColor, + setBorderColor, + } +} From 2f0ce218e0c0a3d9d89a90061a88f51469eb9a09 Mon Sep 17 00:00:00 2001 From: Devansh Khetan <56186839+Devanshk9@users.noreply.github.com> Date: Tue, 11 Nov 2025 22:18:06 +0530 Subject: [PATCH 02/15] fix: fixing z-index --- src/components/TextEditor/components/TableBorderMenu.vue | 4 +--- .../TextEditor/extensions/tables/table-border-menu-plugin.ts | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/TextEditor/components/TableBorderMenu.vue b/src/components/TextEditor/components/TableBorderMenu.vue index 9c0cd42b5..df46cee4e 100644 --- a/src/components/TextEditor/components/TableBorderMenu.vue +++ b/src/components/TextEditor/components/TableBorderMenu.vue @@ -1,16 +1,14 @@