diff --git a/services/web/app/src/Features/Project/ProjectController.js b/services/web/app/src/Features/Project/ProjectController.js index e015d47871..4ec958f735 100644 --- a/services/web/app/src/Features/Project/ProjectController.js +++ b/services/web/app/src/Features/Project/ProjectController.js @@ -683,6 +683,21 @@ const ProjectController = { cb() }) }, + sourceEditorToolbarAssigment(cb) { + SplitTestHandler.getAssignment( + req, + res, + 'source-editor-toolbar', + (error, assignment) => { + // do not fail editor load if assignment fails + if (error) { + cb(null, { variant: 'default' }) + } else { + cb(null, assignment) + } + } + ) + }, historyViewAssignment(cb) { SplitTestHandler.getAssignment( req, @@ -729,6 +744,7 @@ const ProjectController = { pdfjsAssignment, editorLeftMenuAssignment, richTextAssignment, + sourceEditorToolbarAssigment, historyViewAssignment, reviewPanelAssignment, } @@ -919,6 +935,9 @@ const ProjectController = { pdfjsVariant: pdfjsAssignment.variant, debugPdfDetach, showLegacySourceEditor, + showSourceToolbar: + !showLegacySourceEditor && + sourceEditorToolbarAssigment.variant === 'enabled', showSymbolPalette, galileoEnabled, galileoFeatures, diff --git a/services/web/app/views/project/editor/editor-pane.pug b/services/web/app/views/project/editor/editor-pane.pug index d19dde250b..66fb1217c0 100644 --- a/services/web/app/views/project/editor/editor-pane.pug +++ b/services/web/app/views/project/editor/editor-pane.pug @@ -22,6 +22,7 @@ include ./file-view .editor-container.full-size( + class={"has-source-toolbar" : showSourceToolbar}, ng-show="ui.view == 'editor' && editor.multiSelectedCount === 0" vertical-resizable-panes="south-pane-resizer" vertical-resizable-panes-hidden-externally-on="south-pane-toggled" diff --git a/services/web/app/views/project/editor/meta.pug b/services/web/app/views/project/editor/meta.pug index 68d14e5c10..15c5a6b433 100644 --- a/services/web/app/views/project/editor/meta.pug +++ b/services/web/app/views/project/editor/meta.pug @@ -22,6 +22,7 @@ meta(name="ol-wsRetryHandshake" data-type="json" content=settings.wsRetryHandsha meta(name="ol-pdfjsVariant" content=pdfjsVariant) meta(name="ol-debugPdfDetach" data-type="boolean" content=debugPdfDetach) meta(name="ol-showLegacySourceEditor", data-type="boolean" content=showLegacySourceEditor) +meta(name="ol-showSourceToolbar", data-type="boolean" content=showSourceToolbar) meta(name="ol-showSymbolPalette" data-type="boolean" content=showSymbolPalette) meta(name="ol-galileoEnabled" data-type="string" content=galileoEnabled) meta(name="ol-galileoPromptWords" data-type="string" content=galileoPromptWords) diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 560fbb5dd7..8a61400235 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -1016,6 +1016,7 @@ "toolbar_insert_figure": "", "toolbar_insert_inline_math": "", "toolbar_insert_link": "", + "toolbar_insert_math": "", "toolbar_insert_table": "", "toolbar_numbered_list": "", "toolbar_redo": "", diff --git a/services/web/frontend/js/features/source-editor/components/codemirror-toolbar.tsx b/services/web/frontend/js/features/source-editor/components/codemirror-toolbar.tsx index cd566a2007..fb038297bf 100644 --- a/services/web/frontend/js/features/source-editor/components/codemirror-toolbar.tsx +++ b/services/web/frontend/js/features/source-editor/components/codemirror-toolbar.tsx @@ -13,6 +13,13 @@ import { ToolbarOverflow } from './toolbar/overflow' import useDropdown from '../../../shared/hooks/use-dropdown' import { getPanel } from '@codemirror/view' import { createToolbarPanel } from '../extensions/toolbar/toolbar-panel' +import EditorSwitch from './editor-switch' +import SwitchToPDFButton from './switch-to-pdf-button' +import { DetacherSynctexControl } from '../../pdf-preview/components/detach-synctex-control' +import DetachCompileButtonWrapper from '../../pdf-preview/components/detach-compile-button-wrapper' +import getMeta from '../../../utils/meta' +import { isVisual } from '../extensions/visual/visual' +import SplitTestBadge from '../../../shared/components/split-test-badge' export const CodeMirrorToolbar = () => { const view = useCodeMirrorViewContext() @@ -26,7 +33,10 @@ export const CodeMirrorToolbar = () => { } const Toolbar = memo(function Toolbar() { + const showSourceToolbar: boolean = getMeta('ol-showSourceToolbar') + const state = useCodeMirrorStateContext() + const view = useCodeMirrorViewContext() const [overflowed, setOverflowed] = useState(false) const [collapsed, setCollapsed] = useState(false) @@ -85,9 +95,13 @@ const Toolbar = memo(function Toolbar() { } return ( -
+
+ {showSourceToolbar && } -
+
+
+ {!isVisual(view) && ( + + )} + {showSourceToolbar && ( + <> + + + + + )}
+ Overleaf has upgraded the source editor. You can still use the old editor + by selecting "Source (legacy)". +
+
+ Click to learn more and give feedback + + ) + + return ( + + + {content} + + + ) +} + +const showLegacySourceEditor: boolean = getMeta('ol-showLegacySourceEditor') +const visualEditorNameVariant: string = getMeta('ol-visualEditorNameVariant') +const isParticipatingInVisualEditorNamingTest: boolean = getMeta( + 'ol-isParticipatingInVisualEditorNamingTest' +) + +function EditorSwitch() { + const [newSourceEditor, setNewSourceEditor] = useScopeValue( + 'editor.newSourceEditor' + ) + const [richText, setRichText] = useScopeValue('editor.showRichText') + const sourceName = + visualEditorNameVariant === 'code-visual' + ? 'Code Editor' + : visualEditorNameVariant === 'source-visual' + ? 'Source Editor' + : 'Source' + + const [visual, setVisual] = useScopeValue('editor.showVisual') + + const [docName] = useScopeValue('editor.open_doc_name') + const richTextAvailable = isValidTeXFile(docName) + const richTextOrVisual = richText || (richTextAvailable && visual) + + const handleChange = useCallback( + event => { + const editorType = event.target.value + + switch (editorType) { + case 'ace': + setRichText(false) + setVisual(false) + setNewSourceEditor(false) + break + + case 'cm6': + setRichText(false) + setVisual(false) + setNewSourceEditor(true) + break + + case 'rich-text': + if (getMeta('ol-richTextVariant') === 'cm6') { + setRichText(false) + setVisual(true) + setNewSourceEditor(true) + } else { + setRichText(true) + setVisual(false) + } + + break + } + + sendMB('editor-switch-change', { editorType }) + }, + [setRichText, setVisual, setNewSourceEditor] + ) + + return ( +
+ {showLegacySourceEditor ? : null} + +
+ Editor mode. + + + + + {showLegacySourceEditor ? ( + <> + + + + ) : null} + + +
+ + {!!richTextOrVisual && !isParticipatingInVisualEditorNamingTest && ( + + )} +
+ ) +} + +const RichTextToggle: FC<{ + checked: boolean + disabled: boolean + handleChange: (event: ChangeEvent) => void +}> = ({ checked, disabled, handleChange }) => { + const { t } = useTranslation() + + const richTextName = + visualEditorNameVariant === 'default' ? 'Rich Text' : 'Visual Editor' + + const toggle = ( + + + + + ) + + if (disabled) { + return ( + + {toggle} + + ) + } + + return toggle +} + +export default memo(EditorSwitch) diff --git a/services/web/frontend/js/features/source-editor/components/editor-switch.tsx b/services/web/frontend/js/features/source-editor/components/editor-switch.tsx index f168902d01..7eb052b781 100644 --- a/services/web/frontend/js/features/source-editor/components/editor-switch.tsx +++ b/services/web/frontend/js/features/source-editor/components/editor-switch.tsx @@ -7,41 +7,6 @@ import isValidTeXFile from '../../../main/is-valid-tex-file' import { useTranslation } from 'react-i18next' import SplitTestBadge from '../../../shared/components/split-test-badge' -function Badge() { - const content = ( - <> - Overleaf has upgraded the source editor. You can still use the old editor - by selecting "Source (legacy)". -
-
- Click to learn more and give feedback - - ) - - return ( - - - {content} - - - ) -} - -const showLegacySourceEditor: boolean = getMeta('ol-showLegacySourceEditor') - function EditorSwitch() { const { t } = useTranslation() const [newSourceEditor, setNewSourceEditor] = useScopeValue( @@ -91,8 +56,6 @@ function EditorSwitch() { return (
- {showLegacySourceEditor ? : null} -
Editor mode. @@ -109,23 +72,6 @@ function EditorSwitch() { {t('code_editor')} - {showLegacySourceEditor ? ( - <> - - - - ) : null} - void -}> = memo(function ButtonMenu({ icon, id, label, altCommand, children }) { +}> = memo(function ButtonMenu({ + icon, + id, + label, + materialIcon, + altCommand, + children, +}) { const target = useRef(null) const { open, onToggle, ref } = useDropdown() const view = useCodeMirrorViewContext() @@ -39,7 +48,7 @@ export const ToolbarButtonMenu: FC<{ }} ref={target} > - + {materialIcon ? : } ) diff --git a/services/web/frontend/js/features/source-editor/components/toolbar/math-dropdown.tsx b/services/web/frontend/js/features/source-editor/components/toolbar/math-dropdown.tsx new file mode 100644 index 0000000000..78f214ec35 --- /dev/null +++ b/services/web/frontend/js/features/source-editor/components/toolbar/math-dropdown.tsx @@ -0,0 +1,49 @@ +import { ListGroupItem } from 'react-bootstrap' +import { ToolbarButtonMenu } from './button-menu' +import { emitCommandEvent } from '../../extensions/toolbar/utils/analytics' +import MaterialIcon from '../../../../shared/components/material-icon' +import { useTranslation } from 'react-i18next' +import { useCodeMirrorViewContext } from '../codemirror-editor' +import { + wrapInDisplayMath, + wrapInInlineMath, +} from '../../extensions/toolbar/commands' + +export function MathDropdown() { + const { t } = useTranslation() + const view = useCodeMirrorViewContext() + + return ( + + { + emitCommandEvent(view, 'toolbar-inline-math') + event.preventDefault() + wrapInInlineMath(view) + view.focus() + }} + > + + {t('toolbar_insert_inline_math')} + + { + emitCommandEvent(view, 'toolbar-display-math') + event.preventDefault() + wrapInDisplayMath(view) + view.focus() + }} + > + + {t('toolbar_insert_display_math')} + + + ) +} diff --git a/services/web/frontend/js/features/source-editor/components/toolbar/toolbar-items.tsx b/services/web/frontend/js/features/source-editor/components/toolbar/toolbar-items.tsx index 1e8282bbf5..07f51df2d6 100644 --- a/services/web/frontend/js/features/source-editor/components/toolbar/toolbar-items.tsx +++ b/services/web/frontend/js/features/source-editor/components/toolbar/toolbar-items.tsx @@ -13,9 +13,10 @@ import { redo, undo } from '@codemirror/commands' import * as commands from '../../extensions/toolbar/commands' import { SectionHeadingDropdown } from './section-heading-dropdown' import { canAddComment } from '../../extensions/toolbar/comments' -import { useTranslation } from 'react-i18next' import getMeta from '../../../../utils/meta' import { InsertFigureDropdown } from './insert-figure-dropdown' +import { useTranslation } from 'react-i18next' +import { MathDropdown } from './math-dropdown' const isMac = /Mac/.test(window.navigator?.platform) @@ -98,22 +99,7 @@ export const ToolbarItems: FC<{ )} {showGroup('group-math') && (
- - + Extension> = importOverleafModules( 'sourceEditorExtensions' @@ -120,6 +122,7 @@ export const createExtensions = (options: Record): Extension[] => [ emptyLineFiller(), trackChanges(options.currentDoc, options.changeManager), visual(options.currentDoc, options.visual), + isSplitTestEnabled('source-editor-toolbar') ? toolbarPanel() : [], verticalOverflow(), highlightActiveLine(options.visual.visual), // The built-in extension that highlights the active line in the gutter. diff --git a/services/web/frontend/js/features/source-editor/extensions/toolbar/toolbar-panel.ts b/services/web/frontend/js/features/source-editor/extensions/toolbar/toolbar-panel.ts index adc3543855..3e8fd9a957 100644 --- a/services/web/frontend/js/features/source-editor/extensions/toolbar/toolbar-panel.ts +++ b/services/web/frontend/js/features/source-editor/extensions/toolbar/toolbar-panel.ts @@ -75,6 +75,9 @@ export const toolbarPanel = () => [ '& .list-group-item': { width: '100%', textAlign: 'start', + display: 'flex', + alignItems: 'center', + gap: '5px', }, }, '.ol-cm-toolbar-button-group': { @@ -90,6 +93,9 @@ export const toolbarPanel = () => [ '&.ol-cm-toolbar-end': { borderLeft: 'none', }, + '&.ol-cm-toolbar-stretch': { + flex: 1, + }, '&.overflow-hidden': { borderLeft: 'none', }, @@ -99,6 +105,9 @@ export const toolbarPanel = () => [ padding: 0, }, }, + '.formatting-buttons-wrapper': { + flex: 1, + }, '.ol-cm-toolbar-button': { display: 'inline-flex', alignItems: 'center', @@ -148,8 +157,10 @@ export const toolbarPanel = () => [ }, }, '.ol-cm-toolbar-end': { - flex: 1, justifyContent: 'flex-end', + '& .badge': { + marginRight: '5px', + }, }, '.ol-cm-toolbar-overflow-toggle': { display: 'none', diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/visual.ts b/services/web/frontend/js/features/source-editor/extensions/visual/visual.ts index 5eafc262b0..1b382de99c 100644 --- a/services/web/frontend/js/features/source-editor/extensions/visual/visual.ts +++ b/services/web/frontend/js/features/source-editor/extensions/visual/visual.ts @@ -17,11 +17,12 @@ import { findEffect } from '../../utils/effects' import { forceParsing, syntaxTree } from '@codemirror/language' import { hasLanguageLoadedEffect } from '../language' import { restoreScrollPosition } from '../scroll-position' -import { toolbarPanel } from '../toolbar/toolbar-panel' import { CurrentDoc } from '../../../../../../types/current-doc' import isValidTeXFile from '../../../../main/is-valid-tex-file' import { listItemMarker } from './list-item-marker' import { figureModalPasteHandler } from '../figure-modal' +import { isSplitTestEnabled } from '../../../../utils/splitTestUtils' +import { toolbarPanel } from '../toolbar/toolbar-panel' type Options = { visual: boolean @@ -197,8 +198,8 @@ const extension = (options: Options) => [ atomicDecorations(options), skipPreambleWithCursor, visualKeymap, - toolbarPanel(), scrollJumpAdjuster, + isSplitTestEnabled('source-editor-toolbar') ? [] : toolbarPanel(), showContentWhenParsed, figureModalPasteHandler(), ] diff --git a/services/web/frontend/stories/editor-switch.stories.js b/services/web/frontend/stories/editor-switch.stories.js index 2db4255768..7ce62fbcef 100644 --- a/services/web/frontend/stories/editor-switch.stories.js +++ b/services/web/frontend/stories/editor-switch.stories.js @@ -1,4 +1,4 @@ -import EditorSwitch from '../js/features/source-editor/components/editor-switch' +import EditorSwitch from '../js/features/source-editor/components/editor-switch-legacy' import { ScopeDecorator } from './decorators/scope' export default { diff --git a/services/web/frontend/stylesheets/app/editor.less b/services/web/frontend/stylesheets/app/editor.less index ff08e83f32..2a89c79c90 100644 --- a/services/web/frontend/stylesheets/app/editor.less +++ b/services/web/frontend/stylesheets/app/editor.less @@ -95,8 +95,14 @@ #editor, #editor-rich-text { .full-size; +} + +.editor-container #editor { top: @editor-toolbar-height; } +.editor-container.has-source-toolbar #editor { + top: 0; +} .pdf-empty, .no-history-available, diff --git a/services/web/frontend/stylesheets/app/editor/compile-button.less b/services/web/frontend/stylesheets/app/editor/compile-button.less index 2e60739752..0e371843f6 100644 --- a/services/web/frontend/stylesheets/app/editor/compile-button.less +++ b/services/web/frontend/stylesheets/app/editor/compile-button.less @@ -13,13 +13,16 @@ margin-left: 6px; } -.detach-compile-button-container when (@is-new-css = false) { - margin-right: -5px; -} +// only apply for legacy editor +.toolbar-pdf-right { + .detach-compile-button-container when (@is-new-css = false) { + margin-right: -5px; + } -// because 2px border on :active state -.detach-compile-button-container when (@is-new-css = true) { - margin-right: -3px; + // because 2px border on :active state + .detach-compile-button-container when (@is-new-css = true) { + margin-right: -3px; + } } .btn-striped-animated { diff --git a/services/web/frontend/stylesheets/app/editor/toolbar.less b/services/web/frontend/stylesheets/app/editor/toolbar.less index dfc47bde63..31d535d3d1 100644 --- a/services/web/frontend/stylesheets/app/editor/toolbar.less +++ b/services/web/frontend/stylesheets/app/editor/toolbar.less @@ -299,6 +299,7 @@ .editor-toggle-switch { display: flex; align-items: center; + white-space: nowrap; .toggle-switch { margin-left: 5px; @@ -317,6 +318,10 @@ padding-right: 8px; border-right: none; } + + .badge { + margin-right: 5px; + } } /************************************** diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 0387528214..cb41bba382 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -1636,6 +1636,7 @@ "toolbar_insert_figure": "Insert Figure", "toolbar_insert_inline_math": "Insert Inline Math", "toolbar_insert_link": "Insert Link", + "toolbar_insert_math": "Insert Math", "toolbar_insert_table": "Insert Table", "toolbar_numbered_list": "Numbered List", "toolbar_redo": "Redo", diff --git a/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-toolbar.spec.tsx b/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-toolbar.spec.tsx index a049b3c146..fa2404de64 100644 --- a/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-toolbar.spec.tsx +++ b/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-toolbar.spec.tsx @@ -18,7 +18,7 @@ const clickToolbarButton = (text: string) => { } const Container: FC = ({ children }) => ( -
{children}
+
{children}
) const mountEditor = (content: string) => { @@ -97,6 +97,7 @@ describe(' toolbar in Rich Text mode', function () { mountEditor('2+3=5') selectAll() + clickToolbarButton('Insert Math') clickToolbarButton('Insert Inline Math') cy.get('.cm-content').should('have.text', '\\(2+3=5\\)') }) @@ -105,6 +106,7 @@ describe(' toolbar in Rich Text mode', function () { mountEditor('2+3=5') selectAll() + clickToolbarButton('Insert Math') clickToolbarButton('Insert Display Math') cy.get('.cm-content').should('have.text', '\\[2+3=5\\]') }) diff --git a/services/web/test/frontend/features/source-editor/components/figure-modal.spec.tsx b/services/web/test/frontend/features/source-editor/components/figure-modal.spec.tsx index 34e0e2c0b4..a8558a5d6e 100644 --- a/services/web/test/frontend/features/source-editor/components/figure-modal.spec.tsx +++ b/services/web/test/frontend/features/source-editor/components/figure-modal.spec.tsx @@ -4,7 +4,7 @@ import { mockScope, rootFolderId } from '../helpers/mock-scope' import { FC } from 'react' const Container: FC = ({ children }) => ( -
{children}
+
{children}
) const clickToolbarButton = (text: string) => {