[cm6] Add toolbar to Source Mode for Beta users (#13429)

* [cm6] toolbar for source mode

* top:0 for new toolbar

* empty div for extensions

* fix legacy css top pos

* show source toolbar split test

* prettier

* show beta icon in source editor

* dropdown toolbar wip

* fix wrong conflict resolve

* math dropdown, chrome extension fixes

* math dropdown cleanup

* sort en.json

* fix sort en.json

* using isVisual

* getMeta in component, pug update

* using flex grow

* toolbar beta badge

* remove extra whitespace

* has-legacy-toolbar class

* Increase container size

* fix tests

* prettier

* styling fixes, using SplitTestBadge

* only show source toolbar if flag is set

* fix typo

---------

Co-authored-by: Alf Eaton <alf.eaton@overleaf.com>
GitOrigin-RevId: 34b01a9421f4a0d6defc40925c5092901575946e
This commit is contained in:
Domagoj Kriskovic 2023-06-30 11:46:46 +02:00 committed by Copybot
parent a14e2aecfb
commit 17452b51d7
21 changed files with 357 additions and 88 deletions

View file

@ -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,

View file

@ -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"

View file

@ -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)

View file

@ -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": "",

View file

@ -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 (
<div className="ol-cm-toolbar" ref={resizeRef}>
<div className="ol-cm-toolbar toolbar-editor" ref={resizeRef}>
{showSourceToolbar && <EditorSwitch />}
<ToolbarItems state={state} />
<div className="ol-cm-toolbar-button-group" ref={overflowBeforeRef}>
<div
className="ol-cm-toolbar-button-group ol-cm-toolbar-stretch"
ref={overflowBeforeRef}
>
<ToolbarOverflow
overflowed={overflowed}
target={overflowBeforeRef.current ?? undefined}
@ -97,6 +111,7 @@ const Toolbar = memo(function Toolbar() {
>
<ToolbarItems state={state} overflowed={overflowedItemsRef.current} />
</ToolbarOverflow>
<div className="formatting-buttons-wrapper" />
</div>
<div className="ol-cm-toolbar-button-group ol-cm-toolbar-end">
<ToolbarButton
@ -106,6 +121,19 @@ const Toolbar = memo(function Toolbar() {
active={searchPanelOpen(state)}
icon="search"
/>
{!isVisual(view) && (
<SplitTestBadge
splitTestName="source-editor-toolbar"
displayOnVariants={['enabled']}
/>
)}
{showSourceToolbar && (
<>
<SwitchToPDFButton />
<DetacherSynctexControl />
<DetachCompileButtonWrapper />
</>
)}
</div>
<div className="ol-cm-toolbar-button-group hidden">
<ToolbarButton

View file

@ -0,0 +1,197 @@
import { ChangeEvent, FC, memo, useCallback } from 'react'
import useScopeValue from '../../../shared/hooks/use-scope-value'
import Tooltip from '../../../shared/components/tooltip'
import { sendMB } from '../../../infrastructure/event-tracking'
import getMeta from '../../../utils/meta'
import SplitTestBadge from '../../../shared/components/split-test-badge'
import isValidTeXFile from '../../../main/is-valid-tex-file'
import { useTranslation } from 'react-i18next'
function Badge() {
const content = (
<>
Overleaf has upgraded the source editor. You can still use the old editor
by selecting "Source (legacy)".
<br />
<br />
Click to learn more and give feedback
</>
)
return (
<Tooltip
id="editor-switch"
description={content}
overlayProps={{
placement: 'bottom',
delayHide: 100,
}}
tooltipProps={{ className: 'tooltip-wide' }}
>
<a
href="https://forms.gle/GmSs6odZRKRp3VX98"
target="_blank"
rel="noopener noreferrer"
className="info-badge"
>
<span className="sr-only">{content}</span>
</a>
</Tooltip>
)
}
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 (
<div className="editor-toggle-switch">
{showLegacySourceEditor ? <Badge /> : null}
<fieldset className="toggle-switch">
<legend className="sr-only">Editor mode.</legend>
<input
type="radio"
name="editor"
value="cm6"
id="editor-switch-cm6"
className="toggle-switch-input"
checked={!richTextOrVisual && !!newSourceEditor}
onChange={handleChange}
/>
<label htmlFor="editor-switch-cm6" className="toggle-switch-label">
<span>{sourceName}</span>
</label>
{showLegacySourceEditor ? (
<>
<input
type="radio"
name="editor"
value="ace"
id="editor-switch-ace"
className="toggle-switch-input"
checked={!richTextOrVisual && !newSourceEditor}
onChange={handleChange}
/>
<label htmlFor="editor-switch-ace" className="toggle-switch-label">
<span>Source (legacy)</span>
</label>
</>
) : null}
<RichTextToggle
checked={!!richTextOrVisual}
disabled={!richTextAvailable}
handleChange={handleChange}
/>
</fieldset>
{!!richTextOrVisual && !isParticipatingInVisualEditorNamingTest && (
<SplitTestBadge splitTestName="rich-text" displayOnVariants={['cm6']} />
)}
</div>
)
}
const RichTextToggle: FC<{
checked: boolean
disabled: boolean
handleChange: (event: ChangeEvent<HTMLInputElement>) => void
}> = ({ checked, disabled, handleChange }) => {
const { t } = useTranslation()
const richTextName =
visualEditorNameVariant === 'default' ? 'Rich Text' : 'Visual Editor'
const toggle = (
<span>
<input
type="radio"
name="editor"
value="rich-text"
id="editor-switch-rich-text"
className="toggle-switch-input"
checked={checked}
onChange={handleChange}
disabled={disabled}
/>
<label htmlFor="editor-switch-rich-text" className="toggle-switch-label">
<span>{richTextName}</span>
</label>
</span>
)
if (disabled) {
return (
<Tooltip
description={t('rich_text_is_only_available_for_tex_files')}
id="rich-text-toggle-tooltip"
overlayProps={{ placement: 'bottom' }}
tooltipProps={{ className: 'tooltip-wide' }}
>
{toggle}
</Tooltip>
)
}
return toggle
}
export default memo(EditorSwitch)

View file

@ -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)".
<br />
<br />
Click to learn more and give feedback
</>
)
return (
<Tooltip
id="editor-switch"
description={content}
overlayProps={{
placement: 'bottom',
delayHide: 100,
}}
tooltipProps={{ className: 'tooltip-wide' }}
>
<a
href="https://forms.gle/GmSs6odZRKRp3VX98"
target="_blank"
rel="noopener noreferrer"
className="info-badge"
>
<span className="sr-only">{content}</span>
</a>
</Tooltip>
)
}
const showLegacySourceEditor: boolean = getMeta('ol-showLegacySourceEditor')
function EditorSwitch() {
const { t } = useTranslation()
const [newSourceEditor, setNewSourceEditor] = useScopeValue(
@ -91,8 +56,6 @@ function EditorSwitch() {
return (
<div className="editor-toggle-switch">
{showLegacySourceEditor ? <Badge /> : null}
<fieldset className="toggle-switch">
<legend className="sr-only">Editor mode.</legend>
@ -109,23 +72,6 @@ function EditorSwitch() {
<span>{t('code_editor')}</span>
</label>
{showLegacySourceEditor ? (
<>
<input
type="radio"
name="editor"
value="ace"
id="editor-switch-ace"
className="toggle-switch-input"
checked={!richTextOrVisual && !newSourceEditor}
onChange={handleChange}
/>
<label htmlFor="editor-switch-ace" className="toggle-switch-label">
<span>Source (legacy)</span>
</label>
</>
) : null}
<RichTextToggle
checked={!!richTextOrVisual}
disabled={!richTextAvailable}

View file

@ -6,13 +6,22 @@ import Tooltip from '../../../../shared/components/tooltip'
import { EditorView } from '@codemirror/view'
import { emitCommandEvent } from '../../extensions/toolbar/utils/analytics'
import { useCodeMirrorViewContext } from '../codemirror-editor'
import MaterialIcon from '../../../../shared/components/material-icon'
export const ToolbarButtonMenu: FC<{
id: string
label: string
icon: string
materialIcon?: boolean
altCommand?: (view: EditorView) => void
}> = memo(function ButtonMenu({ icon, id, label, altCommand, children }) {
}> = memo(function ButtonMenu({
icon,
id,
label,
materialIcon,
altCommand,
children,
}) {
const target = useRef<any>(null)
const { open, onToggle, ref } = useDropdown()
const view = useCodeMirrorViewContext()
@ -39,7 +48,7 @@ export const ToolbarButtonMenu: FC<{
}}
ref={target}
>
<Icon type={icon} fw />
{materialIcon ? <MaterialIcon type={icon} /> : <Icon type={icon} fw />}
</Button>
)

View file

@ -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 (
<ToolbarButtonMenu
id="toolbar-math"
label={t('toolbar_insert_math')}
icon="calculate"
materialIcon
>
<ListGroupItem
aria-label={t('toolbar_insert_inline_math')}
onClick={event => {
emitCommandEvent(view, 'toolbar-inline-math')
event.preventDefault()
wrapInInlineMath(view)
view.focus()
}}
>
<MaterialIcon type="123" />
<span>{t('toolbar_insert_inline_math')}</span>
</ListGroupItem>
<ListGroupItem
aria-label={t('toolbar_insert_display_math')}
onClick={event => {
emitCommandEvent(view, 'toolbar-display-math')
event.preventDefault()
wrapInDisplayMath(view)
view.focus()
}}
>
<MaterialIcon type="view_day" />
<span>{t('toolbar_insert_display_math')}</span>
</ListGroupItem>
</ToolbarButtonMenu>
)
}

View file

@ -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') && (
<div className="ol-cm-toolbar-button-group" data-overflow="group-math">
<ToolbarButton
id="toolbar-inline-math"
label={t('toolbar_insert_inline_math')}
command={commands.wrapInInlineMath}
icon="π"
textIcon
className="ol-cm-toolbar-button-math"
/>
<ToolbarButton
id="toolbar-display-math"
label={t('toolbar_insert_display_math')}
command={commands.wrapInDisplayMath}
icon="Σ"
textIcon
className="ol-cm-toolbar-button-math"
/>
<MathDropdown />
<ToolbarButton
id="toolbar-toggle-symbol-palette"
label={t('toolbar_toggle_symbol_palette')}

View file

@ -1,6 +1,6 @@
import App from '../../../base'
import { react2angular } from 'react2angular'
import EditorSwitch from '../components/editor-switch'
import EditorSwitch from '../components/editor-switch-legacy'
import { rootContext } from '../../../shared/context/root-context'
App.component('editorSwitch', react2angular(rootContext.use(EditorSwitch)))

View file

@ -45,7 +45,9 @@ import { keymaps } from './keymaps'
import { shortcuts } from './shortcuts'
import { effectListeners } from './effect-listeners'
import { highlightSpecialChars } from './highlight-special-chars'
import { toolbarPanel } from './toolbar/toolbar-panel'
import { geometryChangeEvent } from './geometry-change-event'
import { isSplitTestEnabled } from '../../../utils/splitTestUtils'
const moduleExtensions: Array<() => Extension> = importOverleafModules(
'sourceEditorExtensions'
@ -120,6 +122,7 @@ export const createExtensions = (options: Record<string, any>): 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.

View file

@ -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',

View file

@ -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(),
]

View file

@ -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 {

View file

@ -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,

View file

@ -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 {

View file

@ -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;
}
}
/**************************************

View file

@ -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",

View file

@ -18,7 +18,7 @@ const clickToolbarButton = (text: string) => {
}
const Container: FC = ({ children }) => (
<div style={{ width: 785, height: 785 }}>{children}</div>
<div style={{ width: 1500, height: 785 }}>{children}</div>
)
const mountEditor = (content: string) => {
@ -97,6 +97,7 @@ describe('<CodeMirrorEditor/> 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('<CodeMirrorEditor/> 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\\]')
})

View file

@ -4,7 +4,7 @@ import { mockScope, rootFolderId } from '../helpers/mock-scope'
import { FC } from 'react'
const Container: FC = ({ children }) => (
<div style={{ width: 785, height: 785 }}>{children}</div>
<div style={{ width: 1500, height: 785 }}>{children}</div>
)
const clickToolbarButton = (text: string) => {