mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-29 20:13:46 -05:00
Merge pull request #14559 from overleaf/mj-table-add-row-borders
[visual] Table generator improvements GitOrigin-RevId: 8d3d1b382c68c13480b3aa50b6764903ff59ae81
This commit is contained in:
parent
8e6d6f8689
commit
2516f271b1
11 changed files with 253 additions and 58 deletions
|
@ -6,7 +6,7 @@ interface CellInputProps
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CellInputRef = {
|
export type CellInputRef = {
|
||||||
focus: () => void
|
focus: (options?: FocusOptions) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CellInput = forwardRef<CellInputRef, CellInputProps>(
|
export const CellInput = forwardRef<CellInputRef, CellInputProps>(
|
||||||
|
@ -14,9 +14,9 @@ export const CellInput = forwardRef<CellInputRef, CellInputProps>(
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null)
|
const inputRef = useRef<HTMLTextAreaElement>(null)
|
||||||
useImperativeHandle(ref, () => {
|
useImperativeHandle(ref, () => {
|
||||||
return {
|
return {
|
||||||
focus() {
|
focus(options) {
|
||||||
inputRef.current?.focus()
|
|
||||||
inputRef.current?.setSelectionRange(value.length, value.length)
|
inputRef.current?.setSelectionRange(value.length, value.length)
|
||||||
|
inputRef.current?.focus(options)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { typesetNodeIntoElement } from '../../extensions/visual/utils/typeset-co
|
||||||
import { parser } from '../../lezer-latex/latex.mjs'
|
import { parser } from '../../lezer-latex/latex.mjs'
|
||||||
import { useTableContext } from './contexts/table-context'
|
import { useTableContext } from './contexts/table-context'
|
||||||
import { CellInput, CellInputRef } from './cell-input'
|
import { CellInput, CellInputRef } from './cell-input'
|
||||||
|
import { useCodeMirrorViewContext } from '../codemirror-editor'
|
||||||
|
|
||||||
export const Cell: FC<{
|
export const Cell: FC<{
|
||||||
cellData: CellData
|
cellData: CellData
|
||||||
|
@ -40,6 +41,7 @@ export const Cell: FC<{
|
||||||
commitCellData,
|
commitCellData,
|
||||||
} = useEditingContext()
|
} = useEditingContext()
|
||||||
const inputRef = useRef<CellInputRef>(null)
|
const inputRef = useRef<CellInputRef>(null)
|
||||||
|
const view = useCodeMirrorViewContext()
|
||||||
|
|
||||||
const editing =
|
const editing =
|
||||||
editingCellData?.rowIndex === rowIndex &&
|
editingCellData?.rowIndex === rowIndex &&
|
||||||
|
@ -132,6 +134,9 @@ export const Cell: FC<{
|
||||||
.replaceAll(/(^&|[^\\]&)/g, match =>
|
.replaceAll(/(^&|[^\\]&)/g, match =>
|
||||||
match.length === 1 ? '\\&' : `${match[0]}\\&`
|
match.length === 1 ? '\\&' : `${match[0]}\\&`
|
||||||
)
|
)
|
||||||
|
.replaceAll(/(^%|[^\\]%)/g, match =>
|
||||||
|
match.length === 1 ? '\\%' : `${match[0]}\\%`
|
||||||
|
)
|
||||||
.replaceAll('\\\\', '')
|
.replaceAll('\\\\', '')
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
@ -142,7 +147,7 @@ export const Cell: FC<{
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isFocused && !editing && cellRef.current) {
|
if (isFocused && !editing && cellRef.current) {
|
||||||
cellRef.current.focus()
|
cellRef.current.focus({ preventScroll: true })
|
||||||
}
|
}
|
||||||
}, [isFocused, editing])
|
}, [isFocused, editing])
|
||||||
|
|
||||||
|
@ -161,11 +166,12 @@ export const Cell: FC<{
|
||||||
.then(async MathJax => {
|
.then(async MathJax => {
|
||||||
if (renderDiv.current) {
|
if (renderDiv.current) {
|
||||||
await MathJax.typesetPromise([renderDiv.current])
|
await MathJax.typesetPromise([renderDiv.current])
|
||||||
|
view.requestMeasure()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
}
|
}
|
||||||
}, [cellData.content, editing])
|
}, [cellData.content, editing, view])
|
||||||
|
|
||||||
const onInput = useCallback(
|
const onInput = useCallback(
|
||||||
e => {
|
e => {
|
||||||
|
@ -227,6 +233,7 @@ export const Cell: FC<{
|
||||||
inSelection && selection?.bordersLeft(rowIndex, columnIndex, table),
|
inSelection && selection?.bordersLeft(rowIndex, columnIndex, table),
|
||||||
'selection-edge-right':
|
'selection-edge-right':
|
||||||
inSelection && selection?.bordersRight(rowIndex, columnIndex, table),
|
inSelection && selection?.bordersRight(rowIndex, columnIndex, table),
|
||||||
|
editing,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{body}
|
{body}
|
||||||
|
|
|
@ -57,6 +57,7 @@ export const EditingContextProvider: FC = ({ children }) => {
|
||||||
view.dispatch({
|
view.dispatch({
|
||||||
changes: { from, to, insert: content },
|
changes: { from, to, insert: content },
|
||||||
})
|
})
|
||||||
|
view.requestMeasure()
|
||||||
setCellData(null)
|
setCellData(null)
|
||||||
},
|
},
|
||||||
[view, table, initialContent]
|
[view, table, initialContent]
|
||||||
|
@ -68,6 +69,7 @@ export const EditingContextProvider: FC = ({ children }) => {
|
||||||
}
|
}
|
||||||
if (!cellData.dirty) {
|
if (!cellData.dirty) {
|
||||||
setCellData(null)
|
setCellData(null)
|
||||||
|
setInitialContent(undefined)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const { rowIndex, cellIndex, content } = cellData
|
const { rowIndex, cellIndex, content } = cellData
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {
|
||||||
KeyboardEvent,
|
KeyboardEvent,
|
||||||
KeyboardEventHandler,
|
KeyboardEventHandler,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
|
@ -15,6 +16,8 @@ import {
|
||||||
import { useEditingContext } from './contexts/editing-context'
|
import { useEditingContext } from './contexts/editing-context'
|
||||||
import { useTableContext } from './contexts/table-context'
|
import { useTableContext } from './contexts/table-context'
|
||||||
import { useCodeMirrorViewContext } from '../codemirror-editor'
|
import { useCodeMirrorViewContext } from '../codemirror-editor'
|
||||||
|
import { undo, redo } from '@codemirror/commands'
|
||||||
|
import { ChangeSpec } from '@codemirror/state'
|
||||||
|
|
||||||
type NavigationKey =
|
type NavigationKey =
|
||||||
| 'ArrowRight'
|
| 'ArrowRight'
|
||||||
|
@ -28,6 +31,8 @@ type NavigationMap = {
|
||||||
[key in NavigationKey]: [() => TableSelection, () => TableSelection]
|
[key in NavigationKey]: [() => TableSelection, () => TableSelection]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isMac = /Mac/.test(window.navigator?.platform)
|
||||||
|
|
||||||
export const Table: FC = () => {
|
export const Table: FC = () => {
|
||||||
const { selection, setSelection } = useSelectionContext()
|
const { selection, setSelection } = useSelectionContext()
|
||||||
const {
|
const {
|
||||||
|
@ -70,6 +75,7 @@ export const Table: FC = () => {
|
||||||
|
|
||||||
const isCharacterInput = useCallback((event: KeyboardEvent) => {
|
const isCharacterInput = useCallback((event: KeyboardEvent) => {
|
||||||
return (
|
return (
|
||||||
|
Boolean(event.code) && // is a keyboard key
|
||||||
event.key?.length === 1 &&
|
event.key?.length === 1 &&
|
||||||
!event.ctrlKey &&
|
!event.ctrlKey &&
|
||||||
!event.metaKey &&
|
!event.metaKey &&
|
||||||
|
@ -79,6 +85,7 @@ export const Table: FC = () => {
|
||||||
|
|
||||||
const onKeyDown: KeyboardEventHandler = useCallback(
|
const onKeyDown: KeyboardEventHandler = useCallback(
|
||||||
event => {
|
event => {
|
||||||
|
const commandKey = isMac ? event.metaKey : event.ctrlKey
|
||||||
if (event.code === 'Enter' && !event.shiftKey) {
|
if (event.code === 'Enter' && !event.shiftKey) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
@ -116,6 +123,18 @@ export const Table: FC = () => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
clearCells(selection)
|
clearCells(selection)
|
||||||
|
view.requestMeasure()
|
||||||
|
setTimeout(() => {
|
||||||
|
if (tableRef.current) {
|
||||||
|
const { minY } = selection.normalized()
|
||||||
|
const row = tableRef.current.querySelectorAll('tbody tr')[minY]
|
||||||
|
if (row) {
|
||||||
|
if (row.getBoundingClientRect().top < 0) {
|
||||||
|
row.scrollIntoView({ block: 'center' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
} else if (Object.prototype.hasOwnProperty.call(navigation, event.code)) {
|
} else if (Object.prototype.hasOwnProperty.call(navigation, event.code)) {
|
||||||
const [defaultNavigation, shiftNavigation] =
|
const [defaultNavigation, shiftNavigation] =
|
||||||
navigation[event.code as NavigationKey]
|
navigation[event.code as NavigationKey]
|
||||||
|
@ -146,6 +165,21 @@ export const Table: FC = () => {
|
||||||
startEditing(selection.to.row, selection.to.cell)
|
startEditing(selection.to.row, selection.to.cell)
|
||||||
updateCellData(event.key)
|
updateCellData(event.key)
|
||||||
setSelection(new TableSelection(selection.to, selection.to))
|
setSelection(new TableSelection(selection.to, selection.to))
|
||||||
|
} else if (
|
||||||
|
!cellData &&
|
||||||
|
event.key === 'z' &&
|
||||||
|
!event.shiftKey &&
|
||||||
|
commandKey
|
||||||
|
) {
|
||||||
|
event.preventDefault()
|
||||||
|
undo(view)
|
||||||
|
} else if (
|
||||||
|
!cellData &&
|
||||||
|
(event.key === 'y' ||
|
||||||
|
(event.key === 'Z' && event.shiftKey && commandKey))
|
||||||
|
) {
|
||||||
|
event.preventDefault()
|
||||||
|
redo(view)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
@ -163,6 +197,67 @@ export const Table: FC = () => {
|
||||||
tableData,
|
tableData,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onPaste = (event: ClipboardEvent) => {
|
||||||
|
if (cellData || !selection) {
|
||||||
|
// We're editing a cell, so allow browser to insert there
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
event.preventDefault()
|
||||||
|
const changes: ChangeSpec[] = []
|
||||||
|
const data = event.clipboardData?.getData('text/plain')
|
||||||
|
if (data) {
|
||||||
|
const rows = data.split('\n')
|
||||||
|
const { minX, minY } = selection.normalized()
|
||||||
|
for (let row = 0; row < rows.length; row++) {
|
||||||
|
if (tableData.rows.length <= minY + row) {
|
||||||
|
// TODO: Add rows
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const cells = rows[row].split('\t')
|
||||||
|
for (let cell = 0; cell < cells.length; cell++) {
|
||||||
|
if (tableData.columns.length <= minX + cell) {
|
||||||
|
// TODO: Add columns
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const cellData = tableData.getCell(minY + row, minX + cell)
|
||||||
|
changes.push({
|
||||||
|
from: cellData.from,
|
||||||
|
to: cellData.to,
|
||||||
|
insert: cells[cell],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
view.dispatch({ changes })
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCopy = (event: ClipboardEvent) => {
|
||||||
|
if (cellData || !selection) {
|
||||||
|
// We're editing a cell, so allow browser to insert there
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
event.preventDefault()
|
||||||
|
const { minX, minY, maxX, maxY } = selection.normalized()
|
||||||
|
const content = []
|
||||||
|
for (let row = minY; row <= maxY; row++) {
|
||||||
|
const rowContent = []
|
||||||
|
for (let cell = minX; cell <= maxX; cell++) {
|
||||||
|
rowContent.push(tableData.getCell(row, cell).content)
|
||||||
|
}
|
||||||
|
content.push(rowContent.join('\t'))
|
||||||
|
}
|
||||||
|
navigator.clipboard.writeText(content.join('\n'))
|
||||||
|
}
|
||||||
|
window.addEventListener('paste', onPaste)
|
||||||
|
window.addEventListener('copy', onCopy)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('paste', onPaste)
|
||||||
|
window.removeEventListener('copy', onCopy)
|
||||||
|
}
|
||||||
|
}, [cellData, selection, tableData, view])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
|
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
|
||||||
<table
|
<table
|
||||||
|
|
|
@ -133,6 +133,7 @@ function parseTabularBody(
|
||||||
node: SyntaxNode,
|
node: SyntaxNode,
|
||||||
state: EditorState
|
state: EditorState
|
||||||
): ParsedTableBody {
|
): ParsedTableBody {
|
||||||
|
const firstChild = node.firstChild
|
||||||
const body: ParsedTableBody = {
|
const body: ParsedTableBody = {
|
||||||
rows: [
|
rows: [
|
||||||
{
|
{
|
||||||
|
@ -155,7 +156,7 @@ function parseTabularBody(
|
||||||
return getLastRow().cells[getLastRow().cells.length - 1]
|
return getLastRow().cells[getLastRow().cells.length - 1]
|
||||||
}
|
}
|
||||||
for (
|
for (
|
||||||
let currentChild: SyntaxNode | null = node;
|
let currentChild: SyntaxNode | null = firstChild;
|
||||||
currentChild;
|
currentChild;
|
||||||
currentChild = currentChild.nextSibling
|
currentChild = currentChild.nextSibling
|
||||||
) {
|
) {
|
||||||
|
@ -256,7 +257,8 @@ function parseTabularBody(
|
||||||
continue
|
continue
|
||||||
} else if (
|
} else if (
|
||||||
currentChild.type.is('NewLine') ||
|
currentChild.type.is('NewLine') ||
|
||||||
currentChild.type.is('Whitespace')
|
currentChild.type.is('Whitespace') ||
|
||||||
|
currentChild.type.is('Comment')
|
||||||
) {
|
) {
|
||||||
const lastCell = getLastCell()
|
const lastCell = getLastCell()
|
||||||
if (!lastCell?.multiColumn) {
|
if (!lastCell?.multiColumn) {
|
||||||
|
@ -306,11 +308,22 @@ function parseTabularBody(
|
||||||
getLastRow().position.to = currentChild.to
|
getLastRow().position.to = currentChild.to
|
||||||
}
|
}
|
||||||
const lastRow = getLastRow()
|
const lastRow = getLastRow()
|
||||||
if (lastRow.cells.length === 1 && lastRow.cells[0].content.trim() === '') {
|
if (
|
||||||
|
body.rows.length > 1 &&
|
||||||
|
lastRow.cells.length === 1 &&
|
||||||
|
lastRow.cells[0].content.trim() === ''
|
||||||
|
) {
|
||||||
// Remove the last row if it's empty, but move hlines up to previous row
|
// Remove the last row if it's empty, but move hlines up to previous row
|
||||||
const hlines = lastRow.hlines.map(hline => ({ ...hline, atBottom: true }))
|
const hlines = lastRow.hlines.map(hline => ({ ...hline, atBottom: true }))
|
||||||
body.rows.pop()
|
body.rows.pop()
|
||||||
getLastRow().hlines.push(...hlines)
|
getLastRow().hlines.push(...hlines)
|
||||||
|
const lastLineContents = state.sliceDoc(
|
||||||
|
lastRow.position.from,
|
||||||
|
lastRow.position.to
|
||||||
|
)
|
||||||
|
const lastLineOffset =
|
||||||
|
lastLineContents.length - lastLineContents.trimEnd().length
|
||||||
|
getLastRow().position.to = lastRow.position.to - lastLineOffset
|
||||||
}
|
}
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
@ -339,7 +352,7 @@ export function generateTable(
|
||||||
const columns = parseColumnSpecifications(
|
const columns = parseColumnSpecifications(
|
||||||
state.sliceDoc(specification.from, specification.to)
|
state.sliceDoc(specification.from, specification.to)
|
||||||
)
|
)
|
||||||
const body = node.getChild('Content')?.getChild('TabularContent')?.firstChild
|
const body = node.getChild('Content')?.getChild('TabularContent')
|
||||||
if (!body) {
|
if (!body) {
|
||||||
throw new Error('Missing table body')
|
throw new Error('Missing table body')
|
||||||
}
|
}
|
||||||
|
|
|
@ -113,6 +113,8 @@ export const tableGeneratorTheme = EditorView.baseTheme({
|
||||||
'border-bottom-color': 'var(--table-generator-active-border-color)',
|
'border-bottom-color': 'var(--table-generator-active-border-color)',
|
||||||
'border-bottom-width': 'var(--table-generator-active-border-width)',
|
'border-bottom-width': 'var(--table-generator-active-border-width)',
|
||||||
},
|
},
|
||||||
|
'overflow-x': 'auto',
|
||||||
|
'overflow-y': 'hidden',
|
||||||
},
|
},
|
||||||
|
|
||||||
'.table-generator-table': {
|
'.table-generator-table': {
|
||||||
|
@ -122,8 +124,11 @@ export const tableGeneratorTheme = EditorView.baseTheme({
|
||||||
cursor: 'default',
|
cursor: 'default',
|
||||||
|
|
||||||
'& td': {
|
'& td': {
|
||||||
padding: '0 0.25em',
|
'&:not(.editing)': {
|
||||||
|
padding: '0 0.25em',
|
||||||
|
},
|
||||||
'max-width': '200px',
|
'max-width': '200px',
|
||||||
|
'vertical-align': 'top',
|
||||||
|
|
||||||
'&.alignment-left': {
|
'&.alignment-left': {
|
||||||
'text-align': 'left',
|
'text-align': 'left',
|
||||||
|
@ -280,11 +285,16 @@ export const tableGeneratorTheme = EditorView.baseTheme({
|
||||||
'background-color': 'transparent',
|
'background-color': 'transparent',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '1.5em',
|
height: '1.5em',
|
||||||
|
'min-height': '100%',
|
||||||
border: '1px solid var(--table-generator-toolbar-shadow-color)',
|
border: '1px solid var(--table-generator-toolbar-shadow-color)',
|
||||||
padding: '0',
|
padding: '0 0.25em',
|
||||||
resize: 'none',
|
resize: 'none',
|
||||||
'box-sizing': 'border-box',
|
'box-sizing': 'border-box',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
'&:focus, &:focus-visible': {
|
||||||
|
outline: '2px solid var(--table-generator-focus-border-color)',
|
||||||
|
'outline-offset': '-2px',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
'.table-generator-border-options-coming-soon': {
|
'.table-generator-border-options-coming-soon': {
|
||||||
|
|
|
@ -1,20 +1,59 @@
|
||||||
import { EditorState } from '@codemirror/state'
|
import { EditorState } from '@codemirror/state'
|
||||||
import { SyntaxNode } from '@lezer/common'
|
import { SyntaxNode } from '@lezer/common'
|
||||||
|
import { COMMAND_SUBSTITUTIONS } from '../visual-widgets/character'
|
||||||
|
|
||||||
const isUnknownCommandWithName = (
|
const isUnknownCommandWithName = (
|
||||||
node: SyntaxNode,
|
node: SyntaxNode,
|
||||||
command: string,
|
command: string,
|
||||||
getText: (from: number, to: number) => string
|
getText: (from: number, to: number) => string
|
||||||
): boolean => {
|
): boolean => {
|
||||||
if (!node.type.is('UnknownCommand')) {
|
const commandName = getUnknownCommandName(node, getText)
|
||||||
|
if (commandName === undefined) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
return commandName === command
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUnknownCommandName = (
|
||||||
|
node: SyntaxNode,
|
||||||
|
getText: (from: number, to: number) => string
|
||||||
|
): string | undefined => {
|
||||||
|
if (!node.type.is('UnknownCommand')) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
const commandNameNode = node.getChild('CtrlSeq')
|
const commandNameNode = node.getChild('CtrlSeq')
|
||||||
if (!commandNameNode) {
|
if (!commandNameNode) {
|
||||||
return false
|
return undefined
|
||||||
}
|
}
|
||||||
const commandName = getText(commandNameNode.from, commandNameNode.to)
|
const commandName = getText(commandNameNode.from, commandNameNode.to)
|
||||||
return commandName === command
|
return commandName
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeMapping = {
|
||||||
|
elementType: keyof HTMLElementTagNameMap
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
type MarkupMapping = {
|
||||||
|
[command: string]: NodeMapping
|
||||||
|
}
|
||||||
|
const MARKUP_COMMANDS: MarkupMapping = {
|
||||||
|
'\\textit': {
|
||||||
|
elementType: 'i',
|
||||||
|
},
|
||||||
|
'\\textbf': {
|
||||||
|
elementType: 'b',
|
||||||
|
},
|
||||||
|
'\\emph': {
|
||||||
|
elementType: 'em',
|
||||||
|
},
|
||||||
|
'\\texttt': {
|
||||||
|
elementType: 'span',
|
||||||
|
className: 'ol-cm-command-texttt',
|
||||||
|
},
|
||||||
|
'\\textsc': {
|
||||||
|
elementType: 'span',
|
||||||
|
className: 'ol-cm-command-textsc',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -56,28 +95,27 @@ export function typesetNodeIntoElement(
|
||||||
ancestor().append(
|
ancestor().append(
|
||||||
document.createTextNode(getText(from, childNode.from))
|
document.createTextNode(getText(from, childNode.from))
|
||||||
)
|
)
|
||||||
|
|
||||||
from = childNode.from
|
from = childNode.from
|
||||||
}
|
}
|
||||||
if (isUnknownCommandWithName(childNode, '\\textit', getText)) {
|
|
||||||
pushAncestor(document.createElement('i'))
|
if (childNode.type.is('UnknownCommand')) {
|
||||||
const textArgument = childNode.getChild('TextArgument')
|
const commandNameNode = childNode.getChild('CtrlSeq')
|
||||||
from = textArgument?.getChild('LongArg')?.from ?? childNode.to
|
if (commandNameNode) {
|
||||||
} else if (isUnknownCommandWithName(childNode, '\\textbf', getText)) {
|
const commandName = getText(commandNameNode.from, commandNameNode.to)
|
||||||
pushAncestor(document.createElement('b'))
|
const mapping: NodeMapping | undefined = MARKUP_COMMANDS[commandName]
|
||||||
const textArgument = childNode.getChild('TextArgument')
|
if (mapping) {
|
||||||
from = textArgument?.getChild('LongArg')?.from ?? childNode.to
|
const element = document.createElement(mapping.elementType)
|
||||||
} else if (isUnknownCommandWithName(childNode, '\\emph', getText)) {
|
if (mapping.className) {
|
||||||
pushAncestor(document.createElement('em'))
|
element.classList.add(mapping.className)
|
||||||
const textArgument = childNode.getChild('TextArgument')
|
}
|
||||||
from = textArgument?.getChild('LongArg')?.from ?? childNode.to
|
pushAncestor(element)
|
||||||
} else if (isUnknownCommandWithName(childNode, '\\texttt', getText)) {
|
const textArgument = childNode.getChild('TextArgument')
|
||||||
const spanElement = document.createElement('span')
|
from = textArgument?.getChild('LongArg')?.from ?? childNode.to
|
||||||
spanElement.classList.add('ol-cm-command-texttt')
|
return
|
||||||
pushAncestor(spanElement)
|
}
|
||||||
const textArgument = childNode.getChild('TextArgument')
|
}
|
||||||
from = textArgument?.getChild('LongArg')?.from ?? childNode.to
|
}
|
||||||
} else if (isUnknownCommandWithName(childNode, '\\and', getText)) {
|
if (isUnknownCommandWithName(childNode, '\\and', getText)) {
|
||||||
const spanElement = document.createElement('span')
|
const spanElement = document.createElement('span')
|
||||||
spanElement.classList.add('ol-cm-command-and')
|
spanElement.classList.add('ol-cm-command-and')
|
||||||
pushAncestor(spanElement)
|
pushAncestor(spanElement)
|
||||||
|
@ -94,16 +132,22 @@ export function typesetNodeIntoElement(
|
||||||
} else if (childNode.type.is('LineBreak')) {
|
} else if (childNode.type.is('LineBreak')) {
|
||||||
ancestor().appendChild(document.createElement('br'))
|
ancestor().appendChild(document.createElement('br'))
|
||||||
from = childNode.to
|
from = childNode.to
|
||||||
|
} else if (childNode.type.is('UnknownCommand')) {
|
||||||
|
const command = getText(childNode.from, childNode.to)
|
||||||
|
const symbol = COMMAND_SUBSTITUTIONS.get(command.trim())
|
||||||
|
if (symbol !== undefined) {
|
||||||
|
ancestor().append(document.createTextNode(symbol))
|
||||||
|
from = childNode.to
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
function leave(childNodeRef) {
|
function leave(childNodeRef) {
|
||||||
const childNode = childNodeRef.node
|
const childNode = childNodeRef.node
|
||||||
|
const commandName = getUnknownCommandName(childNode, getText)
|
||||||
if (
|
if (
|
||||||
isUnknownCommandWithName(childNode, '\\and', getText) ||
|
(commandName && Boolean(MARKUP_COMMANDS[commandName])) ||
|
||||||
isUnknownCommandWithName(childNode, '\\textit', getText) ||
|
isUnknownCommandWithName(childNode, '\\and', getText)
|
||||||
isUnknownCommandWithName(childNode, '\\textbf', getText) ||
|
|
||||||
isUnknownCommandWithName(childNode, '\\emph', getText) ||
|
|
||||||
isUnknownCommandWithName(childNode, '\\texttt', getText)
|
|
||||||
) {
|
) {
|
||||||
const typeSetElement = popAncestor()
|
const typeSetElement = popAncestor()
|
||||||
ancestor().appendChild(typeSetElement)
|
ancestor().appendChild(typeSetElement)
|
||||||
|
|
|
@ -26,7 +26,7 @@ export class CharacterWidget extends WidgetType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const SUBSTITUTIONS = new Map([
|
export const COMMAND_SUBSTITUTIONS = new Map([
|
||||||
['\\', ' '], // a trimmed \\ '
|
['\\', ' '], // a trimmed \\ '
|
||||||
['\\%', '\u0025'],
|
['\\%', '\u0025'],
|
||||||
['\\_', '\u005F'],
|
['\\_', '\u005F'],
|
||||||
|
@ -151,12 +151,12 @@ const SUBSTITUTIONS = new Map([
|
||||||
export function createCharacterCommand(
|
export function createCharacterCommand(
|
||||||
command: string
|
command: string
|
||||||
): CharacterWidget | undefined {
|
): CharacterWidget | undefined {
|
||||||
const substitution = SUBSTITUTIONS.get(command)
|
const substitution = COMMAND_SUBSTITUTIONS.get(command)
|
||||||
if (substitution !== undefined) {
|
if (substitution !== undefined) {
|
||||||
return new CharacterWidget(substitution)
|
return new CharacterWidget(substitution)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasCharacterSubstitution(command: string): boolean {
|
export function hasCharacterSubstitution(command: string): boolean {
|
||||||
return SUBSTITUTIONS.has(command)
|
return COMMAND_SUBSTITUTIONS.has(command)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
|
|
||||||
export class TabularWidget extends WidgetType {
|
export class TabularWidget extends WidgetType {
|
||||||
private element: HTMLElement | undefined
|
private element: HTMLElement | undefined
|
||||||
private readonly parseResult: ParsedTableData
|
private readonly parseResult: ParsedTableData | null = null
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private tabularNode: SyntaxNode,
|
private tabularNode: SyntaxNode,
|
||||||
|
@ -19,10 +19,17 @@ export class TabularWidget extends WidgetType {
|
||||||
state: EditorState
|
state: EditorState
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
this.parseResult = generateTable(tabularNode, state)
|
try {
|
||||||
|
this.parseResult = generateTable(tabularNode, state)
|
||||||
|
} catch (e) {
|
||||||
|
this.parseResult = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isValid() {
|
isValid() {
|
||||||
|
if (!this.parseResult) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
for (const row of this.parseResult.table.rows) {
|
for (const row of this.parseResult.table.rows) {
|
||||||
const rowLength = row.cells.reduce(
|
const rowLength = row.cells.reduce(
|
||||||
(acc, cell) => acc + (cell.multiColumn?.columnSpan ?? 1),
|
(acc, cell) => acc + (cell.multiColumn?.columnSpan ?? 1),
|
||||||
|
@ -49,15 +56,17 @@ export class TabularWidget extends WidgetType {
|
||||||
if (this.tableNode) {
|
if (this.tableNode) {
|
||||||
this.element.classList.add('ol-cm-environment-table')
|
this.element.classList.add('ol-cm-environment-table')
|
||||||
}
|
}
|
||||||
ReactDOM.render(
|
if (this.parseResult) {
|
||||||
<Tabular
|
ReactDOM.render(
|
||||||
view={view}
|
<Tabular
|
||||||
tabularNode={this.tabularNode}
|
view={view}
|
||||||
parsedTableData={this.parseResult}
|
tabularNode={this.tabularNode}
|
||||||
tableNode={this.tableNode}
|
parsedTableData={this.parseResult}
|
||||||
/>,
|
tableNode={this.tableNode}
|
||||||
this.element
|
/>,
|
||||||
)
|
this.element
|
||||||
|
)
|
||||||
|
}
|
||||||
return this.element
|
return this.element
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,6 +80,9 @@ export class TabularWidget extends WidgetType {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDOM(dom: HTMLElement, view: EditorView): boolean {
|
updateDOM(dom: HTMLElement, view: EditorView): boolean {
|
||||||
|
if (!this.parseResult) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
this.element = dom
|
this.element = dom
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Tabular
|
<Tabular
|
||||||
|
|
|
@ -315,15 +315,22 @@ cell 3 & cell 4 \\\\
|
||||||
cy.get('.table-generator').findByText('cell 1').click()
|
cy.get('.table-generator').findByText('cell 1').click()
|
||||||
cy.get('.table-generator-floating-toolbar').as('toolbar').should('exist')
|
cy.get('.table-generator-floating-toolbar').as('toolbar').should('exist')
|
||||||
|
|
||||||
|
// Set border theme to "All borders" so that we can check that theme is
|
||||||
|
// preserved when adding new rows and columns
|
||||||
|
cy.get('@toolbar').findByText('No borders').click()
|
||||||
|
cy.get('.table-generator').findByText('All borders').click()
|
||||||
|
|
||||||
cy.get('.table-generator').findByText('cell 1').click()
|
cy.get('.table-generator').findByText('cell 1').click()
|
||||||
cy.get('@toolbar').findByLabelText('Insert').click()
|
cy.get('@toolbar').findByLabelText('Insert').click()
|
||||||
cy.get('.table-generator').findByText('Insert column left').click()
|
cy.get('.table-generator').findByText('Insert column left').click()
|
||||||
checkTable([['', 'cell 1']])
|
checkTable([['', 'cell 1']])
|
||||||
|
checkBordersWithNoMultiColumn([true, true], [true, true, true])
|
||||||
|
|
||||||
cy.get('.table-generator').findByText('cell 1').click()
|
cy.get('.table-generator').findByText('cell 1').click()
|
||||||
cy.get('@toolbar').findByLabelText('Insert').click()
|
cy.get('@toolbar').findByLabelText('Insert').click()
|
||||||
cy.get('.table-generator').findByText('Insert column right').click()
|
cy.get('.table-generator').findByText('Insert column right').click()
|
||||||
checkTable([['', 'cell 1', '']])
|
checkTable([['', 'cell 1', '']])
|
||||||
|
checkBordersWithNoMultiColumn([true, true], [true, true, true, true])
|
||||||
|
|
||||||
cy.get('.table-generator').findByText('cell 1').click()
|
cy.get('.table-generator').findByText('cell 1').click()
|
||||||
cy.get('@toolbar').findByLabelText('Insert').click()
|
cy.get('@toolbar').findByLabelText('Insert').click()
|
||||||
|
@ -332,6 +339,10 @@ cell 3 & cell 4 \\\\
|
||||||
['', '', ''],
|
['', '', ''],
|
||||||
['', 'cell 1', ''],
|
['', 'cell 1', ''],
|
||||||
])
|
])
|
||||||
|
checkBordersWithNoMultiColumn(
|
||||||
|
[true, true, true],
|
||||||
|
[true, true, true, true]
|
||||||
|
)
|
||||||
|
|
||||||
cy.get('.table-generator').findByText('cell 1').click()
|
cy.get('.table-generator').findByText('cell 1').click()
|
||||||
cy.get('@toolbar').findByLabelText('Insert').click()
|
cy.get('@toolbar').findByLabelText('Insert').click()
|
||||||
|
@ -341,6 +352,10 @@ cell 3 & cell 4 \\\\
|
||||||
['', 'cell 1', ''],
|
['', 'cell 1', ''],
|
||||||
['', '', ''],
|
['', '', ''],
|
||||||
])
|
])
|
||||||
|
checkBordersWithNoMultiColumn(
|
||||||
|
[true, true, true, true],
|
||||||
|
[true, true, true, true]
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Removes the table on toolbar button click', function () {
|
it('Removes the table on toolbar button click', function () {
|
||||||
|
|
|
@ -467,14 +467,11 @@ describe('<CodeMirrorEditor/> in Visual mode', function () {
|
||||||
'title with <span class="ol-cm-command-texttt"><b>command</b></span>'
|
'title with <span class="ol-cm-command-texttt"><b>command</b></span>'
|
||||||
)
|
)
|
||||||
|
|
||||||
// unsupported commands
|
|
||||||
cy.get('@second-line').type(deleteLine)
|
cy.get('@second-line').type(deleteLine)
|
||||||
cy.get('@second-line').type('\\title{{}Title with \\& ampersands}')
|
cy.get('@second-line').type('\\title{{}Title with \\& ampersands}')
|
||||||
cy.get('.ol-cm-title').should(
|
cy.get('.ol-cm-title').should('contain.html', 'Title with & ampersands')
|
||||||
'contain.html',
|
|
||||||
'Title with \\& ampersands'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
// unsupported command
|
||||||
cy.get('@second-line').type(deleteLine)
|
cy.get('@second-line').type(deleteLine)
|
||||||
cy.get('@second-line').type('\\title{{}My \\LaTeX{{}} document}')
|
cy.get('@second-line').type('\\title{{}My \\LaTeX{{}} document}')
|
||||||
cy.get('.ol-cm-title').should('contain.html', 'My \\LaTeX{} document')
|
cy.get('.ol-cm-title').should('contain.html', 'My \\LaTeX{} document')
|
||||||
|
|
Loading…
Reference in a new issue