mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -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": "",
|
"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": "",
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
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
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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) => (
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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':
|
||||||
|
|
|
@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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": "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": "You’re currently seeing the last 24 hours of changes in this project.",
|
"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_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": "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!",
|
"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_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 you’re 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 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_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",
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
Loading…
Reference in a new issue