Merge pull request #14428 from overleaf/mj-table-gen-cleanup

[visual] Table generator tweaks and improvements

GitOrigin-RevId: 621820c613924bc201ec85ea249edd6c9b6f88b2
This commit is contained in:
Mathias Jakobsen 2023-08-24 10:19:26 +01:00 committed by Copybot
parent b987e59d60
commit 8b5278c076
14 changed files with 593 additions and 420 deletions

View file

@ -36,6 +36,7 @@ export const Cell: FC<{
cellData: editingCellData, cellData: editingCellData,
updateCellData: update, updateCellData: update,
startEditing, startEditing,
commitCellData,
} = useEditingContext() } = useEditingContext()
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
@ -167,6 +168,7 @@ export const Cell: FC<{
className="table-generator-cell-input" className="table-generator-cell-input"
ref={inputRef} ref={inputRef}
value={editingCellData.content} value={editingCellData.content}
onBlur={commitCellData}
style={{ width: `inherit` }} style={{ width: `inherit` }}
onChange={e => { onChange={e => {
update(filterInput(e.target.value)) update(filterInput(e.target.value))

View file

@ -333,6 +333,17 @@ export class TableSelection {
const { minY, maxY } = this.normalized() const { minY, maxY } = this.normalized()
return maxY - minY + 1 return maxY - minY + 1
} }
maximumCellWidth(table: TableData) {
const { minX, maxX, minY, maxY } = this.normalized()
let maxWidth = 1
for (let row = minY; row <= maxY; ++row) {
const start = table.getCellIndex(row, minX)
const end = table.getCellIndex(row, maxX)
maxWidth = Math.max(maxWidth, end - start + 1)
}
return maxWidth
}
} }
const SelectionContext = createContext< const SelectionContext = createContext<

View file

@ -19,6 +19,7 @@ import { CodeMirrorViewContextProvider } from '../codemirror-editor'
import { TableProvider } from './contexts/table-context' import { TableProvider } from './contexts/table-context'
import { TabularProvider, useTabularContext } from './contexts/tabular-context' import { TabularProvider, useTabularContext } from './contexts/tabular-context'
import Icon from '../../../../shared/components/icon' import Icon from '../../../../shared/components/icon'
import { BorderTheme } from './toolbar/commands'
export type ColumnDefinition = { export type ColumnDefinition = {
alignment: 'left' | 'center' | 'right' | 'paragraph' alignment: 'left' | 'center' | 'right' | 'paragraph'
@ -91,6 +92,56 @@ export class TableData {
} }
throw new Error("Couldn't find cell boundaries") throw new Error("Couldn't find cell boundaries")
} }
getBorderTheme(): BorderTheme | null {
if (this.rows.length === 0 || this.columns.length === 0) {
return null
}
const lastRow = this.rows[this.rows.length - 1]
const hasBottomBorder = lastRow.borderBottom > 0
const firstColumn = this.columns[0]
const hasLeftBorder = firstColumn.borderLeft > 0
for (const row of this.rows) {
if (hasBottomBorder === (row.borderTop === 0)) {
return null
}
}
// If we had the first, we have verified that we have the rest
const hasAllRowBorders = hasBottomBorder
for (const column of this.columns) {
if (hasLeftBorder === (column.borderRight === 0)) {
return null
}
}
for (const row of this.rows) {
for (const cell of row.cells) {
if (cell.multiColumn) {
if (cell.multiColumn.columns.specification.length === 0) {
return null
}
const firstCell = cell.multiColumn.columns.specification[0]
if (hasLeftBorder === (firstCell.borderLeft === 0)) {
return null
}
for (const column of cell.multiColumn.columns.specification) {
if (hasLeftBorder === (column.borderRight === 0)) {
return null
}
}
}
}
}
// If we had the first, we have verified that we have the rest
const hasAllColumnBorders = hasLeftBorder
if (hasAllRowBorders && hasAllColumnBorders) {
return BorderTheme.FULLY_BORDERED
} else {
return BorderTheme.NO_BORDERS
}
}
} }
export type Positions = { export type Positions = {

View file

@ -342,13 +342,21 @@ export const insertRow = (
below: boolean, below: boolean,
table: TableData table: TableData
) => { ) => {
// TODO: Handle borders
const { maxY, minY } = selection.normalized() const { maxY, minY } = selection.normalized()
const rowsToInsert = selection.height()
const from = below const from = below
? positions.rowPositions[maxY].to ? positions.rowPositions[maxY].to
: positions.rowPositions[minY].from : positions.rowPositions[minY].from
const numberOfColumns = table.columns.length const numberOfColumns = table.columns.length
const insert = `\n${' &'.repeat(numberOfColumns - 1)}\\\\` const borderTheme = table.getBorderTheme()
const border = borderTheme === BorderTheme.FULLY_BORDERED ? '\\hline' : ''
const initialHline =
borderTheme === BorderTheme.FULLY_BORDERED && !below && minY === 0
? '\\hline'
: ''
const insert = `${initialHline}\n${' &'.repeat(
numberOfColumns - 1
)}\\\\${border}`.repeat(rowsToInsert)
view.dispatch({ changes: { from, to: from, insert } }) view.dispatch({ changes: { from, to: from, insert } })
if (!below) { if (!below) {
return selection return selection
@ -366,9 +374,9 @@ export const insertColumn = (
after: boolean, after: boolean,
table: TableData table: TableData
) => { ) => {
// TODO: Handle borders
const selection = initialSelection.explode(table) const selection = initialSelection.explode(table)
const { maxX, minX } = selection.normalized() const { maxX, minX } = selection.normalized()
const columnsToInsert = selection.maximumCellWidth(table)
const changes: ChangeSpec[] = [] const changes: ChangeSpec[] = []
const targetColumn = after ? maxX : minX const targetColumn = after ? maxX : minX
for (let row = 0; row < positions.rowPositions.length; row++) { for (let row = 0; row < positions.rowPositions.length; row++) {
@ -377,7 +385,7 @@ export const insertColumn = (
const from = after ? target.to : target.from const from = after ? target.to : target.from
changes.push({ changes.push({
from, from,
insert: ' &', insert: ' &'.repeat(columnsToInsert),
}) })
} }
@ -386,12 +394,25 @@ export const insertColumn = (
positions.columnDeclarations.to positions.columnDeclarations.to
) )
const columnSpecification = parseColumnSpecifications(specification) const columnSpecification = parseColumnSpecifications(specification)
columnSpecification.splice(after ? maxX + 1 : minX, 0, { const borderTheme = table.getBorderTheme()
alignment: 'left', const borderRight = borderTheme === BorderTheme.FULLY_BORDERED ? 1 : 0
borderLeft: 0, const targetIndex = after ? maxX + 1 : minX
borderRight: 0, columnSpecification.splice(
content: 'l', targetIndex,
}) 0,
...Array.from({ length: columnsToInsert }, () => ({
alignment: 'left' as const,
borderLeft: 0,
borderRight,
content: 'l',
}))
)
if (targetIndex === 0 && borderTheme === BorderTheme.FULLY_BORDERED) {
columnSpecification[0].borderLeft = Math.max(
1,
columnSpecification[0].borderLeft
)
}
changes.push({ changes.push({
from: positions.columnDeclarations.from, from: positions.columnDeclarations.from,
to: positions.columnDeclarations.to, to: positions.columnDeclarations.to,
@ -581,8 +602,9 @@ export const mergeCells = (
cellContent.push(table.getCell(minY, i).content) cellContent.push(table.getCell(minY, i).content)
} }
const content = cellContent.join(' ').trim() const content = cellContent.join(' ').trim()
// TODO: respect border theme const border =
const preamble = '\\multicolumn{' + (maxX - minX + 1) + '}{c}{' table.getBorderTheme() === BorderTheme.FULLY_BORDERED ? '|' : ''
const preamble = `\\multicolumn{${maxX - minX + 1}}{${border}c${border}}{`
const postamble = '}' const postamble = '}'
const { from } = table.getCell(minY, minX) const { from } = table.getCell(minY, minX)
const { to } = table.getCell(minY, maxX) const { to } = table.getCell(minY, maxX)

View file

@ -1,4 +1,4 @@
import { memo } from 'react' import { memo, useMemo } from 'react'
import { useSelectionContext } from '../contexts/selection-context' import { useSelectionContext } from '../contexts/selection-context'
import { ToolbarButton } from './toolbar-button' import { ToolbarButton } from './toolbar-button'
import { ToolbarButtonMenu } from './toolbar-button-menu' import { ToolbarButtonMenu } from './toolbar-button-menu'
@ -20,19 +20,49 @@ import {
import { useCodeMirrorViewContext } from '../../codemirror-editor' import { useCodeMirrorViewContext } from '../../codemirror-editor'
import { useTableContext } from '../contexts/table-context' import { useTableContext } from '../contexts/table-context'
const borderThemeLabel = (theme: BorderTheme | null) => {
switch (theme) {
case BorderTheme.FULLY_BORDERED:
return 'All borders'
case BorderTheme.NO_BORDERS:
return 'No borders'
default:
return 'Custom borders'
}
}
export const Toolbar = memo(function Toolbar() { export const Toolbar = memo(function Toolbar() {
const { selection, setSelection } = useSelectionContext() const { selection, setSelection } = useSelectionContext()
const view = useCodeMirrorViewContext() const view = useCodeMirrorViewContext()
const { positions, rowSeparators, cellSeparators, tableEnvironment, table } = const { positions, rowSeparators, cellSeparators, tableEnvironment, table } =
useTableContext() useTableContext()
const borderDropdownLabel = useMemo(
() => borderThemeLabel(table.getBorderTheme()),
[table]
)
const captionLabel = useMemo(() => {
if (!tableEnvironment?.caption) {
return 'No caption'
}
if (tableEnvironment.caption.from < positions.tabular.from) {
return 'Caption above'
}
return 'Caption below'
}, [tableEnvironment, positions.tabular.from])
if (!selection) { if (!selection) {
return null return null
} }
const columnsToInsert = selection.maximumCellWidth(table)
const rowsToInsert = selection.height()
return ( return (
<div className="table-generator-floating-toolbar"> <div className="table-generator-floating-toolbar">
<ToolbarDropdown <ToolbarDropdown
id="table-generator-caption-dropdown" id="table-generator-caption-dropdown"
label="Caption below" label={captionLabel}
disabled={!tableEnvironment} disabled={!tableEnvironment}
> >
<button <button
@ -68,7 +98,7 @@ export const Toolbar = memo(function Toolbar() {
</ToolbarDropdown> </ToolbarDropdown>
<ToolbarDropdown <ToolbarDropdown
id="table-generator-borders-dropdown" id="table-generator-borders-dropdown"
label="All borders" label={borderDropdownLabel}
> >
<button <button
className="ol-cm-toolbar-menu-item" className="ol-cm-toolbar-menu-item"
@ -209,7 +239,9 @@ export const Toolbar = memo(function Toolbar() {
}} }}
> >
<span className="table-generator-button-label"> <span className="table-generator-button-label">
Insert column left {columnsToInsert === 1
? 'Insert column left'
: `Insert ${columnsToInsert} columns left`}
</span> </span>
</button> </button>
<button <button
@ -223,7 +255,9 @@ export const Toolbar = memo(function Toolbar() {
}} }}
> >
<span className="table-generator-button-label"> <span className="table-generator-button-label">
Insert column right {columnsToInsert === 1
? 'Insert column right'
: `Insert ${columnsToInsert} columns right`}
</span> </span>
</button> </button>
<hr /> <hr />
@ -236,7 +270,9 @@ export const Toolbar = memo(function Toolbar() {
}} }}
> >
<span className="table-generator-button-label"> <span className="table-generator-button-label">
Insert row above {rowsToInsert === 1
? 'Insert row above'
: `Insert ${rowsToInsert} rows above`}
</span> </span>
</button> </button>
<button <button
@ -248,7 +284,9 @@ export const Toolbar = memo(function Toolbar() {
}} }}
> >
<span className="table-generator-button-label"> <span className="table-generator-button-label">
Insert row below {rowsToInsert === 1
? 'Insert row below'
: `Insert ${rowsToInsert} rows below`}
</span> </span>
</button> </button>
</ToolbarDropdown> </ToolbarDropdown>
@ -260,6 +298,7 @@ export const Toolbar = memo(function Toolbar() {
label="Delete table" label="Delete table"
command={() => { command={() => {
removeNodes(view, tableEnvironment?.table ?? positions.tabular) removeNodes(view, tableEnvironment?.table ?? positions.tabular)
view.focus()
}} }}
/> />
</div> </div>

View file

@ -213,7 +213,7 @@ function parseTabularBody(
if (!tabularArgument) { if (!tabularArgument) {
throw new Error('Invalid multicolumn definition: missing cell content') throw new Error('Invalid multicolumn definition: missing cell content')
} }
if (getLastCell()?.content) { if (getLastCell()?.content.trim()) {
throw new Error( throw new Error(
'Invalid multicolumn definition: multicolumn must be at the start of a cell' 'Invalid multicolumn definition: multicolumn must be at the start of a cell'
) )

View file

@ -109,7 +109,7 @@ const SizeGrid: FC<{
onMouseEnter={() => { onMouseEnter={() => {
setCurrentSize({ sizeX: x, sizeY: y }) setCurrentSize({ sizeX: x, sizeY: y })
}} }}
onMouseDown={() => onSizeSelected(x, y)} onMouseUp={() => onSizeSelected(x, y)}
/> />
))} ))}
</tr> </tr>

View file

@ -71,7 +71,7 @@ export const insertFigure: Command = view => {
export const insertTable = (view: EditorView, sizeX: number, sizeY: number) => { export const insertTable = (view: EditorView, sizeX: number, sizeY: number) => {
const { state, dispatch } = view const { state, dispatch } = view
const { pos, suffix } = ensureEmptyLine(state, state.selection.main) const { pos, suffix } = ensureEmptyLine(state, state.selection.main)
const template = `\n\\begin{table}{#{}} const template = `\n\\begin{table}[#{}]
\t\\centering \t\\centering
\\begin{tabular}{${'c'.repeat(sizeX)}} \\begin{tabular}{${'c'.repeat(sizeX)}}
${('\t\t' + '#{} & #{}'.repeat(sizeX - 1) + '\\\\\n').repeat( ${('\t\t' + '#{} & #{}'.repeat(sizeX - 1) + '\\\\\n').repeat(

View file

@ -255,6 +255,15 @@ export const toolbarPanel = () => [
}, },
}, },
}, },
'&.overall-theme-dark .ol-cm-toolbar-table-grid-popover': {
color: '#fff',
},
'&.overall-theme-dark .ol-cm-toolbar-table-grid': {
'& td.active': {
outlineColor: 'white',
background: 'rgb(125, 125, 125)',
},
},
'.ol-cm-toolbar-table-grid': { '.ol-cm-toolbar-table-grid': {
borderCollapse: 'separate', borderCollapse: 'separate',
tableLayout: 'fixed', tableLayout: 'fixed',
@ -281,6 +290,7 @@ export const toolbarPanel = () => [
'.ol-cm-toolbar-table-grid-popover': { '.ol-cm-toolbar-table-grid-popover': {
padding: '8px', padding: '8px',
marginLeft: '80px', marginLeft: '80px',
backgroundColor: 'var(--editor-toolbar-bg)',
}, },
}), }),
] ]

View file

@ -0,0 +1,424 @@
import { EditorView } from '@codemirror/view'
export const tableGeneratorTheme = EditorView.baseTheme({
'&dark .table-generator': {
'--table-generator-active-border-color': '#ccc',
'--table-generator-coming-soon-background-color': '#41464f',
'--table-generator-coming-soon-color': '#fff',
'--table-generator-divider-color': 'rgba(125,125,125,0.3)',
'--table-generator-dropdown-divider-color': 'rgba(125,125,125,0.3)',
'--table-generator-focus-border-color': '#5d7498',
'--table-generator-inactive-border-color': '#888',
'--table-generator-selected-background-color': '#ffffff2a',
'--table-generator-selector-background-color': '#777',
'--table-generator-selector-hover-color': '#3265b2',
'--table-generator-toolbar-background': 'var(--editor-toolbar-bg)',
'--table-generator-toolbar-button-active-background':
'rgba(125, 125, 125, 0.4)',
'--table-generator-toolbar-button-color': 'var(--toolbar-btn-color)',
'--table-generator-toolbar-button-hover-background':
'rgba(125, 125, 125, 0.2)',
'--table-generator-toolbar-dropdown-border-color': 'rgba(125,125,125,0.3)',
'--table-generator-toolbar-dropdown-disabled-background':
'rgba(125,125,125,0.3)',
'--table-generator-toolbar-dropdown-disabled-color': '#999',
'--table-generator-toolbar-shadow-color': '#1e253029',
},
'&light .table-generator': {
'--table-generator-active-border-color': '#666',
'--table-generator-coming-soon-background-color': 'var(--neutral-10)',
'--table-generator-coming-soon-color': 'var(--neutral-70)',
'--table-generator-divider-color': 'var(--neutral-20)',
'--table-generator-dropdown-divider-color': 'var(--neutral-20)',
'--table-generator-focus-border-color': '#97b6e5',
'--table-generator-inactive-border-color': '#dedede',
'--table-generator-selected-background-color': 'var(--blue-10)',
'--table-generator-selector-background-color': 'var(--neutral-30)',
'--table-generator-selector-hover-color': '#3265b2',
'--table-generator-toolbar-background': '#fff',
'--table-generator-toolbar-button-active-background':
'rgba(47, 58, 76, 0.16)',
'--table-generator-toolbar-button-color': 'var(--neutral-70)',
'--table-generator-toolbar-button-hover-background':
'rgba(47, 58, 76, 0.08)',
'--table-generator-toolbar-dropdown-border-color': 'var(--neutral-60)',
'--table-generator-toolbar-dropdown-disabled-background': '#f2f2f2',
'--table-generator-toolbar-dropdown-disabled-color': 'var(--neutral-40)',
'--table-generator-toolbar-shadow-color': '#1e253029',
},
'.table-generator': {
position: 'relative',
'--table-generator-inactive-border-width': '1px',
'--table-generator-active-border-width': '1px',
'--table-generator-selector-handle-buffer': '12px',
'--table-generator-focus-border-width': '2px',
'--table-generator-focus-negative-border-width': '-2px',
},
'.table-generator-cell.selected': {
'background-color': 'var(--table-generator-selected-background-color)',
},
'.table-generator-cell:focus-visible': {
outline: '2px dotted var(--table-generator-focus-border-color)',
},
'.table-generator-cell': {
border:
'var(--table-generator-inactive-border-width) dashed var(--table-generator-inactive-border-color)',
'min-width': '40px',
height: '30px',
'&.selection-edge-top': {
'--shadow-top':
'0 var(--table-generator-focus-negative-border-width) 0 var(--table-generator-focus-border-color)',
},
'&.selection-edge-bottom': {
'--shadow-bottom':
'0 var(--table-generator-focus-border-width) 0 var(--table-generator-focus-border-color)',
},
'&.selection-edge-left': {
'--shadow-left':
'var(--table-generator-focus-negative-border-width) 0 0 var(--table-generator-focus-border-color)',
},
'&.selection-edge-right': {
'--shadow-right':
'var(--table-generator-focus-border-width) 0 0 var(--table-generator-focus-border-color)',
},
'box-shadow':
'var(--shadow-top, 0 0 0 transparent), var(--shadow-bottom, 0 0 0 transparent), var(--shadow-left, 0 0 0 transparent), var(--shadow-right, 0 0 0 transparent)',
'&.table-generator-cell-border-left': {
'border-left-style': 'solid',
'border-left-color': 'var(--table-generator-active-border-color)',
'border-left-width': 'var(--table-generator-active-border-width)',
},
'&.table-generator-cell-border-right': {
'border-right-style': 'solid',
'border-right-color': 'var(--table-generator-active-border-color)',
'border-right-width': 'var(--table-generator-active-border-width)',
},
'&.table-generator-row-border-top': {
'border-top-style': 'solid',
'border-top-color': 'var(--table-generator-active-border-color)',
'border-top-width': 'var(--table-generator-active-border-width)',
},
'&.table-generator-row-border-bottom': {
'border-bottom-style': 'solid',
'border-bottom-color': 'var(--table-generator-active-border-color)',
'border-bottom-width': 'var(--table-generator-active-border-width)',
},
},
'.table-generator-table': {
'table-layout': 'fixed',
'max-width': '80%',
margin: '0 auto',
cursor: 'default',
'& td': {
padding: '0 0.25em',
'max-width': '200px',
'&.alignment-left': {
'text-align': 'left',
},
'&.alignment-right': {
'text-align': 'right',
},
'&.alignment-center': {
'text-align': 'center',
},
'&.alignment-paragraph': {
'text-align': 'justify',
},
},
'& .table-generator-selector-cell': {
padding: '0',
border: 'none !important',
position: 'relative',
cursor: 'pointer',
'&.row-selector': {
width: 'calc(var(--table-generator-selector-handle-buffer) + 8px)',
'&::after': {
width: '4px',
height: 'calc(100% - 8px)',
},
},
'&.column-selector': {
height: 'calc(var(--table-generator-selector-handle-buffer) + 8px)',
'&::after': {
width: 'calc(100% - 8px)',
height: '4px',
},
},
'&::after': {
content: '""',
display: 'block',
position: 'absolute',
bottom: '4px',
right: '4px',
width: 'calc(100% - 8px)',
height: 'calc(100% - 8px)',
'background-color': 'var(--table-generator-selector-background-color)',
'border-radius': '4px',
},
'&:hover::after': {
'background-color': 'var(--table-generator-selector-hover-color)',
},
'&.fully-selected::after': {
'background-color': 'var(--table-generator-selector-hover-color)',
},
},
},
'.table-generator-floating-toolbar': {
position: 'absolute',
top: '-36px',
left: '0',
right: '0',
margin: '0 auto',
'z-index': '1',
'border-radius': '4px',
width: 'max-content',
'background-color': 'var(--table-generator-toolbar-background)',
'box-shadow': '0px 2px 4px 0px var(--table-generator-toolbar-shadow-color)',
padding: '4px',
height: '40px',
display: 'flex',
},
'.table-generator-toolbar-button': {
display: 'inline-flex',
'align-items': 'center',
'justify-content': 'center',
margin: '0',
'background-color': 'transparent',
border: 'none',
'border-radius': '4px',
'line-height': '1',
overflow: 'hidden',
color: 'var(--table-generator-toolbar-button-color)',
'text-align': 'center',
padding: '4px',
'&:not(first-child)': {
'margin-left': '4px',
},
'&:not(:last-child)': {
'margin-right': '4px',
},
'& > span': {
'font-size': '24px',
},
'&:hover, &:focus': {
'background-color':
'var(--table-generator-toolbar-button-hover-background)',
},
'&:active, &.active': {
'background-color':
'var(--table-generator-toolbar-button-active-background)',
},
'&:hover, &:focus, &:active, &.active': {
'box-shadow': 'none',
},
'&[aria-disabled="true"]': {
'&:hover, &:focus, &:active, &.active': {
'background-color': 'transparent',
},
opacity: '0.2',
},
},
'.table-generator-button-group': {
display: 'inline-flex',
'align-items': 'center',
'justify-content': 'center',
'line-height': '1',
overflow: 'hidden',
'&:not(:first-child)': {
'border-left': '1px solid var(--table-generator-divider-color)',
'padding-left': '8px',
'margin-left': '8px',
},
},
'.table-generator-button-menu-popover': {
'background-color': 'var(--table-generator-toolbar-background) !important',
'& .popover-content': {
padding: '4px',
},
'& .list-group': {
margin: '0',
padding: '0',
},
'& > .arrow': {
display: 'none',
},
},
'.table-generator-cell-input': {
'max-width': 'calc(200px - 0.5em)',
width: '100%',
'background-color': 'transparent',
border: '1px solid var(--table-generator-toolbar-shadow-color)',
padding: '0',
},
'.table-generator-border-options-coming-soon': {
display: 'flex',
margin: '4px',
'font-size': '12px',
background: 'var(--table-generator-coming-soon-background-color)',
color: 'var(--table-generator-coming-soon-color)',
padding: '8px',
gap: '6px',
'align-items': 'flex-start',
'max-width': '240px',
'font-family': 'Lato',
'& .info-icon': {
flex: ' 0 0 24px',
},
},
'.table-generator-toolbar-dropdown-toggle': {
border: '1px solid var(--table-generator-toolbar-dropdown-border-color)',
'box-shadow': 'none',
background: 'transparent',
'white-space': 'nowrap',
color: 'var(--table-generator-toolbar-button-color)',
'border-radius': '4px',
padding: '6px 8px',
gap: '8px',
'min-width': '120px',
'font-size': '14px',
display: 'flex',
'align-items': 'center',
'justify-content': 'space-between',
'font-family': 'Lato',
'&:not(:first-child)': {
'margin-left': '8px',
},
'&[aria-disabled="true"]': {
'background-color':
'var(--table-generator-toolbar-dropdown-disabled-background)',
color: 'var(--table-generator-toolbar-dropdown-disabled-color)',
},
},
'.table-generator-toolbar-dropdown-popover': {
'max-width': '300px',
background: 'var(--table-generator-toolbar-background) !important',
'& .popover-content': {
padding: '0',
},
'& > .arrow': {
display: 'none',
},
},
'.table-generator-toolbar-dropdown-menu': {
display: 'flex',
'flex-direction': 'column',
'min-width': '200px',
'& > button': {
border: 'none',
'box-shadow': 'none',
background: 'transparent',
'white-space': 'nowrap',
color: 'var(--table-generator-toolbar-button-color)',
'border-radius': '0',
'font-size': '14px',
display: 'flex',
'align-items': 'center',
'justify-content': 'space-between',
'column-gap': '8px',
'align-self': 'stretch',
padding: '12px 8px',
'font-family': 'Lato',
'& .table-generator-button-label': {
'align-self': 'stretch',
flex: '1 0 auto',
'text-align': 'left',
},
'&:hover, &:focus': {
'background-color':
'var(--table-generator-toolbar-button-hover-background)',
},
'&:active, &.active': {
'background-color':
'var(--table-generator-toolbar-button-active-background)',
},
'&:hover, &:focus, &:active, &.active': {
'box-shadow': 'none',
},
'&[aria-disabled="true"]': {
'&:hover, &:focus, &:active, &.active': {
'background-color': 'transparent',
},
color: 'var(--table-generator-toolbar-dropdown-disabled-color)',
},
},
'& > hr': {
background: 'var(--table-generator-dropdown-divider-color)',
margin: '2px 8px',
display: 'block',
'box-sizing': 'content-box',
border: '0',
height: '1px',
},
},
'.table-generator-error': {
background:
'linear-gradient(0deg, #f9f1f1, #f9f1f1), linear-gradient(0deg, #f5beba, #f5beba)',
display: 'flex',
'justify-content': 'space-between',
color: 'black',
border: '1px solid #f5beba',
'font-family': 'Lato',
'margin-bottom': '0',
'& .table-generator-error-message': {
flex: '1 0 auto',
},
'& .table-generator-error-icon': {
color: '#b83a33',
'margin-right': '12px',
},
},
'.table-generator-filler-row': {
border: 'none !important',
'& td': {
'min-width': '40px',
},
},
})

View file

@ -26,6 +26,7 @@ import { toolbarPanel } from '../toolbar/toolbar-panel'
import { selectDecoratedArgument } from './select-decorated-argument' import { selectDecoratedArgument } from './select-decorated-argument'
import { pasteHtml } from './paste-html' import { pasteHtml } from './paste-html'
import { commandTooltip } from '../command-tooltip' import { commandTooltip } from '../command-tooltip'
import { tableGeneratorTheme } from './table-generator'
type Options = { type Options = {
visual: boolean visual: boolean
@ -208,4 +209,5 @@ const extension = (options: Options) => [
showContentWhenParsed, showContentWhenParsed,
figureModalPasteHandler(), figureModalPasteHandler(),
isSplitTestEnabled('paste-html') ? pasteHtml : [], isSplitTestEnabled('paste-html') ? pasteHtml : [],
isSplitTestEnabled('table-generator') ? tableGeneratorTheme : [],
] ]

View file

@ -19,7 +19,6 @@
@import './editor/dictionary.less'; @import './editor/dictionary.less';
@import './editor/compile-button.less'; @import './editor/compile-button.less';
@import './editor/figure-modal.less'; @import './editor/figure-modal.less';
@import './editor/table-generator.less';
@ui-layout-toggler-def-height: 50px; @ui-layout-toggler-def-height: 50px;
@ui-resizer-size: 7px; @ui-resizer-size: 7px;

View file

@ -1,397 +0,0 @@
@table-generator-active-border-color: #666;
@table-generator-inactive-border-color: #dedede;
@table-generator-focus-border-width: 2px;
@table-generator-focus-negative-border-width: -2px;
@table-generator-focus-border-color: #97b6e5;
@table-generator-selector-hover-color: #3265b2;
@table-generator-selector-handle-buffer: 12px;
@table-generator-toolbar-shadow-color: #1e253029;
@table-generator-toolbar-background: #fff;
@table-generator-inactive-border-width: 1px;
@table-generator-active-border-width: 1px;
.table-generator-cell {
border: @table-generator-inactive-border-width dashed
@table-generator-inactive-border-color;
min-width: 40px;
height: 30px;
}
.table-generator-cell-border-left {
border-left-style: solid;
border-left-color: @table-generator-active-border-color;
border-left-width: @table-generator-active-border-width;
}
.table-generator-cell-border-right {
border-right-style: solid;
border-right-color: @table-generator-active-border-color;
border-right-width: @table-generator-active-border-width;
}
.table-generator-row-border-top {
border-top-style: solid;
border-top-color: @table-generator-active-border-color;
border-top-width: @table-generator-active-border-width;
}
.table-generator-row-border-bottom {
border-bottom-style: solid;
border-bottom-color: @table-generator-active-border-color;
border-bottom-width: @table-generator-active-border-width;
}
.table-generator-cell.selected {
background-color: @blue-10;
}
.table-generator-cell:focus-visible {
outline: 2px dotted @table-generator-focus-border-color;
}
.table-generator-cell {
&.selection-edge-top {
--shadow-top: 0 @table-generator-focus-negative-border-width 0
@table-generator-focus-border-color;
}
&.selection-edge-bottom {
--shadow-bottom: 0 @table-generator-focus-border-width 0
@table-generator-focus-border-color;
}
&.selection-edge-left {
--shadow-left: @table-generator-focus-negative-border-width 0 0
@table-generator-focus-border-color;
}
&.selection-edge-right {
--shadow-right: @table-generator-focus-border-width 0 0
@table-generator-focus-border-color;
}
box-shadow: var(--shadow-top, 0 0 0 transparent),
var(--shadow-bottom, 0 0 0 transparent),
var(--shadow-left, 0 0 0 transparent),
var(--shadow-right, 0 0 0 transparent);
}
.table-generator-table {
table-layout: fixed;
max-width: 80%;
margin: 0 auto;
cursor: default;
& td {
padding: 0 0.25em;
max-width: 200px;
&.alignment-left {
text-align: left;
}
&.alignment-right {
text-align: right;
}
&.alignment-center {
text-align: center;
}
&.alignment-paragraph {
text-align: justify;
}
}
.table-generator-selector-cell {
padding: 0;
border: none !important;
position: relative;
cursor: pointer;
&.row-selector {
width: @table-generator-selector-handle-buffer + 8px;
&::after {
width: 4px;
height: calc(100% - 8px);
}
}
&.column-selector {
height: @table-generator-selector-handle-buffer + 8px;
&::after {
width: calc(100% - 8px);
height: 4px;
}
}
&::after {
content: '';
display: block;
position: absolute;
bottom: 4px;
right: 4px;
width: calc(100% - 8px);
height: calc(100% - 8px);
background-color: @neutral-30;
border-radius: 4px;
}
&:hover::after {
background-color: @neutral-40;
}
&.fully-selected::after {
background-color: @table-generator-selector-hover-color;
}
}
}
.table-generator {
position: relative;
}
.table-generator-floating-toolbar {
position: absolute;
top: -36px;
left: 0;
right: 0;
margin: 0 auto;
z-index: 1;
border-radius: 4px;
width: max-content;
background-color: @table-generator-toolbar-background;
box-shadow: 0px 2px 4px 0px @table-generator-toolbar-shadow-color;
padding: 4px;
height: 40px;
display: flex;
}
.table-generator-toolbar-button {
display: inline-flex;
align-items: center;
justify-content: center;
margin: 0;
background-color: transparent;
border: none;
border-radius: 4px;
line-height: 1;
overflow: hidden;
color: @neutral-70;
text-align: center;
padding: 4px;
&:not(:first-child) {
margin-left: 4px;
}
&:not(:last-child) {
margin-right: 4px;
}
& > span {
font-size: 24px;
}
&:hover,
&:focus {
background-color: rgba(47, 58, 76, 0.08);
}
&:active,
&.active {
background-color: rgba(47, 58, 76, 0.16);
}
&:hover,
&:focus,
&:active,
&.active {
color: inherit;
box-shadow: none;
}
&[aria-disabled='true'] {
&:hover,
&:focus,
&:active,
&.active {
background-color: transparent;
}
color: @neutral-40;
}
}
.table-generator-button-group {
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
overflow: hidden;
&:not(:first-child) {
border-left: 1px solid @neutral-20;
padding-left: 8px;
margin-left: 8px;
}
}
.table-generator-button-menu-popover {
.popover-content {
padding: 4px;
}
.list-group {
margin: 0;
padding: 0;
}
& > .arrow {
display: none;
}
}
.table-generator-cell-input {
max-width: calc(200px - 0.5em);
width: 100%;
background-color: transparent;
border: 1px solid @table-generator-toolbar-shadow-color;
border: 0;
padding: 0;
}
.table-generator-border-options-coming-soon {
display: flex;
margin: 4px;
font-size: 12px;
background: @neutral-10;
color: @neutral-70;
padding: 8px;
gap: 6px;
align-items: flex-start;
max-width: 240px;
& .info-icon {
flex: 0 0 24px;
}
}
.table-generator-toolbar-dropdown-toggle {
border: 1px solid @neutral-60;
box-shadow: none;
background: transparent;
white-space: nowrap;
color: inherit;
border-radius: 4px;
padding: 6px 8px;
gap: 8px;
min-width: 120px;
font-size: 14px;
display: flex;
align-items: center;
justify-content: space-between;
font-family: @font-family-sans-serif;
&:not(:first-child) {
margin-left: 8px;
}
&[aria-disabled='true'] {
background-color: #f2f2f2;
color: @neutral-40;
}
}
.table-generator-toolbar-dropdown-popover {
max-width: 300px;
.popover-content {
padding: 0;
}
& > .arrow {
display: none;
}
}
.table-generator-toolbar-dropdown-menu {
display: flex;
flex-direction: column;
min-width: 200px;
& > button {
border: none;
box-shadow: none;
background: transparent;
white-space: nowrap;
color: inherit;
border-radius: 0;
font-size: 14px;
display: flex;
align-items: center;
justify-content: space-between;
column-gap: 8px;
align-self: stretch;
padding: 12px 8px;
font-family: @font-family-sans-serif;
.table-generator-button-label {
align-self: stretch;
flex: 1 0 auto;
text-align: left;
}
&:hover,
&:focus {
background-color: rgba(47, 58, 76, 0.08);
}
&:active,
&.active {
background-color: rgba(47, 58, 76, 0.16);
}
&:hover,
&:focus,
&:active,
&.active {
color: inherit;
box-shadow: none;
}
&[aria-disabled='true'] {
&:hover,
&:focus,
&:active,
&.active {
background-color: transparent;
}
color: @neutral-40;
}
}
& > hr {
background: @neutral-20;
margin: 2px 8px;
display: block;
box-sizing: content-box;
border: 0;
height: 1px;
}
}
.table-generator-error {
background: linear-gradient(0deg, #f9f1f1, #f9f1f1),
linear-gradient(0deg, #f5beba, #f5beba);
display: flex;
justify-content: space-between;
color: black;
border: 1px solid #f5beba;
font-family: @font-family-sans-serif;
margin-bottom: 0;
.table-generator-error-message {
flex: 1 0 auto;
}
.table-generator-error-icon {
color: #b83a33;
margin-right: 12px;
}
}
.table-generator-filler-row {
border: none !important;
td {
min-width: 40px;
}
}

View file

@ -23,4 +23,14 @@
--toolbar-btn-color: @toolbar-btn-color; --toolbar-btn-color: @toolbar-btn-color;
--editor-toolbar-bg: @editor-toolbar-bg; --editor-toolbar-bg: @editor-toolbar-bg;
--neutral-10: @neutral-10;
--neutral-20: @neutral-20;
--neutral-30: @neutral-30;
--neutral-40: @neutral-40;
--neutral-60: @neutral-60;
--neutral-70: @neutral-70;
--neutral-80: @neutral-80;
--neutral-90: @neutral-90;
--blue-10: @blue-10;
} }