From 3f29aa219520ff6eb53eeacd4e33016b255fcce7 Mon Sep 17 00:00:00 2001 From: Alf Eaton Date: Thu, 15 Feb 2024 09:45:50 +0000 Subject: [PATCH] Ensure that each editor theme is only created once (#17095) GitOrigin-RevId: 3551e02fab44fae7fcab5cb12886d45969e3990f --- .../js/features/history/extensions/theme.ts | 49 +- .../source-editor/extensions/annotations.ts | 18 +- .../extensions/bracket-matching.ts | 12 +- .../extensions/cursor-highlights.ts | 76 +-- .../extensions/draw-selection.ts | 24 +- .../extensions/empty-line-filler.ts | 12 +- .../source-editor/extensions/go-to-line.ts | 90 +-- .../extensions/highlight-active-line.ts | 12 +- .../extensions/inline-background.ts | 6 +- .../source-editor/extensions/search.ts | 242 ++++---- .../extensions/spelling/index.ts | 28 +- .../source-editor/extensions/theme.ts | 90 +-- .../extensions/toolbar/toolbar-panel.ts | 555 +++++++++--------- .../extensions/visual/pasted-content.tsx | 106 ++-- .../extensions/visual/visual-theme.ts | 6 +- 15 files changed, 679 insertions(+), 647 deletions(-) diff --git a/services/web/frontend/js/features/history/extensions/theme.ts b/services/web/frontend/js/features/history/extensions/theme.ts index 706fdd1609..9a95f557d4 100644 --- a/services/web/frontend/js/features/history/extensions/theme.ts +++ b/services/web/frontend/js/features/history/extensions/theme.ts @@ -13,6 +13,7 @@ export type Options = { const optionsThemeConf = new Compartment() export const theme = (options: Options) => [ + baseTheme, optionsThemeConf.of(createThemeFromOptions(options)), ] @@ -35,36 +36,46 @@ const createThemeFromOptions = ({ // Theme styles that depend on settings const fontFamilyValue = fontFamilies[fontFamily]?.join(', ') return [ - EditorView.theme({ - '&.cm-editor': { + EditorView.editorAttributes.of({ + style: Object.entries({ '--font-size': `${fontSize}px`, '--source-font-family': fontFamilyValue, '--line-height': lineHeights[lineHeight], - }, - '.cm-content': { - fontSize: 'var(--font-size)', - fontFamily: 'var(--source-font-family)', - lineHeight: 'var(--line-height)', - color: '#000', - }, - '.cm-gutters': { - fontSize: 'var(--font-size)', - lineHeight: 'var(--line-height)', - }, + }) + .map(([key, value]) => `${key}: ${value}`) + .join(';'), + }), + // Set variables for tooltips, which are outside the editor + // TODO: set these on document.body, or a new container element for the tooltips, without using a style mod + EditorView.baseTheme({ '.cm-tooltip': { - // Set variables for tooltips, which are outside the editor '--font-size': `${fontSize}px`, '--source-font-family': fontFamilyValue, - // NOTE: fontFamily is not set here, as most tooltips use the UI font - fontSize: 'var(--font-size)', - }, - '.cm-lineNumbers': { - fontFamily: 'var(--source-font-family)', }, }), ] } +const baseTheme = EditorView.baseTheme({ + '.cm-content': { + fontSize: 'var(--font-size)', + fontFamily: 'var(--source-font-family)', + lineHeight: 'var(--line-height)', + color: '#000', + }, + '.cm-gutters': { + fontSize: 'var(--font-size)', + lineHeight: 'var(--line-height)', + }, + '.cm-lineNumbers': { + fontFamily: 'var(--source-font-family)', + }, + '.cm-tooltip': { + // NOTE: fontFamily is not set here, as most tooltips use the UI font + fontSize: 'var(--font-size)', + }, +}) + export const setOptionsTheme = (options: Options): TransactionSpec => { return { effects: optionsThemeConf.reconfigure(createThemeFromOptions(options)), diff --git a/services/web/frontend/js/features/source-editor/extensions/annotations.ts b/services/web/frontend/js/features/source-editor/extensions/annotations.ts index b0a13c3ddb..eb03ac94ef 100644 --- a/services/web/frontend/js/features/source-editor/extensions/annotations.ts +++ b/services/web/frontend/js/features/source-editor/extensions/annotations.ts @@ -24,16 +24,18 @@ export const annotations = () => [ lintGutter({ hoverTime: 0, }), - /** - * A theme which moves the lint gutter outside the line numbers. - */ - EditorView.baseTheme({ - '.cm-gutter-lint': { - order: -1, - }, - }), + annotationsTheme, ] +/** + * A theme which moves the lint gutter outside the line numbers. + */ +const annotationsTheme = EditorView.baseTheme({ + '.cm-gutter-lint': { + order: -1, + }, +}) + export const lintSourceConfig = { delay: 100, // Show highlights only for errors diff --git a/services/web/frontend/js/features/source-editor/extensions/bracket-matching.ts b/services/web/frontend/js/features/source-editor/extensions/bracket-matching.ts index afcdf5c44e..0dda7b34f8 100644 --- a/services/web/frontend/js/features/source-editor/extensions/bracket-matching.ts +++ b/services/web/frontend/js/features/source-editor/extensions/bracket-matching.ts @@ -134,9 +134,11 @@ export const bracketSelection = (): Extension[] => [ return false }, }), - EditorView.baseTheme({ - '.cm-matchingBracket': { - pointerEvents: 'none', - }, - }), + matchingBracketTheme, ] + +const matchingBracketTheme = EditorView.baseTheme({ + '.cm-matchingBracket': { + pointerEvents: 'none', + }, +}) diff --git a/services/web/frontend/js/features/source-editor/extensions/cursor-highlights.ts b/services/web/frontend/js/features/source-editor/extensions/cursor-highlights.ts index 1f9ca1aed3..724a23fe6a 100644 --- a/services/web/frontend/js/features/source-editor/extensions/cursor-highlights.ts +++ b/services/web/frontend/js/features/source-editor/extensions/cursor-highlights.ts @@ -26,49 +26,51 @@ export const cursorHighlights = () => { return [ cursorHighlightsState, cursorHighlightsLayer, + cursorHighlightsTheme, hoverTooltip(cursorTooltip, { hoverTime: 1, }), - EditorView.theme({ - '.ol-cm-cursorHighlightsLayer': { - zIndex: 100, - contain: 'size style', - pointerEvents: 'none', - }, - '.ol-cm-cursorHighlight': { - color: 'hsl(var(--hue), 70%, 50%)', - borderLeft: '2px solid hsl(var(--hue), 70%, 50%)', - display: 'inline-block', - height: '1.6em', - position: 'absolute', - pointerEvents: 'none', - }, - '.ol-cm-cursorHighlight:before': { - content: "''", - position: 'absolute', - left: '-2px', - top: '-5px', - height: '5px', - width: '5px', - borderWidth: '3px 3px 2px 2px', - borderStyle: 'solid', - borderColor: 'inherit', - }, - '.ol-cm-cursorHighlightLabel': { - lineHeight: 1, - backgroundColor: 'hsl(var(--hue), 70%, 50%)', - padding: '1em 1em', - fontSize: '0.8rem', - fontFamily: 'Lato, sans-serif', - color: 'white', - fontWeight: 700, - whiteSpace: 'nowrap', - pointerEvents: 'none', - }, - }), ] } +const cursorHighlightsTheme = EditorView.theme({ + '.ol-cm-cursorHighlightsLayer': { + zIndex: 100, + contain: 'size style', + pointerEvents: 'none', + }, + '.ol-cm-cursorHighlight': { + color: 'hsl(var(--hue), 70%, 50%)', + borderLeft: '2px solid hsl(var(--hue), 70%, 50%)', + display: 'inline-block', + height: '1.6em', + position: 'absolute', + pointerEvents: 'none', + }, + '.ol-cm-cursorHighlight:before': { + content: "''", + position: 'absolute', + left: '-2px', + top: '-5px', + height: '5px', + width: '5px', + borderWidth: '3px 3px 2px 2px', + borderStyle: 'solid', + borderColor: 'inherit', + }, + '.ol-cm-cursorHighlightLabel': { + lineHeight: 1, + backgroundColor: 'hsl(var(--hue), 70%, 50%)', + padding: '1em 1em', + fontSize: '0.8rem', + fontFamily: 'Lato, sans-serif', + color: 'white', + fontWeight: 700, + whiteSpace: 'nowrap', + pointerEvents: 'none', + }, +}) + class HighlightRangeValue extends RangeValue { mapMode = MapMode.Simple diff --git a/services/web/frontend/js/features/source-editor/extensions/draw-selection.ts b/services/web/frontend/js/features/source-editor/extensions/draw-selection.ts index 0c7df441e7..ae21998f38 100644 --- a/services/web/frontend/js/features/source-editor/extensions/draw-selection.ts +++ b/services/web/frontend/js/features/source-editor/extensions/draw-selection.ts @@ -12,24 +12,22 @@ import { updateHasReviewPanelToggledEffect } from './changes/change-manager' * of the coords to cover the full line height. */ export const drawSelection = () => { - return [cursorLayer, selectionLayer, hideNativeSelection] + return [cursorLayer, selectionLayer, Prec.highest(hideNativeSelection)] } const canHidePrimary = !browser.ios -const hideNativeSelection = Prec.highest( - EditorView.theme({ - '.cm-line': { - 'caret-color': canHidePrimary ? 'transparent !important' : null, - '& ::selection': { - backgroundColor: 'transparent !important', - }, - '&::selection': { - backgroundColor: 'transparent !important', - }, +const hideNativeSelection = EditorView.theme({ + '.cm-line': { + 'caret-color': canHidePrimary ? 'transparent !important' : null, + '& ::selection': { + backgroundColor: 'transparent !important', }, - }) -) + '&::selection': { + backgroundColor: 'transparent !important', + }, + }, +}) const cursorLayer = layer({ above: true, diff --git a/services/web/frontend/js/features/source-editor/extensions/empty-line-filler.ts b/services/web/frontend/js/features/source-editor/extensions/empty-line-filler.ts index 0d84b868e1..647463d608 100644 --- a/services/web/frontend/js/features/source-editor/extensions/empty-line-filler.ts +++ b/services/web/frontend/js/features/source-editor/extensions/empty-line-filler.ts @@ -70,10 +70,12 @@ export const emptyLineFiller = () => { }, } ), - EditorView.baseTheme({ - '.ol-cm-filler': { - padding: '0 2px', - }, - }), + emptyLineFillerTheme, ] } + +const emptyLineFillerTheme = EditorView.baseTheme({ + '.ol-cm-filler': { + padding: '0 2px', + }, +}) diff --git a/services/web/frontend/js/features/source-editor/extensions/go-to-line.ts b/services/web/frontend/js/features/source-editor/extensions/go-to-line.ts index cd2dd90be7..04630305f3 100644 --- a/services/web/frontend/js/features/source-editor/extensions/go-to-line.ts +++ b/services/web/frontend/js/features/source-editor/extensions/go-to-line.ts @@ -17,49 +17,51 @@ export const goToLinePanel = () => { }, ]) ), - EditorView.baseTheme({ - '.cm-panel.cm-gotoLine': { - padding: '10px', - fontSize: '14px', - '& label': { - margin: 0, - fontSize: '14px', - '& .cm-textfield': { - margin: '0 10px', - maxWidth: '100px', - height: '34px', - padding: '5px 16px', - fontSize: '14px', - fontWeight: 'normal', - lineHeight: 'var(--line-height-base)', - color: 'var(--input-color)', - backgroundColor: '#fff', - backgroundImage: 'none', - borderRadius: 'var(--input-border-radius)', - boxShadow: 'inset 0 1px 1px rgb(0 0 0 / 8%)', - transition: - 'border-color ease-in-out .15s, box-shadow ease-in-out .15s', - '&:focus-visible': { - outline: 'none', - }, - '&:focus': { - borderColor: 'var(--input-border-focus)', - }, - }, - }, - '& .cm-button': { - padding: '4px 16px 5px', - textTransform: 'capitalize', - fontSize: '14px', - lineHeight: 'var(--line-height-base)', - userSelect: 'none', - backgroundImage: 'none', - backgroundColor: 'var(--btn-default-bg)', - borderRadius: 'var(--btn-border-radius-base)', - border: '0 solid transparent', - color: '#fff', - }, - }, - }), + gotoLineTheme, ] } + +const gotoLineTheme = EditorView.baseTheme({ + '.cm-panel.cm-gotoLine': { + padding: '10px', + fontSize: '14px', + '& label': { + margin: 0, + fontSize: '14px', + '& .cm-textfield': { + margin: '0 10px', + maxWidth: '100px', + height: '34px', + padding: '5px 16px', + fontSize: '14px', + fontWeight: 'normal', + lineHeight: 'var(--line-height-base)', + color: 'var(--input-color)', + backgroundColor: '#fff', + backgroundImage: 'none', + borderRadius: 'var(--input-border-radius)', + boxShadow: 'inset 0 1px 1px rgb(0 0 0 / 8%)', + transition: + 'border-color ease-in-out .15s, box-shadow ease-in-out .15s', + '&:focus-visible': { + outline: 'none', + }, + '&:focus': { + borderColor: 'var(--input-border-focus)', + }, + }, + }, + '& .cm-button': { + padding: '4px 16px 5px', + textTransform: 'capitalize', + fontSize: '14px', + lineHeight: 'var(--line-height-base)', + userSelect: 'none', + backgroundImage: 'none', + backgroundColor: 'var(--btn-default-bg)', + borderRadius: 'var(--btn-border-radius-base)', + border: '0 solid transparent', + color: '#fff', + }, + }, +}) diff --git a/services/web/frontend/js/features/source-editor/extensions/highlight-active-line.ts b/services/web/frontend/js/features/source-editor/extensions/highlight-active-line.ts index 8fedf1c194..30d5810a70 100644 --- a/services/web/frontend/js/features/source-editor/extensions/highlight-active-line.ts +++ b/services/web/frontend/js/features/source-editor/extensions/highlight-active-line.ts @@ -20,14 +20,16 @@ export const highlightActiveLine = (visual: boolean) => { return sourceOnly(visual, [ activeLineLayer, singleLineHighlighter, - EditorView.baseTheme({ - '.ol-cm-activeLineLayer': { - pointerEvents: 'none', - }, - }), + highlightActiveLineTheme, ]) } +const highlightActiveLineTheme = EditorView.baseTheme({ + '.ol-cm-activeLineLayer': { + pointerEvents: 'none', + }, +}) + /** * Line decoration approach used for non-wrapped lines, adapted from built-in * CodeMirror 6 highlightActiveLine, licensed under the MIT license: diff --git a/services/web/frontend/js/features/source-editor/extensions/inline-background.ts b/services/web/frontend/js/features/source-editor/extensions/inline-background.ts index 52b28a0b46..8b40fc4988 100644 --- a/services/web/frontend/js/features/source-editor/extensions/inline-background.ts +++ b/services/web/frontend/js/features/source-editor/extensions/inline-background.ts @@ -36,10 +36,8 @@ function measureHalfLeading(view: EditorView) { } function createTheme(halfLeading: number) { - return EditorView.theme({ - '.cm-content': { - '--half-leading': halfLeading + 'px', - }, + return EditorView.contentAttributes.of({ + style: `--half-leading: ${halfLeading}px`, }) } diff --git a/services/web/frontend/js/features/source-editor/extensions/search.ts b/services/web/frontend/js/features/source-editor/extensions/search.ts index 72eb773cad..59859da7dd 100644 --- a/services/web/frontend/js/features/source-editor/extensions/search.ts +++ b/services/web/frontend/js/features/source-editor/extensions/search.ts @@ -251,126 +251,126 @@ export const search = () => { } } }), - - // search form theme - EditorView.theme({ - '.ol-cm-search-form': { - padding: '10px', - display: 'flex', - gap: '10px', - background: 'var(--ol-blue-gray-1)', - '--ol-cm-search-form-focus-shadow': - 'inset 0 1px 1px rgb(0 0 0 / 8%), 0 0 8px rgb(102 175 233 / 60%)', - '--ol-cm-search-form-error-shadow': - 'inset 0 1px 1px rgb(0 0 0 / 8%), 0 0 8px var(--input-shadow-danger-color)', - }, - '.ol-cm-search-controls': { - display: 'grid', - gridTemplateColumns: 'auto auto', - gridTemplateRows: 'auto auto', - gap: '10px', - }, - '.ol-cm-search-form-row': { - display: 'flex', - gap: '10px', - justifyContent: 'space-between', - }, - '.ol-cm-search-form-group': { - display: 'flex', - gap: '10px', - alignItems: 'center', - }, - '.ol-cm-search-input-group': { - border: '1px solid var(--input-border)', - borderRadius: '20px', - background: 'white', - width: '100%', - maxWidth: '25em', - '& input[type="text"]': { - background: 'none', - boxShadow: 'none', - }, - '& input[type="text"]:focus': { - outline: 'none', - boxShadow: 'none', - }, - '& .btn.btn': { - background: 'var(--ol-blue-gray-0)', - color: 'var(--ol-blue-gray-3)', - borderRadius: '50%', - height: '2em', - display: 'inline-flex', - alignItems: 'center', - justifyContent: 'center', - width: '2em', - marginRight: '3px', - '&.checked': { - color: '#fff', - backgroundColor: 'var(--ol-blue)', - }, - '&:active': { - boxShadow: 'none', - }, - }, - '&:focus-within': { - borderColor: 'var(--input-border-focus)', - boxShadow: 'var(--ol-cm-search-form-focus-shadow)', - }, - }, - '.ol-cm-search-input-group.ol-cm-search-input-error': { - '&:focus-within': { - borderColor: 'var(--input-border-danger)', - boxShadow: 'var(--ol-cm-search-form-error-shadow)', - }, - }, - '.input-group .ol-cm-search-form-input': { - border: 'none', - }, - '.ol-cm-search-input-button': { - background: '#fff', - color: 'inherit', - border: 'none', - }, - '.ol-cm-search-input-button.focused': { - borderColor: 'var(--input-border-focus)', - boxShadow: 'var(--ol-cm-search-form-focus-shadow)', - }, - '.ol-cm-search-form-button-group': { - flexShrink: 0, - }, - '.ol-cm-search-form-position': { - flexShrink: 0, - color: 'var(--ol-blue-gray-4)', - }, - '.ol-cm-search-hidden-inputs': { - position: 'absolute', - left: '-10000px', - }, - '.ol-cm-search-form-close': { - flex: 1, - }, - '.ol-cm-search-replace-input': { - order: 3, - }, - '.ol-cm-search-replace-buttons': { - order: 4, - }, - '.ol-cm-stored-selection': { - background: 'rgba(125, 125, 125, 0.1)', - paddingTop: 'var(--half-leading)', - paddingBottom: 'var(--half-leading)', - }, - // set the default "match" style - '.cm-selectionMatch, .cm-searchMatch': { - backgroundColor: 'transparent', - outlineOffset: '-1px', - paddingTop: 'var(--half-leading)', - paddingBottom: 'var(--half-leading)', - }, - // make sure selectionMatch inside searchMatch doesn't have a background colour - '.cm-searchMatch .cm-selectionMatch': { - backgroundColor: 'transparent !important', - }, - }), + searchFormTheme, ] } + +const searchFormTheme = EditorView.theme({ + '.ol-cm-search-form': { + padding: '10px', + display: 'flex', + gap: '10px', + background: 'var(--ol-blue-gray-1)', + '--ol-cm-search-form-focus-shadow': + 'inset 0 1px 1px rgb(0 0 0 / 8%), 0 0 8px rgb(102 175 233 / 60%)', + '--ol-cm-search-form-error-shadow': + 'inset 0 1px 1px rgb(0 0 0 / 8%), 0 0 8px var(--input-shadow-danger-color)', + }, + '.ol-cm-search-controls': { + display: 'grid', + gridTemplateColumns: 'auto auto', + gridTemplateRows: 'auto auto', + gap: '10px', + }, + '.ol-cm-search-form-row': { + display: 'flex', + gap: '10px', + justifyContent: 'space-between', + }, + '.ol-cm-search-form-group': { + display: 'flex', + gap: '10px', + alignItems: 'center', + }, + '.ol-cm-search-input-group': { + border: '1px solid var(--input-border)', + borderRadius: '20px', + background: 'white', + width: '100%', + maxWidth: '25em', + '& input[type="text"]': { + background: 'none', + boxShadow: 'none', + }, + '& input[type="text"]:focus': { + outline: 'none', + boxShadow: 'none', + }, + '& .btn.btn': { + background: 'var(--ol-blue-gray-0)', + color: 'var(--ol-blue-gray-3)', + borderRadius: '50%', + height: '2em', + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + width: '2em', + marginRight: '3px', + '&.checked': { + color: '#fff', + backgroundColor: 'var(--ol-blue)', + }, + '&:active': { + boxShadow: 'none', + }, + }, + '&:focus-within': { + borderColor: 'var(--input-border-focus)', + boxShadow: 'var(--ol-cm-search-form-focus-shadow)', + }, + }, + '.ol-cm-search-input-group.ol-cm-search-input-error': { + '&:focus-within': { + borderColor: 'var(--input-border-danger)', + boxShadow: 'var(--ol-cm-search-form-error-shadow)', + }, + }, + '.input-group .ol-cm-search-form-input': { + border: 'none', + }, + '.ol-cm-search-input-button': { + background: '#fff', + color: 'inherit', + border: 'none', + }, + '.ol-cm-search-input-button.focused': { + borderColor: 'var(--input-border-focus)', + boxShadow: 'var(--ol-cm-search-form-focus-shadow)', + }, + '.ol-cm-search-form-button-group': { + flexShrink: 0, + }, + '.ol-cm-search-form-position': { + flexShrink: 0, + color: 'var(--ol-blue-gray-4)', + }, + '.ol-cm-search-hidden-inputs': { + position: 'absolute', + left: '-10000px', + }, + '.ol-cm-search-form-close': { + flex: 1, + }, + '.ol-cm-search-replace-input': { + order: 3, + }, + '.ol-cm-search-replace-buttons': { + order: 4, + }, + '.ol-cm-stored-selection': { + background: 'rgba(125, 125, 125, 0.1)', + paddingTop: 'var(--half-leading)', + paddingBottom: 'var(--half-leading)', + }, + // set the default "match" style + '.cm-selectionMatch, .cm-searchMatch': { + backgroundColor: 'transparent', + outlineOffset: '-1px', + paddingTop: 'var(--half-leading)', + paddingBottom: 'var(--half-leading)', + }, + // make sure selectionMatch inside searchMatch doesn't have a background colour + '.cm-searchMatch .cm-selectionMatch': { + backgroundColor: 'transparent !important', + }, +}) diff --git a/services/web/frontend/js/features/source-editor/extensions/spelling/index.ts b/services/web/frontend/js/features/source-editor/extensions/spelling/index.ts index 3d32a350cc..7256921c65 100644 --- a/services/web/frontend/js/features/source-editor/extensions/spelling/index.ts +++ b/services/web/frontend/js/features/source-editor/extensions/spelling/index.ts @@ -31,19 +31,7 @@ type Options = { spellCheckLanguage?: string } */ export const spelling = ({ spellCheckLanguage }: Options) => { return [ - EditorView.baseTheme({ - '.ol-cm-spelling-error': { - textDecorationColor: 'red', - textDecorationLine: 'underline', - textDecorationStyle: 'dotted', - textDecorationThickness: '2px', - textDecorationSkipInk: 'none', - textUnderlineOffset: '0.2em', - }, - '.cm-tooltip.ol-cm-spelling-context-menu-tooltip': { - borderWidth: '0', - }, - }), + spellingTheme, parserWatcher, spellCheckLanguageConf.of(spellCheckLanguageFacet.of(spellCheckLanguage)), spellCheckField, @@ -54,6 +42,20 @@ export const spelling = ({ spellCheckLanguage }: Options) => { ] } +const spellingTheme = EditorView.baseTheme({ + '.ol-cm-spelling-error': { + textDecorationColor: 'red', + textDecorationLine: 'underline', + textDecorationStyle: 'dotted', + textDecorationThickness: '2px', + textDecorationSkipInk: 'none', + textUnderlineOffset: '0.2em', + }, + '.cm-tooltip.ol-cm-spelling-context-menu-tooltip': { + borderWidth: '0', + }, +}) + const spellCheckField = StateField.define({ create(state) { const [spellCheckLanguage] = state.facet(spellCheckLanguageFacet) diff --git a/services/web/frontend/js/features/source-editor/extensions/theme.ts b/services/web/frontend/js/features/source-editor/extensions/theme.ts index ac59bfe9c5..f3694fd82e 100644 --- a/services/web/frontend/js/features/source-editor/extensions/theme.ts +++ b/services/web/frontend/js/features/source-editor/extensions/theme.ts @@ -74,44 +74,21 @@ const createThemeFromOptions = ({ return [ EditorView.editorAttributes.of({ class: overallTheme === '' ? 'overall-theme-dark' : 'overall-theme-light', + style: Object.entries({ + '--font-size': `${fontSize}px`, + '--source-font-family': fontFamilies[fontFamily]?.join(', '), + '--line-height': lineHeights[lineHeight], + }) + .map(([key, value]) => `${key}: ${value}`) + .join(';'), }), + // set variables for tooltips, which are outside the editor + // TODO: set these on document.body, or a new container element for the tooltips, without using a style mod EditorView.theme({ - '&.cm-editor': { - // set variables - '--font-size': `${fontSize}px`, - '--source-font-family': fontFamilies[fontFamily]?.join(', '), - '--line-height': lineHeights[lineHeight], - }, - '.cm-content': { - fontSize: 'var(--font-size)', - fontFamily: 'var(--source-font-family)', - lineHeight: 'var(--line-height)', - }, - '.cm-cursor-primary': { - fontSize: 'var(--font-size)', - fontFamily: 'var(--source-font-family)', - lineHeight: 'var(--line-height)', - }, - '.cm-gutters': { - fontSize: 'var(--font-size)', - lineHeight: 'var(--line-height)', - }, '.cm-tooltip': { - // set variables for tooltips, which are outside the editor '--font-size': `${fontSize}px`, '--source-font-family': fontFamilies[fontFamily]?.join(', '), '--line-height': lineHeights[lineHeight], - // NOTE: fontFamily is not set here, as most tooltips use the UI font - fontSize: 'var(--font-size)', - }, - '.cm-panel': { - fontSize: 'var(--font-size)', - }, - '.cm-foldGutter .cm-gutterElement > span': { - height: 'calc(var(--font-size) * var(--line-height))', - }, - '.cm-lineNumbers': { - fontFamily: 'var(--source-font-family)', }, }), ] @@ -121,6 +98,33 @@ const createThemeFromOptions = ({ * Base styles that can have &dark and &light variants */ const baseTheme = EditorView.baseTheme({ + '.cm-content': { + fontSize: 'var(--font-size)', + fontFamily: 'var(--source-font-family)', + lineHeight: 'var(--line-height)', + }, + '.cm-cursor-primary': { + fontSize: 'var(--font-size)', + fontFamily: 'var(--source-font-family)', + lineHeight: 'var(--line-height)', + }, + '.cm-gutters': { + fontSize: 'var(--font-size)', + lineHeight: 'var(--line-height)', + }, + '.cm-tooltip': { + // NOTE: fontFamily is not set here, as most tooltips use the UI font + fontSize: 'var(--font-size)', + }, + '.cm-panel': { + fontSize: 'var(--font-size)', + }, + '.cm-foldGutter .cm-gutterElement > span': { + height: 'calc(var(--font-size) * var(--line-height))', + }, + '.cm-lineNumbers': { + fontFamily: 'var(--source-font-family)', + }, // use a background color for lint error ranges '.cm-lintRange-error': { padding: 'var(--half-leading, 0) 0', @@ -255,17 +259,25 @@ const staticTheme = EditorView.theme({ }, }) +const themeCache = new Map() + const loadSelectedTheme = async (editorTheme: string) => { if (!editorTheme) { editorTheme = 'textmate' // use the default theme if unset } - const { theme, highlightStyle, dark } = await import( - /* webpackChunkName: "cm6-theme" */ `../themes/cm6/${editorTheme}.json` - ) + if (!themeCache.has(editorTheme)) { + const { theme, highlightStyle, dark } = await import( + /* webpackChunkName: "cm6-theme" */ `../themes/cm6/${editorTheme}.json` + ) - return [ - EditorView.theme(theme, { dark }), - EditorView.theme(highlightStyle, { dark }), - ] + const extension = [ + EditorView.theme(theme, { dark }), + EditorView.theme(highlightStyle, { dark }), + ] + + themeCache.set(editorTheme, extension) + } + + return themeCache.get(editorTheme) } 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 92e67d484a..e499dd4cd2 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 @@ -21,285 +21,284 @@ export function createToolbarPanel() { return { dom, top: true } } +const toolbarTheme = EditorView.theme({ + '.ol-cm-toolbar': { + backgroundColor: 'var(--editor-toolbar-bg)', + color: 'var(--toolbar-btn-color)', + flex: 1, + display: 'flex', + overflowX: 'hidden', + }, + '&.overall-theme-dark .ol-cm-toolbar': { + '& img': { + filter: 'invert(1)', + }, + }, + '.ol-cm-toolbar-overflow': { + display: 'flex', + flexWrap: 'wrap', + }, + '#popover-toolbar-overflow': { + padding: 0, + borderColor: 'rgba(125, 125, 125, 0.2)', + backgroundColor: 'var(--editor-toolbar-bg)', + color: 'var(--toolbar-btn-color)', + '& .popover-content': { + padding: 0, + }, + '& .arrow': { + borderBottomColor: 'rgba(125, 125, 125, 0.2)', + '&:after': { + borderBottomColor: 'var(--editor-toolbar-bg)', + }, + }, + }, + '.ol-cm-toolbar-button-menu-popover': { + '& > .popover-content': { + padding: 0, + }, + '& .arrow': { + display: 'none', + }, + '& .list-group': { + marginBottom: 0, + backgroundColor: 'var(--editor-toolbar-bg)', + borderRadius: '4px', + }, + '& .list-group-item': { + width: '100%', + textAlign: 'start', + display: 'flex', + alignItems: 'center', + gap: '5px', + color: 'var(--toolbar-btn-color)', + borderColor: 'var(--editor-toolbar-bg)', + background: 'none', + '&:hover, &:focus': { + backgroundColor: 'rgba(125, 125, 125, 0.2)', + }, + }, + }, + '.ol-cm-toolbar-button-group': { + display: 'flex', + alignItems: 'center', + whiteSpace: 'nowrap', + flexWrap: 'nowrap', + padding: '0 4px', + margin: '4px 0', + lineHeight: '1', + '&:not(:first-of-type)': { + borderLeft: '1px solid rgba(125, 125, 125, 0.3)', + '&.ol-cm-toolbar-end': { + borderLeft: 'none', + }, + '&.ol-cm-toolbar-stretch': { + flex: 1, + }, + '&.overflow-hidden': { + borderLeft: 'none', + }, + }, + '&.overflow-hidden': { + width: 0, + padding: 0, + }, + }, + '.formatting-buttons-wrapper': { + flex: 1, + }, + '.ol-cm-toolbar-button': { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + padding: '0', + margin: '0 1px', + backgroundColor: 'transparent', + border: 'none', + borderRadius: '1px', + lineHeight: '1', + width: '24px', + height: '24px', + overflow: 'hidden', + '&:hover, &:focus, &:active, &.active': { + backgroundColor: 'rgba(125, 125, 125, 0.1)', + color: 'inherit', + boxShadow: 'none', + '&[aria-disabled="true"]': { + opacity: '0.2', + }, + }, + '&.active, &:active': { + backgroundColor: 'rgba(125, 125, 125, 0.2)', + }, + '&[aria-disabled="true"]': { + opacity: '0.2', + cursor: 'not-allowed', + }, + '.overflow-hidden &': { + display: 'none', + }, + '&.ol-cm-toolbar-button-math': { + fontFamily: '"Noto Serif", serif', + fontSize: '16px', + fontWeight: 700, + }, + }, + '&.overall-theme-dark .ol-cm-toolbar-button': { + opacity: 0.8, + '&:hover, &:focus, &:active, &.active': { + backgroundColor: 'rgba(125, 125, 125, 0.2)', + }, + '&.active, &:active': { + backgroundColor: 'rgba(125, 125, 125, 0.4)', + }, + '&[aria-disabled="true"]': { + opacity: 0.2, + }, + }, + '.ol-cm-toolbar-end': { + justifyContent: 'flex-end', + '& .badge': { + marginRight: '5px', + }, + }, + '.ol-cm-toolbar-overflow-toggle': { + display: 'none', + '&.ol-cm-toolbar-overflow-toggle-visible': { + display: 'flex', + }, + }, + '.ol-cm-toolbar-menu-toggle': { + background: 'transparent', + boxShadow: 'none !important', + border: 'none', + whiteSpace: 'nowrap', + color: 'inherit', + borderRadius: '0', + opacity: 0.8, + width: '120px', + fontSize: '13px', + fontFamily: 'Lato', + fontWeight: '700', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: '5px 6px', + '&:hover, &:focus, &.active': { + backgroundColor: 'rgba(125, 125, 125, 0.1)', + opacity: '1', + color: 'inherit', + }, + '& .caret': { + marginTop: '0', + }, + }, + '.ol-cm-toolbar-menu-popover': { + border: 'none', + borderRadius: '0', + borderBottomLeftRadius: '4px', + borderBottomRightRadius: '4px', + boxShadow: '0 2px 5px rgb(0 0 0 / 20%)', + backgroundColor: 'var(--editor-toolbar-bg)', + color: 'var(--toolbar-btn-color)', + padding: '0', + '&.bottom': { + marginTop: '1px', + }, + '&.top': { + marginBottom: '1px', + }, + '& .arrow': { + display: 'none', + }, + '& .popover-content': { + padding: '0', + }, + '& .ol-cm-toolbar-menu': { + width: '120px', + display: 'flex', + flexDirection: 'column', + boxSizing: 'border-box', + fontSize: '14px', + }, + '& .ol-cm-toolbar-menu-item': { + border: 'none', + background: 'none', + padding: '4px 12px', + height: '40px', + display: 'flex', + alignItems: 'center', + fontWeight: 'bold', + '&.ol-cm-toolbar-menu-item-active': { + backgroundColor: 'rgba(125, 125, 125, 0.1)', + }, + '&:hover': { + backgroundColor: 'rgba(125, 125, 125, 0.2)', + color: 'inherit', + }, + '&.section-level-section': { + fontSize: '1.44em', + }, + '&.section-level-subsection': { + fontSize: '1.2em', + }, + '&.section-level-body': { + fontWeight: 'normal', + }, + }, + }, + '&.overall-theme-dark .ol-cm-toolbar-table-grid-popover': { + color: '#fff', + }, + '&.overall-theme-dark .ol-cm-toolbar-table-grid': { + '& td.active': { + outlineColor: 'white', + background: 'rgb(125, 125, 125)', + }, + }, + '.ol-cm-toolbar-table-grid': { + borderCollapse: 'separate', + tableLayout: 'fixed', + fontSize: '6px', + cursor: 'pointer', + width: '160px', + '& td': { + outline: '1px solid #E7E9EE', + outlineOffset: '-2px', + width: '16px', + height: '16px', + + '&.active': { + outlineColor: '#3265B2', + background: '#F1F4F9', + }, + }, + }, + '.ol-cm-toolbar-table-size-label': { + maxWidth: '160px', + fontFamily: 'Lato, sans-serif', + fontSize: '12px', + }, + '.ol-cm-toolbar-table-grid-popover': { + maxWidth: 'unset', + padding: '8px', + boxShadow: '0 5px 10px rgba(0, 0, 0, 0.2)', + borderRadius: '4px', + backgroundColor: 'var(--editor-toolbar-bg)', + pointerEvents: 'all', + }, + '.ol-cm-toolbar-button-menu-popover-unstyled': { + maxWidth: 'unset', + background: 'transparent', + border: 0, + padding: '0 8px 8px 160px', + boxShadow: 'none', + pointerEvents: 'none', + }, +}) + /** * A panel which contains the editor toolbar, provided by a state field which allows the toolbar to be toggled, * and styles for the toolbar. */ -export const toolbarPanel = () => [ - toolbarState, - EditorView.theme({ - '.ol-cm-toolbar': { - backgroundColor: 'var(--editor-toolbar-bg)', - color: 'var(--toolbar-btn-color)', - flex: 1, - display: 'flex', - overflowX: 'hidden', - }, - '&.overall-theme-dark .ol-cm-toolbar': { - '& img': { - filter: 'invert(1)', - }, - }, - '.ol-cm-toolbar-overflow': { - display: 'flex', - flexWrap: 'wrap', - }, - '#popover-toolbar-overflow': { - padding: 0, - borderColor: 'rgba(125, 125, 125, 0.2)', - backgroundColor: 'var(--editor-toolbar-bg)', - color: 'var(--toolbar-btn-color)', - '& .popover-content': { - padding: 0, - }, - '& .arrow': { - borderBottomColor: 'rgba(125, 125, 125, 0.2)', - '&:after': { - borderBottomColor: 'var(--editor-toolbar-bg)', - }, - }, - }, - '.ol-cm-toolbar-button-menu-popover': { - '& > .popover-content': { - padding: 0, - }, - '& .arrow': { - display: 'none', - }, - '& .list-group': { - marginBottom: 0, - backgroundColor: 'var(--editor-toolbar-bg)', - borderRadius: '4px', - }, - '& .list-group-item': { - width: '100%', - textAlign: 'start', - display: 'flex', - alignItems: 'center', - gap: '5px', - color: 'var(--toolbar-btn-color)', - borderColor: 'var(--editor-toolbar-bg)', - background: 'none', - '&:hover, &:focus': { - backgroundColor: 'rgba(125, 125, 125, 0.2)', - }, - }, - }, - '.ol-cm-toolbar-button-group': { - display: 'flex', - alignItems: 'center', - whiteSpace: 'nowrap', - flexWrap: 'nowrap', - padding: '0 4px', - margin: '4px 0', - lineHeight: '1', - '&:not(:first-of-type)': { - borderLeft: '1px solid rgba(125, 125, 125, 0.3)', - '&.ol-cm-toolbar-end': { - borderLeft: 'none', - }, - '&.ol-cm-toolbar-stretch': { - flex: 1, - }, - '&.overflow-hidden': { - borderLeft: 'none', - }, - }, - '&.overflow-hidden': { - width: 0, - padding: 0, - }, - }, - '.formatting-buttons-wrapper': { - flex: 1, - }, - '.ol-cm-toolbar-button': { - display: 'inline-flex', - alignItems: 'center', - justifyContent: 'center', - padding: '0', - margin: '0 1px', - backgroundColor: 'transparent', - border: 'none', - borderRadius: '1px', - lineHeight: '1', - width: '24px', - height: '24px', - overflow: 'hidden', - '&:hover, &:focus, &:active, &.active': { - backgroundColor: 'rgba(125, 125, 125, 0.1)', - color: 'inherit', - boxShadow: 'none', - '&[aria-disabled="true"]': { - opacity: '0.2', - }, - }, - '&.active, &:active': { - backgroundColor: 'rgba(125, 125, 125, 0.2)', - }, - '&[aria-disabled="true"]': { - opacity: '0.2', - cursor: 'not-allowed', - }, - '.overflow-hidden &': { - display: 'none', - }, - '&.ol-cm-toolbar-button-math': { - fontFamily: '"Noto Serif", serif', - fontSize: '16px', - fontWeight: 700, - }, - }, - '&.overall-theme-dark .ol-cm-toolbar-button': { - opacity: 0.8, - '&:hover, &:focus, &:active, &.active': { - backgroundColor: 'rgba(125, 125, 125, 0.2)', - }, - '&.active, &:active': { - backgroundColor: 'rgba(125, 125, 125, 0.4)', - }, - '&[aria-disabled="true"]': { - opacity: 0.2, - }, - }, - '.ol-cm-toolbar-end': { - justifyContent: 'flex-end', - '& .badge': { - marginRight: '5px', - }, - }, - '.ol-cm-toolbar-overflow-toggle': { - display: 'none', - '&.ol-cm-toolbar-overflow-toggle-visible': { - display: 'flex', - }, - }, - '.ol-cm-toolbar-menu-toggle': { - background: 'transparent', - boxShadow: 'none !important', - border: 'none', - whiteSpace: 'nowrap', - color: 'inherit', - borderRadius: '0', - opacity: 0.8, - width: '120px', - fontSize: '13px', - fontFamily: 'Lato', - fontWeight: '700', - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - padding: '5px 6px', - '&:hover, &:focus, &.active': { - backgroundColor: 'rgba(125, 125, 125, 0.1)', - opacity: '1', - color: 'inherit', - }, - '& .caret': { - marginTop: '0', - }, - }, - '.ol-cm-toolbar-menu-popover': { - border: 'none', - borderRadius: '0', - borderBottomLeftRadius: '4px', - borderBottomRightRadius: '4px', - boxShadow: '0 2px 5px rgb(0 0 0 / 20%)', - backgroundColor: 'var(--editor-toolbar-bg)', - color: 'var(--toolbar-btn-color)', - padding: '0', - '&.bottom': { - marginTop: '1px', - }, - '&.top': { - marginBottom: '1px', - }, - '& .arrow': { - display: 'none', - }, - '& .popover-content': { - padding: '0', - }, - '& .ol-cm-toolbar-menu': { - width: '120px', - display: 'flex', - flexDirection: 'column', - boxSizing: 'border-box', - fontSize: '14px', - }, - '& .ol-cm-toolbar-menu-item': { - border: 'none', - background: 'none', - padding: '4px 12px', - height: '40px', - display: 'flex', - alignItems: 'center', - fontWeight: 'bold', - '&.ol-cm-toolbar-menu-item-active': { - backgroundColor: 'rgba(125, 125, 125, 0.1)', - }, - '&:hover': { - backgroundColor: 'rgba(125, 125, 125, 0.2)', - color: 'inherit', - }, - '&.section-level-section': { - fontSize: '1.44em', - }, - '&.section-level-subsection': { - fontSize: '1.2em', - }, - '&.section-level-body': { - fontWeight: 'normal', - }, - }, - }, - '&.overall-theme-dark .ol-cm-toolbar-table-grid-popover': { - color: '#fff', - }, - '&.overall-theme-dark .ol-cm-toolbar-table-grid': { - '& td.active': { - outlineColor: 'white', - background: 'rgb(125, 125, 125)', - }, - }, - '.ol-cm-toolbar-table-grid': { - borderCollapse: 'separate', - tableLayout: 'fixed', - fontSize: '6px', - cursor: 'pointer', - width: '160px', - '& td': { - outline: '1px solid #E7E9EE', - outlineOffset: '-2px', - width: '16px', - height: '16px', - - '&.active': { - outlineColor: '#3265B2', - background: '#F1F4F9', - }, - }, - }, - '.ol-cm-toolbar-table-size-label': { - maxWidth: '160px', - fontFamily: 'Lato, sans-serif', - fontSize: '12px', - }, - '.ol-cm-toolbar-table-grid-popover': { - maxWidth: 'unset', - padding: '8px', - boxShadow: '0 5px 10px rgba(0, 0, 0, 0.2)', - borderRadius: '4px', - backgroundColor: 'var(--editor-toolbar-bg)', - pointerEvents: 'all', - }, - '.ol-cm-toolbar-button-menu-popover-unstyled': { - maxWidth: 'unset', - background: 'transparent', - border: 0, - padding: '0 8px 8px 160px', - boxShadow: 'none', - pointerEvents: 'none', - }, - }), -] +export const toolbarPanel = () => [toolbarState, toolbarTheme] diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/pasted-content.tsx b/services/web/frontend/js/features/source-editor/extensions/visual/pasted-content.tsx index bab7862dc2..e6628ae699 100644 --- a/services/web/frontend/js/features/source-editor/extensions/visual/pasted-content.tsx +++ b/services/web/frontend/js/features/source-editor/extensions/visual/pasted-content.tsx @@ -42,6 +42,59 @@ export const storePastedContent = ( effects: pastedContentEffect.of({ content, formatted }), }) +const pastedContentTheme = EditorView.baseTheme({ + '.ol-cm-pasted-content-menu-toggle': { + background: 'none', + borderRadius: '8px', + border: '1px solid rgb(125, 125, 125)', + margin: '0 4px', + opacity: '0.7', + '&:hover': { + opacity: '1', + }, + }, + '.ol-cm-pasted-content-menu-popover': { + maxWidth: 'unset', + '& .popover-content': { + padding: 0, + }, + }, + '&dark .ol-cm-pasted-content-menu-popover': { + background: 'rgba(0, 0, 0)', + }, + '.ol-cm-pasted-content-menu': { + display: 'flex', + flexDirection: 'column', + boxSizing: 'border-box', + fontSize: '14px', + fontFamily: '"Lato", sans-serif', + }, + '.ol-cm-pasted-content-menu-item': { + border: 'none', + background: 'none', + padding: '8px 16px', + width: '100%', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + whiteSpace: 'nowrap', + gap: '12px', + '&[aria-disabled="true"]': { + color: 'rgba(125, 125, 125, 0.5)', + }, + '&:hover': { + backgroundColor: 'rgba(125, 125, 125, 0.2)', + }, + }, + '.ol-cm-pasted-content-menu-item-label': { + flex: 1, + textAlign: 'left', + }, + '.ol-cm-pasted-content-menu-item-shortcut': { + textAlign: 'right', + }, +}) + export const pastedContent = StateField.define<{ content: PastedContent formatted: boolean @@ -88,58 +141,7 @@ export const pastedContent = StateField.define<{ return Decoration.set(decorations, true) }), - EditorView.baseTheme({ - '.ol-cm-pasted-content-menu-toggle': { - background: 'none', - borderRadius: '8px', - border: '1px solid rgb(125, 125, 125)', - margin: '0 4px', - opacity: '0.7', - '&:hover': { - opacity: '1', - }, - }, - '.ol-cm-pasted-content-menu-popover': { - maxWidth: 'unset', - '& .popover-content': { - padding: 0, - }, - }, - '&dark .ol-cm-pasted-content-menu-popover': { - background: 'rgba(0, 0, 0)', - }, - '.ol-cm-pasted-content-menu': { - display: 'flex', - flexDirection: 'column', - boxSizing: 'border-box', - fontSize: '14px', - fontFamily: '"Lato", sans-serif', - }, - '.ol-cm-pasted-content-menu-item': { - border: 'none', - background: 'none', - padding: '8px 16px', - width: '100%', - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - whiteSpace: 'nowrap', - gap: '12px', - '&[aria-disabled="true"]': { - color: 'rgba(125, 125, 125, 0.5)', - }, - '&:hover': { - backgroundColor: 'rgba(125, 125, 125, 0.2)', - }, - }, - '.ol-cm-pasted-content-menu-item-label': { - flex: 1, - textAlign: 'left', - }, - '.ol-cm-pasted-content-menu-item-shortcut': { - textAlign: 'right', - }, - }), + pastedContentTheme, ] }, }) diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/visual-theme.ts b/services/web/frontend/js/features/source-editor/extensions/visual/visual-theme.ts index b4966f5548..6443c06c38 100644 --- a/services/web/frontend/js/features/source-editor/extensions/visual/visual-theme.ts +++ b/services/web/frontend/js/features/source-editor/extensions/visual/visual-theme.ts @@ -439,10 +439,8 @@ const currentWidth = Facet.define({ function createContentWidthTheme(contentWidth: string) { return [ currentWidth.of(contentWidth), - EditorView.theme({ - '&.cm-editor': { - '--content-width': contentWidth, - }, + EditorView.editorAttributes.of({ + style: `--content-width: ${contentWidth}`, }), ] }