mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-29 05:03:33 -05:00
[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:
parent
a14e2aecfb
commit
17452b51d7
21 changed files with 357 additions and 88 deletions
|
@ -683,6 +683,21 @@ const ProjectController = {
|
||||||
cb()
|
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) {
|
historyViewAssignment(cb) {
|
||||||
SplitTestHandler.getAssignment(
|
SplitTestHandler.getAssignment(
|
||||||
req,
|
req,
|
||||||
|
@ -729,6 +744,7 @@ const ProjectController = {
|
||||||
pdfjsAssignment,
|
pdfjsAssignment,
|
||||||
editorLeftMenuAssignment,
|
editorLeftMenuAssignment,
|
||||||
richTextAssignment,
|
richTextAssignment,
|
||||||
|
sourceEditorToolbarAssigment,
|
||||||
historyViewAssignment,
|
historyViewAssignment,
|
||||||
reviewPanelAssignment,
|
reviewPanelAssignment,
|
||||||
}
|
}
|
||||||
|
@ -919,6 +935,9 @@ const ProjectController = {
|
||||||
pdfjsVariant: pdfjsAssignment.variant,
|
pdfjsVariant: pdfjsAssignment.variant,
|
||||||
debugPdfDetach,
|
debugPdfDetach,
|
||||||
showLegacySourceEditor,
|
showLegacySourceEditor,
|
||||||
|
showSourceToolbar:
|
||||||
|
!showLegacySourceEditor &&
|
||||||
|
sourceEditorToolbarAssigment.variant === 'enabled',
|
||||||
showSymbolPalette,
|
showSymbolPalette,
|
||||||
galileoEnabled,
|
galileoEnabled,
|
||||||
galileoFeatures,
|
galileoFeatures,
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
include ./file-view
|
include ./file-view
|
||||||
|
|
||||||
.editor-container.full-size(
|
.editor-container.full-size(
|
||||||
|
class={"has-source-toolbar" : showSourceToolbar},
|
||||||
ng-show="ui.view == 'editor' && editor.multiSelectedCount === 0"
|
ng-show="ui.view == 'editor' && editor.multiSelectedCount === 0"
|
||||||
vertical-resizable-panes="south-pane-resizer"
|
vertical-resizable-panes="south-pane-resizer"
|
||||||
vertical-resizable-panes-hidden-externally-on="south-pane-toggled"
|
vertical-resizable-panes-hidden-externally-on="south-pane-toggled"
|
||||||
|
|
|
@ -22,6 +22,7 @@ meta(name="ol-wsRetryHandshake" data-type="json" content=settings.wsRetryHandsha
|
||||||
meta(name="ol-pdfjsVariant" content=pdfjsVariant)
|
meta(name="ol-pdfjsVariant" content=pdfjsVariant)
|
||||||
meta(name="ol-debugPdfDetach" data-type="boolean" content=debugPdfDetach)
|
meta(name="ol-debugPdfDetach" data-type="boolean" content=debugPdfDetach)
|
||||||
meta(name="ol-showLegacySourceEditor", data-type="boolean" content=showLegacySourceEditor)
|
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-showSymbolPalette" data-type="boolean" content=showSymbolPalette)
|
||||||
meta(name="ol-galileoEnabled" data-type="string" content=galileoEnabled)
|
meta(name="ol-galileoEnabled" data-type="string" content=galileoEnabled)
|
||||||
meta(name="ol-galileoPromptWords" data-type="string" content=galileoPromptWords)
|
meta(name="ol-galileoPromptWords" data-type="string" content=galileoPromptWords)
|
||||||
|
|
|
@ -1016,6 +1016,7 @@
|
||||||
"toolbar_insert_figure": "",
|
"toolbar_insert_figure": "",
|
||||||
"toolbar_insert_inline_math": "",
|
"toolbar_insert_inline_math": "",
|
||||||
"toolbar_insert_link": "",
|
"toolbar_insert_link": "",
|
||||||
|
"toolbar_insert_math": "",
|
||||||
"toolbar_insert_table": "",
|
"toolbar_insert_table": "",
|
||||||
"toolbar_numbered_list": "",
|
"toolbar_numbered_list": "",
|
||||||
"toolbar_redo": "",
|
"toolbar_redo": "",
|
||||||
|
|
|
@ -13,6 +13,13 @@ import { ToolbarOverflow } from './toolbar/overflow'
|
||||||
import useDropdown from '../../../shared/hooks/use-dropdown'
|
import useDropdown from '../../../shared/hooks/use-dropdown'
|
||||||
import { getPanel } from '@codemirror/view'
|
import { getPanel } from '@codemirror/view'
|
||||||
import { createToolbarPanel } from '../extensions/toolbar/toolbar-panel'
|
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 = () => {
|
export const CodeMirrorToolbar = () => {
|
||||||
const view = useCodeMirrorViewContext()
|
const view = useCodeMirrorViewContext()
|
||||||
|
@ -26,7 +33,10 @@ export const CodeMirrorToolbar = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Toolbar = memo(function Toolbar() {
|
const Toolbar = memo(function Toolbar() {
|
||||||
|
const showSourceToolbar: boolean = getMeta('ol-showSourceToolbar')
|
||||||
|
|
||||||
const state = useCodeMirrorStateContext()
|
const state = useCodeMirrorStateContext()
|
||||||
|
const view = useCodeMirrorViewContext()
|
||||||
|
|
||||||
const [overflowed, setOverflowed] = useState(false)
|
const [overflowed, setOverflowed] = useState(false)
|
||||||
const [collapsed, setCollapsed] = useState(false)
|
const [collapsed, setCollapsed] = useState(false)
|
||||||
|
@ -85,9 +95,13 @@ const Toolbar = memo(function Toolbar() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ol-cm-toolbar" ref={resizeRef}>
|
<div className="ol-cm-toolbar toolbar-editor" ref={resizeRef}>
|
||||||
|
{showSourceToolbar && <EditorSwitch />}
|
||||||
<ToolbarItems state={state} />
|
<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
|
<ToolbarOverflow
|
||||||
overflowed={overflowed}
|
overflowed={overflowed}
|
||||||
target={overflowBeforeRef.current ?? undefined}
|
target={overflowBeforeRef.current ?? undefined}
|
||||||
|
@ -97,6 +111,7 @@ const Toolbar = memo(function Toolbar() {
|
||||||
>
|
>
|
||||||
<ToolbarItems state={state} overflowed={overflowedItemsRef.current} />
|
<ToolbarItems state={state} overflowed={overflowedItemsRef.current} />
|
||||||
</ToolbarOverflow>
|
</ToolbarOverflow>
|
||||||
|
<div className="formatting-buttons-wrapper" />
|
||||||
</div>
|
</div>
|
||||||
<div className="ol-cm-toolbar-button-group ol-cm-toolbar-end">
|
<div className="ol-cm-toolbar-button-group ol-cm-toolbar-end">
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
|
@ -106,6 +121,19 @@ const Toolbar = memo(function Toolbar() {
|
||||||
active={searchPanelOpen(state)}
|
active={searchPanelOpen(state)}
|
||||||
icon="search"
|
icon="search"
|
||||||
/>
|
/>
|
||||||
|
{!isVisual(view) && (
|
||||||
|
<SplitTestBadge
|
||||||
|
splitTestName="source-editor-toolbar"
|
||||||
|
displayOnVariants={['enabled']}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showSourceToolbar && (
|
||||||
|
<>
|
||||||
|
<SwitchToPDFButton />
|
||||||
|
<DetacherSynctexControl />
|
||||||
|
<DetachCompileButtonWrapper />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="ol-cm-toolbar-button-group hidden">
|
<div className="ol-cm-toolbar-button-group hidden">
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
|
|
|
@ -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)
|
|
@ -7,41 +7,6 @@ import isValidTeXFile from '../../../main/is-valid-tex-file'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import SplitTestBadge from '../../../shared/components/split-test-badge'
|
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() {
|
function EditorSwitch() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [newSourceEditor, setNewSourceEditor] = useScopeValue(
|
const [newSourceEditor, setNewSourceEditor] = useScopeValue(
|
||||||
|
@ -91,8 +56,6 @@ function EditorSwitch() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="editor-toggle-switch">
|
<div className="editor-toggle-switch">
|
||||||
{showLegacySourceEditor ? <Badge /> : null}
|
|
||||||
|
|
||||||
<fieldset className="toggle-switch">
|
<fieldset className="toggle-switch">
|
||||||
<legend className="sr-only">Editor mode.</legend>
|
<legend className="sr-only">Editor mode.</legend>
|
||||||
|
|
||||||
|
@ -109,23 +72,6 @@ function EditorSwitch() {
|
||||||
<span>{t('code_editor')}</span>
|
<span>{t('code_editor')}</span>
|
||||||
</label>
|
</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
|
<RichTextToggle
|
||||||
checked={!!richTextOrVisual}
|
checked={!!richTextOrVisual}
|
||||||
disabled={!richTextAvailable}
|
disabled={!richTextAvailable}
|
||||||
|
|
|
@ -6,13 +6,22 @@ import Tooltip from '../../../../shared/components/tooltip'
|
||||||
import { EditorView } from '@codemirror/view'
|
import { EditorView } from '@codemirror/view'
|
||||||
import { emitCommandEvent } from '../../extensions/toolbar/utils/analytics'
|
import { emitCommandEvent } from '../../extensions/toolbar/utils/analytics'
|
||||||
import { useCodeMirrorViewContext } from '../codemirror-editor'
|
import { useCodeMirrorViewContext } from '../codemirror-editor'
|
||||||
|
import MaterialIcon from '../../../../shared/components/material-icon'
|
||||||
|
|
||||||
export const ToolbarButtonMenu: FC<{
|
export const ToolbarButtonMenu: FC<{
|
||||||
id: string
|
id: string
|
||||||
label: string
|
label: string
|
||||||
icon: string
|
icon: string
|
||||||
|
materialIcon?: boolean
|
||||||
altCommand?: (view: EditorView) => void
|
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 target = useRef<any>(null)
|
||||||
const { open, onToggle, ref } = useDropdown()
|
const { open, onToggle, ref } = useDropdown()
|
||||||
const view = useCodeMirrorViewContext()
|
const view = useCodeMirrorViewContext()
|
||||||
|
@ -39,7 +48,7 @@ export const ToolbarButtonMenu: FC<{
|
||||||
}}
|
}}
|
||||||
ref={target}
|
ref={target}
|
||||||
>
|
>
|
||||||
<Icon type={icon} fw />
|
{materialIcon ? <MaterialIcon type={icon} /> : <Icon type={icon} fw />}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -13,9 +13,10 @@ import { redo, undo } from '@codemirror/commands'
|
||||||
import * as commands from '../../extensions/toolbar/commands'
|
import * as commands from '../../extensions/toolbar/commands'
|
||||||
import { SectionHeadingDropdown } from './section-heading-dropdown'
|
import { SectionHeadingDropdown } from './section-heading-dropdown'
|
||||||
import { canAddComment } from '../../extensions/toolbar/comments'
|
import { canAddComment } from '../../extensions/toolbar/comments'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import getMeta from '../../../../utils/meta'
|
import getMeta from '../../../../utils/meta'
|
||||||
import { InsertFigureDropdown } from './insert-figure-dropdown'
|
import { InsertFigureDropdown } from './insert-figure-dropdown'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { MathDropdown } from './math-dropdown'
|
||||||
|
|
||||||
const isMac = /Mac/.test(window.navigator?.platform)
|
const isMac = /Mac/.test(window.navigator?.platform)
|
||||||
|
|
||||||
|
@ -98,22 +99,7 @@ export const ToolbarItems: FC<{
|
||||||
)}
|
)}
|
||||||
{showGroup('group-math') && (
|
{showGroup('group-math') && (
|
||||||
<div className="ol-cm-toolbar-button-group" data-overflow="group-math">
|
<div className="ol-cm-toolbar-button-group" data-overflow="group-math">
|
||||||
<ToolbarButton
|
<MathDropdown />
|
||||||
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"
|
|
||||||
/>
|
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
id="toolbar-toggle-symbol-palette"
|
id="toolbar-toggle-symbol-palette"
|
||||||
label={t('toolbar_toggle_symbol_palette')}
|
label={t('toolbar_toggle_symbol_palette')}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import App from '../../../base'
|
import App from '../../../base'
|
||||||
import { react2angular } from 'react2angular'
|
import { react2angular } from 'react2angular'
|
||||||
import EditorSwitch from '../components/editor-switch'
|
import EditorSwitch from '../components/editor-switch-legacy'
|
||||||
import { rootContext } from '../../../shared/context/root-context'
|
import { rootContext } from '../../../shared/context/root-context'
|
||||||
|
|
||||||
App.component('editorSwitch', react2angular(rootContext.use(EditorSwitch)))
|
App.component('editorSwitch', react2angular(rootContext.use(EditorSwitch)))
|
||||||
|
|
|
@ -45,7 +45,9 @@ import { keymaps } from './keymaps'
|
||||||
import { shortcuts } from './shortcuts'
|
import { shortcuts } from './shortcuts'
|
||||||
import { effectListeners } from './effect-listeners'
|
import { effectListeners } from './effect-listeners'
|
||||||
import { highlightSpecialChars } from './highlight-special-chars'
|
import { highlightSpecialChars } from './highlight-special-chars'
|
||||||
|
import { toolbarPanel } from './toolbar/toolbar-panel'
|
||||||
import { geometryChangeEvent } from './geometry-change-event'
|
import { geometryChangeEvent } from './geometry-change-event'
|
||||||
|
import { isSplitTestEnabled } from '../../../utils/splitTestUtils'
|
||||||
|
|
||||||
const moduleExtensions: Array<() => Extension> = importOverleafModules(
|
const moduleExtensions: Array<() => Extension> = importOverleafModules(
|
||||||
'sourceEditorExtensions'
|
'sourceEditorExtensions'
|
||||||
|
@ -120,6 +122,7 @@ export const createExtensions = (options: Record<string, any>): Extension[] => [
|
||||||
emptyLineFiller(),
|
emptyLineFiller(),
|
||||||
trackChanges(options.currentDoc, options.changeManager),
|
trackChanges(options.currentDoc, options.changeManager),
|
||||||
visual(options.currentDoc, options.visual),
|
visual(options.currentDoc, options.visual),
|
||||||
|
isSplitTestEnabled('source-editor-toolbar') ? toolbarPanel() : [],
|
||||||
verticalOverflow(),
|
verticalOverflow(),
|
||||||
highlightActiveLine(options.visual.visual),
|
highlightActiveLine(options.visual.visual),
|
||||||
// The built-in extension that highlights the active line in the gutter.
|
// The built-in extension that highlights the active line in the gutter.
|
||||||
|
|
|
@ -75,6 +75,9 @@ export const toolbarPanel = () => [
|
||||||
'& .list-group-item': {
|
'& .list-group-item': {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
textAlign: 'start',
|
textAlign: 'start',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '5px',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'.ol-cm-toolbar-button-group': {
|
'.ol-cm-toolbar-button-group': {
|
||||||
|
@ -90,6 +93,9 @@ export const toolbarPanel = () => [
|
||||||
'&.ol-cm-toolbar-end': {
|
'&.ol-cm-toolbar-end': {
|
||||||
borderLeft: 'none',
|
borderLeft: 'none',
|
||||||
},
|
},
|
||||||
|
'&.ol-cm-toolbar-stretch': {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
'&.overflow-hidden': {
|
'&.overflow-hidden': {
|
||||||
borderLeft: 'none',
|
borderLeft: 'none',
|
||||||
},
|
},
|
||||||
|
@ -99,6 +105,9 @@ export const toolbarPanel = () => [
|
||||||
padding: 0,
|
padding: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'.formatting-buttons-wrapper': {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
'.ol-cm-toolbar-button': {
|
'.ol-cm-toolbar-button': {
|
||||||
display: 'inline-flex',
|
display: 'inline-flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
@ -148,8 +157,10 @@ export const toolbarPanel = () => [
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'.ol-cm-toolbar-end': {
|
'.ol-cm-toolbar-end': {
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
|
'& .badge': {
|
||||||
|
marginRight: '5px',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'.ol-cm-toolbar-overflow-toggle': {
|
'.ol-cm-toolbar-overflow-toggle': {
|
||||||
display: 'none',
|
display: 'none',
|
||||||
|
|
|
@ -17,11 +17,12 @@ import { findEffect } from '../../utils/effects'
|
||||||
import { forceParsing, syntaxTree } from '@codemirror/language'
|
import { forceParsing, syntaxTree } from '@codemirror/language'
|
||||||
import { hasLanguageLoadedEffect } from '../language'
|
import { hasLanguageLoadedEffect } from '../language'
|
||||||
import { restoreScrollPosition } from '../scroll-position'
|
import { restoreScrollPosition } from '../scroll-position'
|
||||||
import { toolbarPanel } from '../toolbar/toolbar-panel'
|
|
||||||
import { CurrentDoc } from '../../../../../../types/current-doc'
|
import { CurrentDoc } from '../../../../../../types/current-doc'
|
||||||
import isValidTeXFile from '../../../../main/is-valid-tex-file'
|
import isValidTeXFile from '../../../../main/is-valid-tex-file'
|
||||||
import { listItemMarker } from './list-item-marker'
|
import { listItemMarker } from './list-item-marker'
|
||||||
import { figureModalPasteHandler } from '../figure-modal'
|
import { figureModalPasteHandler } from '../figure-modal'
|
||||||
|
import { isSplitTestEnabled } from '../../../../utils/splitTestUtils'
|
||||||
|
import { toolbarPanel } from '../toolbar/toolbar-panel'
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
visual: boolean
|
visual: boolean
|
||||||
|
@ -197,8 +198,8 @@ const extension = (options: Options) => [
|
||||||
atomicDecorations(options),
|
atomicDecorations(options),
|
||||||
skipPreambleWithCursor,
|
skipPreambleWithCursor,
|
||||||
visualKeymap,
|
visualKeymap,
|
||||||
toolbarPanel(),
|
|
||||||
scrollJumpAdjuster,
|
scrollJumpAdjuster,
|
||||||
|
isSplitTestEnabled('source-editor-toolbar') ? [] : toolbarPanel(),
|
||||||
showContentWhenParsed,
|
showContentWhenParsed,
|
||||||
figureModalPasteHandler(),
|
figureModalPasteHandler(),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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'
|
import { ScopeDecorator } from './decorators/scope'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -95,8 +95,14 @@
|
||||||
#editor,
|
#editor,
|
||||||
#editor-rich-text {
|
#editor-rich-text {
|
||||||
.full-size;
|
.full-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-container #editor {
|
||||||
top: @editor-toolbar-height;
|
top: @editor-toolbar-height;
|
||||||
}
|
}
|
||||||
|
.editor-container.has-source-toolbar #editor {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.pdf-empty,
|
.pdf-empty,
|
||||||
.no-history-available,
|
.no-history-available,
|
||||||
|
|
|
@ -13,13 +13,16 @@
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detach-compile-button-container when (@is-new-css = false) {
|
// only apply for legacy editor
|
||||||
margin-right: -5px;
|
.toolbar-pdf-right {
|
||||||
}
|
.detach-compile-button-container when (@is-new-css = false) {
|
||||||
|
margin-right: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
// because 2px border on :active state
|
// because 2px border on :active state
|
||||||
.detach-compile-button-container when (@is-new-css = true) {
|
.detach-compile-button-container when (@is-new-css = true) {
|
||||||
margin-right: -3px;
|
margin-right: -3px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-striped-animated {
|
.btn-striped-animated {
|
||||||
|
|
|
@ -299,6 +299,7 @@
|
||||||
.editor-toggle-switch {
|
.editor-toggle-switch {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
.toggle-switch {
|
.toggle-switch {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
|
@ -317,6 +318,10 @@
|
||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
border-right: none;
|
border-right: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**************************************
|
/**************************************
|
||||||
|
|
|
@ -1636,6 +1636,7 @@
|
||||||
"toolbar_insert_figure": "Insert Figure",
|
"toolbar_insert_figure": "Insert Figure",
|
||||||
"toolbar_insert_inline_math": "Insert Inline Math",
|
"toolbar_insert_inline_math": "Insert Inline Math",
|
||||||
"toolbar_insert_link": "Insert Link",
|
"toolbar_insert_link": "Insert Link",
|
||||||
|
"toolbar_insert_math": "Insert Math",
|
||||||
"toolbar_insert_table": "Insert Table",
|
"toolbar_insert_table": "Insert Table",
|
||||||
"toolbar_numbered_list": "Numbered List",
|
"toolbar_numbered_list": "Numbered List",
|
||||||
"toolbar_redo": "Redo",
|
"toolbar_redo": "Redo",
|
||||||
|
|
|
@ -18,7 +18,7 @@ const clickToolbarButton = (text: string) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Container: FC = ({ children }) => (
|
const Container: FC = ({ children }) => (
|
||||||
<div style={{ width: 785, height: 785 }}>{children}</div>
|
<div style={{ width: 1500, height: 785 }}>{children}</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
const mountEditor = (content: string) => {
|
const mountEditor = (content: string) => {
|
||||||
|
@ -97,6 +97,7 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
|
||||||
mountEditor('2+3=5')
|
mountEditor('2+3=5')
|
||||||
selectAll()
|
selectAll()
|
||||||
|
|
||||||
|
clickToolbarButton('Insert Math')
|
||||||
clickToolbarButton('Insert Inline Math')
|
clickToolbarButton('Insert Inline Math')
|
||||||
cy.get('.cm-content').should('have.text', '\\(2+3=5\\)')
|
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')
|
mountEditor('2+3=5')
|
||||||
selectAll()
|
selectAll()
|
||||||
|
|
||||||
|
clickToolbarButton('Insert Math')
|
||||||
clickToolbarButton('Insert Display Math')
|
clickToolbarButton('Insert Display Math')
|
||||||
cy.get('.cm-content').should('have.text', '\\[2+3=5\\]')
|
cy.get('.cm-content').should('have.text', '\\[2+3=5\\]')
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { mockScope, rootFolderId } from '../helpers/mock-scope'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
|
|
||||||
const Container: FC = ({ children }) => (
|
const Container: FC = ({ children }) => (
|
||||||
<div style={{ width: 785, height: 785 }}>{children}</div>
|
<div style={{ width: 1500, height: 785 }}>{children}</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
const clickToolbarButton = (text: string) => {
|
const clickToolbarButton = (text: string) => {
|
||||||
|
|
Loading…
Reference in a new issue