mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-20 07:10:02 +00:00
EZQMS-378 Wiki table node view (#4123)
Signed-off-by: Alexander Onnikov <alexander.onnikov@xored.com>
This commit is contained in:
parent
74e50f0b87
commit
27740f3f35
@ -0,0 +1,178 @@
|
|||||||
|
<!--
|
||||||
|
//
|
||||||
|
// Copyright © 2023 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License. You may
|
||||||
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
//
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import { IconAdd } from '@hcengineering/ui'
|
||||||
|
import { onDestroy } from 'svelte'
|
||||||
|
import { NodeViewContent, NodeViewProps, NodeViewWrapper } from '../../node-view'
|
||||||
|
import { findTable, insertColumn, insertRow } from './utils'
|
||||||
|
import { TableMap } from '@tiptap/pm/tables'
|
||||||
|
|
||||||
|
export let node: NodeViewProps['node']
|
||||||
|
export let getPos: NodeViewProps['getPos']
|
||||||
|
export let editor: NodeViewProps['editor']
|
||||||
|
export let selected: NodeViewProps['selected']
|
||||||
|
export let decorations: NodeViewProps['decorations']
|
||||||
|
export let extension: NodeViewProps['extension']
|
||||||
|
|
||||||
|
const className = extension.options.HTMLAttributes?.class ?? ''
|
||||||
|
|
||||||
|
let editable = false
|
||||||
|
$: editable = editor.isEditable
|
||||||
|
|
||||||
|
editor.on('selectionUpdate', handleSelectionUpdate)
|
||||||
|
|
||||||
|
let focused = false
|
||||||
|
function handleSelectionUpdate (): void {
|
||||||
|
const from = getPos()
|
||||||
|
const to = from + node.nodeSize
|
||||||
|
|
||||||
|
focused = editor.state.selection.from <= to && editor.state.selection.to >= from
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAddRow (evt: Event): void {
|
||||||
|
evt.stopPropagation()
|
||||||
|
evt.preventDefault()
|
||||||
|
const table = findTable(editor.state.selection)
|
||||||
|
if (table !== undefined) {
|
||||||
|
const { height } = TableMap.get(table.node)
|
||||||
|
const tr = insertRow(table, height, editor.state.tr)
|
||||||
|
editor.view.dispatch(tr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAddColumn (evt: Event): void {
|
||||||
|
evt.stopPropagation()
|
||||||
|
evt.preventDefault()
|
||||||
|
const table = findTable(editor.state.selection)
|
||||||
|
if (table !== undefined) {
|
||||||
|
const { width } = TableMap.get(table.node)
|
||||||
|
const tr = insertColumn(table, width, editor.state.tr)
|
||||||
|
editor.view.dispatch(tr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
editor.off('selectionUpdate', handleSelectionUpdate)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<NodeViewWrapper class="table-node-wrapper" data-drag-handle>
|
||||||
|
<div class="table-wrapper" class:table-selected={focused}>
|
||||||
|
<table class={className}>
|
||||||
|
<NodeViewContent as="tbody" />
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{#if editable && focused}
|
||||||
|
<!-- add col button -->
|
||||||
|
<div class="table-button-container table-button-container__col flex" contenteditable="false">
|
||||||
|
<div class="w-full h-full flex showOnHover">
|
||||||
|
<button class="table-button w-full h-full" on:click={handleAddColumn}>
|
||||||
|
<div class="table-button__dot" />
|
||||||
|
<div class="table-button__icon"><IconAdd size={'small'} /></div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- add row button -->
|
||||||
|
<div class="table-button-container table-button-container__row flex" contenteditable="false">
|
||||||
|
<div class="w-full h-full flex showOnHover">
|
||||||
|
<button class="table-button w-full h-full" on:click={handleAddRow}>
|
||||||
|
<div class="table-button__dot" />
|
||||||
|
<div class="table-button__icon"><IconAdd size={'small'} /></div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</NodeViewWrapper>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.table-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
margin: 1.25rem 0;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -1.25rem;
|
||||||
|
bottom: -1.25rem;
|
||||||
|
left: -1.25rem;
|
||||||
|
right: -1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.table-selected {
|
||||||
|
&::before {
|
||||||
|
border: 1.25rem var(--theme-button-default) solid;
|
||||||
|
border-radius: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-button-container {
|
||||||
|
position: absolute;
|
||||||
|
transition: opacity 0.15s ease-in-out 0.15s;
|
||||||
|
|
||||||
|
&__col {
|
||||||
|
right: -1.25rem;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
.table-button {
|
||||||
|
width: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__row {
|
||||||
|
bottom: -1.25rem;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
.table-button {
|
||||||
|
height: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-button {
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--theme-button-hovered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-button__dot {
|
||||||
|
width: 0.25rem;
|
||||||
|
height: 0.25rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--text-editor-table-marker-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-button__icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.table-button__dot {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.table-button__icon {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
32
packages/text-editor/src/components/extension/table/icons.ts
Normal file
32
packages/text-editor/src/components/extension/table/icons.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2023 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License. You may
|
||||||
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
//
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
export const addSvg = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M8.5 7.49988V3.49988C8.5 3.22374 8.27614 2.99988 8 2.99988C7.72386 2.99988 7.5 3.22374 7.5 3.49988L7.5 7.49988L3.5 7.49988C3.22386 7.49988 3 7.72374 3 7.99988C3 8.27602 3.22386 8.49988 3.5 8.49988H7.5L7.5 12.4999C7.5 12.776 7.72386 12.9999 8 12.9999C8.27614 12.9999 8.5 12.776 8.5 12.4999L8.5 8.49988L12.5 8.49988C12.7761 8.49988 13 8.27602 13 7.99988C13 7.72374 12.7761 7.49988 12.5 7.49988L8.5 7.49988Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>`
|
||||||
|
|
||||||
|
export const handleSvg = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10.0001 9C9.44784 9 9.00012 9.44772 9.00012 10C9.00012 10.5523 9.44784 11 10.0001 11C10.5524 11 11.0001 10.5523 11.0001 10C11.0001 9.44772 10.5524 9 10.0001 9Z" fill="currentColor"/>
|
||||||
|
<path d="M6.00012 9C5.44784 9 5.00012 9.44771 5.00012 10C5.00012 10.5523 5.44784 11 6.00012 11C6.55241 11 7.00012 10.5523 7.00012 10C7.00012 9.44772 6.55241 9 6.00012 9Z" fill="currentColor"/>
|
||||||
|
<path d="M1.00012 10C1.00012 9.44771 1.44784 9 2.00012 9C2.55241 9 3.00012 9.44771 3.00012 10C3.00012 10.5523 2.55241 11 2.00012 11C1.44784 11 1.00012 10.5523 1.00012 10Z" fill="currentColor"/>
|
||||||
|
<path d="M14 9C13.4477 9 13 9.44772 13 10C13 10.5523 13.4477 11 14 11C14.5523 11 15 10.5523 15 10C15 9.44772 14.5523 9 14 9Z" fill="currentColor"/>
|
||||||
|
<path d="M10.0001 5C9.44784 5 9.00012 5.44772 9.00012 6C9.00012 6.55228 9.44784 7 10.0001 7C10.5524 7 11.0001 6.55229 11.0001 6C11.0001 5.44772 10.5524 5 10.0001 5Z" fill="currentColor"/>
|
||||||
|
<path d="M6.00012 5C5.44784 5 5.00012 5.44771 5.00012 6C5.00012 6.55228 5.44784 7 6.00012 7C6.55241 7 7.00012 6.55228 7.00012 6C7.00012 5.44772 6.55241 5 6.00012 5Z" fill="currentColor"/>
|
||||||
|
<path d="M1.00012 6C1.00012 5.44771 1.44784 5 2.00012 5C2.55241 5 3.00012 5.44771 3.00012 6C3.00012 6.55228 2.55241 7 2.00012 7C1.44784 7 1.00012 6.55228 1.00012 6Z" fill="currentColor"/>
|
||||||
|
<path d="M14 5C13.4477 5 13 5.44772 13 6C13 6.55228 13.4477 7 14 7C14.5523 7 15 6.55229 15 6C15 5.44772 14.5523 5 14 5Z" fill="currentColor"/>
|
||||||
|
</svg>`
|
18
packages/text-editor/src/components/extension/table/index.ts
Normal file
18
packages/text-editor/src/components/extension/table/index.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2023 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License. You may
|
||||||
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
//
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
export { Table } from './table'
|
||||||
|
export { TableCell } from './tableCell'
|
||||||
|
export { TableRow } from './tableRow'
|
24
packages/text-editor/src/components/extension/table/table.ts
Normal file
24
packages/text-editor/src/components/extension/table/table.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2023 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License. You may
|
||||||
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
//
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
import TiptapTable from '@tiptap/extension-table'
|
||||||
|
import TableNodeView from './TableNodeView.svelte'
|
||||||
|
import { SvelteNodeViewRenderer } from '../../node-view'
|
||||||
|
|
||||||
|
export const Table = TiptapTable.extend({
|
||||||
|
addNodeView () {
|
||||||
|
return SvelteNodeViewRenderer(TableNodeView, {})
|
||||||
|
}
|
||||||
|
})
|
289
packages/text-editor/src/components/extension/table/tableCell.ts
Normal file
289
packages/text-editor/src/components/extension/table/tableCell.ts
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2023 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License. You may
|
||||||
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
//
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
import { type Editor } from '@tiptap/core'
|
||||||
|
import TiptapTableCell from '@tiptap/extension-table-cell'
|
||||||
|
import { type EditorState, Plugin, PluginKey, type Selection } from '@tiptap/pm/state'
|
||||||
|
import { CellSelection, TableMap } from '@tiptap/pm/tables'
|
||||||
|
import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
||||||
|
|
||||||
|
import { addSvg, handleSvg } from './icons'
|
||||||
|
import { type TableNodeLocation } from './types'
|
||||||
|
import { insertColumn, insertRow, findTable, isColumnSelected, isRowSelected, selectColumn, selectRow } from './utils'
|
||||||
|
|
||||||
|
export const TableCell = TiptapTableCell.extend({
|
||||||
|
addProseMirrorPlugins () {
|
||||||
|
return [...(this.parent?.() ?? []), tableCellDecorationPlugin(this.editor)]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
interface TableCellDecorationPluginState {
|
||||||
|
decorations?: DecorationSet
|
||||||
|
selection?: Selection
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableCellDecorationPlugin = (editor: Editor): Plugin<TableCellDecorationPluginState> => {
|
||||||
|
const key = new PluginKey('table-cell-decoration-plugin')
|
||||||
|
return new Plugin({
|
||||||
|
key,
|
||||||
|
state: {
|
||||||
|
init: (): TableCellDecorationPluginState => {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
apply (tr, prev, oldState, newState) {
|
||||||
|
if (!editor.isEditable) {
|
||||||
|
return { selection: newState.selection, decorations: DecorationSet.empty }
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTable = findTable(newState.selection)
|
||||||
|
|
||||||
|
if (newTable === undefined) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prev.selection === newState.selection) {
|
||||||
|
return prev
|
||||||
|
}
|
||||||
|
|
||||||
|
const decorations = DecorationSet.create(newState.doc, [
|
||||||
|
...columnHandlerDecoration(newState, newTable, editor),
|
||||||
|
...columnInsertDecoration(newState, newTable, editor),
|
||||||
|
...rowHandlerDecoration(newState, newTable, editor),
|
||||||
|
...rowInsertDecoration(newState, newTable, editor),
|
||||||
|
...selectionDecoration(newState, newTable)
|
||||||
|
])
|
||||||
|
return { selection: newState.selection, decorations }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
decorations (state) {
|
||||||
|
return key.getState(state).decorations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnHandlerDecoration = (state: EditorState, table: TableNodeLocation, editor: Editor): Decoration[] => {
|
||||||
|
const decorations: Decoration[] = []
|
||||||
|
|
||||||
|
const { selection } = state
|
||||||
|
|
||||||
|
const tableMap = TableMap.get(table.node)
|
||||||
|
for (let col = 0; col < tableMap.width; col++) {
|
||||||
|
const pos = getTableCellWidgetDecorationPos(table, tableMap, col)
|
||||||
|
|
||||||
|
const handle = document.createElement('div')
|
||||||
|
handle.classList.add('table-col-handle')
|
||||||
|
if (isColumnSelected(col, selection)) {
|
||||||
|
handle.classList.add('table-col-handle__selected')
|
||||||
|
}
|
||||||
|
handle.innerHTML = handleSvg
|
||||||
|
handle.addEventListener('mousedown', (e) => {
|
||||||
|
handleColHandleMouseDown(col, table, e, editor)
|
||||||
|
})
|
||||||
|
decorations.push(Decoration.widget(pos, handle))
|
||||||
|
}
|
||||||
|
|
||||||
|
return decorations
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnInsertDecoration = (state: EditorState, table: TableNodeLocation, editor: Editor): Decoration[] => {
|
||||||
|
const decorations: Decoration[] = []
|
||||||
|
|
||||||
|
const { selection } = state
|
||||||
|
|
||||||
|
const tableMap = TableMap.get(table.node)
|
||||||
|
const { width } = tableMap
|
||||||
|
|
||||||
|
const dom = editor.view.domAtPos(table.start)
|
||||||
|
const tableHeightPx = dom.node.parentElement?.clientHeight ?? 0
|
||||||
|
|
||||||
|
for (let col = 0; col < width; col++) {
|
||||||
|
const show = col < width - 1 && !isColumnSelected(col, selection) && !isColumnSelected(col + 1, selection)
|
||||||
|
|
||||||
|
if (show) {
|
||||||
|
const insert = document.createElement('div')
|
||||||
|
insert.classList.add('table-col-insert')
|
||||||
|
|
||||||
|
const button = document.createElement('button')
|
||||||
|
button.className = 'table-insert-button'
|
||||||
|
button.innerHTML = addSvg
|
||||||
|
button.addEventListener('mousedown', (e) => {
|
||||||
|
handleColInsertMouseDown(col, table, e, editor)
|
||||||
|
})
|
||||||
|
insert.appendChild(button)
|
||||||
|
|
||||||
|
const marker = document.createElement('div')
|
||||||
|
marker.className = 'table-insert-marker'
|
||||||
|
marker.style.height = tableHeightPx + 'px'
|
||||||
|
insert.appendChild(marker)
|
||||||
|
|
||||||
|
const pos = getTableCellWidgetDecorationPos(table, tableMap, col)
|
||||||
|
decorations.push(Decoration.widget(pos, insert))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return decorations
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleColHandleMouseDown = (col: number, table: TableNodeLocation, event: Event, editor: Editor): void => {
|
||||||
|
event.stopPropagation()
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
editor.view.dispatch(selectColumn(table, col, editor.state.tr))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleColInsertMouseDown = (col: number, table: TableNodeLocation, event: Event, editor: Editor): void => {
|
||||||
|
event.stopPropagation()
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
editor.view.dispatch(insertColumn(table, col + 1, editor.state.tr))
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowHandlerDecoration = (state: EditorState, table: TableNodeLocation, editor: Editor): Decoration[] => {
|
||||||
|
const decorations: Decoration[] = []
|
||||||
|
|
||||||
|
const { selection } = state
|
||||||
|
|
||||||
|
const tableMap = TableMap.get(table.node)
|
||||||
|
for (let row = 0; row < tableMap.height; row++) {
|
||||||
|
const pos = getTableCellWidgetDecorationPos(table, tableMap, row * tableMap.width)
|
||||||
|
|
||||||
|
const handle = document.createElement('div')
|
||||||
|
handle.classList.add('table-row-handle')
|
||||||
|
if (isRowSelected(row, selection)) {
|
||||||
|
handle.classList.add('table-row-handle__selected')
|
||||||
|
}
|
||||||
|
handle.innerHTML = handleSvg
|
||||||
|
handle.addEventListener('mousedown', (e) => {
|
||||||
|
handleRowHandleMouseDown(row, table, e, editor)
|
||||||
|
})
|
||||||
|
decorations.push(Decoration.widget(pos, handle))
|
||||||
|
}
|
||||||
|
|
||||||
|
return decorations
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowInsertDecoration = (state: EditorState, table: TableNodeLocation, editor: Editor): Decoration[] => {
|
||||||
|
const decorations: Decoration[] = []
|
||||||
|
|
||||||
|
const { selection } = state
|
||||||
|
|
||||||
|
const tableMap = TableMap.get(table.node)
|
||||||
|
const { height } = tableMap
|
||||||
|
|
||||||
|
const dom = editor.view.domAtPos(table.start)
|
||||||
|
const tableWidthPx = dom.node.parentElement?.clientWidth ?? 0
|
||||||
|
|
||||||
|
for (let row = 0; row < height; row++) {
|
||||||
|
const show = row < height - 1 && !isRowSelected(row, selection) && !isRowSelected(row + 1, selection)
|
||||||
|
|
||||||
|
if (show) {
|
||||||
|
const dot = document.createElement('div')
|
||||||
|
dot.classList.add('table-row-insert')
|
||||||
|
|
||||||
|
const button = document.createElement('button')
|
||||||
|
button.className = 'table-insert-button'
|
||||||
|
button.innerHTML = addSvg
|
||||||
|
button.addEventListener('mousedown', (e) => {
|
||||||
|
handleRowInsertMouseDown(row, table, e, editor)
|
||||||
|
})
|
||||||
|
dot.appendChild(button)
|
||||||
|
|
||||||
|
const marker = document.createElement('div')
|
||||||
|
marker.className = 'table-insert-marker'
|
||||||
|
marker.style.width = tableWidthPx + 'px'
|
||||||
|
dot.appendChild(marker)
|
||||||
|
|
||||||
|
const pos = getTableCellWidgetDecorationPos(table, tableMap, row * tableMap.width)
|
||||||
|
decorations.push(Decoration.widget(pos, dot))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return decorations
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRowHandleMouseDown = (row: number, table: TableNodeLocation, event: Event, editor: Editor): void => {
|
||||||
|
event.stopPropagation()
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
editor.view.dispatch(selectRow(table, row, editor.state.tr))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRowInsertMouseDown = (row: number, table: TableNodeLocation, event: Event, editor: Editor): void => {
|
||||||
|
event.stopPropagation()
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
editor.view.dispatch(insertRow(table, row + 1, editor.state.tr))
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectionDecoration = (state: EditorState, table: TableNodeLocation): Decoration[] => {
|
||||||
|
const decorations: Decoration[] = []
|
||||||
|
|
||||||
|
const { selection } = state
|
||||||
|
|
||||||
|
const tableMap = TableMap.get(table.node)
|
||||||
|
|
||||||
|
if (selection instanceof CellSelection) {
|
||||||
|
const selected: number[] = []
|
||||||
|
|
||||||
|
selection.forEachCell((_node, pos) => {
|
||||||
|
const start = pos - table.pos - 1
|
||||||
|
selected.push(start)
|
||||||
|
})
|
||||||
|
|
||||||
|
selection.forEachCell((node, pos) => {
|
||||||
|
const start = pos - table.pos - 1
|
||||||
|
const borders = getTableCellBorders(start, selected, tableMap)
|
||||||
|
|
||||||
|
const classes = ['table-cell-selected']
|
||||||
|
|
||||||
|
if (borders.top) classes.push('table-cell-selected__border-top')
|
||||||
|
if (borders.bottom) classes.push('table-cell-selected__border-bottom')
|
||||||
|
if (borders.left) classes.push('table-cell-selected__border-left')
|
||||||
|
if (borders.right) classes.push('table-cell-selected__border-right')
|
||||||
|
|
||||||
|
decorations.push(Decoration.node(pos, pos + node.nodeSize, { class: classes.join(' ') }))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return decorations
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTableCellWidgetDecorationPos (table: TableNodeLocation, map: TableMap, index: number): number {
|
||||||
|
const pos = table.node.resolve(map.map[index] + 1)
|
||||||
|
return table.start + pos.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTableCellBorders (
|
||||||
|
cell: number,
|
||||||
|
selection: number[],
|
||||||
|
tableMap: TableMap
|
||||||
|
): { top: boolean, bottom: boolean, left: boolean, right: boolean } {
|
||||||
|
const { width, height } = tableMap
|
||||||
|
const cellIndex = tableMap.map.indexOf(cell)
|
||||||
|
|
||||||
|
const topCell = cellIndex >= width ? tableMap.map[cellIndex - width] : undefined
|
||||||
|
const bottomCell = cellIndex < width * height - width ? tableMap.map[cellIndex + width] : undefined
|
||||||
|
const leftCell = cellIndex % width !== 0 ? tableMap.map[cellIndex - 1] : undefined
|
||||||
|
const rightCell = cellIndex % width !== width - 1 ? tableMap.map[cellIndex + 1] : undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
top: topCell === undefined || !selection.includes(topCell),
|
||||||
|
bottom: bottomCell === undefined || !selection.includes(bottomCell),
|
||||||
|
left: leftCell === undefined || !selection.includes(leftCell),
|
||||||
|
right: rightCell === undefined || !selection.includes(rightCell)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2023 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License. You may
|
||||||
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
//
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
import TiptapTableRow from '@tiptap/extension-table-row'
|
||||||
|
|
||||||
|
export const TableRow = TiptapTableRow.extend({})
|
22
packages/text-editor/src/components/extension/table/types.ts
Normal file
22
packages/text-editor/src/components/extension/table/types.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2023 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License. You may
|
||||||
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
//
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
import { type Node as ProseMirrorNode } from '@tiptap/pm/model'
|
||||||
|
|
||||||
|
export interface TableNodeLocation {
|
||||||
|
pos: number
|
||||||
|
start: number
|
||||||
|
node: ProseMirrorNode
|
||||||
|
}
|
120
packages/text-editor/src/components/extension/table/utils.ts
Normal file
120
packages/text-editor/src/components/extension/table/utils.ts
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2023 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License. You may
|
||||||
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
//
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
import { findParentNode } from '@tiptap/core'
|
||||||
|
import { type Selection, type Transaction } from '@tiptap/pm/state'
|
||||||
|
import { CellSelection, type Rect, TableMap, addColumn, addRow } from '@tiptap/pm/tables'
|
||||||
|
|
||||||
|
import { type TableNodeLocation } from './types'
|
||||||
|
|
||||||
|
export function insertColumn (table: TableNodeLocation, index: number, tr: Transaction): Transaction {
|
||||||
|
const map = TableMap.get(table.node)
|
||||||
|
const rect = {
|
||||||
|
map,
|
||||||
|
tableStart: table.start,
|
||||||
|
table: table.node,
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
bottom: map.height - 1,
|
||||||
|
right: map.width - 1
|
||||||
|
}
|
||||||
|
return addColumn(tr, rect, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function insertRow (table: TableNodeLocation, index: number, tr: Transaction): Transaction {
|
||||||
|
const map = TableMap.get(table.node)
|
||||||
|
const rect = {
|
||||||
|
map,
|
||||||
|
tableStart: table.start,
|
||||||
|
table: table.node,
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
bottom: map.height - 1,
|
||||||
|
right: map.width - 1
|
||||||
|
}
|
||||||
|
return addRow(tr, rect, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectColumn (table: TableNodeLocation, index: number, tr: Transaction): Transaction {
|
||||||
|
const { map } = TableMap.get(table.node)
|
||||||
|
|
||||||
|
const anchorCell = table.start + map[index]
|
||||||
|
const $anchor = tr.doc.resolve(anchorCell)
|
||||||
|
|
||||||
|
return tr.setSelection(CellSelection.colSelection($anchor))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectRow (table: TableNodeLocation, index: number, tr: Transaction): Transaction {
|
||||||
|
const { map, width } = TableMap.get(table.node)
|
||||||
|
|
||||||
|
const anchorCell = table.start + map[index * width]
|
||||||
|
const $anchor = tr.doc.resolve(anchorCell)
|
||||||
|
|
||||||
|
return tr.setSelection(CellSelection.rowSelection($anchor))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectTable (table: TableNodeLocation, tr: Transaction): Transaction {
|
||||||
|
const { map } = TableMap.get(table.node)
|
||||||
|
|
||||||
|
const $head = tr.doc.resolve(table.start + map[0])
|
||||||
|
const $anchor = tr.doc.resolve(table.start + map[map.length - 1])
|
||||||
|
|
||||||
|
return tr.setSelection(new CellSelection($anchor, $head))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isColumnSelected = (columnIndex: number, selection: Selection): boolean => {
|
||||||
|
if (selection instanceof CellSelection) {
|
||||||
|
const { height } = TableMap.get(selection.$anchorCell.node(-1))
|
||||||
|
const rect = { left: columnIndex, right: columnIndex + 1, top: 0, bottom: height }
|
||||||
|
return isRectSelected(rect, selection)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isRowSelected = (rowIndex: number, selection: Selection): boolean => {
|
||||||
|
if (selection instanceof CellSelection) {
|
||||||
|
const { width } = TableMap.get(selection.$anchorCell.node(-1))
|
||||||
|
const rect = { left: 0, right: width, top: rowIndex, bottom: rowIndex + 1 }
|
||||||
|
return isRectSelected(rect, selection)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isTableSelected = (selection: Selection): boolean => {
|
||||||
|
if (selection instanceof CellSelection) {
|
||||||
|
const { height, width } = TableMap.get(selection.$anchorCell.node(-1))
|
||||||
|
const rect = { left: 0, top: 0, right: width, bottom: height }
|
||||||
|
return isRectSelected(rect, selection)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isRectSelected = (rect: Rect, selection: CellSelection): boolean => {
|
||||||
|
const map = TableMap.get(selection.$anchorCell.node(-1))
|
||||||
|
const start = selection.$anchorCell.start(-1)
|
||||||
|
const cells = map.cellsInRect(rect)
|
||||||
|
const selectedCells = map.cellsInRect(
|
||||||
|
map.rectBetween(selection.$anchorCell.pos - start, selection.$headCell.pos - start)
|
||||||
|
)
|
||||||
|
|
||||||
|
return cells.every((cell) => selectedCells.includes(cell))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const findTable = (selection: Selection): TableNodeLocation | undefined => {
|
||||||
|
return findParentNode((node) => node.type.spec.tableRole === 'table')(selection)
|
||||||
|
}
|
@ -1,7 +1,4 @@
|
|||||||
import Table from '@tiptap/extension-table'
|
|
||||||
import TableCell from '@tiptap/extension-table-cell'
|
|
||||||
import TableHeader from '@tiptap/extension-table-header'
|
import TableHeader from '@tiptap/extension-table-header'
|
||||||
import TableRow from '@tiptap/extension-table-row'
|
|
||||||
|
|
||||||
import TaskItem from '@tiptap/extension-task-item'
|
import TaskItem from '@tiptap/extension-task-item'
|
||||||
import TaskList from '@tiptap/extension-task-list'
|
import TaskList from '@tiptap/extension-task-list'
|
||||||
@ -24,6 +21,8 @@ import { SvelteRenderer } from './node-view'
|
|||||||
import { CodemarkExtension } from './extension/codemark'
|
import { CodemarkExtension } from './extension/codemark'
|
||||||
import type { SuggestionKeyDownProps, SuggestionProps } from './extension/suggestion'
|
import type { SuggestionKeyDownProps, SuggestionProps } from './extension/suggestion'
|
||||||
|
|
||||||
|
import { Table, TableCell, TableRow } from './extension/table'
|
||||||
|
|
||||||
export const tableExtensions = [
|
export const tableExtensions = [
|
||||||
Table.configure({
|
Table.configure({
|
||||||
resizable: false,
|
resizable: false,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
const fill: string = 'currentColor'
|
export let fill: string = 'currentColor'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
const fill: string = 'currentColor'
|
export let fill: string = 'currentColor'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
const fill: string = 'currentColor'
|
export let fill: string = 'currentColor'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
const fill: string = 'currentColor'
|
export let fill: string = 'currentColor'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
<svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
const fill: string = 'currentColor'
|
export let fill: string = 'currentColor'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
const fill: string = 'currentColor'
|
export let fill: string = 'currentColor'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
const fill: string = 'currentColor'
|
export let fill: string = 'currentColor'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="svg-{size}" {fill} viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
<svg class="svg-{size}" {fill} viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
const fill: string = 'currentColor'
|
export let fill: string = 'currentColor'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
<svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
const fill: string = 'currentColor'
|
export let fill: string = 'currentColor'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="svg-{size}" {fill} viewBox="0 0 19 24" xmlns="http://www.w3.org/2000/svg">
|
<svg class="svg-{size}" {fill} viewBox="0 0 19 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
const fill: string = 'currentColor'
|
export let fill: string = 'currentColor'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
const fill: string = 'currentColor'
|
export let fill: string = 'currentColor'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
const fill: string = 'currentColor'
|
export let fill: string = 'currentColor'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
export let style: 'table' | 'grid' | 'tableProps' = 'table'
|
export let style: 'table' | 'grid' | 'tableProps' = 'table'
|
||||||
const fill: string = 'currentColor'
|
export let fill: string = 'currentColor'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if style === 'table'}
|
{#if style === 'table'}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
const fill: string = 'currentColor'
|
export let fill: string = 'currentColor'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
|
export let fill: string = 'currentColor'
|
||||||
const fill: string = 'currentColor'
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
const fill: string = 'currentColor'
|
export let fill: string = 'currentColor'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
const fill: string = 'currentColor'
|
export let fill: string = 'currentColor'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
const fill: string = 'currentColor'
|
export let fill: string = 'currentColor'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="svg-{size}" {fill} viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
<svg class="svg-{size}" {fill} viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
const fill: string = 'currentColor'
|
export let fill: string = 'currentColor'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="svg-{size}" {fill} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
<svg class="svg-{size}" {fill} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
|
export let fill: string = 'currentColor'
|
||||||
const fill: string = 'currentColor'
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="svg-{size}" {fill} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
<svg class="svg-{size}" {fill} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
const fill: string = 'currentColor'
|
export let fill: string = 'currentColor'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="svg-{size}" {fill} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
<svg class="svg-{size}" {fill} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
|
export let fill: string = 'currentColor'
|
||||||
const fill: string = 'currentColor'
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="svg-{size}" {fill} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
<svg class="svg-{size}" {fill} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
|
export let fill: string = 'currentColor'
|
||||||
const fill: string = 'currentColor'
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="svg-{size}" {fill} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
<svg class="svg-{size}" {fill} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
const fill: string = 'currentColor'
|
export let fill: string = 'currentColor'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="svg-{size}" {fill} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
<svg class="svg-{size}" {fill} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
const fill: string = 'currentColor'
|
export let fill: string = 'currentColor'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
const fill: string = 'currentColor'
|
export let fill: string = 'currentColor'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="svg-{size}" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg class="svg-{size}" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
const fill: string = 'currentColor'
|
export let fill: string = 'currentColor'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="svg-{size}" {fill} viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
<svg class="svg-{size}" {fill} viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
const fill: string = 'currentColor'
|
export let fill: string = 'currentColor'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
<svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
const fill: string = 'currentColor'
|
export let fill: string = 'currentColor'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
<svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||||
|
@ -20,14 +20,8 @@
|
|||||||
export let as = 'div'
|
export let as = 'div'
|
||||||
|
|
||||||
const { onContentElement } = getNodeViewContext()
|
const { onContentElement } = getNodeViewContext()
|
||||||
|
|
||||||
let element: HTMLElement
|
|
||||||
$: if (element) {
|
|
||||||
element.style.whiteSpace = 'pre-wrap'
|
|
||||||
onContentElement(element)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:element this={as} bind:this={element} {...$$restProps}>
|
<svelte:element this={as} use:onContentElement {...$$restProps}>
|
||||||
<slot />
|
<slot />
|
||||||
</svelte:element>
|
</svelte:element>
|
||||||
|
@ -15,28 +15,13 @@
|
|||||||
//
|
//
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, tick } from 'svelte'
|
|
||||||
import { getNodeViewContext } from './context'
|
import { getNodeViewContext } from './context'
|
||||||
|
|
||||||
export let as = 'div'
|
export let as = 'div'
|
||||||
|
|
||||||
const { onDragStart } = getNodeViewContext()
|
const { onDragStart } = getNodeViewContext()
|
||||||
|
|
||||||
let element: HTMLElement
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
await tick()
|
|
||||||
element.style.whiteSpace = 'normal'
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:element
|
<svelte:element this={as} data-node-view-wrapper="" role="none" on:dragstart={onDragStart} {...$$restProps}>
|
||||||
this={as}
|
|
||||||
bind:this={element}
|
|
||||||
data-node-view-wrapper=""
|
|
||||||
on:dragstart={onDragStart}
|
|
||||||
role="none"
|
|
||||||
{...$$restProps}
|
|
||||||
>
|
|
||||||
<slot />
|
<slot />
|
||||||
</svelte:element>
|
</svelte:element>
|
||||||
|
@ -30,7 +30,7 @@ import { SvelteRenderer } from './svelte-renderer'
|
|||||||
export interface SvelteNodeViewRendererOptions extends NodeViewRendererOptions {
|
export interface SvelteNodeViewRendererOptions extends NodeViewRendererOptions {
|
||||||
update?: (node: ProseMirrorNode, decorations: DecorationWithType[]) => boolean
|
update?: (node: ProseMirrorNode, decorations: DecorationWithType[]) => boolean
|
||||||
contentAs?: string
|
contentAs?: string
|
||||||
contentDOMElementAs?: string
|
contentClass?: string
|
||||||
componentProps?: Record<string, any>
|
componentProps?: Record<string, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,32 +64,17 @@ class SvelteNodeView extends NodeView<SvelteNodeViewComponent, Editor, SvelteNod
|
|||||||
...(this.options.componentProps ?? {})
|
...(this.options.componentProps ?? {})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.node.isLeaf) {
|
|
||||||
this.contentDOMElement = null
|
|
||||||
} else if (this.options.contentDOMElementAs !== undefined) {
|
|
||||||
this.contentDOMElement = document.createElement(this.options.contentDOMElementAs)
|
|
||||||
} else {
|
|
||||||
this.contentDOMElement = document.createElement(this.node.isInline ? 'span' : 'div')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.contentDOMElement !== null) {
|
|
||||||
// For some reason the whiteSpace prop is not inherited properly in Chrome and Safari
|
|
||||||
// With this fix it seems to work fine
|
|
||||||
// See: https://github.com/ueberdosis/tiptap/issues/1197
|
|
||||||
this.contentDOMElement.style.whiteSpace = 'inherit'
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentAs = this.options.contentAs ?? (this.node.isInline ? 'span' : 'div')
|
const contentAs = this.options.contentAs ?? (this.node.isInline ? 'span' : 'div')
|
||||||
|
const contentClass = this.options.contentClass ?? `node-${this.node.type.name}`
|
||||||
|
|
||||||
const target = document.createElement(contentAs)
|
const target = document.createElement(contentAs)
|
||||||
target.classList.add(`node-${this.node.type.name}`)
|
target.classList.add(contentClass)
|
||||||
|
|
||||||
|
this.contentDOMElement = null
|
||||||
const context = createNodeViewContext({
|
const context = createNodeViewContext({
|
||||||
onDragStart: this.onDragStart.bind(this),
|
onDragStart: this.onDragStart.bind(this),
|
||||||
onContentElement: (element) => {
|
onContentElement: (element) => {
|
||||||
if (this.contentDOMElement !== null && !element.contains(this.contentDOMElement)) {
|
this.contentDOMElement = element
|
||||||
element.appendChild(this.contentDOMElement)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -47,7 +47,6 @@
|
|||||||
"@tiptap/extension-task-list": "^2.1.12",
|
"@tiptap/extension-task-list": "^2.1.12",
|
||||||
"@tiptap/extension-typography": "^2.1.12",
|
"@tiptap/extension-typography": "^2.1.12",
|
||||||
"@tiptap/suggestion": "^2.1.12",
|
"@tiptap/suggestion": "^2.1.12",
|
||||||
"@tiptap/prosemirror-tables": "^1.1.4",
|
|
||||||
"prosemirror-model": "^1.19.2",
|
"prosemirror-model": "^1.19.2",
|
||||||
"yjs": "^13.5.52",
|
"yjs": "^13.5.52",
|
||||||
"y-prosemirror": "^1.2.1"
|
"y-prosemirror": "^1.2.1"
|
||||||
|
@ -76,6 +76,8 @@
|
|||||||
--text-editor-highlighted-node-delete-background-color: #F6DCDA;
|
--text-editor-highlighted-node-delete-background-color: #F6DCDA;
|
||||||
--text-editor-highlighted-node-delete-font-color: #54201C;
|
--text-editor-highlighted-node-delete-font-color: #54201C;
|
||||||
|
|
||||||
|
--text-editor-table-marker-color: #bebebf;
|
||||||
|
|
||||||
--theme-clockface-sec-arrow: conic-gradient(at 50% -10px, rgba(255, 0, 0, 0), rgba(255, 0, 0, 0) 49%, #F47758 50%, rgba(255, 0, 0, 0) 51%, rgba(255, 0, 0, 0) 100%);
|
--theme-clockface-sec-arrow: conic-gradient(at 50% -10px, rgba(255, 0, 0, 0), rgba(255, 0, 0, 0) 49%, #F47758 50%, rgba(255, 0, 0, 0) 51%, rgba(255, 0, 0, 0) 100%);
|
||||||
--theme-clockface-sec-holder: #F47758;
|
--theme-clockface-sec-holder: #F47758;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// Copyright © 2022 Hardcore Engineering Inc.
|
// Copyright © 2022, 2023 Hardcore Engineering Inc.
|
||||||
//
|
//
|
||||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License. You may
|
// you may not use this file except in compliance with the License. You may
|
||||||
@ -15,11 +15,15 @@
|
|||||||
|
|
||||||
/* Table */
|
/* Table */
|
||||||
table.proseTable {
|
table.proseTable {
|
||||||
|
--table-selection-border-indent: -0.125rem;
|
||||||
|
--table-selection-border-radius: 0.125rem;
|
||||||
|
--table-selection-border-width: 0.125rem;
|
||||||
|
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
td,
|
td,
|
||||||
th {
|
th {
|
||||||
@ -44,6 +48,199 @@ table.proseTable {
|
|||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
// cell selection
|
||||||
|
|
||||||
|
&.table-cell-selected {
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
border: 0 solid var(--primary-button-focused);
|
||||||
|
border-radius: var(--table-selection-border-radius);
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 110;
|
||||||
|
top: var(--table-selection-border-indent);
|
||||||
|
bottom: var(--table-selection-border-indent);
|
||||||
|
left: var(--table-selection-border-indent);
|
||||||
|
right: var(--table-selection-border-indent);
|
||||||
|
}
|
||||||
|
&__border-top::before {
|
||||||
|
border-top-width: var(--table-selection-border-width);
|
||||||
|
}
|
||||||
|
&__border-bottom::before {
|
||||||
|
border-bottom-width: var(--table-selection-border-width);
|
||||||
|
}
|
||||||
|
&__border-left::before {
|
||||||
|
border-left-width: var(--table-selection-border-width);
|
||||||
|
}
|
||||||
|
&__border-right::before {
|
||||||
|
border-right-width: var(--table-selection-border-width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// row and col handlers
|
||||||
|
|
||||||
|
.table-col-handle,
|
||||||
|
.table-row-handle {
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: var(--theme-button-contrast-hovered);
|
||||||
|
opacity: 0;
|
||||||
|
z-index: 120;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__selected {
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
background-color: var(--primary-button-focused);
|
||||||
|
border: var(--table-selection-border-width) solid var(--primary-button-focused);
|
||||||
|
border-radius: var(--table-selection-border-radius);
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 110;
|
||||||
|
top: var(--table-selection-border-indent);
|
||||||
|
bottom: var(--table-selection-border-indent);
|
||||||
|
left: var(--table-selection-border-indent);
|
||||||
|
right: var(--table-selection-border-indent);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: white;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-col-handle {
|
||||||
|
position: absolute;
|
||||||
|
width: calc(100% + 1px);
|
||||||
|
height: 1.25rem;
|
||||||
|
top: calc(-1.25rem - 1px);
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
&:not(.table-col-handle__selected) {
|
||||||
|
background-color: var(--theme-button-hovered);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
&__selected {
|
||||||
|
&::before {
|
||||||
|
right: -1px;
|
||||||
|
bottom: 0;
|
||||||
|
border-bottom-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row-handle {
|
||||||
|
position: absolute;
|
||||||
|
width: 1.25rem;
|
||||||
|
height: calc(100% + 1px);
|
||||||
|
top: 0;
|
||||||
|
left: calc(-1.25rem - 1px);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
&:not(.table-row-handle__selected) {
|
||||||
|
background-color: var(--theme-button-hovered);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__selected {
|
||||||
|
&::before {
|
||||||
|
bottom: -1px;
|
||||||
|
right: 0;
|
||||||
|
border-right-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// row and col insert
|
||||||
|
|
||||||
|
.table-col-insert,
|
||||||
|
.table-row-insert {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-col-insert {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
top: -1.25rem;
|
||||||
|
right: -0.625rem;
|
||||||
|
width: 1.25rem;
|
||||||
|
|
||||||
|
.table-insert-marker {
|
||||||
|
width: 0.125rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row-insert {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
left: -1.25rem;
|
||||||
|
bottom: -0.625rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
|
||||||
|
.table-insert-marker {
|
||||||
|
height: 0.125rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-insert-button {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--primary-button-focused);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:hover) {
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
border: 2px solid var(--text-editor-table-marker-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-left: calc(0.5rem - 1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover + .table-insert-marker { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-insert-marker {
|
||||||
|
background-color: var(--primary-button-focused);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.proseCode {
|
.proseCode {
|
||||||
|
Loading…
Reference in New Issue
Block a user