mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
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:
parent
1708ccfcf5
commit
80a6424343
18 changed files with 906 additions and 44 deletions
|
@ -54,6 +54,7 @@
|
|||
"additional_licenses": "",
|
||||
"address_line_1": "",
|
||||
"address_second_line_optional": "",
|
||||
"adjust_column_width": "",
|
||||
"aggregate_changed": "",
|
||||
"aggregate_to": "",
|
||||
"agree_with_the_terms": "",
|
||||
|
@ -164,6 +165,9 @@
|
|||
"collabs_per_proj": "",
|
||||
"collabs_per_proj_single": "",
|
||||
"collapse": "",
|
||||
"column_width": "",
|
||||
"column_width_is_custom_click_to_resize": "",
|
||||
"column_width_is_x_click_to_resize": "",
|
||||
"comment": "",
|
||||
"comment_submit_error": "",
|
||||
"commit": "",
|
||||
|
@ -225,6 +229,7 @@
|
|||
"currently_seeing_only_24_hrs_history": "",
|
||||
"currently_signed_in_as_x": "",
|
||||
"currently_subscribed_to_plan": "",
|
||||
"custom": "",
|
||||
"custom_borders": "",
|
||||
"customize_your_group_subscription": "",
|
||||
"customizing_figures": "",
|
||||
|
@ -344,6 +349,7 @@
|
|||
"enabling": "",
|
||||
"end_of_document": "",
|
||||
"enter_6_digit_code": "",
|
||||
"enter_any_size_including_units_or_valid_latex_command": "",
|
||||
"enter_image_url": "",
|
||||
"entry_point": "",
|
||||
"error": "",
|
||||
|
@ -393,6 +399,8 @@
|
|||
"fit_to_height": "",
|
||||
"fit_to_width": "",
|
||||
"fix_issues": "",
|
||||
"fixed_width": "",
|
||||
"fixed_width_wrap_text": "",
|
||||
"fold_line": "",
|
||||
"folder_location": "",
|
||||
"following_paths_conflict": "",
|
||||
|
@ -600,6 +608,7 @@
|
|||
"join_project": "",
|
||||
"join_team_explanation": "",
|
||||
"joining": "",
|
||||
"justify": "",
|
||||
"keep_current_plan": "",
|
||||
"keep_personal_projects_separate": "",
|
||||
"keybindings": "",
|
||||
|
@ -639,6 +648,7 @@
|
|||
"license_for_educational_purposes": "",
|
||||
"limited_offer": "",
|
||||
"line_height": "",
|
||||
"line_width_is_the_width_of_the_line_in_the_current_environment": "",
|
||||
"link": "",
|
||||
"link_account": "",
|
||||
"link_accounts": "",
|
||||
|
@ -854,6 +864,7 @@
|
|||
"pending_additional_licenses": "",
|
||||
"pending_invite": "",
|
||||
"percent_discount_for_groups": "",
|
||||
"percent_is_the_percentage_of_the_line_width": "",
|
||||
"plan": "",
|
||||
"plan_tooltip": "",
|
||||
"please_ask_the_project_owner_to_upgrade_to_track_changes": "",
|
||||
|
@ -1065,6 +1076,7 @@
|
|||
"security": "",
|
||||
"see_changes_in_your_documents_live": "",
|
||||
"select_a_column_or_a_merged_cell_to_align": "",
|
||||
"select_a_column_to_adjust_column_width": "",
|
||||
"select_a_file": "",
|
||||
"select_a_file_figure_modal": "",
|
||||
"select_a_group_optional": "",
|
||||
|
@ -1104,6 +1116,7 @@
|
|||
"session_expired_redirecting_to_login": "",
|
||||
"sessions": "",
|
||||
"set_color": "",
|
||||
"set_column_width": "",
|
||||
"set_up_sso": "",
|
||||
"settings": "",
|
||||
"setup_another_account_under_a_personal_email_address": "",
|
||||
|
@ -1195,6 +1208,7 @@
|
|||
"stop_on_first_error_enabled_title": "",
|
||||
"stop_on_validation_error": "",
|
||||
"store_your_work": "",
|
||||
"stretch_width_to_text": "",
|
||||
"student_disclaimer": "",
|
||||
"subject": "",
|
||||
"subject_area": "",
|
||||
|
@ -1278,6 +1292,7 @@
|
|||
"to_confirm_transfer_enter_email_address": "",
|
||||
"to_insert_or_move_a_caption_make_sure_tabular_is_directly_within_table": "",
|
||||
"to_modify_your_subscription_go_to": "",
|
||||
"to_use_text_wrapping_in_your_table_make_sure_you_include_the_array_package": "",
|
||||
"toggle_compile_options_menu": "",
|
||||
"token": "",
|
||||
"token_limit_reached": "",
|
||||
|
|
|
@ -121,7 +121,7 @@ const Toolbar = memo(function Toolbar() {
|
|||
|
||||
// calculate overflow when active element changes to/from inside a table
|
||||
const insideTable = document.activeElement?.closest(
|
||||
'.table-generator-help-modal,.table-generator'
|
||||
'.table-generator-help-modal,.table-generator,.table-generator-width-modal'
|
||||
)
|
||||
useEffect(() => {
|
||||
if (resizeRef.current) {
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -331,6 +331,42 @@ export class TableSelection {
|
|||
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() {
|
||||
const { minX, maxX } = this.normalized()
|
||||
return maxX - minX + 1
|
||||
|
|
|
@ -14,6 +14,9 @@ const TabularContext = createContext<
|
|||
showHelp: () => void
|
||||
hideHelp: () => void
|
||||
helpShown: boolean
|
||||
columnWidthModalShown: boolean
|
||||
openColumnWidthModal: () => void
|
||||
closeColumnWidthModal: () => void
|
||||
}
|
||||
| undefined
|
||||
>(undefined)
|
||||
|
@ -21,10 +24,29 @@ const TabularContext = createContext<
|
|||
export const TabularProvider: FC = ({ children }) => {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const [helpShown, setHelpShown] = useState(false)
|
||||
const [columnWidthModalShown, setColumnWidthModalShown] = useState(false)
|
||||
const showHelp = useCallback(() => setHelpShown(true), [])
|
||||
const hideHelp = useCallback(() => setHelpShown(false), [])
|
||||
const openColumnWidthModal = useCallback(
|
||||
() => setColumnWidthModalShown(true),
|
||||
[]
|
||||
)
|
||||
const closeColumnWidthModal = useCallback(
|
||||
() => setColumnWidthModalShown(false),
|
||||
[]
|
||||
)
|
||||
return (
|
||||
<TabularContext.Provider value={{ ref, helpShown, showHelp, hideHelp }}>
|
||||
<TabularContext.Provider
|
||||
value={{
|
||||
ref,
|
||||
helpShown,
|
||||
showHelp,
|
||||
hideHelp,
|
||||
columnWidthModalShown,
|
||||
openColumnWidthModal,
|
||||
closeColumnWidthModal,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TabularContext.Provider>
|
||||
)
|
||||
|
|
|
@ -19,6 +19,8 @@ import { useCodeMirrorViewContext } from '../codemirror-editor'
|
|||
import { undo, redo } from '@codemirror/commands'
|
||||
import { ChangeSpec } from '@codemirror/state'
|
||||
import { startCompileKeypress } from '@/features/pdf-preview/hooks/use-compile-triggers'
|
||||
import { useTabularContext } from './contexts/tabular-context'
|
||||
import { ColumnSizeIndicator } from './column-size-indicator'
|
||||
|
||||
type NavigationKey =
|
||||
| 'ArrowRight'
|
||||
|
@ -52,6 +54,7 @@ export const Table: FC = () => {
|
|||
updateCellData,
|
||||
} = useEditingContext()
|
||||
const { table: tableData } = useTableContext()
|
||||
const { openColumnWidthModal, columnWidthModalShown } = useTabularContext()
|
||||
const tableRef = useRef<HTMLTableElement>(null)
|
||||
const view = useCodeMirrorViewContext()
|
||||
const { columns: cellWidths, tableWidth } = useMemo(() => {
|
||||
|
@ -276,8 +279,9 @@ export const Table: FC = () => {
|
|||
if (view.state.readOnly) {
|
||||
return false
|
||||
}
|
||||
if (cellData || !selection) {
|
||||
// We're editing a cell, so allow browser to insert there
|
||||
if (cellData || !selection || columnWidthModalShown) {
|
||||
// We're editing a cell, or modifying column widths,
|
||||
// so allow browser to insert there
|
||||
return false
|
||||
}
|
||||
event.preventDefault()
|
||||
|
@ -312,8 +316,9 @@ export const Table: FC = () => {
|
|||
}
|
||||
|
||||
const onCopy = (event: ClipboardEvent) => {
|
||||
if (cellData || !selection) {
|
||||
// We're editing a cell, so allow browser to insert there
|
||||
if (cellData || !selection || columnWidthModalShown) {
|
||||
// We're editing a cell, or modifying column widths,
|
||||
// so allow browser to copy from there
|
||||
return false
|
||||
}
|
||||
event.preventDefault()
|
||||
|
@ -334,7 +339,22 @@ export const Table: FC = () => {
|
|||
window.removeEventListener('paste', onPaste)
|
||||
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 (
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
|
||||
|
@ -352,6 +372,21 @@ export const Table: FC = () => {
|
|||
))}
|
||||
</colgroup>
|
||||
<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>
|
||||
<td />
|
||||
{tableData.columns.map((_, columnIndex) => (
|
||||
|
|
|
@ -27,6 +27,8 @@ import { BorderTheme } from './toolbar/commands'
|
|||
import { TableGeneratorHelpModal } from './help-modal'
|
||||
import { SplitTestProvider } from '../../../../shared/context/split-test-context'
|
||||
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 = {
|
||||
alignment: 'left' | 'center' | 'right' | 'paragraph'
|
||||
|
@ -35,6 +37,9 @@ export type ColumnDefinition = {
|
|||
content: string
|
||||
cellSpacingLeft: string
|
||||
cellSpacingRight: string
|
||||
customCellDefinition: string
|
||||
isParagraphColumn: boolean
|
||||
size?: WidthSelection
|
||||
}
|
||||
|
||||
export type CellData = {
|
||||
|
@ -252,6 +257,7 @@ export const Tabular: FC<{
|
|||
<EditingContextProvider>
|
||||
<TabularWrapper />
|
||||
</EditingContextProvider>
|
||||
<ColumnWidthModal />
|
||||
</SelectionContextProvider>
|
||||
</TableProvider>
|
||||
<TableGeneratorHelpModal />
|
||||
|
@ -271,7 +277,8 @@ const TabularWrapper: FC = () => {
|
|||
const listener: (event: MouseEvent) => void = event => {
|
||||
if (
|
||||
!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) {
|
||||
setSelection(null)
|
||||
|
|
|
@ -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'
|
||||
}
|
|
@ -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=" "
|
||||
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>
|
||||
)
|
||||
}
|
|
@ -14,6 +14,7 @@ import {
|
|||
extendForwardsOverEmptyLines,
|
||||
} from '../../../extensions/visual/selection'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { WidthSelection } from './column-width-modal/column-width'
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
export enum BorderTheme {
|
||||
|
@ -145,11 +146,15 @@ const addColumnBordersToSpecification = (specification: ColumnDefinition[]) => {
|
|||
export const setAlignment = (
|
||||
view: EditorView,
|
||||
selection: TableSelection,
|
||||
alignment: 'left' | 'right' | 'center',
|
||||
alignment: ColumnDefinition['alignment'],
|
||||
positions: Positions,
|
||||
table: TableData
|
||||
) => {
|
||||
if (selection.isMergedCellSelected(table)) {
|
||||
if (alignment === 'paragraph') {
|
||||
// shouldn't happen
|
||||
return
|
||||
}
|
||||
// change for mergedColumn
|
||||
const { minX, minY } = selection.normalized()
|
||||
const cell = table.getCell(minY, minX)
|
||||
|
@ -189,8 +194,16 @@ export const setAlignment = (
|
|||
continue
|
||||
}
|
||||
columnSpecification[i].alignment = alignment
|
||||
// TODO: This won't work for paragraph, which needs width argument
|
||||
columnSpecification[i].content = alignment[0]
|
||||
if (columnSpecification[i].isParagraphColumn) {
|
||||
columnSpecification[i].customCellDefinition =
|
||||
generateParagraphColumnSpecification(alignment)
|
||||
} else {
|
||||
if (alignment === 'paragraph') {
|
||||
// shouldn't happen
|
||||
continue
|
||||
}
|
||||
columnSpecification[i].content = alignment[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
const newSpecification = generateColumnSpecification(columnSpecification)
|
||||
|
@ -214,10 +227,11 @@ const generateColumnSpecification = (columns: ColumnDefinition[]) => {
|
|||
content,
|
||||
cellSpacingLeft,
|
||||
cellSpacingRight,
|
||||
customCellDefinition,
|
||||
}) =>
|
||||
`${'|'.repeat(
|
||||
borderLeft
|
||||
)}${cellSpacingLeft}${content}${cellSpacingRight}${'|'.repeat(
|
||||
)}${cellSpacingLeft}${customCellDefinition}${content}${cellSpacingRight}${'|'.repeat(
|
||||
borderRight
|
||||
)}`
|
||||
)
|
||||
|
@ -439,6 +453,8 @@ export const insertColumn = (
|
|||
content: 'l',
|
||||
cellSpacingLeft: '',
|
||||
cellSpacingRight: '',
|
||||
customCellDefinition: '',
|
||||
isParagraphColumn: false,
|
||||
}))
|
||||
)
|
||||
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,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import Tooltip from '../../../../../shared/components/tooltip'
|
|||
import { useTabularContext } from '../contexts/tabular-context'
|
||||
import { emitTableGeneratorEvent } from '../analytics'
|
||||
import { useCodeMirrorViewContext } from '../../codemirror-editor'
|
||||
import classNames from 'classnames'
|
||||
|
||||
export const ToolbarDropdown: FC<{
|
||||
id: string
|
||||
|
@ -15,6 +16,7 @@ export const ToolbarDropdown: FC<{
|
|||
tooltip?: string
|
||||
disabled?: boolean
|
||||
disabledTooltip?: string
|
||||
showCaret?: boolean
|
||||
}> = ({
|
||||
id,
|
||||
label,
|
||||
|
@ -24,6 +26,7 @@ export const ToolbarDropdown: FC<{
|
|||
tooltip,
|
||||
disabled,
|
||||
disabledTooltip,
|
||||
showCaret,
|
||||
}) => {
|
||||
const { open, onToggle, ref } = useDropdown()
|
||||
const toggleButtonRef = useRef<HTMLButtonElement | null>(null)
|
||||
|
@ -48,6 +51,7 @@ export const ToolbarDropdown: FC<{
|
|||
>
|
||||
{label && <span>{label}</span>}
|
||||
<MaterialIcon type={icon} />
|
||||
{showCaret && <MaterialIcon type="expand_more" />}
|
||||
</button>
|
||||
)
|
||||
const overlay = tabularRef.current && (
|
||||
|
@ -110,8 +114,10 @@ export const ToolbarDropdownItem: FC<
|
|||
Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick'> & {
|
||||
command: () => void
|
||||
id: string
|
||||
icon?: string
|
||||
active?: boolean
|
||||
}
|
||||
> = ({ children, command, id, ...props }) => {
|
||||
> = ({ children, command, id, icon, active, ...props }) => {
|
||||
const view = useCodeMirrorViewContext()
|
||||
const onClick = useCallback(() => {
|
||||
emitTableGeneratorEvent(view, id)
|
||||
|
@ -119,13 +125,17 @@ export const ToolbarDropdownItem: FC<
|
|||
}, [view, command, id])
|
||||
return (
|
||||
<button
|
||||
className="ol-cm-toolbar-menu-item"
|
||||
className={classNames('ol-cm-toolbar-menu-item', {
|
||||
'ol-cm-toolbar-dropdown-option-active': active,
|
||||
})}
|
||||
role="menuitem"
|
||||
type="button"
|
||||
{...props}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
{icon && <MaterialIcon type={icon} />}
|
||||
<span className="ol-cm-toolbar-dropdown-option-content">{children}</span>
|
||||
{active && <MaterialIcon type="check" />}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
mergeCells,
|
||||
moveCaption,
|
||||
removeCaption,
|
||||
removeColumnWidths,
|
||||
removeNodes,
|
||||
removeRowOrColumns,
|
||||
setAlignment,
|
||||
|
@ -22,6 +23,9 @@ import { useTableContext } from '../contexts/table-context'
|
|||
import { useTabularContext } from '../contexts/tabular-context'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FeedbackBadge } from '@/shared/components/feedback-badge'
|
||||
import classNames from 'classnames'
|
||||
|
||||
type CaptionPosition = 'no_caption' | 'above' | 'below'
|
||||
|
||||
export const Toolbar = memo(function Toolbar() {
|
||||
const { selection, setSelection } = useSelectionContext()
|
||||
|
@ -34,7 +38,7 @@ export const Toolbar = memo(function Toolbar() {
|
|||
table,
|
||||
directTableChild,
|
||||
} = useTableContext()
|
||||
const { showHelp } = useTabularContext()
|
||||
const { showHelp, openColumnWidthModal } = useTabularContext()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const borderDropdownLabel = useMemo(() => {
|
||||
|
@ -48,15 +52,26 @@ export const Toolbar = memo(function Toolbar() {
|
|||
}
|
||||
}, [table, t])
|
||||
|
||||
const captionLabel = useMemo(() => {
|
||||
const captionPosition: CaptionPosition = useMemo(() => {
|
||||
if (!tableEnvironment?.caption) {
|
||||
return t('no_caption')
|
||||
return 'no_caption'
|
||||
}
|
||||
if (tableEnvironment.caption.from < positions.tabular.from) {
|
||||
return t('caption_above')
|
||||
return 'above'
|
||||
}
|
||||
return t('caption_below')
|
||||
}, [tableEnvironment, positions.tabular.from, t])
|
||||
return 'below'
|
||||
}, [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(() => {
|
||||
if (!selection) {
|
||||
|
@ -94,14 +109,27 @@ export const Toolbar = memo(function Toolbar() {
|
|||
}
|
||||
}, [currentAlignment])
|
||||
|
||||
const hasCustomSizes = useMemo(
|
||||
() => table.columns.some(x => x.size),
|
||||
[table.columns]
|
||||
)
|
||||
|
||||
if (!selection) {
|
||||
return null
|
||||
}
|
||||
const columnsToInsert = selection.maximumCellWidth(table)
|
||||
const rowsToInsert = selection.height()
|
||||
|
||||
const onlyFixedWidthColumnsSelected = selection.isOnlyFixedWidthColumns(table)
|
||||
const onlyNonFixedWidthColumnsSelected =
|
||||
selection.isOnlyNonFixedWidthColumns(table)
|
||||
|
||||
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">
|
||||
<ToolbarDropdown
|
||||
id="table-generator-caption-dropdown"
|
||||
|
@ -116,6 +144,7 @@ export const Toolbar = memo(function Toolbar() {
|
|||
command={() => {
|
||||
removeCaption(view, tableEnvironment)
|
||||
}}
|
||||
active={captionPosition === 'no_caption'}
|
||||
>
|
||||
{t('no_caption')}
|
||||
</ToolbarDropdownItem>
|
||||
|
@ -124,6 +153,7 @@ export const Toolbar = memo(function Toolbar() {
|
|||
command={() => {
|
||||
moveCaption(view, positions, 'above', tableEnvironment)
|
||||
}}
|
||||
active={captionPosition === 'above'}
|
||||
>
|
||||
{t('caption_above')}
|
||||
</ToolbarDropdownItem>
|
||||
|
@ -132,6 +162,7 @@ export const Toolbar = memo(function Toolbar() {
|
|||
command={() => {
|
||||
moveCaption(view, positions, 'below', tableEnvironment)
|
||||
}}
|
||||
active={captionPosition === 'below'}
|
||||
>
|
||||
{t('caption_below')}
|
||||
</ToolbarDropdownItem>
|
||||
|
@ -151,11 +182,10 @@ export const Toolbar = memo(function Toolbar() {
|
|||
table
|
||||
)
|
||||
}}
|
||||
active={table.getBorderTheme() === BorderTheme.FULLY_BORDERED}
|
||||
icon="border_all"
|
||||
>
|
||||
<MaterialIcon type="border_all" />
|
||||
<span className="table-generator-button-label">
|
||||
{t('all_borders')}
|
||||
</span>
|
||||
{t('all_borders')}
|
||||
</ToolbarDropdownItem>
|
||||
<ToolbarDropdownItem
|
||||
id="table-generator-borders-no-borders"
|
||||
|
@ -168,11 +198,10 @@ export const Toolbar = memo(function Toolbar() {
|
|||
table
|
||||
)
|
||||
}}
|
||||
active={table.getBorderTheme() === BorderTheme.NO_BORDERS}
|
||||
icon="border_clear"
|
||||
>
|
||||
<MaterialIcon type="border_clear" />
|
||||
<span className="table-generator-button-label">
|
||||
{t('no_borders')}
|
||||
</span>
|
||||
{t('no_borders')}
|
||||
</ToolbarDropdownItem>
|
||||
<div className="table-generator-border-options-coming-soon">
|
||||
<div className="info-icon">
|
||||
|
@ -200,6 +229,7 @@ export const Toolbar = memo(function Toolbar() {
|
|||
command={() => {
|
||||
setAlignment(view, selection, 'left', positions, table)
|
||||
}}
|
||||
active={currentAlignment === 'left'}
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon="format_align_center"
|
||||
|
@ -208,6 +238,7 @@ export const Toolbar = memo(function Toolbar() {
|
|||
command={() => {
|
||||
setAlignment(view, selection, 'center', positions, table)
|
||||
}}
|
||||
active={currentAlignment === 'center'}
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon="format_align_right"
|
||||
|
@ -216,8 +247,66 @@ export const Toolbar = memo(function Toolbar() {
|
|||
command={() => {
|
||||
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>
|
||||
<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
|
||||
icon="cell_merge"
|
||||
id="table-generator-merge-cells"
|
||||
|
@ -361,7 +450,7 @@ export const Toolbar = memo(function Toolbar() {
|
|||
<div className="toolbar-beta-badge">
|
||||
<FeedbackBadge
|
||||
id="table-generator-feedback"
|
||||
url="https://forms.gle/ri3fzV1oQDAjmfmD7"
|
||||
url="https://forms.gle/9dHxXPGugxEHgY3L9"
|
||||
text={<FeedbackBadgeContent />}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -2,8 +2,15 @@ import { EditorState } from '@codemirror/state'
|
|||
import { SyntaxNode } from '@lezer/common'
|
||||
import { CellData, ColumnDefinition, TableData } from './tabular'
|
||||
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 RowPosition = {
|
||||
|
@ -40,6 +47,9 @@ export function parseColumnSpecifications(
|
|||
let currentContent = ''
|
||||
let currentCellSpacingLeft = ''
|
||||
let currentCellSpacingRight = ''
|
||||
let currentCustomCellDefinition = ''
|
||||
let currentIsParagraphColumn = false
|
||||
let currentSize: WidthSelection | undefined
|
||||
function maybeCommit() {
|
||||
if (currentAlignment !== undefined) {
|
||||
columns.push({
|
||||
|
@ -49,6 +59,9 @@ export function parseColumnSpecifications(
|
|||
content: currentContent,
|
||||
cellSpacingLeft: currentCellSpacingLeft,
|
||||
cellSpacingRight: currentCellSpacingRight,
|
||||
customCellDefinition: currentCustomCellDefinition,
|
||||
isParagraphColumn: currentIsParagraphColumn,
|
||||
size: currentSize,
|
||||
})
|
||||
currentAlignment = undefined
|
||||
currentBorderLeft = 0
|
||||
|
@ -56,10 +69,13 @@ export function parseColumnSpecifications(
|
|||
currentContent = ''
|
||||
currentCellSpacingLeft = ''
|
||||
currentCellSpacingRight = ''
|
||||
currentCustomCellDefinition = ''
|
||||
currentIsParagraphColumn = false
|
||||
currentSize = undefined
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < specification.length; i++) {
|
||||
if (ALIGNMENT_CHARACTERS.includes(specification.charAt(i))) {
|
||||
if (COMMIT_CHARACTERS.includes(specification.charAt(i))) {
|
||||
maybeCommit()
|
||||
}
|
||||
const hasAlignment = currentAlignment !== undefined
|
||||
|
@ -85,13 +101,55 @@ export function parseColumnSpecifications(
|
|||
currentAlignment = 'right'
|
||||
currentContent += 'r'
|
||||
break
|
||||
case 'p': {
|
||||
case 'p':
|
||||
case 'm':
|
||||
case 'b': {
|
||||
currentIsParagraphColumn = true
|
||||
currentAlignment = 'paragraph'
|
||||
currentContent += 'p'
|
||||
// TODO: Parse these details
|
||||
if (currentCustomCellDefinition !== '') {
|
||||
// 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 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
|
||||
currentContent += specification.slice(i + 1, argumentEnd + 1)
|
||||
currentContent += columnDefinition.slice(1)
|
||||
i = argumentEnd
|
||||
break
|
||||
}
|
||||
|
@ -109,6 +167,14 @@ export function parseColumnSpecifications(
|
|||
}
|
||||
break
|
||||
}
|
||||
case '>': {
|
||||
const argumentEnd = parseArgument(specification, i + 1)
|
||||
// Include the >
|
||||
const argument = specification.slice(i, argumentEnd + 1)
|
||||
i = argumentEnd
|
||||
currentCustomCellDefinition = argument
|
||||
break
|
||||
}
|
||||
case ' ':
|
||||
case '\n':
|
||||
case '\t':
|
||||
|
|
|
@ -22,10 +22,21 @@ export const tableGeneratorTheme = EditorView.baseTheme({
|
|||
'--table-generator-toolbar-dropdown-disabled-background':
|
||||
'rgba(125,125,125,0.3)',
|
||||
'--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-error-background': '#2c3645',
|
||||
'--table-generator-error-color': '#fff',
|
||||
'--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': {
|
||||
|
@ -48,10 +59,20 @@ export const tableGeneratorTheme = EditorView.baseTheme({
|
|||
'--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-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-error-background': '#F1F4F9',
|
||||
'--table-generator-error-color': 'black',
|
||||
'--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': {
|
||||
|
@ -162,6 +183,7 @@ export const tableGeneratorTheme = EditorView.baseTheme({
|
|||
|
||||
'&::after': {
|
||||
width: '4px',
|
||||
bottom: '4px',
|
||||
height: 'calc(100% - 8px)',
|
||||
},
|
||||
},
|
||||
|
@ -172,6 +194,7 @@ export const tableGeneratorTheme = EditorView.baseTheme({
|
|||
'&::after': {
|
||||
width: 'calc(100% - 8px)',
|
||||
height: '4px',
|
||||
right: '4px',
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -179,8 +202,8 @@ export const tableGeneratorTheme = EditorView.baseTheme({
|
|||
content: '""',
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
bottom: '4px',
|
||||
right: '4px',
|
||||
bottom: '8px',
|
||||
right: '8px',
|
||||
width: 'calc(100% - 8px)',
|
||||
height: 'calc(100% - 8px)',
|
||||
'background-color': 'var(--table-generator-selector-background-color)',
|
||||
|
@ -199,7 +222,6 @@ export const tableGeneratorTheme = EditorView.baseTheme({
|
|||
|
||||
'.table-generator-floating-toolbar': {
|
||||
position: 'absolute',
|
||||
top: '0',
|
||||
transform: 'translateY(-100%)',
|
||||
left: '0',
|
||||
right: '0',
|
||||
|
@ -216,6 +238,9 @@ export const tableGeneratorTheme = EditorView.baseTheme({
|
|||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
rowGap: '8px',
|
||||
'&.table-generator-toolbar-floating-custom-sizes': {
|
||||
top: '-8px',
|
||||
},
|
||||
},
|
||||
|
||||
'.table-generator-toolbar-button': {
|
||||
|
@ -375,6 +400,7 @@ export const tableGeneratorTheme = EditorView.baseTheme({
|
|||
display: 'flex',
|
||||
'flex-direction': 'column',
|
||||
'min-width': '200px',
|
||||
padding: '4px',
|
||||
|
||||
'& > button': {
|
||||
border: 'none',
|
||||
|
@ -382,11 +408,11 @@ export const tableGeneratorTheme = EditorView.baseTheme({
|
|||
background: 'transparent',
|
||||
'white-space': 'nowrap',
|
||||
color: 'var(--table-generator-toolbar-button-color)',
|
||||
'border-radius': '0',
|
||||
'border-radius': '4px',
|
||||
'font-size': '14px',
|
||||
display: 'flex',
|
||||
'align-items': 'center',
|
||||
'justify-content': 'space-between',
|
||||
'justify-content': 'flex-start',
|
||||
'column-gap': '8px',
|
||||
'align-self': 'stretch',
|
||||
padding: '12px 8px',
|
||||
|
@ -398,6 +424,12 @@ export const tableGeneratorTheme = EditorView.baseTheme({
|
|||
'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': {
|
||||
'background-color':
|
||||
'var(--table-generator-toolbar-button-hover-background)',
|
||||
|
@ -408,6 +440,18 @@ export const tableGeneratorTheme = EditorView.baseTheme({
|
|||
'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': {
|
||||
'box-shadow': 'none',
|
||||
},
|
||||
|
@ -428,6 +472,11 @@ export const tableGeneratorTheme = EditorView.baseTheme({
|
|||
border: '0',
|
||||
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':
|
||||
|
@ -465,4 +514,43 @@ export const tableGeneratorTheme = EditorView.baseTheme({
|
|||
'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',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
@import './editor/dictionary.less';
|
||||
@import './editor/compile-button.less';
|
||||
@import './editor/figure-modal.less';
|
||||
@import './editor/table-generator-column-width-modal.less';
|
||||
@import './editor/ide-react.less';
|
||||
|
||||
@ui-layout-toggler-def-height: 50px;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -79,6 +79,7 @@
|
|||
"address": "Address",
|
||||
"address_line_1": "Address",
|
||||
"address_second_line_optional": "Address second line (optional)",
|
||||
"adjust_column_width": "Adjust column width",
|
||||
"admin": "admin",
|
||||
"admin_user_created_message": "Created admin user, <a href=\"__link__\">Log in here</a> to continue",
|
||||
"advanced_reference_search": "Advanced <0>reference search</0>",
|
||||
|
@ -263,6 +264,9 @@
|
|||
"collabs_per_proj": "__collabcount__ collaborators per project",
|
||||
"collabs_per_proj_single": "__collabcount__ collaborator per project",
|
||||
"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_submit_error": "Sorry, there was a problem submitting your comment",
|
||||
"commit": "Commit",
|
||||
|
@ -347,6 +351,7 @@
|
|||
"currently_seeing_only_24_hrs_history": "You’re currently seeing the last 24 hours of changes in this project.",
|
||||
"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.",
|
||||
"custom": "Custom",
|
||||
"custom_borders": "Custom borders",
|
||||
"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.",
|
||||
|
@ -510,6 +515,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 any size (including units) or valid LaTeX command",
|
||||
"enter_image_url": "Enter image URL",
|
||||
"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",
|
||||
|
@ -598,6 +604,8 @@
|
|||
"fit_to_height": "Fit to height",
|
||||
"fit_to_width": "Fit to width",
|
||||
"fix_issues": "Fix issues",
|
||||
"fixed_width": "Fixed width",
|
||||
"fixed_width_wrap_text": "Fixed width, wrap text",
|
||||
"fold_line": "Fold line",
|
||||
"folder_location": "Folder location",
|
||||
"folders": "Folders",
|
||||
|
@ -914,6 +922,7 @@
|
|||
"joining": "Joining",
|
||||
"july": "July",
|
||||
"june": "June",
|
||||
"justify": "Justify",
|
||||
"kb_suggestions_enquiry": "Have you checked our <0>__kbLink__</0>?",
|
||||
"keep_current_plan": "Keep my current plan",
|
||||
"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)",
|
||||
"limited_offer": "Limited offer",
|
||||
"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_account": "Link Account",
|
||||
"link_accounts": "Link Accounts",
|
||||
|
@ -1301,6 +1311,7 @@
|
|||
"per_user_year": "per user / year",
|
||||
"per_year": "per year",
|
||||
"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",
|
||||
"personalized_onboarding": "Personalized onboarding",
|
||||
"personalized_onboarding_info": "We’ll help you get everything set up and then we’re 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_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_to_adjust_column_width": "Select a column to adjust column width",
|
||||
"select_a_file": "Select a File",
|
||||
"select_a_file_figure_modal": "Select a file",
|
||||
"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",
|
||||
"sessions": "Sessions",
|
||||
"set_color": "set color",
|
||||
"set_column_width": "Set column width",
|
||||
"set_new_password": "Set new password",
|
||||
"set_password": "Set Password",
|
||||
"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_validation_error": "Check syntax before compile",
|
||||
"store_your_work": "Store your work on your own infrastructure",
|
||||
"stretch_width_to_text": "Stretch width to text",
|
||||
"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_disclaimer": "The educational discount applies to all students at secondary and postsecondary institutions (schools and universities). We may contact you to confirm that you’re 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_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_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",
|
||||
"token": "token",
|
||||
"token_access_failure": "Cannot grant access; contact the project owner for help",
|
||||
|
|
|
@ -4,7 +4,7 @@ import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/
|
|||
import { mockScope } from '../helpers/mock-scope'
|
||||
|
||||
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[]) => {
|
||||
|
@ -16,7 +16,7 @@ const mountEditor = (content: string | string[]) => {
|
|||
}
|
||||
const scope = mockScope(content)
|
||||
scope.editor.showVisual = true
|
||||
|
||||
cy.viewport(1000, 800)
|
||||
cy.mount(
|
||||
<Container>
|
||||
<EditorProviders scope={scope}>
|
||||
|
|
Loading…
Reference in a new issue