Merge pull request #16048 from overleaf/mj-table-column-width

[visual] Allow setting column widths in table generator

GitOrigin-RevId: 3bd4bb05dd3b29d0bd62fbc41da45eda282fecf4
This commit is contained in:
Mathias Jakobsen 2024-01-18 11:39:24 +00:00 committed by Copybot
parent 1708ccfcf5
commit 80a6424343
18 changed files with 906 additions and 44 deletions

View file

@ -54,6 +54,7 @@
"additional_licenses": "", "additional_licenses": "",
"address_line_1": "", "address_line_1": "",
"address_second_line_optional": "", "address_second_line_optional": "",
"adjust_column_width": "",
"aggregate_changed": "", "aggregate_changed": "",
"aggregate_to": "", "aggregate_to": "",
"agree_with_the_terms": "", "agree_with_the_terms": "",
@ -164,6 +165,9 @@
"collabs_per_proj": "", "collabs_per_proj": "",
"collabs_per_proj_single": "", "collabs_per_proj_single": "",
"collapse": "", "collapse": "",
"column_width": "",
"column_width_is_custom_click_to_resize": "",
"column_width_is_x_click_to_resize": "",
"comment": "", "comment": "",
"comment_submit_error": "", "comment_submit_error": "",
"commit": "", "commit": "",
@ -225,6 +229,7 @@
"currently_seeing_only_24_hrs_history": "", "currently_seeing_only_24_hrs_history": "",
"currently_signed_in_as_x": "", "currently_signed_in_as_x": "",
"currently_subscribed_to_plan": "", "currently_subscribed_to_plan": "",
"custom": "",
"custom_borders": "", "custom_borders": "",
"customize_your_group_subscription": "", "customize_your_group_subscription": "",
"customizing_figures": "", "customizing_figures": "",
@ -344,6 +349,7 @@
"enabling": "", "enabling": "",
"end_of_document": "", "end_of_document": "",
"enter_6_digit_code": "", "enter_6_digit_code": "",
"enter_any_size_including_units_or_valid_latex_command": "",
"enter_image_url": "", "enter_image_url": "",
"entry_point": "", "entry_point": "",
"error": "", "error": "",
@ -393,6 +399,8 @@
"fit_to_height": "", "fit_to_height": "",
"fit_to_width": "", "fit_to_width": "",
"fix_issues": "", "fix_issues": "",
"fixed_width": "",
"fixed_width_wrap_text": "",
"fold_line": "", "fold_line": "",
"folder_location": "", "folder_location": "",
"following_paths_conflict": "", "following_paths_conflict": "",
@ -600,6 +608,7 @@
"join_project": "", "join_project": "",
"join_team_explanation": "", "join_team_explanation": "",
"joining": "", "joining": "",
"justify": "",
"keep_current_plan": "", "keep_current_plan": "",
"keep_personal_projects_separate": "", "keep_personal_projects_separate": "",
"keybindings": "", "keybindings": "",
@ -639,6 +648,7 @@
"license_for_educational_purposes": "", "license_for_educational_purposes": "",
"limited_offer": "", "limited_offer": "",
"line_height": "", "line_height": "",
"line_width_is_the_width_of_the_line_in_the_current_environment": "",
"link": "", "link": "",
"link_account": "", "link_account": "",
"link_accounts": "", "link_accounts": "",
@ -854,6 +864,7 @@
"pending_additional_licenses": "", "pending_additional_licenses": "",
"pending_invite": "", "pending_invite": "",
"percent_discount_for_groups": "", "percent_discount_for_groups": "",
"percent_is_the_percentage_of_the_line_width": "",
"plan": "", "plan": "",
"plan_tooltip": "", "plan_tooltip": "",
"please_ask_the_project_owner_to_upgrade_to_track_changes": "", "please_ask_the_project_owner_to_upgrade_to_track_changes": "",
@ -1065,6 +1076,7 @@
"security": "", "security": "",
"see_changes_in_your_documents_live": "", "see_changes_in_your_documents_live": "",
"select_a_column_or_a_merged_cell_to_align": "", "select_a_column_or_a_merged_cell_to_align": "",
"select_a_column_to_adjust_column_width": "",
"select_a_file": "", "select_a_file": "",
"select_a_file_figure_modal": "", "select_a_file_figure_modal": "",
"select_a_group_optional": "", "select_a_group_optional": "",
@ -1104,6 +1116,7 @@
"session_expired_redirecting_to_login": "", "session_expired_redirecting_to_login": "",
"sessions": "", "sessions": "",
"set_color": "", "set_color": "",
"set_column_width": "",
"set_up_sso": "", "set_up_sso": "",
"settings": "", "settings": "",
"setup_another_account_under_a_personal_email_address": "", "setup_another_account_under_a_personal_email_address": "",
@ -1195,6 +1208,7 @@
"stop_on_first_error_enabled_title": "", "stop_on_first_error_enabled_title": "",
"stop_on_validation_error": "", "stop_on_validation_error": "",
"store_your_work": "", "store_your_work": "",
"stretch_width_to_text": "",
"student_disclaimer": "", "student_disclaimer": "",
"subject": "", "subject": "",
"subject_area": "", "subject_area": "",
@ -1278,6 +1292,7 @@
"to_confirm_transfer_enter_email_address": "", "to_confirm_transfer_enter_email_address": "",
"to_insert_or_move_a_caption_make_sure_tabular_is_directly_within_table": "", "to_insert_or_move_a_caption_make_sure_tabular_is_directly_within_table": "",
"to_modify_your_subscription_go_to": "", "to_modify_your_subscription_go_to": "",
"to_use_text_wrapping_in_your_table_make_sure_you_include_the_array_package": "",
"toggle_compile_options_menu": "", "toggle_compile_options_menu": "",
"token": "", "token": "",
"token_limit_reached": "", "token_limit_reached": "",

View file

