Merge pull request #14559 from overleaf/mj-table-add-row-borders

[visual] Table generator improvements

GitOrigin-RevId: 8d3d1b382c68c13480b3aa50b6764903ff59ae81
This commit is contained in:
Mathias Jakobsen 2023-09-04 09:02:45 +01:00 committed by Copybot
parent 8e6d6f8689
commit 2516f271b1
11 changed files with 253 additions and 58 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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': {
'&:not(.editing)': {
padding: '0 0.25em', 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': {

View file

@ -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 commandNameNode = childNode.getChild('CtrlSeq')
if (commandNameNode) {
const commandName = getText(commandNameNode.from, commandNameNode.to)
const mapping: NodeMapping | undefined = MARKUP_COMMANDS[commandName]
if (mapping) {
const element = document.createElement(mapping.elementType)
if (mapping.className) {
element.classList.add(mapping.className)
}
pushAncestor(element)
const textArgument = childNode.getChild('TextArgument') const textArgument = childNode.getChild('TextArgument')
from = textArgument?.getChild('LongArg')?.from ?? childNode.to from = textArgument?.getChild('LongArg')?.from ?? childNode.to
} else if (isUnknownCommandWithName(childNode, '\\textbf', getText)) { return
pushAncestor(document.createElement('b')) }
const textArgument = childNode.getChild('TextArgument') }
from = textArgument?.getChild('LongArg')?.from ?? childNode.to }
} else if (isUnknownCommandWithName(childNode, '\\emph', getText)) { if (isUnknownCommandWithName(childNode, '\\and', getText)) {
pushAncestor(document.createElement('em'))
const textArgument = childNode.getChild('TextArgument')
from = textArgument?.getChild('LongArg')?.from ?? childNode.to
} else if (isUnknownCommandWithName(childNode, '\\texttt', getText)) {
const spanElement = document.createElement('span')
spanElement.classList.add('ol-cm-command-texttt')
pushAncestor(spanElement)
const textArgument = childNode.getChild('TextArgument')
from = textArgument?.getChild('LongArg')?.from ?? childNode.to
} else 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)

View file

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

View file

@ -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()
try {
this.parseResult = generateTable(tabularNode, state) 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,6 +56,7 @@ 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')
} }
if (this.parseResult) {
ReactDOM.render( ReactDOM.render(
<Tabular <Tabular
view={view} view={view}
@ -58,6 +66,7 @@ export class TabularWidget extends WidgetType {
/>, />,
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

View file

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

View file

@ -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 &amp; ampersands')
'contain.html',
'Title with \\&amp; 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')