@@ -200,6 +229,7 @@ export const Toolbar = memo(function Toolbar() {
command={() => {
setAlignment(view, selection, 'left', positions, table)
}}
+ active={currentAlignment === 'left'}
/>
{
setAlignment(view, selection, 'center', positions, table)
}}
+ active={currentAlignment === 'center'}
/>
{
setAlignment(view, selection, 'right', positions, table)
}}
+ active={currentAlignment === 'right'}
/>
+ {onlyFixedWidthColumnsSelected &&
+ !selection.isMergedCellSelected(table) && (
+ {
+ setAlignment(view, selection, 'paragraph', positions, table)
+ }}
+ active={currentAlignment === 'paragraph'}
+ />
+ )}
+
+
+ removeColumnWidths(view, selection, positions, table)
+ }
+ disabled={!selection.isAnyColumnSelected(table)}
+ >
+ {t('stretch_width_to_text')}
+
+
+ {onlyFixedWidthColumnsSelected
+ ? t('fixed_width')
+ : t('fixed_width_wrap_text')}
+
+ {onlyFixedWidthColumnsSelected && (
+ <>
+
+
+ {t('set_column_width')}
+
+ >
+ )}
+
}
/>
diff --git a/services/web/frontend/js/features/source-editor/components/table-generator/utils.ts b/services/web/frontend/js/features/source-editor/components/table-generator/utils.ts
index 7842a80586..d29b47caa2 100644
--- a/services/web/frontend/js/features/source-editor/components/table-generator/utils.ts
+++ b/services/web/frontend/js/features/source-editor/components/table-generator/utils.ts
@@ -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':
diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/table-generator.ts b/services/web/frontend/js/features/source-editor/extensions/visual/table-generator.ts
index b1a68110e9..4a11cbb896 100644
--- a/services/web/frontend/js/features/source-editor/extensions/visual/table-generator.ts
+++ b/services/web/frontend/js/features/source-editor/extensions/visual/table-generator.ts
@@ -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',
+ },
+ },
})
diff --git a/services/web/frontend/stylesheets/app/editor.less b/services/web/frontend/stylesheets/app/editor.less
index 8954496a1d..34f0e1fee7 100644
--- a/services/web/frontend/stylesheets/app/editor.less
+++ b/services/web/frontend/stylesheets/app/editor.less
@@ -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;
diff --git a/services/web/frontend/stylesheets/app/editor/table-generator-column-width-modal.less b/services/web/frontend/stylesheets/app/editor/table-generator-column-width-modal.less
new file mode 100644
index 0000000000..745920ec54
--- /dev/null
+++ b/services/web/frontend/stylesheets/app/editor/table-generator-column-width-modal.less
@@ -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;
+ }
+}
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index 7d9e1c5e56..b491d6e7f9 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -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,
Log in here to continue",
"advanced_reference_search": "Advanced <0>reference search0>",
@@ -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>array1> 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",
diff --git a/services/web/test/frontend/features/source-editor/components/codemirror-editor-table-generator.spec.tsx b/services/web/test/frontend/features/source-editor/components/codemirror-editor-table-generator.spec.tsx
index 29174704fe..e53e980260 100644
--- a/services/web/test/frontend/features/source-editor/components/codemirror-editor-table-generator.spec.tsx
+++ b/services/web/test/frontend/features/source-editor/components/codemirror-editor-table-generator.spec.tsx
@@ -4,7 +4,7 @@ import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/
import { mockScope } from '../helpers/mock-scope'
const Container: FC = ({ children }) => (
-
{children}
+
{children}
)
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(