Background Color
@@ -61,7 +49,6 @@
:key="'bg-' + color.name"
:aria-label="color.name"
class="h-5 w-5 rounded border"
- :class="color.class"
:style="color.style"
@click="
handleBackgroundColor(
@@ -104,6 +91,7 @@ import LucideTrash from '~icons/lucide/trash-2'
import LucideMerge from '~icons/lucide/merge'
import LucideHeader from '~icons/lucide/panel-top'
import LucidePalette from '~icons/lucide/palette'
+import { computed } from 'vue'
interface TableBorderMenuProps {
show: boolean
@@ -120,30 +108,40 @@ interface TableBorderMenuProps {
const props = defineProps
()
-const menuObjRow = [
- { icon: LucideArrowUp, action: 'addRowBefore' , tooltip: 'Add Row Before'},
- { icon: LucideArrowDown, action: 'addRowAfter' , tooltip: 'Add Row After' },
- { icon: LucideHeader, action: 'toggleHeader' ,tooltip: 'Make Header' },
- { icon: LucideTrash, action: 'deleteRow' , tooltip: 'Delete Row' },
+type MenuAction =
+ | 'addRowBefore'
+ | 'addRowAfter'
+ | 'deleteRow'
+ | 'addColumnBefore'
+ | 'addColumnAfter'
+ | 'deleteColumn'
+ | 'toggleHeader'
+
+interface MenuItem {
+ icon: any
+ action: MenuAction
+ tooltip: string
+}
+
+const menuObjRow: MenuItem[] = [
+ { icon: LucideArrowUp, action: 'addRowBefore', tooltip: 'Add Row Before' },
+ { icon: LucideArrowDown, action: 'addRowAfter', tooltip: 'Add Row After' },
+ { icon: LucideHeader, action: 'toggleHeader', tooltip: 'Make Header' },
+ { icon: LucideTrash, action: 'deleteRow', tooltip: 'Delete Row' },
]
-const menuObjColumn = [
- { icon: LucideArrowLeft, action: 'addColumnBefore' , tooltip: 'Add Column Before'},
- { icon: LucideArrowRight, action: 'addColumnAfter' , tooltip: 'Add Column After' },
- { icon: LucideHeader, action: 'toggleHeader' , tooltip: "Make Header" },
- { icon: LucideTrash, action: 'deleteColumn' , tooltip: 'Delete Column'},
+const menuObjColumn: MenuItem[] = [
+ { icon: LucideArrowLeft, action: 'addColumnBefore', tooltip: 'Add Column Before' },
+ { icon: LucideArrowRight, action: 'addColumnAfter', tooltip: 'Add Column After' },
+ { icon: LucideHeader, action: 'toggleHeader', tooltip: 'Make Header' },
+ { icon: LucideTrash, action: 'deleteColumn', tooltip: 'Delete Column' },
]
+
+const menuObj = computed(() => props.axis === 'row' ? menuObjRow : menuObjColumn)
const emit = defineEmits<{
- addRowBefore: []
- addRowAfter: []
- deleteRow: []
- addColumnBefore: []
- addColumnAfter: []
- deleteColumn: []
- mergeCells: []
- toggleHeader: []
- setBackgroundColor: [color: string | null]
- setBorderColor: [color: string | null]
+ (e: MenuAction | 'mergeCells'): void
+ (e: 'setBackgroundColor', color: string | null): void
+ (e: 'setBorderColor', color: string | null): void
}>()
const backgroundColors = [
diff --git a/src/components/TextEditor/extensions/tables/TableCellNodeView.vue b/src/components/TextEditor/extensions/tables/TableCellNodeView.vue
deleted file mode 100644
index 9f330fbd0..000000000
--- a/src/components/TextEditor/extensions/tables/TableCellNodeView.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/components/TextEditor/extensions/tables/TableHeaderNodeView.vue b/src/components/TextEditor/extensions/tables/TableHeaderNodeView.vue
deleted file mode 100644
index c294bac0d..000000000
--- a/src/components/TextEditor/extensions/tables/TableHeaderNodeView.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/components/TextEditor/extensions/tables/table-border-menu-plugin.ts b/src/components/TextEditor/extensions/tables/table-border-menu-plugin.ts
index 3e15a9d8d..d45450670 100644
--- a/src/components/TextEditor/extensions/tables/table-border-menu-plugin.ts
+++ b/src/components/TextEditor/extensions/tables/table-border-menu-plugin.ts
@@ -8,6 +8,7 @@ export const tableBorderMenuPluginKey = new PluginKey('tableBorderMenu')
export function tableBorderMenuPlugin(editor: Editor) {
let currentRowHandle: HTMLElement | null = null
let currentColHandle: HTMLElement | null = null
+ let currentTableId: string | null = null
let hideTimeout: NodeJS.Timeout | null = null
const clearHandles = () => {
@@ -17,6 +18,7 @@ export function tableBorderMenuPlugin(editor: Editor) {
currentColHandle?.remove()
currentRowHandle = null
currentColHandle = null
+ currentTableId = null
}, 100)
}
@@ -33,6 +35,7 @@ export function tableBorderMenuPlugin(editor: Editor) {
handleDOMEvents: {
mousemove(view, event) {
const target = event.target as HTMLElement
+
if (
target.closest('.table-row-handle-overlay') ||
target.closest('.table-col-handle-overlay')
@@ -40,15 +43,34 @@ export function tableBorderMenuPlugin(editor: Editor) {
cancelClear()
return false
}
+
const cell = target.closest('td, th')
if (!cell || !cell.closest('.ProseMirror table')) {
clearHandles()
return false
- } else {
- cancelClear()
}
+
+ cancelClear()
+
const row = cell.closest('tr')!
const table = cell.closest('table')!
+
+ const tableId = Array.from(
+ view.dom.querySelectorAll('.ProseMirror table'),
+ )
+ .indexOf(table as HTMLTableElement)
+ .toString()
+
+ if (currentTableId && currentTableId !== tableId) {
+ currentRowHandle?.remove()
+ currentColHandle?.remove()
+ currentRowHandle = null
+ currentColHandle = null
+ currentTableId = null
+ }
+
+ currentTableId = tableId
+
const rowIndex = Array.from(table.querySelectorAll('tr')).indexOf(
row as HTMLTableRowElement,
)
@@ -56,12 +78,33 @@ export function tableBorderMenuPlugin(editor: Editor) {
cell as HTMLTableCellElement,
)
+ if (
+ currentRowHandle &&
+ (currentRowHandle.getAttribute('data-row-id') !==
+ String(rowIndex) ||
+ currentRowHandle.getAttribute('data-table-id') !== tableId)
+ ) {
+ currentRowHandle.remove()
+ currentRowHandle = null
+ }
+ if (
+ currentColHandle &&
+ (currentColHandle.getAttribute('data-col-id') !==
+ String(colIndex) ||
+ currentColHandle.getAttribute('data-table-id') !== tableId)
+ ) {
+ currentColHandle.remove()
+ currentColHandle = null
+ }
+
const editorElement = view.dom.parentElement!
const editorRect = editorElement.getBoundingClientRect()
+ const tableRect = table.getBoundingClientRect()
if (
!currentRowHandle ||
- currentRowHandle.getAttribute('data-row-id') !== String(rowIndex)
+ currentRowHandle.getAttribute('data-row-id') !== String(rowIndex) ||
+ currentRowHandle.getAttribute('data-table-id') !== tableId
) {
currentRowHandle?.remove()
@@ -69,7 +112,7 @@ export function tableBorderMenuPlugin(editor: Editor) {
currentRowHandle.className = 'table-row-handle-overlay'
let iconContainer = document.createElement('div')
- iconContainer.innerHTML = LucideGripVertical
+ iconContainer.innerHTML = LucideGripVertical as unknown as string
currentRowHandle.appendChild(iconContainer)
const svg = iconContainer.querySelector('svg')
if (svg) {
@@ -77,175 +120,194 @@ export function tableBorderMenuPlugin(editor: Editor) {
svg.style.height = '16px'
}
currentRowHandle.setAttribute('data-row-id', String(rowIndex))
+ currentRowHandle.setAttribute('data-table-id', tableId)
const rowRect = row.getBoundingClientRect()
currentRowHandle.style.cssText = `
position: absolute;
- left: ${rowRect.left - editorRect.left - 30}px;
+ left: ${tableRect.left - editorRect.left - 30}px;
top: ${rowRect.top - editorRect.top + rowRect.height / 2 - 10}px;
height: 20px;
- width: 17px;
+ width: 20px;
display: flex;
align-items: center;
justify-content: center;
- font-size: 10px;
- font-weight: bold;
- color: var(--surface-gray-5);
+ color: var(--gray-600);
cursor: pointer;
z-index: 10;
user-select: none;
- background-color: white;
- border: 1px solid var(--outline-gray-3);
+ background-color: var(--gray-50);
+ border: 1px solid var(--gray-200);
border-radius: 4px;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
+ transition: all 0.15s ease;
`
currentRowHandle.addEventListener('mouseenter', function () {
- this.style.backgroundColor = 'var(--outline-gray-1)'
- this.style.color = 'var(--outline-gray-4)'
+ this.style.backgroundColor = 'var(--gray-100)'
+ this.style.borderColor = 'var(--gray-300)'
+ this.style.color = 'var(--gray-700)'
cancelClear()
})
currentRowHandle.addEventListener('mouseleave', function () {
- this.style.backgroundColor = 'white'
- this.style.color = 'var(--surface-ink-8)'
+ this.style.backgroundColor = 'var(--gray-50)'
+ this.style.borderColor = 'var(--gray-200)'
+ this.style.color = 'var(--gray-600)'
clearHandles()
})
currentRowHandle.addEventListener('click', (e) => {
e.preventDefault()
e.stopPropagation()
- const { selection } = view.state
- const isCellSelection = selection instanceof CellSelection
- if (!isCellSelection) {
- editor.chain().focus().selectRow(rowIndex).run()
- }
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,
- },
+ const { selection } = view.state
+ const isCellSelection = selection instanceof CellSelection
+
+ if (!isCellSelection) {
+ const cellPos = view.posAtDOM(cellEl as Node, 0)
+ editor.commands.focus()
+ editor.commands.setTextSelection(cellPos)
+
+ if (editor.commands.selectRow) {
+ editor.commands.selectRow(rowIndex)
+ } else {
+ }
+ } else {
+ }
+
+ const rowEvent = 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.dispatchEvent(rowEvent)
+ window.dispatchEvent(rowEvent)
})
editorElement.appendChild(currentRowHandle)
}
- if (rowIndex === 0) {
- if (
- !currentColHandle ||
- currentColHandle.getAttribute('data-col-id') !== String(colIndex)
- ) {
- currentColHandle?.remove()
-
- currentColHandle = document.createElement('div')
- currentColHandle.className = 'table-col-handle-overlay'
- let iconContainer = document.createElement('div')
- iconContainer.innerHTML = LucideGripVertical
- const svg = iconContainer.querySelector('svg')
- if (svg) {
- svg.style.width = '16px'
- svg.style.height = '16px'
- }
+ if (
+ !currentColHandle ||
+ currentColHandle.getAttribute('data-col-id') !== String(colIndex) ||
+ currentColHandle.getAttribute('data-table-id') !== tableId
+ ) {
+ currentColHandle?.remove()
+
+ currentColHandle = document.createElement('div')
+ currentColHandle.className = 'table-col-handle-overlay'
+ let iconContainer = document.createElement('div')
+ iconContainer.innerHTML = LucideGripVertical as unknown as string
+ const svg = iconContainer.querySelector('svg')
+ if (svg) {
+ svg.style.width = '16px'
+ svg.style.height = '16px'
+ }
+
+ currentColHandle.appendChild(iconContainer)
+ currentColHandle.setAttribute('data-col-id', String(colIndex))
+ currentColHandle.setAttribute('data-table-id', tableId)
+
+ const cellRect = cell.getBoundingClientRect()
+
+ currentColHandle.style.cssText = `
+ position: absolute;
+ left: ${cellRect.left - editorRect.left + cellRect.width / 2 - 10}px;
+ top: ${tableRect.top - editorRect.top - 30}px;
+ height: 20px;
+ width: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--gray-600);
+ cursor: pointer;
+ z-index: 10;
+ user-select: none;
+ background-color: var(--gray-50);
+ border: 1px solid var(--gray-200);
+ border-radius: 4px;
+ box-shadow: 0 1px 2px rgba(0,0,0,0.05);
+ transition: all 0.15s ease;
+ `
+
+ currentColHandle.addEventListener('mouseenter', function () {
+ this.style.backgroundColor = 'var(--gray-100)'
+ this.style.borderColor = 'var(--gray-300)'
+ this.style.color = 'var(--gray-700)'
+ cancelClear()
+ })
- currentColHandle.appendChild(iconContainer)
- currentColHandle.setAttribute('data-col-id', String(colIndex))
+ currentColHandle.addEventListener('mouseleave', function () {
+ this.style.backgroundColor = 'var(--gray-50)'
+ this.style.borderColor = 'var(--gray-200)'
+ this.style.color = 'var(--gray-600)'
+ clearHandles()
+ })
+
+ currentColHandle.addEventListener('click', (e) => {
+ e.preventDefault()
+ e.stopPropagation()
const cellRect = cell.getBoundingClientRect()
+ const editorRect = editorElement.getBoundingClientRect()
+ const menuHeight = 40
+ const gap = 8
- 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: 17px;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 10px;
- 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);
- `
-
- currentColHandle.addEventListener('mouseenter', function () {
- this.style.backgroundColor = 'var(--outline-gray-1)'
- this.style.color = 'var(--outline-gray-4)'
- cancelClear()
- })
+ const { selection } = view.state
+ const isCellSelection = selection instanceof CellSelection
- currentColHandle.addEventListener('mouseleave', function () {
- this.style.backgroundColor = 'white'
- this.style.color = 'var(--surface-ink-8)'
- clearHandles()
- })
+ if (!isCellSelection) {
+ const cellPos = view.posAtDOM(cell as Node, 0)
+ editor.commands.focus()
+ editor.commands.setTextSelection(cellPos)
- currentColHandle.addEventListener('click', (e) => {
- e.preventDefault()
- e.stopPropagation()
- const { selection } = view.state
- const isCellSelection = selection instanceof CellSelection
- if (!isCellSelection) {
- editor.chain().focus().selectColumn(colIndex).run()
+ if (editor.commands.selectColumn) {
+ editor.commands.selectColumn(colIndex)
+ } else {
}
+ } else {
+ }
- const cellRect = cell.getBoundingClientRect()
- const editorRect = editorElement.getBoundingClientRect()
- const menuHeight = 40
- const gap = 8
-
- window.dispatchEvent(
- new CustomEvent('table-border-click', {
- bubbles: true,
- detail: {
- axis: 'column',
- position: {
- top: cellRect.top - editorRect.top - menuHeight - gap,
- left: cellRect.left - editorRect.left,
- },
- cellInfo: {
- element: cell,
- rowIndex,
- colIndex,
- },
- },
- }),
- )
+ const columnEvent = new CustomEvent('table-border-click', {
+ bubbles: true,
+ detail: {
+ axis: 'column',
+ position: {
+ top: cellRect.top - editorRect.top - menuHeight - gap,
+ left: cellRect.left - editorRect.left,
+ },
+ cellInfo: {
+ element: cell,
+ rowIndex,
+ colIndex,
+ },
+ },
})
+ editorElement.dispatchEvent(columnEvent)
+ window.dispatchEvent(columnEvent)
+ })
- editorElement.appendChild(currentColHandle)
- }
- } else {
- if (currentColHandle) {
- currentColHandle.remove()
- currentColHandle = null
- }
+ editorElement.appendChild(currentColHandle)
}
return false
diff --git a/src/components/TextEditor/extensions/tables/table-extension.ts b/src/components/TextEditor/extensions/tables/table-extension.ts
index b88265521..6b6a9ee94 100644
--- a/src/components/TextEditor/extensions/tables/table-extension.ts
+++ b/src/components/TextEditor/extensions/tables/table-extension.ts
@@ -1,5 +1,4 @@
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'
@@ -12,8 +11,7 @@ export const TableExtension = Table.extend({
addAttributes() {
return {
backgroundColor: {
- default: null,
- renderHTML: (attributes) => {
+ renderHTML(attributes){
if (!attributes.backgroundColor) {
return {}
}
@@ -24,19 +22,17 @@ export const TableExtension = Table.extend({
},
borderColor: {
default: null,
- renderHTML: (attributes) => {
+ renderHTML(attributes){
if (!attributes.borderColor) {
return {}
}
-
return {
class: `${attributes.borderColor}!`,
}
},
},
borderWidth: {
- default: null,
- renderHTML: (attributes) => {
+ renderHTML(attributes){
if (!attributes.borderWidth) {
return {}
}
@@ -51,7 +47,6 @@ export const TableExtension = Table.extend({
addProseMirrorPlugins() {
return [
...(this.parent?.() ?? []),
- tableCellMenuPlugin(this.editor),
tableBorderMenuPlugin(this.editor),
]
},
diff --git a/src/components/TextEditor/extensions/tables/table-header-extension.ts b/src/components/TextEditor/extensions/tables/table-header-extension.ts
index 5aeb27406..fd91d8ca3 100644
--- a/src/components/TextEditor/extensions/tables/table-header-extension.ts
+++ b/src/components/TextEditor/extensions/tables/table-header-extension.ts
@@ -1,13 +1,11 @@
import TableHeader from '@tiptap/extension-table-header'
-import { CellSelection } from 'prosemirror-tables'
export const TableHeaderExtension = TableHeader.extend({
addAttributes() {
return {
...this.parent?.(),
backgroundColor: {
- default: null,
- renderHTML: (attributes) => {
+ renderHTML(attributes) {
if (!attributes.backgroundColor) {
return {}
}
@@ -15,13 +13,9 @@ export const TableHeaderExtension = TableHeader.extend({
class: `${attributes.backgroundColor}`,
}
},
- parseHTML: (element) => {
- return element.style.backgroundColor.replace(/['"]+/g, '')
- },
},
borderColor: {
- default: null,
- renderHTML: (attributes) => {
+ renderHTML (attributes) {
if (!attributes.borderColor) {
return {}
}
@@ -29,13 +23,10 @@ export const TableHeaderExtension = TableHeader.extend({
class: `${attributes.borderColor}-border`,
}
},
- parseHTML: (element) => {
- return element.style.borderColor.replace(/['"]+/g, '')
- },
+
},
borderWidth: {
- default: null,
- renderHTML: (attributes) => {
+ renderHTML(attributes){
if (!attributes.borderWidth) {
return {}
}
@@ -43,41 +34,9 @@ export const TableHeaderExtension = TableHeader.extend({
class: `border-${attributes.borderWidth}`,
}
},
- parseHTML: (element) => {
- return element.style.borderWidth.replace(/['"]+/g, '')
- },
},
}
},
-
- addCommands() {
- return {
- selectRow:
- (cellPos: number) =>
- ({ tr, state, dispatch }) => {
- try {
- const $cell = state.doc.resolve(cellPos + 1)
- const sel = CellSelection.rowSelection($cell)
- if (dispatch) dispatch(tr.setSelection(sel))
- return true
- } catch {
- return false
- }
- },
- selectColumn:
- (cellPos: number) =>
- ({ tr, state, dispatch }) => {
- try {
- const $cell = state.doc.resolve(cellPos + 1)
- const sel = CellSelection.colSelection($cell)
- if (dispatch) dispatch(tr.setSelection(sel))
- return true
- } catch {
- return false
- }
- },
- }
- },
})
diff --git a/src/components/TextEditor/extensions/tables/table-menu-extension.ts b/src/components/TextEditor/extensions/tables/table-menu-extension.ts
deleted file mode 100644
index 3fdedc546..000000000
--- a/src/components/TextEditor/extensions/tables/table-menu-extension.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-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
index 96dbbbd59..8ca2ac555 100644
--- a/src/components/TextEditor/extensions/tables/table-row-extension.ts
+++ b/src/components/TextEditor/extensions/tables/table-row-extension.ts
@@ -5,8 +5,7 @@ export const TableRowExtension = TableRow.extend({
return {
...this.parent?.(),
backgroundColor: {
- default: null,
- renderHTML: (attributes) => {
+ renderHTML(attributes){
if (!attributes.backgroundColor) {
return {}
}
@@ -14,27 +13,19 @@ export const TableRowExtension = TableRow.extend({
class: `${attributes.backgroundColor}`
}
},
- parseHTML: (element) => {
- return element.style.backgroundColor.replace(/['"]+/g, '')
- },
},
borderColor: {
- default: null,
- renderHTML: (attributes) => {
+ renderHTML(attributes) {
if (!attributes.borderColor) {
return {}
}
return {
class: `${attributes.borderColor}!`,
}
- },
- parseHTML: (element) => {
- return element.style.borderColor.replace(/['"]+/g, '')
- },
+ }
},
borderWidth: {
- default: null,
- renderHTML: (attributes) => {
+ renderHTML(attributes){
if (!attributes.borderWidth) {
return {}
}
@@ -42,9 +33,6 @@ export const TableRowExtension = TableRow.extend({
class: `border-${attributes.borderWidth}`,
}
},
- parseHTML: (element) => {
- return element.style.borderWidth.replace(/['"]+/g, '')
- },
},
}
},
diff --git a/src/components/TextEditor/extensions/tables/table-selection-extension.ts b/src/components/TextEditor/extensions/tables/table-selection-extension.ts
index 188c9ef02..a5c1cdcf8 100644
--- a/src/components/TextEditor/extensions/tables/table-selection-extension.ts
+++ b/src/components/TextEditor/extensions/tables/table-selection-extension.ts
@@ -11,17 +11,16 @@ declare module '@tiptap/core' {
}
function getCellsInRow(rowIndex: number, table: any, map: TableMap) {
+ //map is used to get dimensions of the table
const cells = []
const seenPositions = new Set()
const rowStart = rowIndex * map.width
-
+
for (let i = 0; i < map.width; i++) {
const cellPos = map.map[rowStart + i]
-
if (seenPositions.has(cellPos)) {
continue
}
-
const cell = table.node.nodeAt(cellPos)
if (cell) {
seenPositions.add(cellPos)
diff --git a/src/components/TextEditor/extensions/tables/use-table-menu.ts b/src/components/TextEditor/extensions/tables/use-table-menu.ts
index 880b34ff2..00fec77b2 100644
--- a/src/components/TextEditor/extensions/tables/use-table-menu.ts
+++ b/src/components/TextEditor/extensions/tables/use-table-menu.ts
@@ -40,52 +40,42 @@ export function useTableMenu(editor: Ref) {
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().toggleHeaderCell().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(() => {
From 5acbc00295b8024c57a0223b1634009653b97040 Mon Sep 17 00:00:00 2001
From: Devansh Khetan <56186839+Devanshk9@users.noreply.github.com>
Date: Fri, 14 Nov 2025 14:05:50 +0530
Subject: [PATCH 05/15] feat: individual cell selection
---
.../extensions/tables/TableBorderMenu.vue | 16 +-
.../tables/table-border-menu-plugin.ts | 172 ++++++++++++++----
.../extensions/tables/table-extension.ts | 2 +
.../extensions/tables/table-styles.css | 8 +-
4 files changed, 157 insertions(+), 41 deletions(-)
diff --git a/src/components/TextEditor/extensions/tables/TableBorderMenu.vue b/src/components/TextEditor/extensions/tables/TableBorderMenu.vue
index bade7b2b3..45a986371 100644
--- a/src/components/TextEditor/extensions/tables/TableBorderMenu.vue
+++ b/src/components/TextEditor/extensions/tables/TableBorderMenu.vue
@@ -21,7 +21,7 @@
-
+
-
+
-
+
-
+
+
-
+
Background Color
@@ -68,7 +69,9 @@
:class="color.borderClass"
@click="
handleBorderColor(
- color.name === 'Default' ? null : color.name.toLowerCase(),
+ color.name === 'Default'
+ ? 'transparent'
+ : color.name.toLowerCase(),
)
"
:title="color.name"
@@ -77,6 +80,35 @@
+
+
+
+
+
+
+
+
+
+
@@ -91,7 +123,8 @@ import LucideTrash from '~icons/lucide/trash-2'
import LucideMerge from '~icons/lucide/merge'
import LucideHeader from '~icons/lucide/panel-top'
import LucidePalette from '~icons/lucide/palette'
-import { computed } from 'vue'
+import LucideFrame from '~icons/lucide/frame'
+import { computed , ref, watch} from 'vue'
interface TableBorderMenuProps {
show: boolean
@@ -131,8 +164,16 @@ const menuObjRow: MenuItem[] = [
{ icon: LucideTrash, action: 'deleteRow', tooltip: 'Delete Row' },
]
const menuObjColumn: MenuItem[] = [
- { icon: LucideArrowLeft, action: 'addColumnBefore', tooltip: 'Add Column Before' },
- { icon: LucideArrowRight, action: 'addColumnAfter', tooltip: 'Add Column After' },
+ {
+ icon: LucideArrowLeft,
+ action: 'addColumnBefore',
+ tooltip: 'Add Column Before',
+ },
+ {
+ icon: LucideArrowRight,
+ action: 'addColumnAfter',
+ tooltip: 'Add Column After',
+ },
{ icon: LucideHeader, action: 'toggleHeader', tooltip: 'Make Header' },
{ icon: LucideTrash, action: 'deleteColumn', tooltip: 'Delete Column' },
]
@@ -143,17 +184,26 @@ const menuObjIndividual: MenuItem[] = [
const menuObj = computed(() =>
props.axis === 'row'
- ? menuObjRow // If axis is 'row'
+ ? menuObjRow
: props.axis === 'column'
- ? menuObjColumn // Else if axis is 'column'
- : menuObjIndividual // Else (for any other axis value or if undefined)
-);
+ ? menuObjColumn
+ : menuObjIndividual,
+)
const emit = defineEmits<{
(e: MenuAction | 'mergeCells'): void
(e: 'setBackgroundColor', color: string | null): void
(e: 'setBorderColor', color: string | null): void
+ (e: 'setBorderWidth', width: number): void
}>()
+const borderWidth = ref(1)
+
+watch(borderWidth, (newWidth: number) => {
+ if (newWidth && newWidth > 0) {
+ emit('setBorderWidth', newWidth)
+ }
+})
+
const backgroundColors = [
{ name: 'Default', style: 'background: #fff;' },
{ name: 'Red', style: 'background: var(--table-bg-red);' },
@@ -211,7 +261,6 @@ const borderColors = [
borderClass: 'border-2 border-gray-600 dark:border-dark-gray-400',
},
]
-
function handleBackgroundColor(color: string | null) {
emit('setBackgroundColor', color)
}
diff --git a/src/components/TextEditor/extensions/tables/table-border-menu-plugin.ts b/src/components/TextEditor/extensions/tables/table-border-menu-plugin.ts
index 2d244eb0d..4b482867f 100644
--- a/src/components/TextEditor/extensions/tables/table-border-menu-plugin.ts
+++ b/src/components/TextEditor/extensions/tables/table-border-menu-plugin.ts
@@ -67,12 +67,17 @@ export function tableBorderMenuPlugin(editor: Editor) {
const cell = target.closest('td, th')
if (!cell || !cell.closest('.ProseMirror table')) {
clearHandles()
- clearCellTrigger()
+ const { selection } = view.state
+ if (!(selection instanceof CellSelection)) {
+ clearCellTrigger()
+ }
return false
}
-
cancelClear()
- cancelCellTriggerClear()
+ const { selection } = view.state
+ if (!(selection instanceof CellSelection)) {
+ cancelCellTriggerClear()
+ }
const row = cell.closest('tr')!
const table = cell.closest('table')!
@@ -191,21 +196,10 @@ export function tableBorderMenuPlugin(editor: Editor) {
const editorRect = editorElement.getBoundingClientRect()
const menuHeight = 25
const gap = 8
-
- const { selection } = view.state
- const isCellSelection = selection instanceof CellSelection
-
- if (!isCellSelection) {
- const cellPos = view.posAtDOM(cellEl as Node, 0)
- editor.commands.focus()
- editor.commands.setTextSelection(cellPos)
-
- if (editor.commands.selectRow) {
- editor.commands.selectRow(rowIndex)
- } else {
- }
- } else {
- }
+ const cellPos = view.posAtDOM(cellEl as Node, 0)
+ editor.commands.focus()
+ editor.commands.setTextSelection(cellPos)
+ editor.commands.selectRow(rowIndex)
const rowEvent = new CustomEvent('table-border-click', {
bubbles: true,
@@ -296,20 +290,10 @@ export function tableBorderMenuPlugin(editor: Editor) {
const menuHeight = 30
const gap = 8
- const { selection } = view.state
- const isCellSelection = selection instanceof CellSelection
-
- if (!isCellSelection) {
- const cellPos = view.posAtDOM(cell as Node, 0)
- editor.commands.focus()
- editor.commands.setTextSelection(cellPos)
-
- if (editor.commands.selectColumn) {
- editor.commands.selectColumn(colIndex)
- } else {
- }
- } else {
- }
+ const cellPos = view.posAtDOM(cell as Node, 0)
+ editor.commands.focus()
+ editor.commands.setTextSelection(cellPos)
+ editor.commands.selectColumn(colIndex)
const columnEvent = new CustomEvent('table-border-click', {
bubbles: true,
@@ -369,7 +353,7 @@ export function tableBorderMenuPlugin(editor: Editor) {
const circle = document.createElementNS(svgNS, 'circle')
circle.setAttribute('cx', '12')
circle.setAttribute('cy', '12')
- circle.setAttribute('r', '4') // Smaller radius for a dot
+ circle.setAttribute('r', '4')
svg.appendChild(circle)
currentCellTrigger.appendChild(svg)
@@ -388,9 +372,14 @@ export function tableBorderMenuPlugin(editor: Editor) {
e.preventDefault()
e.stopPropagation()
- const cellPos = view.posAtDOM(cell as Node, 0)
- editor.commands.focus()
- editor.commands.setTextSelection(cellPos)
+ const { selection } = view.state
+ const isCellSelection = selection instanceof CellSelection
+
+ if (!isCellSelection) {
+ const cellPos = view.posAtDOM(cell as Node, 0)
+ editor.commands.focus()
+ editor.commands.setTextSelection(cellPos)
+ }
const triggerRect = currentCellTrigger!.getBoundingClientRect()
const editorRect = editorElement.getBoundingClientRect()
@@ -407,7 +396,8 @@ export function tableBorderMenuPlugin(editor: Editor) {
element: cell,
rowIndex,
colIndex,
- isIndividualCell: true,
+ isIndividualCell: !isCellSelection,
+ isMultiCellSelection: isCellSelection,
},
},
})
diff --git a/src/components/TextEditor/extensions/tables/table-cell-extension.ts b/src/components/TextEditor/extensions/tables/table-cell-extension.ts
index 43d01dc25..ef9ebd83f 100644
--- a/src/components/TextEditor/extensions/tables/table-cell-extension.ts
+++ b/src/components/TextEditor/extensions/tables/table-cell-extension.ts
@@ -5,8 +5,7 @@ export const TableCellExtension = TableCell.extend({
return {
...this.parent?.(),
backgroundColor: {
- default: null,
- renderHTML: (attributes) => {
+ renderHTML(attributes){
if (!attributes.backgroundColor) {
return {}
}
@@ -14,38 +13,26 @@ export const TableCellExtension = TableCell.extend({
class: `${attributes.backgroundColor}`
}
},
- parseHTML: (element) => {
- return element.style.backgroundColor.replace(/['"]+/g, '')
- },
},
borderColor: {
- default: null,
- renderHTML: (attributes) => {
+ renderHTML(attributes){
if (!attributes.borderColor) {
return {}
}
-
return {
class: `${attributes.borderColor}-border`,
}
},
- parseHTML: (element) => {
- return element.style.borderColor.replace(/['"]+/g, '')
- },
},
borderWidth: {
- default: null,
- renderHTML: (attributes) => {
+ renderHTML(attributes){
if (!attributes.borderWidth) {
return {}
}
return {
- class: `border-${attributes.borderWidth}`,
+ style: `border-width: ${attributes.borderWidth};`,
}
},
- parseHTML: (element) => {
- return element.style.borderWidth.replace(/['"]+/g, '')
- },
},
}
},
diff --git a/src/components/TextEditor/extensions/tables/table-header-extension.ts b/src/components/TextEditor/extensions/tables/table-header-extension.ts
index fd91d8ca3..e24326427 100644
--- a/src/components/TextEditor/extensions/tables/table-header-extension.ts
+++ b/src/components/TextEditor/extensions/tables/table-header-extension.ts
@@ -31,7 +31,7 @@ export const TableHeaderExtension = TableHeader.extend({
return {}
}
return {
- class: `border-${attributes.borderWidth}`,
+ style: `border-width: ${attributes.borderWidth};`,
}
},
},
diff --git a/src/components/TextEditor/extensions/tables/table-individual-cell-plugin.ts b/src/components/TextEditor/extensions/tables/table-individual-cell-plugin.ts
new file mode 100644
index 000000000..0a1d4496e
--- /dev/null
+++ b/src/components/TextEditor/extensions/tables/table-individual-cell-plugin.ts
@@ -0,0 +1,208 @@
+import { Plugin, PluginKey } from '@tiptap/pm/state'
+import { Editor } from '@tiptap/core'
+import LucideCircle from '~icons/lucide/circle?raw'
+import { CellSelection } from 'prosemirror-tables'
+
+export const tableIndividualCellPluginKey = new PluginKey('tableCellMenu')
+
+export function tableBorderMenuPlugin(editor: Editor) {
+ let currentCellHandle: HTMLElement | null = null
+ let currentTableId: string | null = null
+ let hideTimeout: NodeJS.Timeout | null = null
+
+ const clearHandles = () => {
+ if (hideTimeout) clearTimeout(hideTimeout)
+ hideTimeout = setTimeout(() => {
+ currentCellHandle?.remove()
+ currentCellHandle = null
+ currentTableId = null
+ }, 100)
+ }
+
+ const cancelClear = () => {
+ if (hideTimeout) {
+ clearTimeout(hideTimeout)
+ hideTimeout = null
+ }
+ }
+
+ return new Plugin({
+ key: tableIndividualCellPluginKey,
+ 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
+ }
+
+ cancelClear()
+
+ const row = cell.closest('tr')!
+ const table = cell.closest('table')!
+
+ const tableId = Array.from(
+ view.dom.querySelectorAll('.ProseMirror table'),
+ )
+ .indexOf(table as HTMLTableElement)
+ .toString()
+
+ if (currentTableId && currentTableId !== tableId) {
+currentCellHandle?.remove()
+ currentCellHandle = null
+ currentTableId = null
+ }
+
+ currentTableId = tableId
+
+ const rowIndex = Array.from(table.querySelectorAll('tr')).indexOf(
+ row as HTMLTableRowElement,
+ )
+ const colIndex = Array.from(row.querySelectorAll('td, th')).indexOf(
+ cell as HTMLTableCellElement,
+ )
+
+ if (
+ currentCellHandle &&
+ (currentCellHandle.getAttribute('data-row-id') !==
+ String(rowIndex) ||
+ currentCellHandle.getAttribute('data-table-id') !== tableId)
+ ) {
+ currentCellHandle.remove()
+ currentCellHandle = null
+ }
+
+ const editorElement = view.dom.parentElement!
+ const editorRect = editorElement.getBoundingClientRect()
+ const tableRect = table.getBoundingClientRect()
+
+ if (
+ !currentCellHandle ||
+ currentCellHandle.getAttribute('data-row-id') !== String(rowIndex) ||
+ currentCellHandle.getAttribute('data-table-id') !== tableId
+ ) {
+ currentCellHandle?.remove()
+
+ currentCellHandle = document.createElement('div')
+ currentCellHandle.className = 'table-row-handle-overlay'
+
+ let iconContainer = document.createElement('div')
+ iconContainer.innerHTML = LucideCircle as unknown as string
+ currentCellHandle.appendChild(iconContainer)
+ const svg = iconContainer.querySelector('svg')
+ if (svg) {
+ svg.style.width = '13px'
+ svg.style.height = '13px'
+ }
+ currentCellHandle.setAttribute('data-row-id', String(rowIndex))
+ currentCellHandle.setAttribute('data-table-id', tableId)
+
+ const rowRect = row.getBoundingClientRect()
+
+ currentCellHandle.style.cssText = `
+ position: absolute;
+ left: ${tableRect.left - editorRect.left - 7}px;
+ top: ${rowRect.top - editorRect.top + rowRect.height / 2 - 10}px;
+ height: 16px;
+ width: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--ink-gray-7);
+ cursor: pointer;
+ z-index: 10;
+ user-select: none;
+ background-color: var(--surface-white);
+ border: 1px solid var(--outline-gray-2);
+ border-radius: 4px;
+ box-shadow: 0 1px 2px rgba(0,0,0,0.05);
+ transition: all 0.15s ease;
+ `
+
+ currentCellHandle.addEventListener('mouseenter', function () {
+ this.style.backgroundColor = 'var(--surface-gray-2)'
+ this.style.borderColor = 'var(--outline-gray-3)'
+ this.style.color = 'var(--surface-gray-7)'
+ cancelClear()
+ })
+
+ currentCellHandle.addEventListener('mouseleave', function () {
+ this.style.backgroundColor = 'var(--surface-white)'
+ this.style.borderColor = 'var(--outline-gray-2)'
+ this.style.color = 'var(--ink-gray-7)'
+ clearHandles()
+ })
+
+ currentCellHandle.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
+
+ const { selection } = view.state
+ const isCellSelection = selection instanceof CellSelection
+
+ if (!isCellSelection) {
+ const cellPos = view.posAtDOM(cellEl as Node, 0)
+ editor.commands.focus()
+ editor.commands.setTextSelection(cellPos)
+
+ if (editor.commands.selectRow) {
+ editor.commands.selectRow(rowIndex)
+ } else {
+ }
+ } else {
+ }
+
+ const rowEvent = 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.dispatchEvent(rowEvent)
+ window.dispatchEvent(rowEvent)
+ })
+
+ editorElement.appendChild(currentCellHandle)
+ }
+ return false
+ },
+ },
+ },
+ view() {
+ return {
+ destroy() {
+ currentCellHandle?.remove()
+ currentCellHandle = null
+ },
+ }
+ },
+ })
+}
diff --git a/src/components/TextEditor/extensions/tables/table-selection-extension.ts b/src/components/TextEditor/extensions/tables/table-selection-extension.ts
index a5c1cdcf8..f9ccd4830 100644
--- a/src/components/TextEditor/extensions/tables/table-selection-extension.ts
+++ b/src/components/TextEditor/extensions/tables/table-selection-extension.ts
@@ -11,7 +11,6 @@ declare module '@tiptap/core' {
}
function getCellsInRow(rowIndex: number, table: any, map: TableMap) {
- //map is used to get dimensions of the table
const cells = []
const seenPositions = new Set()
const rowStart = rowIndex * map.width
diff --git a/src/components/TextEditor/extensions/tables/table-styles.css b/src/components/TextEditor/extensions/tables/table-styles.css
index 41b512ec4..79fe763a3 100644
--- a/src/components/TextEditor/extensions/tables/table-styles.css
+++ b/src/components/TextEditor/extensions/tables/table-styles.css
@@ -1,19 +1,16 @@
-/* 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;
}
-/* Prosemirror specific table styles */
+
.ProseMirror table .selectedCell:after {
z-index: 2;
position: absolute;
@@ -220,6 +217,15 @@
.ProseMirror table th.rose-border {
border: 2px solid var(--table-border-pink);
}
+.ProseMirror table td.transparent-border,
+.ProseMirror table th.transparent-border {
+ border: 1px solid transparent;
+}
+.ProseMirror table td.transparent-border:hover,
+.ProseMirror table th.transparent-border:hover {
+ border-bottom: 1px solid rgb(98, 179, 255) !important;
+ transition: border-bottom 0.5s;
+}
.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
index 00fec77b2..e34faa33a 100644
--- a/src/components/TextEditor/extensions/tables/use-table-menu.ts
+++ b/src/components/TextEditor/extensions/tables/use-table-menu.ts
@@ -73,11 +73,16 @@ export function useTableMenu(editor: Ref
) {
const setBackgroundColor = (color: string | null) => {
editor.value?.chain().focus().setCellAttribute('backgroundColor', color).run()
}
-
+
const setBorderColor = (color: string | null) => {
editor.value?.chain().focus().setCellAttribute('borderColor', color).run()
}
+ const setBorderWidth = (width: number | null) => {
+ const borderWidthValue = width ? `${width}px` : null
+ editor.value?.chain().focus().setCellAttribute('borderWidth', borderWidthValue).run()
+ }
+
const canMergeCells = computed(() => {
return editor.value?.can().mergeCells() ?? false
})
@@ -108,5 +113,6 @@ export function useTableMenu(editor: Ref) {
toggleHeader,
setBackgroundColor,
setBorderColor,
+ setBorderWidth
}
}
From 0bc20b84f0b9cb0b74df1590319a49d2532ca83f Mon Sep 17 00:00:00 2001
From: Devansh Khetan <56186839+Devanshk9@users.noreply.github.com>
Date: Fri, 14 Nov 2025 17:31:59 +0530
Subject: [PATCH 07/15] fix: cell handler position
---
.../extensions/tables/table-border-menu-plugin.ts | 7 +++----
.../TextEditor/extensions/tables/table-styles.css | 10 +++++-----
2 files changed, 8 insertions(+), 9 deletions(-)
diff --git a/src/components/TextEditor/extensions/tables/table-border-menu-plugin.ts b/src/components/TextEditor/extensions/tables/table-border-menu-plugin.ts
index 4b482867f..7739407cb 100644
--- a/src/components/TextEditor/extensions/tables/table-border-menu-plugin.ts
+++ b/src/components/TextEditor/extensions/tables/table-border-menu-plugin.ts
@@ -1,7 +1,6 @@
import { Plugin, PluginKey } from '@tiptap/pm/state'
import { Editor } from '@tiptap/core'
import LucideGripVertical from '~icons/lucide/grip-vertical?raw'
-import LucideCircle from '~icons/lucide/dot?raw'
import { CellSelection } from 'prosemirror-tables'
export const tableBorderMenuPluginKey = new PluginKey('tableBorderMenu')
@@ -199,7 +198,7 @@ export function tableBorderMenuPlugin(editor: Editor) {
const cellPos = view.posAtDOM(cellEl as Node, 0)
editor.commands.focus()
editor.commands.setTextSelection(cellPos)
- editor.commands.selectRow(rowIndex)
+ editor.commands.selectRow(rowIndex)
const rowEvent = new CustomEvent('table-border-click', {
bubbles: true,
@@ -331,8 +330,8 @@ export function tableBorderMenuPlugin(editor: Editor) {
currentCellTrigger.style.cssText = `
position: absolute;
- left: ${cellRect.right - editorRect.left - 9}px;
- top: ${cellRect.top - editorRect.top + 8}px;
+ left: ${cellRect.left - editorRect.left + cellRect.width - 9}px;
+ top: ${cellRect.top - editorRect.top + cellRect.height / 2 - 9}px;
display: flex;
align-items: center;
justify-content: center;
diff --git a/src/components/TextEditor/extensions/tables/table-styles.css b/src/components/TextEditor/extensions/tables/table-styles.css
index 79fe763a3..78180086d 100644
--- a/src/components/TextEditor/extensions/tables/table-styles.css
+++ b/src/components/TextEditor/extensions/tables/table-styles.css
@@ -219,13 +219,13 @@
}
.ProseMirror table td.transparent-border,
.ProseMirror table th.transparent-border {
- border: 1px solid transparent;
+ border: 2px solid transparent;
}
-.ProseMirror table td.transparent-border:hover,
+/* .ProseMirror table td.transparent-border:hover,
.ProseMirror table th.transparent-border:hover {
- border-bottom: 1px solid rgb(98, 179, 255) !important;
- transition: border-bottom 0.5s;
-}
+ border: 1px solid rgb(98, 179, 255) !important;
+ transition: border-bottom 0.5s ease-out, transform 0.5s ease-in;
+} */
.ProseMirror table td.gray-border,
.ProseMirror table th.gray-border {
border: 2px solid var(--table-border-gray);
From 66b626cb4a9a0cd87b5f5453b0d006ad68194aac Mon Sep 17 00:00:00 2001
From: Devansh Khetan <56186839+Devanshk9@users.noreply.github.com>
Date: Tue, 18 Nov 2025 20:07:13 +0530
Subject: [PATCH 08/15] fix: positions of handler menu
---
.../extensions/tables/TableBorderMenu.vue | 1 +
.../tables/table-border-menu-plugin.ts | 37 +++++++++++++------
.../tables/table-individual-cell-plugin.ts | 18 ++++-----
3 files changed, 35 insertions(+), 21 deletions(-)
diff --git a/src/components/TextEditor/extensions/tables/TableBorderMenu.vue b/src/components/TextEditor/extensions/tables/TableBorderMenu.vue
index d72e95d4f..07c7512e8 100644
--- a/src/components/TextEditor/extensions/tables/TableBorderMenu.vue
+++ b/src/components/TextEditor/extensions/tables/TableBorderMenu.vue
@@ -5,6 +5,7 @@
:style="{
top: position.top + 'px',
left: position.left + 'px',
+ transform: axis === 'column' ? 'translateX(-50%)' : undefined,
}"
@click.stop
>
diff --git a/src/components/TextEditor/extensions/tables/table-border-menu-plugin.ts b/src/components/TextEditor/extensions/tables/table-border-menu-plugin.ts
index 7739407cb..32ce9f2fa 100644
--- a/src/components/TextEditor/extensions/tables/table-border-menu-plugin.ts
+++ b/src/components/TextEditor/extensions/tables/table-border-menu-plugin.ts
@@ -126,6 +126,8 @@ export function tableBorderMenuPlugin(editor: Editor) {
const editorElement = view.dom.parentElement!
const editorRect = editorElement.getBoundingClientRect()
const tableRect = table.getBoundingClientRect()
+ const editorScrollLeft = editorElement.scrollLeft
+ const editorScrollTop = editorElement.scrollTop
if (
!currentRowHandle ||
@@ -152,8 +154,8 @@ export function tableBorderMenuPlugin(editor: Editor) {
currentRowHandle.style.cssText = `
position: absolute;
- left: ${tableRect.left - editorRect.left - 7}px;
- top: ${rowRect.top - editorRect.top + rowRect.height / 2 - 10}px;
+ left: ${tableRect.left - editorRect.left + editorScrollLeft - 7}px;
+ top: ${rowRect.top - editorRect.top + editorScrollTop + rowRect.height / 2 - 10}px;
height: 16px;
width: 12px;
display: flex;
@@ -200,13 +202,17 @@ export function tableBorderMenuPlugin(editor: Editor) {
editor.commands.setTextSelection(cellPos)
editor.commands.selectRow(rowIndex)
+ const rowHandleLeft =
+ tableRect.left - editorRect.left + editorScrollLeft - 7
+ const rowHandleCenter = rowHandleLeft + 6
+
const rowEvent = new CustomEvent('table-border-click', {
bubbles: true,
detail: {
axis: 'row',
position: {
- top: cellRect.top - editorRect.top - menuHeight - gap,
- left: cellRect.left - editorRect.left,
+ top: cellRect.top - editorRect.top + editorScrollTop - menuHeight - gap,
+ left: rowHandleCenter,
},
cellInfo: {
element: cellEl,
@@ -247,8 +253,8 @@ export function tableBorderMenuPlugin(editor: Editor) {
currentColHandle.style.cssText = `
position: absolute;
- left: ${cellRect.left - editorRect.left + cellRect.width / 2 - 10}px;
- top: ${tableRect.top - editorRect.top - 7}px;
+ left: ${cellRect.left - editorRect.left + editorScrollLeft + cellRect.width / 2 - 10}px;
+ top: ${tableRect.top - editorRect.top + editorScrollTop - 7}px;
height: 16px;
width: 12px;
display: flex;
@@ -294,13 +300,20 @@ export function tableBorderMenuPlugin(editor: Editor) {
editor.commands.setTextSelection(cellPos)
editor.commands.selectColumn(colIndex)
+ const columnHandleTop =
+ tableRect.top - editorRect.top + editorScrollTop - 7
+
const columnEvent = new CustomEvent('table-border-click', {
bubbles: true,
detail: {
axis: 'column',
position: {
- top: cellRect.top - editorRect.top - menuHeight - gap,
- left: cellRect.left - editorRect.left,
+ top: columnHandleTop - menuHeight - gap,
+ left:
+ cellRect.left -
+ editorRect.left +
+ editorScrollLeft +
+ cellRect.width / 2,
},
cellInfo: {
element: cell,
@@ -330,8 +343,8 @@ export function tableBorderMenuPlugin(editor: Editor) {
currentCellTrigger.style.cssText = `
position: absolute;
- left: ${cellRect.left - editorRect.left + cellRect.width - 9}px;
- top: ${cellRect.top - editorRect.top + cellRect.height / 2 - 9}px;
+ left: ${cellRect.left - editorRect.left + editorScrollLeft + cellRect.width - 9}px;
+ top: ${cellRect.top - editorRect.top + editorScrollTop + cellRect.height / 2 - 9}px;
display: flex;
align-items: center;
justify-content: center;
@@ -388,8 +401,8 @@ export function tableBorderMenuPlugin(editor: Editor) {
detail: {
axis: 'cell',
position: {
- top: triggerRect.bottom - editorRect.top - 25,
- left: triggerRect.left - editorRect.left,
+ top: triggerRect.bottom - editorRect.top + editorScrollTop - 25,
+ left: triggerRect.left - editorRect.left + editorScrollLeft,
},
cellInfo: {
element: cell,
diff --git a/src/components/TextEditor/extensions/tables/table-individual-cell-plugin.ts b/src/components/TextEditor/extensions/tables/table-individual-cell-plugin.ts
index 0a1d4496e..8913a7f26 100644
--- a/src/components/TextEditor/extensions/tables/table-individual-cell-plugin.ts
+++ b/src/components/TextEditor/extensions/tables/table-individual-cell-plugin.ts
@@ -86,6 +86,8 @@ currentCellHandle?.remove()
const editorElement = view.dom.parentElement!
const editorRect = editorElement.getBoundingClientRect()
const tableRect = table.getBoundingClientRect()
+ const editorScrollLeft = editorElement.scrollLeft
+ const editorScrollTop = editorElement.scrollTop
if (
!currentCellHandle ||
@@ -112,8 +114,8 @@ currentCellHandle?.remove()
currentCellHandle.style.cssText = `
position: absolute;
- left: ${tableRect.left - editorRect.left - 7}px;
- top: ${rowRect.top - editorRect.top + rowRect.height / 2 - 10}px;
+ left: ${tableRect.left - editorRect.left + editorScrollLeft - 7}px;
+ top: ${rowRect.top - editorRect.top + editorScrollTop + rowRect.height / 2 - 10}px;
height: 16px;
width: 12px;
display: flex;
@@ -163,12 +165,10 @@ currentCellHandle?.remove()
const cellPos = view.posAtDOM(cellEl as Node, 0)
editor.commands.focus()
editor.commands.setTextSelection(cellPos)
-
- if (editor.commands.selectRow) {
- editor.commands.selectRow(rowIndex)
- } else {
- }
+ editor.commands.selectRow(rowIndex)
} else {
+ editor.commands.focus()
+ editor.commands.selectRow(rowIndex)
}
const rowEvent = new CustomEvent('table-border-click', {
@@ -176,8 +176,8 @@ currentCellHandle?.remove()
detail: {
axis: 'row',
position: {
- top: cellRect.top - editorRect.top - menuHeight - gap,
- left: cellRect.left - editorRect.left,
+ top: cellRect.top - editorRect.top + editorScrollTop - menuHeight - gap,
+ left: cellRect.left - editorRect.left + editorScrollLeft,
},
cellInfo: {
element: cellEl,
From a89470eaf46fb872e007ca7eb968abf850eb0db4 Mon Sep 17 00:00:00 2001
From: Devansh Khetan <56186839+Devanshk9@users.noreply.github.com>
Date: Fri, 21 Nov 2025 21:32:40 +0530
Subject: [PATCH 09/15] fix: optimizing tables further
---
src/components/TextEditor/TextEditor.vue | 45 +----
.../TextEditor/TextEditorBubbleMenu.vue | 1 +
.../tables/TableBorderMenuContainer.vue | 174 ++++++++++++++++++
.../tables/table-border-menu-plugin.ts | 49 +++--
.../extensions/tables/table-extension.ts | 8 -
5 files changed, 208 insertions(+), 69 deletions(-)
create mode 100644 src/components/TextEditor/extensions/tables/TableBorderMenuContainer.vue
diff --git a/src/components/TextEditor/TextEditor.vue b/src/components/TextEditor/TextEditor.vue
index ed464ab31..f98d0afad 100644
--- a/src/components/TextEditor/TextEditor.vue
+++ b/src/components/TextEditor/TextEditor.vue
@@ -13,24 +13,7 @@
:buttons="fixedMenu"
/>
-
+
@@ -89,8 +72,7 @@ import { TextEditorEmits, TextEditorProps } from './types'
import TableCellExtension from './extensions/tables/table-cell-extension'
import TableHeaderExtension from './extensions/tables/table-header-extension'
import TableRowExtension from './extensions/tables/table-row-extension'
-import TableBorderMenu from './extensions/tables/TableBorderMenu.vue'
-import { useTableMenu } from './extensions/tables/use-table-menu'
+import TableBorderMenuContainer from './extensions/tables/TableBorderMenuContainer.vue'
import { TableCommandsExtension } from './extensions/tables/table-selection-extension'
function defaultUploadFunction(file: File) {
@@ -119,24 +101,6 @@ const emit = defineEmits()
const editor = ref(null)
-const {
- showTableBorderMenu,
- tableBorderAxis,
- tableBorderMenuPos,
- tableCellInfo,
- canMergeCells,
- addRowBefore,
- addRowAfter,
- deleteRow,
- addColumnBefore,
- addColumnAfter,
- deleteColumn,
- mergeCells,
- toggleHeader,
- setBackgroundColor,
- setBorderColor,
- setBorderWidth
-} = useTableMenu(editor)
const attrs = useAttrs()
const attrsClass = computed(() => normalizeClass(attrs.class))
@@ -203,6 +167,7 @@ onMounted(() => {
code: false,
codeBlock: false,
heading: false,
+ table: false,
}).extend({
addKeyboardShortcuts() {
return {
@@ -216,9 +181,7 @@ onMounted(() => {
? props.starterkitOptions.heading
: {}),
}),
- Table.configure({
- resizable: true,
- }),
+
TableExtension.configure({
resizable: true,
}),
diff --git a/src/components/TextEditor/TextEditorBubbleMenu.vue b/src/components/TextEditor/TextEditorBubbleMenu.vue
index 0490f8124..951a47087 100644
--- a/src/components/TextEditor/TextEditorBubbleMenu.vue
+++ b/src/components/TextEditor/TextEditorBubbleMenu.vue
@@ -4,6 +4,7 @@
class="bubble-menu rounded-md shadow-sm"
:tippy-options="{ duration: 100 }"
:editor="editor"
+ :should-show="shouldShow"
v-bind="options"
>
-
-
+
+
+
+
+
diff --git a/src/components/TextEditor/TextEditor.vue b/src/components/TextEditor/TextEditor.vue
index 908ca471e..e424d41b7 100644
--- a/src/components/TextEditor/TextEditor.vue
+++ b/src/components/TextEditor/TextEditor.vue
@@ -40,9 +40,8 @@ defineOptions({ inheritAttrs: false })
import { Editor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
-import { Placeholder } from '@tiptap/extensions'
+import Placeholder from '@tiptap/extension-placeholder'
import Typography from '@tiptap/extension-typography'
-import { TextStyleKit } from '@tiptap/extension-text-style'
import TaskItem from '@tiptap/extension-task-item'
import TaskList from '@tiptap/extension-task-list'
import TextAlign from '@tiptap/extension-text-align'
@@ -50,7 +49,6 @@ import { ImageExtension } from './extensions/image'
import ImageViewerExtension from './image-viewer-extension'
import { VideoExtension } from './video-extension'
import { IframeExtension } from './extensions/iframe'
-import { TocNodeExtension } from './extensions/toc-node'
import LinkExtension from './link-extension'
import { TextStyle } from '@tiptap/extension-text-style'
import NamedColorExtension from './extensions/color'
@@ -166,6 +164,7 @@ onMounted(() => {
codeBlock: false,
heading: false,
table: false,
+ link: false, // Disable link in StarterKit since we use LinkExtension separately
}).extend({
addKeyboardShortcuts() {
return {
@@ -195,10 +194,7 @@ onMounted(() => {
TextAlign.configure({
types: ['heading', 'paragraph'],
}),
- TextStyleKit.configure({
- backgroundColor: false,
- color: false,
- }),
+ TextStyle,
NamedColorExtension,
NamedHighlightExtension,
ExtendedCode,
@@ -214,7 +210,6 @@ onMounted(() => {
uploadFunction: props.uploadFunction || defaultUploadFunction,
}),
IframeExtension,
- TocNodeExtension,
LinkExtension.configure({
openOnClick: false,
}),
@@ -244,9 +239,6 @@ onMounted(() => {
uploadFunction: props.uploadFunction || defaultUploadFunction,
}),
StyleClipboardExtension,
- // NodeRange.configure({
- // key: null,
- // }),
...(props.extensions || []),
],
onUpdate: ({ editor }) => {
@@ -262,9 +254,6 @@ onMounted(() => {
emit('blur', event)
},
})
- // editor.value.on('selectionUpdate', ({ editor }) => {
- // console.log('Selection:', editor.state.selection)
- // })
})
onBeforeUnmount(() => {
diff --git a/src/components/TextEditor/TextEditorBubbleMenu.vue b/src/components/TextEditor/TextEditorBubbleMenu.vue
index 9457aadd0..bfbc63165 100644
--- a/src/components/TextEditor/TextEditorBubbleMenu.vue
+++ b/src/components/TextEditor/TextEditorBubbleMenu.vue
@@ -15,7 +15,7 @@