@ -121,7 +121,7 @@ const Toolbar = memo(function Toolbar() {
// calculate overflow when active element changes to/from inside a table // calculate overflow when active element changes to/from inside a table
const insideTable = document.activeElement?.closest( const insideTable = document.activeElement?.closest(
'.table-generator-help-modal,.table-generator' '.table-generator-help-modal,.table-generator,.table-generator-width-modal'
) )
useEffect(() => { useEffect(() => {
if (resizeRef.current) { if (resizeRef.current) {

View file

@ -0,0 +1,61 @@
import MaterialIcon from '@/shared/components/material-icon'
import { WidthSelection } from './toolbar/column-width-modal/column-width'
import { useMemo } from 'react'
import { Button } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import Tooltip from '@/shared/components/tooltip'
import { useSelectionContext } from './contexts/selection-context'
function roundIfNeeded(width: number) {
return width.toFixed(2).replace(/\.0+$/, '')
}
export const ColumnSizeIndicator = ({
size,
onClick,
}: {
size: WidthSelection
onClick: () => void
}) => {
const { t } = useTranslation()
const { selection } = useSelectionContext()
const { unit, width } = size
const formattedWidth = useMemo(() => {
if (unit === 'custom') {
return width
}
return `${roundIfNeeded(width)}${unit}`
}, [unit, width])
if (!selection) {
return null
}
return (
<Tooltip
id="tooltip-column-width-button"
description={
unit === 'custom'
? t('column_width_is_custom_click_to_resize')
: t('column_width_is_x_click_to_resize', {
width: formattedWidth,
})
}
overlayProps={{ delay: 0, placement: 'bottom' }}
>
<Button
bsStyle={null}
className="table-generator-column-indicator-button"
onClick={onClick}
>
<MaterialIcon
type="format_text_wrap"
className="table-generator-column-indicator-icon"
/>
<span className="table-generator-column-indicator-label">
{formattedWidth}
</span>
</Button>
</Tooltip>
)
}

View file

@ -331,6 +331,42 @@ export class TableSelection {
return true return true
} }
isOnlyFixedWidthColumns(table: TableData) {
const { minX, maxX } = this.normalized()
for (let cell = minX; cell <= maxX; ++cell) {
if (!this.isColumnSelected(cell, table)) {
return false
}
if (!table.columns[cell].isParagraphColumn) {
return false
}
}
return true
}
isOnlyParagraphCells(table: TableData) {
const { minX, maxX } = this.normalized()
for (let cell = minX; cell <= maxX; ++cell) {
if (!table.columns[cell].isParagraphColumn) {
return false
}
}
return true
}
isOnlyNonFixedWidthColumns(table: TableData) {
const { minX, maxX } = this.normalized()
for (let cell = minX; cell <= maxX; ++cell) {
if (!this.isColumnSelected(cell, table)) {
return false
}
if (table.columns[cell].isParagraphColumn) {
return false
}
}
return true
}
width() { width() {
const { minX, maxX } = this.normalized() const { minX, maxX } = this.normalized()
return maxX - minX + 1 return maxX - minX + 1

View file

@ -14,6 +14,9 @@ const TabularContext = createContext<
showHelp: () => void showHelp: () => void
hideHelp: () => void hideHelp: () => void
helpShown: boolean helpShown: boolean
columnWidthModalShown: boolean
openColumnWidthModal: () => void
closeColumnWidthModal: () => void
} }
| undefined | undefined
>(undefined) >(undefined)
@ -21,10 +24,29 @@ const TabularContext = createContext<
export const TabularProvider: FC = ({ children }) => { export const TabularProvider: FC = ({ children }) => {
const ref = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null)
const [helpShown, setHelpShown] = useState(false) const [helpShown, setHelpShown] = useState(false)
const [columnWidthModalShown, setColumnWidthModalShown] = useState(false)
const showHelp = useCallback(() => setHelpShown(true), []) const showHelp = useCallback(() => setHelpShown(true), [])
const hideHelp = useCallback(() => setHelpShown(false), []) const hideHelp = useCallback(() => setHelpShown(false), [])
const openColumnWidthModal = useCallback(
() => setColumnWidthModalShown(true),
[]
)
const closeColumnWidthModal = useCallback(
() => setColumnWidthModalShown(false),
[]
)
return ( return (
<TabularContext.Provider value={{ ref, helpShown, showHelp, hideHelp }}> <TabularContext.Provider
value={{
ref,
helpShown,
showHelp,
hideHelp,
columnWidthModalShown,
openColumnWidthModal,
closeColumnWidthModal,
}}
>
{children} {children}
</TabularContext.Provider> </TabularContext.Provider>
) )

View file

@ -19,6 +19,8 @@ import { useCodeMirrorViewContext } from '../codemirror-editor'
import { undo, redo } from '@codemirror/commands' import { undo, redo } from '@codemirror/commands'
import { ChangeSpec } from '@codemirror/state' import { ChangeSpec } from '@codemirror/state'
import { startCompileKeypress } from '@/features/pdf-preview/hooks/use-compile-triggers' import { startCompileKeypress } from '@/features/pdf-preview/hooks/use-compile-triggers'
import { useTabularContext } from './contexts/tabular-context'
import { ColumnSizeIndicator } from './column-size-indicator'
type NavigationKey = type NavigationKey =
| 'ArrowRight' | 'ArrowRight'
@ -52,6 +54,7 @@ export const Table: FC = () => {
updateCellData, updateCellData,
} = useEditingContext() } = useEditingContext()
const { table: tableData } = useTableContext() const { table: tableData } = useTableContext()
const { openColumnWidthModal, columnWidthModalShown } = useTabularContext()
const tableRef = useRef<HTMLTableElement>(null) const tableRef = useRef<HTMLTableElement>(null)
const view = useCodeMirrorViewContext() const view = useCodeMirrorViewContext()
const { columns: cellWidths, tableWidth } = useMemo(() => { const { columns: cellWidths, tableWidth } = useMemo(() => {
@ -276,8 +279,9 @@ export const Table: FC = () => {
if (view.state.readOnly) { if (view.state.readOnly) {
return false return false
} }
if (cellData || !selection) { if (cellData || !selection || columnWidthModalShown) {
// We're editing a cell, so allow browser to insert there // We're editing a cell, or modifying column widths,
// so allow browser to insert there
return false return false
} }
event.preventDefault() event.preventDefault()
@ -312,8 +316,9 @@ export const Table: FC = () => {
} }
const onCopy = (event: ClipboardEvent) => { const onCopy = (event: ClipboardEvent) => {
if (cellData || !selection) { if (cellData || !selection || columnWidthModalShown) {
// We're editing a cell, so allow browser to insert there // We're editing a cell, or modifying column widths,
// so allow browser to copy from there
return false return false
} }
event.preventDefault() event.preventDefault()
@ -334,7 +339,22 @@ export const Table: FC = () => {
window.removeEventListener('paste', onPaste) window.removeEventListener('paste', onPaste)
window.removeEventListener('copy', onCopy) window.removeEventListener('copy', onCopy)
} }
}, [cellData, selection, tableData, view]) }, [cellData, selection, tableData, view, columnWidthModalShown])
const hasCustomSizes = useMemo(
() => tableData.columns.some(x => x.size),
[tableData.columns]
)
const onSizeClick = (index: number) => {
setSelection(
new TableSelection(
{ row: 0, cell: index },
{ row: tableData.rows.length - 1, cell: index }
)
)
openColumnWidthModal()
}
return ( return (
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
@ -352,6 +372,21 @@ export const Table: FC = () => {
))} ))}
</colgroup> </colgroup>
<thead> <thead>
{hasCustomSizes && (
<tr className="table-generator-column-widths-row">
<td />
{tableData.columns.map((column, columnIndex) => (
<td align="center" key={columnIndex}>
{column.size && (
<ColumnSizeIndicator
size={column.size}
onClick={() => onSizeClick(columnIndex)}
/>
)}
</td>
))}
</tr>
)}
<tr> <tr>
<td /> <td />
{tableData.columns.map((_, columnIndex) => ( {tableData.columns.map((_, columnIndex) => (

View file

@ -27,6 +27,8 @@ import { BorderTheme } from './toolbar/commands'
import { TableGeneratorHelpModal } from './help-modal' import { TableGeneratorHelpModal } from './help-modal'
import { SplitTestProvider } from '../../../../shared/context/split-test-context' import { SplitTestProvider } from '../../../../shared/context/split-test-context'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ColumnWidthModal } from './toolbar/column-width-modal/modal'
import { WidthSelection } from './toolbar/column-width-modal/column-width'
export type ColumnDefinition = { export type ColumnDefinition = {
alignment: 'left' | 'center' | 'right' | 'paragraph' alignment: 'left' | 'center' | 'right' | 'paragraph'
@ -35,6 +37,9 @@ export type ColumnDefinition = {
content: string content: string
cellSpacingLeft: string cellSpacingLeft: string
cellSpacingRight: string cellSpacingRight: string
customCellDefinition: string
isParagraphColumn: boolean
size?: WidthSelection
} }
export type CellData = { export type CellData = {
@ -252,6 +257,7 @@ export const Tabular: FC<{
<EditingContextProvider> <EditingContextProvider>
<TabularWrapper /> <TabularWrapper />
</EditingContextProvider> </EditingContextProvider>
<ColumnWidthModal />
</SelectionContextProvider> </SelectionContextProvider>
</TableProvider> </TableProvider>
<TableGeneratorHelpModal /> <TableGeneratorHelpModal />
@ -271,7 +277,8 @@ const TabularWrapper: FC = () => {
const listener: (event: MouseEvent) => void = event => { const listener: (event: MouseEvent) => void = event => {
if ( if (
!ref.current?.contains(event.target as Node) && !ref.current?.contains(event.target as Node) &&
!(event.target as HTMLElement).closest('.table-generator-help-modal') !(event.target as HTMLElement).closest('.table-generator-help-modal') &&
!(event.target as HTMLElement).closest('.table-generator-width-modal')
) { ) {
if (selection) { if (selection) {
setSelection(null) setSelection(null)

View file

@ -0,0 +1,45 @@
export const ABSOLUTE_SIZE_REGEX = /[pbm]\{\s*(\d*[.]?\d+)\s*(mm|cm|pt|in)\s*\}/
export const RELATIVE_SIZE_REGEX =
/[pbm]\{\s*(\d*[.]?\d+)\s*\\(linewidth|textwidth|columnwidth)\s*\}/
export type AbsoluteWidthUnits = 'mm' | 'cm' | 'in' | 'pt'
export type RelativeWidthCommand = 'linewidth' | 'textwidth' | 'columnwidth'
export type WidthUnit = AbsoluteWidthUnits | '%' | 'custom'
const ABSOLUTE_UNITS = ['mm', 'cm', 'in', 'pt'] as const
export const UNITS: WidthUnit[] = ['%', ...ABSOLUTE_UNITS, 'custom']
type PercentageWidth = {
unit: '%'
width: number
command?: RelativeWidthCommand
}
type CustomWidth = {
unit: 'custom'
width: string
}
type AbsoluteWidth = {
unit: Exclude<WidthUnit, '%' | 'custom'>
width: number
}
export type WidthSelection = PercentageWidth | CustomWidth | AbsoluteWidth
export const isPercentageWidth = (
width: WidthSelection
): width is PercentageWidth => {
return width.unit === '%'
}
export const isAbsoluteWidth = (
width: WidthSelection
): width is AbsoluteWidth => {
return (ABSOLUTE_UNITS as readonly string[]).includes(width.unit)
}
export const isCustomWidth = (width: WidthSelection): width is CustomWidth => {
return width.unit === 'custom'
}

View file

@ -0,0 +1,211 @@
import AccessibleModal from '@/shared/components/accessible-modal'
import { Button, Modal, Form, FormGroup } from 'react-bootstrap'
import { useTabularContext } from '../../contexts/tabular-context'
import { Select } from '@/shared/components/select'
import { Trans, useTranslation } from 'react-i18next'
import {
FormEventHandler,
memo,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import { useSelectionContext } from '../../contexts/selection-context'
import { useTableContext } from '../../contexts/table-context'
import { setColumnWidth } from '../commands'
import { UNITS, WidthSelection, WidthUnit } from './column-width'
import { useCodeMirrorViewContext } from '../../../codemirror-editor'
import { CopyToClipboard } from '@/shared/components/copy-to-clipboard'
import Tooltip from '@/shared/components/tooltip'
import Icon from '@/shared/components/icon'
type UnitDescription = { label: string; tooltip?: string } | undefined
export const ColumnWidthModal = memo(function ColumnWidthModal() {
const { columnWidthModalShown } = useTabularContext()
if (!columnWidthModalShown) {
return null
}
return <ColumnWidthModalBody />
})
const ColumnWidthModalBody = () => {
const { columnWidthModalShown, closeColumnWidthModal } = useTabularContext()
const view = useCodeMirrorViewContext()
const { selection } = useSelectionContext()
const { positions, table } = useTableContext()
const { t } = useTranslation()
const [currentUnit, setCurrentUnit] = useState<WidthUnit | undefined | null>(
'%'
)
const [currentWidth, setCurrentWidth] = useState<string>('')
const inputRef = useRef<HTMLInputElement>(null)
const unitHelp: UnitDescription = useMemo(() => {
switch (currentUnit) {
case '%':
return {
label: t('percent_is_the_percentage_of_the_line_width'),
tooltip: t(
'line_width_is_the_width_of_the_line_in_the_current_environment'
),
}
case 'custom':
return {
label: t('enter_any_size_including_units_or_valid_latex_command'),
}
default:
return undefined
}
}, [currentUnit, t])
useEffect(() => {
if (columnWidthModalShown) {
inputRef.current?.focus()
if (
!selection ||
selection.width() !== 1 ||
!table.columns[selection.to.cell].isParagraphColumn ||
!table.columns[selection.to.cell].size
) {
setCurrentUnit('%')
setCurrentWidth('')
return
}
const { to } = selection
const columnIndexToReadWidthAndUnit = to.cell
const column = table.columns[columnIndexToReadWidthAndUnit]
const size = column.size!
if (size.unit === '%') {
setCurrentUnit('%')
const widthWithUpToTwoDecimalPlaces = Math.round(size.width * 100) / 100
setCurrentWidth(widthWithUpToTwoDecimalPlaces.toString())
} else if (size.unit === 'custom') {
setCurrentUnit('custom')
// Slice off p{ and }
setCurrentWidth(column.content.slice(2, -1))
} else {
setCurrentUnit(size.unit)
setCurrentWidth(size.width.toString())
}
}
}, [columnWidthModalShown, selection, table])
const onSubmit: FormEventHandler<Form> = useCallback(
e => {
e.preventDefault()
if (selection && currentUnit) {
const currentWidthNumber = parseFloat(currentWidth)
let newWidth: WidthSelection
if (currentUnit === 'custom') {
newWidth = { unit: 'custom', width: currentWidth }
} else {
newWidth = { unit: currentUnit, width: currentWidthNumber }
}
setColumnWidth(view, selection, newWidth, positions, table)
}
closeColumnWidthModal()
return false
},
[
closeColumnWidthModal,
currentUnit,
currentWidth,
positions,
selection,
table,
view,
]
)
return (
<AccessibleModal
show={columnWidthModalShown}
onHide={closeColumnWidthModal}
className="table-generator-width-modal"
>
<Form onSubmit={onSubmit}>
<Modal.Header closeButton>
<Modal.Title>{t('set_column_width')}</Modal.Title>
</Modal.Header>
<Modal.Body>
<div className="clearfix">
<FormGroup className="col-md-8 p-0 mb-0">
<label
className="table-generator-width-label"
htmlFor="column-width-modal-width"
>
{t('column_width')}
</label>
<input
id="column-width-modal-width"
value={currentWidth}
required
onChange={e => setCurrentWidth(e.target.value)}
type={currentUnit === 'custom' ? 'text' : 'number'}
className="form-control"
ref={inputRef}
/>
</FormGroup>
<FormGroup className="col-md-4 mb-0">
<Select
label="&nbsp;"
items={UNITS}
itemToKey={x => x ?? ''}
itemToString={x => (x === 'custom' ? t('custom') : x ?? '')}
onSelectedItemChanged={item => setCurrentUnit(item)}
defaultItem={currentUnit}
/>
</FormGroup>
</div>
{unitHelp && (
<p className="my-1">
{unitHelp.label}{' '}
{unitHelp.tooltip && (
<Tooltip
id="table-generator-unit-tooltip"
description={unitHelp.tooltip}
overlayProps={{ delay: 0, placement: 'top' }}
>
<Icon type="question-circle" fw />
</Tooltip>
)}
</p>
)}
<div className="mt-2">
<Trans
i18nKey="to_use_text_wrapping_in_your_table_make_sure_you_include_the_array_package"
// eslint-disable-next-line react/jsx-key
components={[<b />, <code />]}
/>
</div>
<div className="mt-1 table-generator-usepackage-copy">
<code>
\usepackage{'{'}array{'}'}
</code>
<CopyToClipboard
content="\\usepackage{array} % required for text wrapping in tables"
tooltipId="table-generator-array-copy"
/>
</div>
</Modal.Body>
<Modal.Footer>
<Button
bsStyle={null}
className="btn-secondary"
onClick={() => {
closeColumnWidthModal()
}}
>
{t('cancel')}
</Button>
<Button bsStyle={null} className="btn-primary" type="submit">
{t('ok')}
</Button>
</Modal.Footer>
</Form>
</AccessibleModal>
)
}

View file

@ -14,6 +14,7 @@ import {
extendForwardsOverEmptyLines, extendForwardsOverEmptyLines,
} from '../../../extensions/visual/selection' } from '../../../extensions/visual/selection'
import { debugConsole } from '@/utils/debugging' import { debugConsole } from '@/utils/debugging'
import { WidthSelection } from './column-width-modal/column-width'
/* eslint-disable no-unused-vars */ /* eslint-disable no-unused-vars */
export enum BorderTheme { export enum BorderTheme {
@ -145,11 +146,15 @@ const addColumnBordersToSpecification = (specification: ColumnDefinition[]) => {
export const setAlignment = ( export const setAlignment = (
view: EditorView, view: EditorView,
selection: TableSelection, selection: TableSelection,
alignment: 'left' | 'right' | 'center', alignment: ColumnDefinition['alignment'],
positions: Positions, positions: Positions,
table: TableData table: TableData
) => { ) => {
if (selection.isMergedCellSelected(table)) { if (selection.isMergedCellSelected(table)) {
if (alignment === 'paragraph') {
// shouldn't happen
return
}
// change for mergedColumn // change for mergedColumn
const { minX, minY } = selection.normalized() const { minX, minY } = selection.normalized()
const cell = table.getCell(minY, minX) const cell = table.getCell(minY, minX)
@ -189,8 +194,16 @@ export const setAlignment = (
continue continue
} }
columnSpecification[i].alignment = alignment columnSpecification[i].alignment = alignment
// TODO: This won't work for paragraph, which needs width argument if (columnSpecification[i].isParagraphColumn) {
columnSpecification[i].content = alignment[0] columnSpecification[i].customCellDefinition =
generateParagraphColumnSpecification(alignment)
} else {
if (alignment === 'paragraph') {
// shouldn't happen
continue
}
columnSpecification[i].content = alignment[0]
}
} }
} }
const newSpecification = generateColumnSpecification(columnSpecification) const newSpecification = generateColumnSpecification(columnSpecification)
@ -214,10 +227,11 @@ const generateColumnSpecification = (columns: ColumnDefinition[]) => {
content, content,
cellSpacingLeft, cellSpacingLeft,
cellSpacingRight, cellSpacingRight,
customCellDefinition,
}) => }) =>
`${'|'.repeat( `${'|'.repeat(
borderLeft borderLeft
)}${cellSpacingLeft}${content}${cellSpacingRight}${'|'.repeat( )}${cellSpacingLeft}${customCellDefinition}${content}${cellSpacingRight}${'|'.repeat(
borderRight borderRight
)}` )}`
) )
@ -439,6 +453,8 @@ export const insertColumn = (
content: 'l', content: 'l',
cellSpacingLeft: '', cellSpacingLeft: '',
cellSpacingRight: '', cellSpacingRight: '',
customCellDefinition: '',
isParagraphColumn: false,
})) }))
) )
if (targetIndex === 0 && borderTheme === BorderTheme.FULLY_BORDERED) { if (targetIndex === 0 && borderTheme === BorderTheme.FULLY_BORDERED) {
@ -650,3 +666,135 @@ export const mergeCells = (
}, },
}) })
} }
const getSuffixForUnit = (
unit: WidthSelection['unit'],
currentSize?: WidthSelection
) => {
if (unit === 'custom') {
return ''
}
if (unit === '%') {
if (currentSize?.unit === '%' && currentSize.command) {
return `\\${currentSize.command}`
} else {
return '\\linewidth'
}
}
return unit
}
const COMMAND_FOR_PARAGRAPH_ALIGNMENT: Record<
ColumnDefinition['alignment'],
string
> = {
left: '\\raggedright',
right: '\\raggedleft',
center: '\\centering',
paragraph: '',
}
const transformColumnWidth = (width: WidthSelection) => {
if (width.unit === '%') {
return width.width / 100
} else {
return width.width
}
}
const generateParagraphColumnSpecification = (
alignment: ColumnDefinition['alignment']
) => {
if (alignment === 'paragraph') {
return ''
}
return `>{${COMMAND_FOR_PARAGRAPH_ALIGNMENT[alignment]}\\arraybackslash}`
}
function getParagraphAlignmentCharacter(
column: ColumnDefinition
): 'p' | 'm' | 'b' {
if (!column.isParagraphColumn) {
return 'p'
}
const currentAlignmentCharacter = column.content[0]
if (currentAlignmentCharacter === 'm' || currentAlignmentCharacter === 'b') {
return currentAlignmentCharacter
}
return 'p'
}
export const setColumnWidth = (
view: EditorView,
selection: TableSelection,
newWidth: WidthSelection,
positions: Positions,
table: TableData
) => {
const { minX, maxX } = selection.normalized()
const specification = view.state.sliceDoc(
positions.columnDeclarations.from,
positions.columnDeclarations.to
)
const columnSpecification = parseColumnSpecifications(specification)
for (let i = minX; i <= maxX; i++) {
if (selection.isColumnSelected(i, table)) {
const suffix = getSuffixForUnit(newWidth.unit, table.columns[i].size)
const widthValue = transformColumnWidth(newWidth)
columnSpecification[i].customCellDefinition =
generateParagraphColumnSpecification(columnSpecification[i].alignment)
// Reuse paragraph alignment characters to preserve m and b columns
const alignmentCharacter = getParagraphAlignmentCharacter(
columnSpecification[i]
)
columnSpecification[
i
].content = `${alignmentCharacter}{${widthValue}${suffix}}`
}
}
const newSpecification = generateColumnSpecification(columnSpecification)
view.dispatch({
changes: [
{
from: positions.columnDeclarations.from,
to: positions.columnDeclarations.to,
insert: newSpecification,
},
],
})
}
export const removeColumnWidths = (
view: EditorView,
selection: TableSelection,
positions: Positions,
table: TableData
) => {
const { minX, maxX } = selection.normalized()
const specification = view.state.sliceDoc(
positions.columnDeclarations.from,
positions.columnDeclarations.to
)
const columnSpecification = parseColumnSpecifications(specification)
for (let i = minX; i <= maxX; i++) {
if (selection.isColumnSelected(i, table)) {
columnSpecification[i].customCellDefinition = ''
if (columnSpecification[i].alignment === 'paragraph') {
columnSpecification[i].content = 'l'
columnSpecification[i].alignment = 'left'
} else {
columnSpecification[i].content = columnSpecification[i].alignment[0]
}
}
}
const newSpecification = generateColumnSpecification(columnSpecification)
view.dispatch({
changes: [
{
from: positions.columnDeclarations.from,
to: positions.columnDeclarations.to,
insert: newSpecification,
},
],
})
}

View file

@ -6,6 +6,7 @@ import Tooltip from '../../../../../shared/components/tooltip'
import { useTabularContext } from '../contexts/tabular-context' import { useTabularContext } from '../contexts/tabular-context'
import { emitTableGeneratorEvent } from '../analytics' import { emitTableGeneratorEvent } from '../analytics'
import { useCodeMirrorViewContext } from '../../codemirror-editor' import { useCodeMirrorViewContext } from '../../codemirror-editor'
import classNames from 'classnames'
export const ToolbarDropdown: FC<{ export const ToolbarDropdown: FC<{
id: string id: string
@ -15,6 +16,7 @@ export const ToolbarDropdown: FC<{
tooltip?: string tooltip?: string
disabled?: boolean disabled?: boolean
disabledTooltip?: string disabledTooltip?: string
showCaret?: boolean
}> = ({ }> = ({
id, id,
label, label,
@ -24,6 +26,7 @@ export const ToolbarDropdown: FC<{
tooltip, tooltip,
disabled, disabled,
disabledTooltip, disabledTooltip,
showCaret,
}) => { }) => {
const { open, onToggle, ref } = useDropdown() const { open, onToggle, ref } = useDropdown()
const toggleButtonRef = useRef<HTMLButtonElement | null>(null) const toggleButtonRef = useRef<HTMLButtonElement | null>(null)
@ -48,6 +51,7 @@ export const ToolbarDropdown: FC<{
> >
{label && <span>{label}</span>} {label && <span>{label}</span>}
<MaterialIcon type={icon} /> <MaterialIcon type={icon} />
{showCaret && <MaterialIcon type="expand_more" />}
</button> </button>
) )
const overlay = tabularRef.current && ( const overlay = tabularRef.current && (
@ -110,8 +114,10 @@ export const ToolbarDropdownItem: FC<
Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick'> & { Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick'> & {
command: () => void command: () => void
id: string id: string
icon?: string
active?: boolean
} }
> = ({ children, command, id, ...props }) => { > = ({ children, command, id, icon, active, ...props }) => {
const view = useCodeMirrorViewContext() const view = useCodeMirrorViewContext()
const onClick = useCallback(() => { const onClick = useCallback(() => {
emitTableGeneratorEvent(view, id) emitTableGeneratorEvent(view, id)
@ -119,13 +125,17 @@ export const ToolbarDropdownItem: FC<
}, [view, command, id]) }, [view, command, id])
return ( return (
<button <button
className="ol-cm-toolbar-menu-item" className={classNames('ol-cm-toolbar-menu-item', {
'ol-cm-toolbar-dropdown-option-active': active,
})}
role="menuitem" role="menuitem"
type="button" type="button"
{...props} {...props}
onClick={onClick} onClick={onClick}
> >
{children} {icon && <MaterialIcon type={icon} />}
<span className="ol-cm-toolbar-dropdown-option-content">{children}</span>
{active && <MaterialIcon type="check" />}
</button> </button>
) )
} }

View file

@ -11,6 +11,7 @@ import {
mergeCells, mergeCells,
moveCaption, moveCaption,
removeCaption, removeCaption,
removeColumnWidths,
removeNodes, removeNodes,
removeRowOrColumns, removeRowOrColumns,
setAlignment, setAlignment,
@ -22,6 +23,9 @@ import { useTableContext } from '../contexts/table-context'
import { useTabularContext } from '../contexts/tabular-context' import { useTabularContext } from '../contexts/tabular-context'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { FeedbackBadge } from '@/shared/components/feedback-badge' import { FeedbackBadge } from '@/shared/components/feedback-badge'
import classNames from 'classnames'
type CaptionPosition = 'no_caption' | 'above' | 'below'
export const Toolbar = memo(function Toolbar() { export const Toolbar = memo(function Toolbar() {
const { selection, setSelection } = useSelectionContext() const { selection, setSelection } = useSelectionContext()
@ -34,7 +38,7 @@ export const Toolbar = memo(function Toolbar() {
table, table,
directTableChild, directTableChild,
} = useTableContext() } = useTableContext()
const { showHelp } = useTabularContext() const { showHelp, openColumnWidthModal } = useTabularContext()
const { t } = useTranslation() const { t } = useTranslation()
const borderDropdownLabel = useMemo(() => { const borderDropdownLabel = useMemo(() => {
@ -48,15 +52,26 @@ export const Toolbar = memo(function Toolbar() {
} }
}, [table, t]) }, [table, t])
const captionLabel = useMemo(() => { const captionPosition: CaptionPosition = useMemo(() => {
if (!tableEnvironment?.caption) { if (!tableEnvironment?.caption) {
return t('no_caption') return 'no_caption'
} }
if (tableEnvironment.caption.from < positions.tabular.from) { if (tableEnvironment.caption.from < positions.tabular.from) {
return t('caption_above') return 'above'
} }
return t('caption_below') return 'below'
}, [tableEnvironment, positions.tabular.from, t]) }, [tableEnvironment, positions])
const captionLabel = useMemo(() => {
switch (captionPosition) {
case 'no_caption':
return t('no_caption')
case 'above':
return t('caption_above')
case 'below':
return t('caption_below')
}
}, [t, captionPosition])
const currentAlignment = useMemo(() => { const currentAlignment = useMemo(() => {
if (!selection) { if (!selection) {
@ -94,14 +109,27 @@ export const Toolbar = memo(function Toolbar() {
} }
}, [currentAlignment]) }, [currentAlignment])
const hasCustomSizes = useMemo(
() => table.columns.some(x => x.size),
[table.columns]
)
if (!selection) { if (!selection) {
return null return null
} }
const columnsToInsert = selection.maximumCellWidth(table) const columnsToInsert = selection.maximumCellWidth(table)
const rowsToInsert = selection.height() const rowsToInsert = selection.height()
const onlyFixedWidthColumnsSelected = selection.isOnlyFixedWidthColumns(table)
const onlyNonFixedWidthColumnsSelected =
selection.isOnlyNonFixedWidthColumns(table)
return ( return (
<div className="table-generator-floating-toolbar"> <div
className={classNames('table-generator-floating-toolbar', {
'table-generator-toolbar-floating-custom-sizes': hasCustomSizes,
})}
>
<div className="table-generator-button-group"> <div className="table-generator-button-group">
<ToolbarDropdown <ToolbarDropdown
id="table-generator-caption-dropdown" id="table-generator-caption-dropdown"
@ -116,6 +144,7 @@ export const Toolbar = memo(function Toolbar() {
command={() => { command={() => {
removeCaption(view, tableEnvironment) removeCaption(view, tableEnvironment)
}} }}
active={captionPosition === 'no_caption'}
> >
{t('no_caption')} {t('no_caption')}
</ToolbarDropdownItem> </ToolbarDropdownItem>
@ -124,6 +153,7 @@ export const Toolbar = memo(function Toolbar() {
command={() => { command={() => {
moveCaption(view, positions, 'above', tableEnvironment) moveCaption(view, positions, 'above', tableEnvironment)
}} }}
active={captionPosition === 'above'}
> >
{t('caption_above')} {t('caption_above')}
</ToolbarDropdownItem> </ToolbarDropdownItem>
@ -132,6 +162,7 @@ export const Toolbar = memo(function Toolbar() {
command={() => { command={() => {
moveCaption(view, positions, 'below', tableEnvironment) moveCaption(view, positions, 'below', tableEnvironment)
}} }}
active={captionPosition === 'below'}
> >
{t('caption_below')} {t('caption_below')}
</ToolbarDropdownItem> </ToolbarDropdownItem>
@ -151,11 +182,10 @@ export const Toolbar = memo(function Toolbar() {
table table
) )
}} }}
active={table.getBorderTheme() === BorderTheme.FULLY_BORDERED}
icon="border_all"
> >
<MaterialIcon type="border_all" /> {t('all_borders')}
<span className="table-generator-button-label">
{t('all_borders')}
</span>
</ToolbarDropdownItem> </ToolbarDropdownItem>
<ToolbarDropdownItem <ToolbarDropdownItem
id="table-generator-borders-no-borders" id="table-generator-borders-no-borders"
@ -168,11 +198,10 @@ export const Toolbar = memo(function Toolbar() {
table table
) )
}} }}
active={table.getBorderTheme() === BorderTheme.NO_BORDERS}
icon="border_clear"
> >
<MaterialIcon type="border_clear" /> {t('no_borders')}
<span className="table-generator-button-label">
{t('no_borders')}
</span>
</ToolbarDropdownItem> </ToolbarDropdownItem>
<div className="table-generator-border-options-coming-soon"> <div className="table-generator-border-options-coming-soon">
<div className="info-icon"> <div className="info-icon">
@ -200,6 +229,7 @@ export const Toolbar = memo(function Toolbar() {
command={() => { command={() => {
setAlignment(view, selection, 'left', positions, table) setAlignment(view, selection, 'left', positions, table)
}} }}
active={currentAlignment === 'left'}
/> />
<ToolbarButton <ToolbarButton
icon="format_align_center" icon="format_align_center"
@ -208,6 +238,7 @@ export const Toolbar = memo(function Toolbar() {
command={() => { command={() => {
setAlignment(view, selection, 'center', positions, table) setAlignment(view, selection, 'center', positions, table)
}} }}
active={currentAlignment === 'center'}
/> />
<ToolbarButton <ToolbarButton
icon="format_align_right" icon="format_align_right"
@ -216,8 +247,66 @@ export const Toolbar = memo(function Toolbar() {
command={() => { command={() => {
setAlignment(view, selection, 'right', positions, table) setAlignment(view, selection, 'right', positions, table)
}} }}
active={currentAlignment === 'right'}
/> />
{onlyFixedWidthColumnsSelected &&
!selection.isMergedCellSelected(table) && (
<ToolbarButton
icon="format_align_justify"
id="table-generator-align-justify"
label={t('justify')}
command={() => {
setAlignment(view, selection, 'paragraph', positions, table)
}}
active={currentAlignment === 'paragraph'}
/>
)}
</ToolbarButtonMenu> </ToolbarButtonMenu>
<ToolbarDropdown
id="format_text_wrap"
btnClassName="table-generator-toolbar-button"
icon={
selection.isOnlyParagraphCells(table) ? 'format_text_wrap' : 'width'
}
tooltip={t('adjust_column_width')}
disabled={!selection.isAnyColumnSelected(table)}
disabledTooltip={t('select_a_column_to_adjust_column_width')}
showCaret
>
<ToolbarDropdownItem
id="table-generator-unwrap-text"
icon="width"
active={onlyNonFixedWidthColumnsSelected}
command={() =>
removeColumnWidths(view, selection, positions, table)
}
disabled={!selection.isAnyColumnSelected(table)}
>
{t('stretch_width_to_text')}
</ToolbarDropdownItem>
<ToolbarDropdownItem
id="table-generator-wrap-text"
icon="format_text_wrap"
active={onlyFixedWidthColumnsSelected}
command={openColumnWidthModal}
disabled={!selection.isAnyColumnSelected(table)}
>
{onlyFixedWidthColumnsSelected
? t('fixed_width')
: t('fixed_width_wrap_text')}
</ToolbarDropdownItem>
{onlyFixedWidthColumnsSelected && (
<>
<hr />
<ToolbarDropdownItem
id="table-generator-resize"
command={openColumnWidthModal}
>
{t('set_column_width')}
</ToolbarDropdownItem>
</>
)}
</ToolbarDropdown>
<ToolbarButton <ToolbarButton
icon="cell_merge" icon="cell_merge"
id="table-generator-merge-cells" id="table-generator-merge-cells"
@ -361,7 +450,7 @@ export const Toolbar = memo(function Toolbar() {
<div className="toolbar-beta-badge"> <div className="toolbar-beta-badge">
<FeedbackBadge <FeedbackBadge
id="table-generator-feedback" id="table-generator-feedback"
url="https://forms.gle/ri3fzV1oQDAjmfmD7" url="https://forms.gle/9dHxXPGugxEHgY3L9"
text={<FeedbackBadgeContent />} text={<FeedbackBadgeContent />}
/> />
</div> </div>

View file

@ -2,8 +2,15 @@ import { EditorState } from '@codemirror/state'
import { SyntaxNode } from '@lezer/common' import { SyntaxNode } from '@lezer/common'
import { CellData, ColumnDefinition, TableData } from './tabular' import { CellData, ColumnDefinition, TableData } from './tabular'
import { TableEnvironmentData } from './contexts/table-context' import { TableEnvironmentData } from './contexts/table-context'
import {
ABSOLUTE_SIZE_REGEX,
AbsoluteWidthUnits,
RELATIVE_SIZE_REGEX,
RelativeWidthCommand,
WidthSelection,
} from './toolbar/column-width-modal/column-width'
const ALIGNMENT_CHARACTERS = ['c', 'l', 'r', 'p'] const COMMIT_CHARACTERS = ['c', 'l', 'r', 'p', 'm', 'b', '>']
export type CellPosition = { from: number; to: number } export type CellPosition = { from: number; to: number }
export type RowPosition = { export type RowPosition = {
@ -40,6 +47,9 @@ export function parseColumnSpecifications(
let currentContent = '' let currentContent = ''
let currentCellSpacingLeft = '' let currentCellSpacingLeft = ''
let currentCellSpacingRight = '' let currentCellSpacingRight = ''
let currentCustomCellDefinition = ''
let currentIsParagraphColumn = false
let currentSize: WidthSelection | undefined
function maybeCommit() { function maybeCommit() {
if (currentAlignment !== undefined) { if (currentAlignment !== undefined) {
columns.push({ columns.push({
@ -49,6 +59,9 @@ export function parseColumnSpecifications(
content: currentContent, content: currentContent,
cellSpacingLeft: currentCellSpacingLeft, cellSpacingLeft: currentCellSpacingLeft,
cellSpacingRight: currentCellSpacingRight, cellSpacingRight: currentCellSpacingRight,
customCellDefinition: currentCustomCellDefinition,
isParagraphColumn: currentIsParagraphColumn,
size: currentSize,
}) })
currentAlignment = undefined currentAlignment = undefined
currentBorderLeft = 0 currentBorderLeft = 0
@ -56,10 +69,13 @@ export function parseColumnSpecifications(
currentContent = '' currentContent = ''
currentCellSpacingLeft = '' currentCellSpacingLeft = ''
currentCellSpacingRight = '' currentCellSpacingRight = ''
currentCustomCellDefinition = ''
currentIsParagraphColumn = false
currentSize = undefined
} }
} }
for (let i = 0; i < specification.length; i++) { for (let i = 0; i < specification.length; i++) {
if (ALIGNMENT_CHARACTERS.includes(specification.charAt(i))) { if (COMMIT_CHARACTERS.includes(specification.charAt(i))) {
maybeCommit() maybeCommit()
} }
const hasAlignment = currentAlignment !== undefined const hasAlignment = currentAlignment !== undefined
@ -85,13 +101,55 @@ export function parseColumnSpecifications(
currentAlignment = 'right' currentAlignment = 'right'
currentContent += 'r' currentContent += 'r'
break break
case 'p': { case 'p':
case 'm':
case 'b': {
currentIsParagraphColumn = true
currentAlignment = 'paragraph' currentAlignment = 'paragraph'
currentContent += 'p' if (currentCustomCellDefinition !== '') {
// TODO: Parse these details // Maybe we have another alignment hidden in here
const match = currentCustomCellDefinition.match(
/>\{\s*\\(raggedleft|raggedright|centering)\s*\\arraybackslash\s*\}/
)
if (match) {
switch (match[1]) {
case 'raggedleft':
currentAlignment = 'right'
break
case 'raggedright':
currentAlignment = 'left'
break
case 'centering':
currentAlignment = 'center'
break
}
}
}
currentContent += char
const argumentEnd = parseArgument(specification, i + 1) const argumentEnd = parseArgument(specification, i + 1)
const columnDefinition = specification.slice(i, argumentEnd + 1)
const absoluteSizeMatch = columnDefinition.match(ABSOLUTE_SIZE_REGEX)
const relativeSizeMatch = columnDefinition.match(RELATIVE_SIZE_REGEX)
if (absoluteSizeMatch) {
currentSize = {
unit: absoluteSizeMatch[2] as AbsoluteWidthUnits,
width: parseFloat(absoluteSizeMatch[1]),
}
} else if (relativeSizeMatch) {
const widthAsFraction = parseFloat(relativeSizeMatch[1]) || 0
currentSize = {
unit: '%',
width: widthAsFraction * 100,
command: relativeSizeMatch[2] as RelativeWidthCommand,
}
} else {
currentSize = {
unit: 'custom',
width: columnDefinition.slice(2, -1),
}
}
// Don't include the p twice // Don't include the p twice
currentContent += specification.slice(i + 1, argumentEnd + 1) currentContent += columnDefinition.slice(1)
i = argumentEnd i = argumentEnd
break break
} }
@ -109,6 +167,14 @@ export function parseColumnSpecifications(
} }
break break
} }
case '>': {
const argumentEnd = parseArgument(specification, i + 1)
// Include the >
const argument = specification.slice(i, argumentEnd + 1)
i = argumentEnd
currentCustomCellDefinition = argument
break
}
case ' ': case ' ':
case '\n': case '\n':
case '\t': case '\t':

View file

@ -22,10 +22,21 @@ export const tableGeneratorTheme = EditorView.baseTheme({
'--table-generator-toolbar-dropdown-disabled-background': '--table-generator-toolbar-dropdown-disabled-background':
'rgba(125,125,125,0.3)', 'rgba(125,125,125,0.3)',
'--table-generator-toolbar-dropdown-disabled-color': '#999', '--table-generator-toolbar-dropdown-disabled-color': '#999',
'--table-generator-toolbar-dropdown-active-background': 'var(--green-10)',
'--table-generator-toolbar-dropdown-active-color': 'var(--green-70)',
'--table-generator-toolbar-dropdown-active-hover-background':
'var(--green-10)',
'--table-generator-toolbar-dropdown-active-active-background':
'var(--green-20)',
'--table-generator-toolbar-shadow-color': '#1e253029', '--table-generator-toolbar-shadow-color': '#1e253029',
'--table-generator-error-background': '#2c3645', '--table-generator-error-background': '#2c3645',
'--table-generator-error-color': '#fff', '--table-generator-error-color': '#fff',
'--table-generator-error-border-color': '#677283', '--table-generator-error-border-color': '#677283',
'--table-generator-column-size-indicator-background': 'var(--neutral-80)',
'--table-generator-column-size-indicator-hover-background':
'var(--neutral-70)',
'--table-generator-column-size-indicator-color': 'white',
'--table-generator-column-size-indicator-hover-color': 'white',
}, },
'&light .table-generator': { '&light .table-generator': {
@ -48,10 +59,20 @@ export const tableGeneratorTheme = EditorView.baseTheme({
'--table-generator-toolbar-dropdown-border-color': 'var(--neutral-60)', '--table-generator-toolbar-dropdown-border-color': 'var(--neutral-60)',
'--table-generator-toolbar-dropdown-disabled-background': '#f2f2f2', '--table-generator-toolbar-dropdown-disabled-background': '#f2f2f2',
'--table-generator-toolbar-dropdown-disabled-color': 'var(--neutral-40)', '--table-generator-toolbar-dropdown-disabled-color': 'var(--neutral-40)',
'--table-generator-toolbar-dropdown-active-background': 'var(--green-10)',
'--table-generator-toolbar-dropdown-active-color': 'var(--green-70)',
'--table-generator-toolbar-dropdown-active-hover-background':
'var(--green-10)',
'--table-generator-toolbar-dropdown-active-active-background':
'var(--green-20)',
'--table-generator-toolbar-shadow-color': '#1e253029', '--table-generator-toolbar-shadow-color': '#1e253029',
'--table-generator-error-background': '#F1F4F9', '--table-generator-error-background': '#F1F4F9',
'--table-generator-error-color': 'black', '--table-generator-error-color': 'black',
'--table-generator-error-border-color': '#C3D0E3', '--table-generator-error-border-color': '#C3D0E3',
'--table-generator-column-size-indicator-background': '#E7E9EE',
'--table-generator-column-size-indicator-hover-background': '#D7DADF',
'--table-generator-column-size-indicator-color': 'black',
'--table-generator-column-size-indicator-hover-color': 'black',
}, },
'.table-generator': { '.table-generator': {
@ -162,6 +183,7 @@ export const tableGeneratorTheme = EditorView.baseTheme({
'&::after': { '&::after': {
width: '4px', width: '4px',
bottom: '4px',
height: 'calc(100% - 8px)', height: 'calc(100% - 8px)',
}, },
}, },
@ -172,6 +194,7 @@ export const tableGeneratorTheme = EditorView.baseTheme({
'&::after': { '&::after': {
width: 'calc(100% - 8px)', width: 'calc(100% - 8px)',
height: '4px', height: '4px',
right: '4px',
}, },
}, },
@ -179,8 +202,8 @@ export const tableGeneratorTheme = EditorView.baseTheme({
content: '""', content: '""',
display: 'block', display: 'block',
position: 'absolute', position: 'absolute',
bottom: '4px', bottom: '8px',
right: '4px', right: '8px',
width: 'calc(100% - 8px)', width: 'calc(100% - 8px)',
height: 'calc(100% - 8px)', height: 'calc(100% - 8px)',
'background-color': 'var(--table-generator-selector-background-color)', 'background-color': 'var(--table-generator-selector-background-color)',
@ -199,7 +222,6 @@ export const tableGeneratorTheme = EditorView.baseTheme({
'.table-generator-floating-toolbar': { '.table-generator-floating-toolbar': {
position: 'absolute', position: 'absolute',
top: '0',
transform: 'translateY(-100%)', transform: 'translateY(-100%)',
left: '0', left: '0',
right: '0', right: '0',
@ -216,6 +238,9 @@ export const tableGeneratorTheme = EditorView.baseTheme({
display: 'flex', display: 'flex',
flexWrap: 'wrap', flexWrap: 'wrap',
rowGap: '8px', rowGap: '8px',
'&.table-generator-toolbar-floating-custom-sizes': {
top: '-8px',
},
}, },
'.table-generator-toolbar-button': { '.table-generator-toolbar-button': {
@ -375,6 +400,7 @@ export const tableGeneratorTheme = EditorView.baseTheme({
display: 'flex', display: 'flex',
'flex-direction': 'column', 'flex-direction': 'column',
'min-width': '200px', 'min-width': '200px',
padding: '4px',
'& > button': { '& > button': {
border: 'none', border: 'none',
@ -382,11 +408,11 @@ export const tableGeneratorTheme = EditorView.baseTheme({
background: 'transparent', background: 'transparent',
'white-space': 'nowrap', 'white-space': 'nowrap',
color: 'var(--table-generator-toolbar-button-color)', color: 'var(--table-generator-toolbar-button-color)',
'border-radius': '0', 'border-radius': '4px',
'font-size': '14px', 'font-size': '14px',
display: 'flex', display: 'flex',
'align-items': 'center', 'align-items': 'center',
'justify-content': 'space-between', 'justify-content': 'flex-start',
'column-gap': '8px', 'column-gap': '8px',
'align-self': 'stretch', 'align-self': 'stretch',
padding: '12px 8px', padding: '12px 8px',
@ -398,6 +424,12 @@ export const tableGeneratorTheme = EditorView.baseTheme({
'text-align': 'left', 'text-align': 'left',
}, },
'&.ol-cm-toolbar-dropdown-option-active': {
'background-color':
'var(--table-generator-toolbar-dropdown-active-background)',
color: 'var(--table-generator-toolbar-dropdown-active-color)',
},
'&:hover, &:focus': { '&:hover, &:focus': {
'background-color': 'background-color':
'var(--table-generator-toolbar-button-hover-background)', 'var(--table-generator-toolbar-button-hover-background)',
@ -408,6 +440,18 @@ export const tableGeneratorTheme = EditorView.baseTheme({
'var(--table-generator-toolbar-button-active-background)', 'var(--table-generator-toolbar-button-active-background)',
}, },
'&.ol-cm-toolbar-dropdown-option-active:hover, &.ol-cm-toolbar-dropdown-option-active:focus':
{
'background-color':
'var(--table-generator-toolbar-dropdown-active-hover-background)',
},
'&.ol-cm-toolbar-dropdown-option-active:active, &.ol-cm-toolbar-dropdown-option-active.active':
{
'background-color':
'var(--table-generator-toolbar-dropdown-active-active-background)',
},
'&:hover, &:focus, &:active, &.active': { '&:hover, &:focus, &:active, &.active': {
'box-shadow': 'none', 'box-shadow': 'none',
}, },
@ -428,6 +472,11 @@ export const tableGeneratorTheme = EditorView.baseTheme({
border: '0', border: '0',
height: '1px', height: '1px',
}, },
'& .ol-cm-toolbar-dropdown-option-content': {
textAlign: 'left',
flexGrow: '1',
},
}, },
'.ol-cm-environment-table.table-generator-error-container, .ol-cm-environment-table.ol-cm-tabular': '.ol-cm-environment-table.table-generator-error-container, .ol-cm-environment-table.ol-cm-tabular':
@ -465,4 +514,43 @@ export const tableGeneratorTheme = EditorView.baseTheme({
'min-width': '40px', 'min-width': '40px',
}, },
}, },
'.table-generator-column-indicator-button': {
verticalAlign: 'middle',
borderRadius: '4px',
padding: '2px 4px 2px 4px',
background: 'var(--table-generator-column-size-indicator-background)',
margin: 0,
border: 'none',
fontFamily: 'Lato, sans-serif',
fontSize: '12px',
lineHeight: '16px',
fontWeight: 400,
display: 'flex',
maxWidth: '100%',
color: 'var(--table-generator-column-size-indicator-color)',
'&:hover': {
background:
'var(--table-generator-column-size-indicator-hover-background)',
color: 'var(--table-generator-column-size-indicator-hover-color)',
},
'& .table-generator-column-indicator-icon': {
fontSize: '16px',
lineHeight: '16px',
},
'& .table-generator-column-indicator-label': {
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
},
},
'.table-generator-column-widths-row': {
height: '20px',
'& td': {
lineHeight: '1',
},
},
}) })

View file

@ -19,6 +19,7 @@
@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-column-width-modal.less';
@import './editor/ide-react.less'; @import './editor/ide-react.less';
@ui-layout-toggler-def-height: 50px; @ui-layout-toggler-def-height: 50px;

View file

@ -0,0 +1,13 @@
.table-generator-width-modal {
.table-generator-width-label {
width: 100%;
}
.table-generator-usepackage-copy {
display: flex;
justify-content: space-between;
padding: 6px 12px;
background: #f4f5f6;
border: 1px solid #d0d5dd;
}
}

View file

@ -79,6 +79,7 @@
"address": "Address", "address": "Address",
"address_line_1": "Address", "address_line_1": "Address",
"address_second_line_optional": "Address second line (optional)", "address_second_line_optional": "Address second line (optional)",
"adjust_column_width": "Adjust column width",
"admin": "admin", "admin": "admin",
"admin_user_created_message": "Created admin user, <a href=\"__link__\">Log in here</a> to continue", "admin_user_created_message": "Created admin user, <a href=\"__link__\">Log in here</a> to continue",
"advanced_reference_search": "Advanced <0>reference search</0>", "advanced_reference_search": "Advanced <0>reference search</0>",
@ -263,6 +264,9 @@
"collabs_per_proj": "__collabcount__ collaborators per project", "collabs_per_proj": "__collabcount__ collaborators per project",
"collabs_per_proj_single": "__collabcount__ collaborator per project", "collabs_per_proj_single": "__collabcount__ collaborator per project",
"collapse": "Collapse", "collapse": "Collapse",
"column_width": "Column width",
"column_width_is_custom_click_to_resize": "Column width is custom. Click to resize",
"column_width_is_x_click_to_resize": "Column width is __width__. Click to resize",
"comment": "Comment", "comment": "Comment",
"comment_submit_error": "Sorry, there was a problem submitting your comment", "comment_submit_error": "Sorry, there was a problem submitting your comment",
"commit": "Commit", "commit": "Commit",
@ -347,6 +351,7 @@
"currently_seeing_only_24_hrs_history": "Youre currently seeing the last 24 hours of changes in this project.", "currently_seeing_only_24_hrs_history": "Youre currently seeing the last 24 hours of changes in this project.",
"currently_signed_in_as_x": "Currently signed in as <0>__userEmail__</0>.", "currently_signed_in_as_x": "Currently signed in as <0>__userEmail__</0>.",
"currently_subscribed_to_plan": "You are currently subscribed to the <0>__planName__</0> plan.", "currently_subscribed_to_plan": "You are currently subscribed to the <0>__planName__</0> plan.",
"custom": "Custom",
"custom_borders": "Custom borders", "custom_borders": "Custom borders",
"custom_resource_portal": "Custom resource portal", "custom_resource_portal": "Custom resource portal",
"custom_resource_portal_info": "You can have your own custom portal page on Overleaf. This is a great place for your users to find out more about Overleaf, access templates, FAQs and Help resources, and sign up to Overleaf.", "custom_resource_portal_info": "You can have your own custom portal page on Overleaf. This is a great place for your users to find out more about Overleaf, access templates, FAQs and Help resources, and sign up to Overleaf.",
@ -510,6 +515,7 @@
"enabling": "Enabling", "enabling": "Enabling",
"end_of_document": "End of document", "end_of_document": "End of document",
"enter_6_digit_code": "Enter 6-digit code", "enter_6_digit_code": "Enter 6-digit code",
"enter_any_size_including_units_or_valid_latex_command": "Enter any size (including units) or valid LaTeX command",
"enter_image_url": "Enter image URL", "enter_image_url": "Enter image URL",
"enter_your_email_address": "Enter your email address", "enter_your_email_address": "Enter your email address",
"enter_your_email_address_below_and_we_will_send_you_a_link_to_reset_your_password": "Enter your email address below, and we will send you a link to reset your password", "enter_your_email_address_below_and_we_will_send_you_a_link_to_reset_your_password": "Enter your email address below, and we will send you a link to reset your password",
@ -598,6 +604,8 @@
"fit_to_height": "Fit to height", "fit_to_height": "Fit to height",
"fit_to_width": "Fit to width", "fit_to_width": "Fit to width",
"fix_issues": "Fix issues", "fix_issues": "Fix issues",
"fixed_width": "Fixed width",
"fixed_width_wrap_text": "Fixed width, wrap text",
"fold_line": "Fold line", "fold_line": "Fold line",
"folder_location": "Folder location", "folder_location": "Folder location",
"folders": "Folders", "folders": "Folders",
@ -914,6 +922,7 @@
"joining": "Joining", "joining": "Joining",
"july": "July", "july": "July",
"june": "June", "june": "June",
"justify": "Justify",
"kb_suggestions_enquiry": "Have you checked our <0>__kbLink__</0>?", "kb_suggestions_enquiry": "Have you checked our <0>__kbLink__</0>?",
"keep_current_plan": "Keep my current plan", "keep_current_plan": "Keep my current plan",
"keep_personal_projects_separate": "Keep personal projects separate", "keep_personal_projects_separate": "Keep personal projects separate",
@ -976,6 +985,7 @@
"license_for_educational_purposes": "This license is for educational purposes (applies to students or faculty using __appName__ for teaching)", "license_for_educational_purposes": "This license is for educational purposes (applies to students or faculty using __appName__ for teaching)",
"limited_offer": "Limited offer", "limited_offer": "Limited offer",
"line_height": "Line Height", "line_height": "Line Height",
"line_width_is_the_width_of_the_line_in_the_current_environment": "Line width is the width of the line in the current environment. e.g. a full page width in single-column layout or half a page width in a two-column layout.",
"link": "Link", "link": "Link",
"link_account": "Link Account", "link_account": "Link Account",
"link_accounts": "Link Accounts", "link_accounts": "Link Accounts",
@ -1301,6 +1311,7 @@
"per_user_year": "per user / year", "per_user_year": "per user / year",
"per_year": "per year", "per_year": "per year",
"percent_discount_for_groups": "__appName__ offers a __percent__% educational discount for groups of __size__ or more.", "percent_discount_for_groups": "__appName__ offers a __percent__% educational discount for groups of __size__ or more.",
"percent_is_the_percentage_of_the_line_width": "% is the percentage of the line width",
"personal": "Personal", "personal": "Personal",
"personalized_onboarding": "Personalized onboarding", "personalized_onboarding": "Personalized onboarding",
"personalized_onboarding_info": "Well help you get everything set up and then were here to answer questions from your users about the platform, templates or LaTeX!", "personalized_onboarding_info": "Well help you get everything set up and then were here to answer questions from your users about the platform, templates or LaTeX!",
@ -1598,6 +1609,7 @@
"see_changes_in_your_documents_live": "See changes in your documents, live", "see_changes_in_your_documents_live": "See changes in your documents, live",
"see_what_has_been": "See what has been ", "see_what_has_been": "See what has been ",
"select_a_column_or_a_merged_cell_to_align": "Select a column or a merged cell to align", "select_a_column_or_a_merged_cell_to_align": "Select a column or a merged cell to align",
"select_a_column_to_adjust_column_width": "Select a column to adjust column width",
"select_a_file": "Select a File", "select_a_file": "Select a File",
"select_a_file_figure_modal": "Select a file", "select_a_file_figure_modal": "Select a file",
"select_a_group_optional": "Select a Group (optional)", "select_a_group_optional": "Select a Group (optional)",
@ -1644,6 +1656,7 @@
"session_expired_redirecting_to_login": "Session Expired. Redirecting to login page in __seconds__ seconds", "session_expired_redirecting_to_login": "Session Expired. Redirecting to login page in __seconds__ seconds",
"sessions": "Sessions", "sessions": "Sessions",
"set_color": "set color", "set_color": "set color",
"set_column_width": "Set column width",
"set_new_password": "Set new password", "set_new_password": "Set new password",
"set_password": "Set Password", "set_password": "Set Password",
"set_up_sso": "Set up SSO", "set_up_sso": "Set up SSO",
@ -1765,6 +1778,7 @@
"stop_on_first_error_enabled_title": "No PDF: Stop on first error enabled", "stop_on_first_error_enabled_title": "No PDF: Stop on first error enabled",
"stop_on_validation_error": "Check syntax before compile", "stop_on_validation_error": "Check syntax before compile",
"store_your_work": "Store your work on your own infrastructure", "store_your_work": "Store your work on your own infrastructure",
"stretch_width_to_text": "Stretch width to text",
"student": "Student", "student": "Student",
"student_and_faculty_support_make_difference": "Student and faculty support make a difference! We can share this information with our contacts at your university when discussing an Overleaf institutional account.", "student_and_faculty_support_make_difference": "Student and faculty support make a difference! We can share this information with our contacts at your university when discussing an Overleaf institutional account.",
"student_disclaimer": "The educational discount applies to all students at secondary and postsecondary institutions (schools and universities). We may contact you to confirm that youre eligible for the discount.", "student_disclaimer": "The educational discount applies to all students at secondary and postsecondary institutions (schools and universities). We may contact you to confirm that youre eligible for the discount.",
@ -1889,6 +1903,7 @@
"to_insert_or_move_a_caption_make_sure_tabular_is_directly_within_table": "To insert or move a caption, make sure \\begin{tabular} is directly within a table environment", "to_insert_or_move_a_caption_make_sure_tabular_is_directly_within_table": "To insert or move a caption, make sure \\begin{tabular} is directly within a table environment",
"to_many_login_requests_2_mins": "This account has had too many login requests. Please wait 2 minutes before trying to log in again", "to_many_login_requests_2_mins": "This account has had too many login requests. Please wait 2 minutes before trying to log in again",
"to_modify_your_subscription_go_to": "To modify your subscription go to", "to_modify_your_subscription_go_to": "To modify your subscription go to",
"to_use_text_wrapping_in_your_table_make_sure_you_include_the_array_package": "<0>Please note:</0> To use text wrapping in your table, make sure you include the <1>array</1> package in your document preamble:",
"toggle_compile_options_menu": "Toggle compile options menu", "toggle_compile_options_menu": "Toggle compile options menu",
"token": "token", "token": "token",
"token_access_failure": "Cannot grant access; contact the project owner for help", "token_access_failure": "Cannot grant access; contact the project owner for help",

View file

@ -4,7 +4,7 @@ import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/
import { mockScope } from '../helpers/mock-scope' import { mockScope } from '../helpers/mock-scope'
const Container: FC = ({ children }) => ( const Container: FC = ({ children }) => (
<div style={{ width: 785, height: 785 }}>{children}</div> <div style={{ width: 1000, height: 800 }}>{children}</div>
) )
const mountEditor = (content: string | string[]) => { const mountEditor = (content: string | string[]) => {
@ -16,7 +16,7 @@ const mountEditor = (content: string | string[]) => {
} }
const scope = mockScope(content) const scope = mockScope(content)
scope.editor.showVisual = true scope.editor.showVisual = true
cy.viewport(1000, 800)
cy.mount( cy.mount(
<Container> <Container>
<EditorProviders scope={scope}> <EditorProviders scope={scope}>