EZQMS-378 Wiki table node view (#4123)

Signed-off-by: Alexander Onnikov <alexander.onnikov@xored.com>
This commit is contained in:
Alexander Onnikov 2023-12-02 00:55:17 +07:00 committed by GitHub
parent 74e50f0b87
commit 27740f3f35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 940 additions and 82 deletions

View File

@ -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>

View 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>`

View 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'

View 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, {})
}
})

View 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)
}
}

View 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.
//
import TiptapTableRow from '@tiptap/extension-table-row'
export const TableRow = TiptapTableRow.extend({})

View 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
}

View 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)
}

View File

@ -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,

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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'}

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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>

View File

@ -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>

View File

@ -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)
}
} }
}) })

View File

@ -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"

View File

@ -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;
} }

View File

@ -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 {