mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-14 20:40:17 -05:00
Merge pull request #20436 from overleaf/ii-bs5-editor-toolbar
[web] BS5 editor toolbar GitOrigin-RevId: a517fd52d648d165e89231d6f5551c026a951c43
This commit is contained in:
parent
9aef0cee70
commit
35728d7681
59 changed files with 1764 additions and 764 deletions
|
@ -1551,7 +1551,9 @@
|
|||
"took_a_while": "",
|
||||
"toolbar_bullet_list": "",
|
||||
"toolbar_choose_section_heading_level": "",
|
||||
"toolbar_code_visual_editor_switch": "",
|
||||
"toolbar_decrease_indent": "",
|
||||
"toolbar_editor": "",
|
||||
"toolbar_format_bold": "",
|
||||
"toolbar_format_italic": "",
|
||||
"toolbar_increase_indent": "",
|
||||
|
@ -1562,7 +1564,10 @@
|
|||
"toolbar_insert_inline_math": "",
|
||||
"toolbar_insert_link": "",
|
||||
"toolbar_insert_math": "",
|
||||
"toolbar_insert_math_and_symbols": "",
|
||||
"toolbar_insert_misc": "",
|
||||
"toolbar_insert_table": "",
|
||||
"toolbar_list_indentation": "",
|
||||
"toolbar_numbered_list": "",
|
||||
"toolbar_redo": "",
|
||||
"toolbar_selected_projects": "",
|
||||
|
@ -1571,8 +1576,12 @@
|
|||
"toolbar_selected_projects_restore": "",
|
||||
"toolbar_table_insert_size_table": "",
|
||||
"toolbar_table_insert_table_lowercase": "",
|
||||
"toolbar_text_formatting": "",
|
||||
"toolbar_text_style": "",
|
||||
"toolbar_toggle_symbol_palette": "",
|
||||
"toolbar_undo": "",
|
||||
"toolbar_undo_redo_actions": "",
|
||||
"toolbar_visibility": "",
|
||||
"tooltip_hide_filetree": "",
|
||||
"tooltip_hide_pdf": "",
|
||||
"tooltip_show_filetree": "",
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import Icon from '../../../shared/components/icon'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import { useLayoutContext } from '../../../shared/context/layout-context'
|
||||
|
||||
function SwitchToEditorButton() {
|
||||
|
@ -21,17 +23,21 @@ function SwitchToEditorButton() {
|
|||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
bsStyle={null}
|
||||
bsSize="xs"
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={handleClick}
|
||||
className="switch-to-editor-btn toolbar-btn-secondary btn-secondary"
|
||||
bs3Props={{
|
||||
bsSize: 'xsmall',
|
||||
className: 'switch-to-editor-btn toolbar-btn-secondary',
|
||||
}}
|
||||
>
|
||||
<Icon type="code" className="toolbar-btn-secondary-icon" />
|
||||
<span className="toolbar-btn-secondary-text">
|
||||
{t('switch_to_editor')}
|
||||
</span>
|
||||
</Button>
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="code" className="toolbar-btn-secondary-icon" />}
|
||||
bs5={<MaterialIcon type="code" />}
|
||||
/>
|
||||
{t('switch_to_editor')}
|
||||
</OLButton>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
useCodeMirrorViewContext,
|
||||
} from './codemirror-context'
|
||||
import { searchPanelOpen } from '@codemirror/search'
|
||||
import { useResizeObserver } from '../../../shared/hooks/use-resize-observer'
|
||||
import { useResizeObserver } from '@/shared/hooks/use-resize-observer'
|
||||
import { ToolbarButton } from './toolbar/toolbar-button'
|
||||
import { ToolbarItems } from './toolbar/toolbar-items'
|
||||
import * as commands from '../extensions/toolbar/commands'
|
||||
|
@ -21,6 +21,8 @@ import { isVisual } from '../extensions/visual/visual'
|
|||
import { language } from '@codemirror/language'
|
||||
import { minimumListDepthForSelection } from '../utils/tree-operations/ancestors'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { bsVersion } from '@/features/utils/bootstrap-5'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export const CodeMirrorToolbar = () => {
|
||||
const view = useCodeMirrorViewContext()
|
||||
|
@ -34,6 +36,7 @@ export const CodeMirrorToolbar = () => {
|
|||
}
|
||||
|
||||
const Toolbar = memo(function Toolbar() {
|
||||
const { t } = useTranslation()
|
||||
const state = useCodeMirrorStateContext()
|
||||
const view = useCodeMirrorViewContext()
|
||||
|
||||
|
@ -140,7 +143,12 @@ const Toolbar = memo(function Toolbar() {
|
|||
const showActions = !state.readOnly && !insideTable
|
||||
|
||||
return (
|
||||
<div className="ol-cm-toolbar toolbar-editor" ref={elementRef}>
|
||||
<div
|
||||
role="toolbar"
|
||||
aria-label={t('toolbar_editor')}
|
||||
className="ol-cm-toolbar toolbar-editor"
|
||||
ref={elementRef}
|
||||
>
|
||||
<EditorSwitch />
|
||||
{showActions && (
|
||||
<ToolbarItems
|
||||
|
@ -179,19 +187,22 @@ const Toolbar = memo(function Toolbar() {
|
|||
label="Toggle Search"
|
||||
command={commands.toggleSearch}
|
||||
active={searchPanelOpen(state)}
|
||||
icon="search"
|
||||
icon={bsVersion({ bs5: 'search', bs3: 'search' }) as string}
|
||||
/>
|
||||
|
||||
<SwitchToPDFButton />
|
||||
<DetacherSynctexControl />
|
||||
<DetachCompileButtonWrapper />
|
||||
</div>
|
||||
<div className="ol-cm-toolbar-button-group hidden">
|
||||
<div
|
||||
className="ol-cm-toolbar-button-group hidden"
|
||||
aria-label={t('toolbar_visibility')}
|
||||
>
|
||||
<ToolbarButton
|
||||
id="toolbar-expand-less"
|
||||
label="Hide Toolbar"
|
||||
command={toggleToolbar}
|
||||
icon="caret-up"
|
||||
icon={bsVersion({ bs5: 'arrow_drop_up', bs3: 'caret-up' }) as string}
|
||||
hidden // enable this once there's a way to show the toolbar again
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { ChangeEvent, FC, memo, useCallback } from 'react'
|
||||
import useScopeValue from '@/shared/hooks/use-scope-value'
|
||||
import Tooltip from '@/shared/components/tooltip'
|
||||
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
||||
import useTutorial from '@/shared/hooks/promotions/use-tutorial'
|
||||
import { sendMB } from '../../../infrastructure/event-tracking'
|
||||
import isValidTeXFile from '../../../main/is-valid-tex-file'
|
||||
|
@ -45,7 +45,10 @@ function EditorSwitch() {
|
|||
)
|
||||
|
||||
return (
|
||||
<div className="editor-toggle-switch">
|
||||
<div
|
||||
className="editor-toggle-switch"
|
||||
aria-label={t('toolbar_code_visual_editor_switch')}
|
||||
>
|
||||
<fieldset className="toggle-switch">
|
||||
<legend className="sr-only">Editor mode.</legend>
|
||||
|
||||
|
@ -101,14 +104,14 @@ const RichTextToggle: FC<{
|
|||
|
||||
if (disabled) {
|
||||
return (
|
||||
<Tooltip
|
||||
<OLTooltip
|
||||
description={t('visual_editor_is_only_available_for_tex_files')}
|
||||
id="rich-text-toggle-tooltip"
|
||||
overlayProps={{ placement: 'bottom' }}
|
||||
tooltipProps={{ className: 'tooltip-wide' }}
|
||||
>
|
||||
{toggle}
|
||||
</Tooltip>
|
||||
</OLTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { Alert } from 'react-bootstrap'
|
||||
import {
|
||||
FigureModalSource,
|
||||
useFigureModalContext,
|
||||
|
@ -12,6 +11,7 @@ import { FigureModalCurrentProjectSource } from './file-sources/figure-modal-pro
|
|||
import { FigureModalUploadFileSource } from './file-sources/figure-modal-upload-source'
|
||||
import { FigureModalUrlSource } from './file-sources/figure-modal-url-source'
|
||||
import { useCallback } from 'react'
|
||||
import OLNotification from '@/features/ui/components/ol/ol-notification'
|
||||
|
||||
const sourceModes = new Map([
|
||||
[FigureModalSource.FILE_TREE, FigureModalCurrentProjectSource],
|
||||
|
@ -44,9 +44,7 @@ export default function FigureModalBody() {
|
|||
return (
|
||||
<>
|
||||
{error && (
|
||||
<Alert bsStyle="danger" onDismiss={onDismiss}>
|
||||
{error}
|
||||
</Alert>
|
||||
<OLNotification type="error" onDismiss={onDismiss} content={error} />
|
||||
)}
|
||||
<Body />
|
||||
<FigureModalFigureOptions />
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { Button } from 'react-bootstrap'
|
||||
import {
|
||||
FigureModalSource,
|
||||
useFigureModalContext,
|
||||
|
@ -7,6 +6,11 @@ import Icon from '../../../../shared/components/icon'
|
|||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { sendMB } from '../../../../infrastructure/event-tracking'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { bsVersion } from '@/features/utils/bootstrap-5'
|
||||
import classnames from 'classnames'
|
||||
|
||||
export const FigureModalFooter: FC<{
|
||||
onInsert: () => void
|
||||
|
@ -14,23 +18,16 @@ export const FigureModalFooter: FC<{
|
|||
onDelete: () => void
|
||||
}> = ({ onInsert, onCancel, onDelete }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="figure-modal-footer">
|
||||
<div className="figure-modal-help-buttons">
|
||||
<HelpToggle />
|
||||
</div>
|
||||
<div className="figure-modal-actions">
|
||||
<Button
|
||||
bsStyle={null}
|
||||
className="btn-secondary"
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<FigureModalAction onInsert={onInsert} onDelete={onDelete} />
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
<HelpToggle />
|
||||
<OLButton variant="secondary" onClick={onCancel}>
|
||||
{t('cancel')}
|
||||
</OLButton>
|
||||
<FigureModalAction onInsert={onInsert} onDelete={onDelete} />
|
||||
<BootstrapVersionSwitcher bs3={<div className="clearfix" />} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -39,25 +36,48 @@ const HelpToggle = () => {
|
|||
const { helpShown, dispatch } = useFigureModalContext()
|
||||
if (helpShown) {
|
||||
return (
|
||||
<Button
|
||||
bsStyle={null}
|
||||
className="btn-link figure-modal-help-link"
|
||||
<OLButton
|
||||
variant="link"
|
||||
className={classnames(
|
||||
'figure-modal-help-link',
|
||||
bsVersion({ bs3: 'pull-left', bs5: 'me-auto' })
|
||||
)}
|
||||
onClick={() => dispatch({ helpShown: false })}
|
||||
>
|
||||
<Icon type="arrow-left" fw />
|
||||
{t('back')}
|
||||
</Button>
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="arrow-left" fw />}
|
||||
bs5={
|
||||
<span>
|
||||
<MaterialIcon
|
||||
type="arrow_left_alt"
|
||||
className="align-text-bottom"
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
/>{' '}
|
||||
{t('back')}
|
||||
</OLButton>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
bsStyle={null}
|
||||
className="btn-link figure-modal-help-link"
|
||||
<OLButton
|
||||
variant="link"
|
||||
className={classnames(
|
||||
'figure-modal-help-link',
|
||||
bsVersion({ bs3: 'pull-left', bs5: 'me-auto' })
|
||||
)}
|
||||
onClick={() => dispatch({ helpShown: true })}
|
||||
>
|
||||
<Icon type="question-circle" fw />
|
||||
{t('help')}
|
||||
</Button>
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="question-circle" fw />}
|
||||
bs5={
|
||||
<span>
|
||||
<MaterialIcon type="help" className="align-text-bottom" />
|
||||
</span>
|
||||
}
|
||||
/>{' '}
|
||||
{t('help')}
|
||||
</OLButton>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -75,38 +95,29 @@ const FigureModalAction: FC<{
|
|||
|
||||
if (sourcePickerShown) {
|
||||
return (
|
||||
<Button
|
||||
bsStyle={null}
|
||||
className="btn-danger"
|
||||
type="button"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<OLButton variant="danger" onClick={onDelete}>
|
||||
{t('delete_figure')}
|
||||
</Button>
|
||||
</OLButton>
|
||||
)
|
||||
}
|
||||
|
||||
if (source === FigureModalSource.EDIT_FIGURE) {
|
||||
return (
|
||||
<Button
|
||||
bsStyle={null}
|
||||
className="btn-success"
|
||||
type="button"
|
||||
<OLButton
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
onInsert()
|
||||
sendMB('figure-modal-edit')
|
||||
}}
|
||||
>
|
||||
{t('done')}
|
||||
</Button>
|
||||
</OLButton>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
bsStyle={null}
|
||||
className="btn-success"
|
||||
type="button"
|
||||
<OLButton
|
||||
variant="primary"
|
||||
disabled={getPath === undefined}
|
||||
onClick={() => {
|
||||
onInsert()
|
||||
|
@ -114,6 +125,6 @@ const FigureModalAction: FC<{
|
|||
}}
|
||||
>
|
||||
{t('insert_figure')}
|
||||
</Button>
|
||||
</OLButton>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
import { FC } from 'react'
|
||||
import Icon from '../../../../shared/components/icon'
|
||||
import Tooltip from '../../../../shared/components/tooltip'
|
||||
import Icon from '@/shared/components/icon'
|
||||
import {
|
||||
useFigureModalContext,
|
||||
useFigureModalExistingFigureContext,
|
||||
} from './figure-modal-context'
|
||||
import { Switcher, SwitcherItem } from '../../../../shared/components/switcher'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
||||
import OLFormCheckbox from '@/features/ui/components/ol/ol-form-checkbox'
|
||||
import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
|
||||
import OLToggleButtonGroup from '@/features/ui/components/ol/ol-toggle-button-group'
|
||||
import OLToggleButton from '@/features/ui/components/ol/ol-toggle-button'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import { bsVersion } from '@/features/utils/bootstrap-5'
|
||||
|
||||
export const FigureModalFigureOptions: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
@ -16,71 +22,119 @@ export const FigureModalFigureOptions: FC = () => {
|
|||
const { hasComplexGraphicsArgument } = useFigureModalExistingFigureContext()
|
||||
return (
|
||||
<>
|
||||
<div className="figure-modal-checkbox-input">
|
||||
<input
|
||||
type="checkbox"
|
||||
<OLFormGroup>
|
||||
<OLFormCheckbox
|
||||
id="figure-modal-caption"
|
||||
data-cy="include-caption-option"
|
||||
defaultChecked={includeCaption}
|
||||
onChange={event => dispatch({ includeCaption: event.target.checked })}
|
||||
className={bsVersion({ bs3: 'figure-modal-checkbox-input' })}
|
||||
label={t('include_caption')}
|
||||
/>
|
||||
<label className="figure-modal-label" htmlFor="figure-modal-caption">
|
||||
{t('include_caption')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="figure-modal-checkbox-input">
|
||||
<input
|
||||
type="checkbox"
|
||||
</OLFormGroup>
|
||||
<OLFormGroup>
|
||||
<OLFormCheckbox
|
||||
id="figure-modal-label"
|
||||
data-cy="include-label-option"
|
||||
defaultChecked={includeLabel}
|
||||
onChange={event => dispatch({ includeLabel: event.target.checked })}
|
||||
className={bsVersion({ bs3: 'figure-modal-checkbox-input' })}
|
||||
label={
|
||||
<span className="figure-modal-label-content">
|
||||
{t('include_label')}
|
||||
<br />
|
||||
<small className="text-muted">
|
||||
{t(
|
||||
'used_when_referring_to_the_figure_elsewhere_in_the_document'
|
||||
)}
|
||||
</small>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<label htmlFor="figure-modal-label" className="mb-0 figure-modal-label">
|
||||
{t('include_label')}
|
||||
<br />
|
||||
<span className="text-muted text-small figure-modal-label-description">
|
||||
{t('used_when_referring_to_the_figure_elsewhere_in_the_document')}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="figure-modal-switcher-input">
|
||||
<div>
|
||||
{t('image_width')}{' '}
|
||||
{hasComplexGraphicsArgument ? (
|
||||
<Tooltip
|
||||
id="figure-modal-image-width-warning-tooltip"
|
||||
description={t('a_custom_size_has_been_used_in_the_latex_code')}
|
||||
overlayProps={{ delay: 0, placement: 'top' }}
|
||||
>
|
||||
<Icon type="exclamation-triangle" fw />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip
|
||||
id="figure-modal-image-width-tooltip"
|
||||
description={t(
|
||||
'the_width_you_choose_here_is_based_on_the_width_of_the_text_in_your_document'
|
||||
)}
|
||||
overlayProps={{ delay: 0, placement: 'bottom' }}
|
||||
>
|
||||
<Icon type="question-circle" fw />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Switcher
|
||||
</OLFormGroup>
|
||||
<OLFormGroup className="mb-0">
|
||||
<div className="figure-modal-switcher-input">
|
||||
<div>
|
||||
{t('image_width')}{' '}
|
||||
{hasComplexGraphicsArgument ? (
|
||||
<OLTooltip
|
||||
id="figure-modal-image-width-warning-tooltip"
|
||||
description={t('a_custom_size_has_been_used_in_the_latex_code')}
|
||||
overlayProps={{ delay: 0, placement: 'top' }}
|
||||
>
|
||||
<span>
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="exclamation-triangle" fw />}
|
||||
bs5={
|
||||
<MaterialIcon
|
||||
type="warning"
|
||||
className="align-text-bottom"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
</OLTooltip>
|
||||
) : (
|
||||
<OLTooltip
|
||||
id="figure-modal-image-width-tooltip"
|
||||
description={t(
|
||||
'the_width_you_choose_here_is_based_on_the_width_of_the_text_in_your_document'
|
||||
)}
|
||||
overlayProps={{ delay: 0, placement: 'bottom' }}
|
||||
>
|
||||
<span>
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="question-circle" fw />}
|
||||
bs5={
|
||||
<MaterialIcon type="help" className="align-text-bottom" />
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
</OLTooltip>
|
||||
)}
|
||||
</div>
|
||||
<OLToggleButtonGroup
|
||||
type="radio"
|
||||
name="figure-width"
|
||||
onChange={value => dispatch({ width: parseFloat(value) })}
|
||||
defaultValue={width === 1 ? '1.0' : width?.toString()}
|
||||
disabled={hasComplexGraphicsArgument}
|
||||
aria-label={t('image_width')}
|
||||
>
|
||||
<SwitcherItem value="0.25" label={t('1_4_width')} />
|
||||
<SwitcherItem value="0.5" label={t('1_2_width')} />
|
||||
<SwitcherItem value="0.75" label={t('3_4_width')} />
|
||||
<SwitcherItem value="1.0" label={t('full_width')} />
|
||||
</Switcher>
|
||||
<OLToggleButton
|
||||
variant="secondary"
|
||||
id="width-25p"
|
||||
disabled={hasComplexGraphicsArgument}
|
||||
value="0.25"
|
||||
>
|
||||
{t('1_4_width')}
|
||||
</OLToggleButton>
|
||||
<OLToggleButton
|
||||
variant="secondary"
|
||||
id="width-50p"
|
||||
disabled={hasComplexGraphicsArgument}
|
||||
value="0.5"
|
||||
>
|
||||
{t('1_2_width')}
|
||||
</OLToggleButton>
|
||||
<OLToggleButton
|
||||
variant="secondary"
|
||||
id="width-75p"
|
||||
disabled={hasComplexGraphicsArgument}
|
||||
value="0.75"
|
||||
>
|
||||
{t('3_4_width')}
|
||||
</OLToggleButton>
|
||||
<OLToggleButton
|
||||
variant="secondary"
|
||||
id="width-100p"
|
||||
disabled={hasComplexGraphicsArgument}
|
||||
value="1.0"
|
||||
>
|
||||
{t('full_width')}
|
||||
</OLToggleButton>
|
||||
</OLToggleButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
</OLFormGroup>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -4,9 +4,11 @@ import {
|
|||
useFigureModalContext,
|
||||
} from './figure-modal-context'
|
||||
import Icon from '../../../../shared/components/icon'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import getMeta from '@/utils/meta'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import { bsVersion } from '@/features/utils/bootstrap-5'
|
||||
|
||||
export const FigureModalSourcePicker: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
@ -16,33 +18,31 @@ export const FigureModalSourcePicker: FC = () => {
|
|||
hasLinkUrlFeature,
|
||||
} = getMeta('ol-ExposedSettings')
|
||||
return (
|
||||
<div className="figure-modal-source-selector">
|
||||
<div className="figure-modal-source-button-grid">
|
||||
<div className="figure-modal-source-button-grid">
|
||||
<FigureModalSourceButton
|
||||
type={FigureModalSource.FILE_UPLOAD}
|
||||
title={t('replace_from_computer')}
|
||||
icon={bsVersion({ bs3: 'upload', bs5: 'upload' }) as string}
|
||||
/>
|
||||
<FigureModalSourceButton
|
||||
type={FigureModalSource.FILE_TREE}
|
||||
title={t('replace_from_project_files')}
|
||||
icon={bsVersion({ bs3: 'archive', bs5: 'inbox' }) as string}
|
||||
/>
|
||||
{(hasLinkedProjectFileFeature || hasLinkedProjectOutputFileFeature) && (
|
||||
<FigureModalSourceButton
|
||||
type={FigureModalSource.FILE_UPLOAD}
|
||||
title={t('replace_from_computer')}
|
||||
icon="upload"
|
||||
type={FigureModalSource.OTHER_PROJECT}
|
||||
title={t('replace_from_another_project')}
|
||||
icon={bsVersion({ bs3: 'folder-open', bs5: 'folder_open' }) as string}
|
||||
/>
|
||||
)}
|
||||
{hasLinkUrlFeature && (
|
||||
<FigureModalSourceButton
|
||||
type={FigureModalSource.FILE_TREE}
|
||||
title={t('replace_from_project_files')}
|
||||
icon="archive"
|
||||
type={FigureModalSource.FROM_URL}
|
||||
title={t('replace_from_url')}
|
||||
icon={bsVersion({ bs3: 'globe', bs5: 'public' }) as string}
|
||||
/>
|
||||
{(hasLinkedProjectFileFeature || hasLinkedProjectOutputFileFeature) && (
|
||||
<FigureModalSourceButton
|
||||
type={FigureModalSource.OTHER_PROJECT}
|
||||
title={t('replace_from_another_project')}
|
||||
icon="folder-open"
|
||||
/>
|
||||
)}
|
||||
{hasLinkUrlFeature && (
|
||||
<FigureModalSourceButton
|
||||
type={FigureModalSource.FROM_URL}
|
||||
title={t('replace_from_url')}
|
||||
icon="globe"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -54,25 +54,29 @@ const FigureModalSourceButton: FC<{
|
|||
}> = ({ type, title, icon }) => {
|
||||
const { dispatch } = useFigureModalContext()
|
||||
return (
|
||||
<Button
|
||||
bsStyle={null}
|
||||
bsClass=""
|
||||
<button
|
||||
type="button"
|
||||
className="figure-modal-source-button"
|
||||
onClick={() => {
|
||||
dispatch({ source: type, sourcePickerShown: false, getPath: undefined })
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
type={icon}
|
||||
className="figure-modal-source-button-icon source-icon"
|
||||
fw
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={
|
||||
<Icon type={icon} className="figure-modal-source-button-icon" fw />
|
||||
}
|
||||
bs5={
|
||||
<MaterialIcon
|
||||
type={icon}
|
||||
className="figure-modal-source-button-icon"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<span className="figure-modal-source-button-title">{title}</span>
|
||||
<Icon
|
||||
type="chevron-right"
|
||||
className="figure-modal-source-button-icon"
|
||||
fw
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="chevron-right" fw />}
|
||||
bs5={<MaterialIcon type="chevron_right" />}
|
||||
/>
|
||||
</Button>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import { Modal } from 'react-bootstrap'
|
||||
import AccessibleModal from '../../../../shared/components/accessible-modal'
|
||||
import OLModal, {
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
OLModalHeader,
|
||||
OLModalTitle,
|
||||
} from '@/features/ui/components/ol/ol-modal'
|
||||
import {
|
||||
FigureModalProvider,
|
||||
FigureModalSource,
|
||||
|
@ -268,9 +272,9 @@ const FigureModalContent = () => {
|
|||
return null
|
||||
}
|
||||
return (
|
||||
<AccessibleModal onHide={hide} className="figure-modal" show>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>
|
||||
<OLModal onHide={hide} className="figure-modal" show>
|
||||
<OLModalHeader closeButton>
|
||||
<OLModalTitle>
|
||||
{helpShown
|
||||
? t('help')
|
||||
: sourcePickerShown
|
||||
|
@ -281,22 +285,22 @@ const FigureModalContent = () => {
|
|||
url="https://forms.gle/PfEtwceYBNQ32DF4A"
|
||||
text="Please click to give feedback about editing figures."
|
||||
/>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
</OLModalTitle>
|
||||
</OLModalHeader>
|
||||
|
||||
<Modal.Body>
|
||||
<OLModalBody>
|
||||
<Suspense fallback={<FullSizeLoadingSpinner minHeight="15rem" />}>
|
||||
<FigureModalBody />
|
||||
</Suspense>
|
||||
</Modal.Body>
|
||||
</OLModalBody>
|
||||
|
||||
<Modal.Footer>
|
||||
<OLModalFooter>
|
||||
<FigureModalFooter
|
||||
onInsert={insert}
|
||||
onCancel={onCancel}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</Modal.Footer>
|
||||
</AccessibleModal>
|
||||
</OLModalFooter>
|
||||
</OLModal>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,19 +1,16 @@
|
|||
import {
|
||||
DetailedHTMLProps,
|
||||
InputHTMLAttributes,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { File, FileOrDirectory } from '../../utils/file'
|
||||
import { Alert } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCurrentProjectFolders } from '@/features/source-editor/hooks/use-current-project-folders'
|
||||
import OLFormControl from '@/features/ui/components/ol/ol-form-control'
|
||||
import OLFormLabel from '@/features/ui/components/ol/ol-form-label'
|
||||
import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
|
||||
import OLNotification from '@/features/ui/components/ol/ol-notification'
|
||||
|
||||
type FileNameInputProps = Omit<
|
||||
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
|
||||
React.ComponentProps<typeof OLFormControl>,
|
||||
'onFocus'
|
||||
> & { targetFolder: File | null }
|
||||
> & { targetFolder: File | null; label: string }
|
||||
|
||||
function findFile(
|
||||
folder: { id: string; name: string },
|
||||
|
@ -52,6 +49,8 @@ function hasOverlap(
|
|||
}
|
||||
|
||||
export const FileNameInput = ({
|
||||
id,
|
||||
label,
|
||||
targetFolder,
|
||||
...props
|
||||
}: FileNameInputProps) => {
|
||||
|
@ -82,12 +81,19 @@ export const FileNameInput = ({
|
|||
}, [])
|
||||
return (
|
||||
<>
|
||||
<input {...props} type="text" onFocus={onFocus} />
|
||||
{overlap && (
|
||||
<Alert bsStyle="warning" className="mt-1 mb-0">
|
||||
{t('a_file_with_that_name_already_exists_and_will_be_overriden')}
|
||||
</Alert>
|
||||
)}
|
||||
<OLFormGroup controlId={id}>
|
||||
<OLFormLabel>{label}</OLFormLabel>
|
||||
<OLFormControl onFocus={onFocus} {...props} />
|
||||
{overlap && (
|
||||
<OLNotification
|
||||
type="warning"
|
||||
content={t(
|
||||
'a_file_with_that_name_already_exists_and_will_be_overriden'
|
||||
)}
|
||||
className="mt-1 mb-0"
|
||||
/>
|
||||
)}
|
||||
</OLFormGroup>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { File } from '../../utils/file'
|
|||
import { Select } from '../../../../shared/components/select'
|
||||
import { useCurrentProjectFolders } from '../../hooks/use-current-project-folders'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
|
||||
|
||||
export const FileRelocator = ({
|
||||
name,
|
||||
|
@ -49,40 +50,36 @@ export const FileRelocator = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
<label
|
||||
htmlFor="figure-modal-relocated-file-name"
|
||||
className="figure-modal-input-label"
|
||||
>
|
||||
{t('file_name_in_this_project_figure_modal')}
|
||||
</label>
|
||||
<FileNameInput
|
||||
id="figure-modal-relocated-file-name"
|
||||
type="text"
|
||||
className="form-control figure-modal-input-field"
|
||||
label={t('file_name_in_this_project_figure_modal')}
|
||||
value={name}
|
||||
disabled={nameDisabled}
|
||||
placeholder="example.jpg"
|
||||
onChange={nameChanged}
|
||||
targetFolder={folder}
|
||||
/>
|
||||
<Select
|
||||
items={folders || []}
|
||||
itemToString={item => {
|
||||
if (item?.path === '' && item?.name === 'rootFolder') {
|
||||
<OLFormGroup>
|
||||
<Select
|
||||
items={folders || []}
|
||||
itemToString={item => {
|
||||
if (item?.path === '' && item?.name === 'rootFolder') {
|
||||
return t('no_folder')
|
||||
}
|
||||
if (item) {
|
||||
return `${item.path}${item.name}`
|
||||
}
|
||||
return t('no_folder')
|
||||
}
|
||||
if (item) {
|
||||
return `${item.path}${item.name}`
|
||||
}
|
||||
return t('no_folder')
|
||||
}}
|
||||
itemToSubtitle={item => item?.path ?? ''}
|
||||
itemToKey={item => item.id}
|
||||
defaultText={t('select_folder_from_project')}
|
||||
label={t('folder_location')}
|
||||
optionalLabel
|
||||
onSelectedItemChanged={selectedFolderChanged}
|
||||
/>
|
||||
}}
|
||||
itemToSubtitle={item => item?.path ?? ''}
|
||||
itemToKey={item => item.id}
|
||||
defaultText={t('select_folder_from_project')}
|
||||
label={t('folder_location')}
|
||||
optionalLabel
|
||||
onSelectedItemChanged={selectedFolderChanged}
|
||||
/>
|
||||
</OLFormGroup>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@ import {
|
|||
OutputEntity,
|
||||
useProjectOutputFiles,
|
||||
} from '../../../../file-tree/hooks/use-project-output-files'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { useCurrentProjectFolders } from '../../../hooks/use-current-project-folders'
|
||||
import { File, isImageEntity } from '../../../utils/file'
|
||||
import { postJSON } from '../../../../../infrastructure/fetch-json'
|
||||
|
@ -23,6 +22,8 @@ import { useTranslation } from 'react-i18next'
|
|||
import { waitForFileTreeUpdate } from '../../../extensions/figure-modal'
|
||||
import { useCodeMirrorViewContext } from '../../codemirror-context'
|
||||
import getMeta from '@/utils/meta'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
|
||||
|
||||
function suggestName(path: string) {
|
||||
const parts = path.split('/')
|
||||
|
@ -132,54 +133,55 @@ export const FigureModalOtherProjectSource: FC = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
items={projects ?? []}
|
||||
itemToString={project => (project ? project.name : '')}
|
||||
itemToKey={item => item._id}
|
||||
defaultText={t('select_a_project_figure_modal')}
|
||||
label={t('project_figure_modal')}
|
||||
disabled={projectsLoading}
|
||||
onSelectedItemChanged={item => {
|
||||
const suggestion = nameDirty ? name : ''
|
||||
setName(suggestion)
|
||||
setSelectedProject(item ?? null)
|
||||
setFile(null)
|
||||
updateDispatch({
|
||||
newSelectedProject: item ?? null,
|
||||
newFile: null,
|
||||
newName: suggestion,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<FileSelector
|
||||
projectId={selectedProject?._id}
|
||||
onSelectedItemChange={item => {
|
||||
const suggestion = nameDirty ? name : suggestName(item?.path ?? '')
|
||||
setName(suggestion)
|
||||
setFile(item ?? null)
|
||||
updateDispatch({
|
||||
newFile: item ?? null,
|
||||
newName: suggestion,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{hasLinkedProjectFileFeature && hasLinkedProjectOutputFileFeature && (
|
||||
<div>
|
||||
or{' '}
|
||||
<Button
|
||||
className="p-0"
|
||||
bsStyle="link"
|
||||
type="button"
|
||||
onClick={() => setUsingOutputFiles(value => !value)}
|
||||
>
|
||||
<span>
|
||||
<OLFormGroup>
|
||||
<Select
|
||||
items={projects ?? []}
|
||||
itemToString={project => (project ? project.name : '')}
|
||||
itemToKey={item => item._id}
|
||||
defaultText={t('select_a_project_figure_modal')}
|
||||
label={t('project_figure_modal')}
|
||||
disabled={projectsLoading}
|
||||
onSelectedItemChanged={item => {
|
||||
const suggestion = nameDirty ? name : ''
|
||||
setName(suggestion)
|
||||
setSelectedProject(item ?? null)
|
||||
setFile(null)
|
||||
updateDispatch({
|
||||
newSelectedProject: item ?? null,
|
||||
newFile: null,
|
||||
newName: suggestion,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</OLFormGroup>
|
||||
<OLFormGroup>
|
||||
<FileSelector
|
||||
projectId={selectedProject?._id}
|
||||
onSelectedItemChange={item => {
|
||||
const suggestion = nameDirty ? name : suggestName(item?.path ?? '')
|
||||
setName(suggestion)
|
||||
setFile(item ?? null)
|
||||
updateDispatch({
|
||||
newFile: item ?? null,
|
||||
newName: suggestion,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{hasLinkedProjectFileFeature && hasLinkedProjectOutputFileFeature && (
|
||||
<div>
|
||||
or{' '}
|
||||
<OLButton
|
||||
variant="link"
|
||||
onClick={() => setUsingOutputFiles(value => !value)}
|
||||
className="p-0 select-from-files-btn"
|
||||
>
|
||||
{usingOutputFiles
|
||||
? t('select_from_project_files')
|
||||
: t('select_from_output_files')}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</OLButton>
|
||||
</div>
|
||||
)}
|
||||
</OLFormGroup>
|
||||
<FileRelocator
|
||||
folder={folder}
|
||||
name={name}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { useFigureModalContext } from '../figure-modal-context'
|
|||
import { filterFiles, isImageFile } from '../../../utils/file'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCurrentProjectFolders } from '@/features/source-editor/hooks/use-current-project-folders'
|
||||
import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
|
||||
|
||||
export const FigureModalCurrentProjectSource: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
@ -15,27 +16,29 @@ export const FigureModalCurrentProjectSource: FC = () => {
|
|||
const { dispatch, selectedItemId } = useFigureModalContext()
|
||||
const noFiles = files?.length === 0
|
||||
return (
|
||||
<Select
|
||||
items={files || []}
|
||||
itemToString={file => (file ? file.name : '')}
|
||||
itemToSubtitle={item => item?.path ?? ''}
|
||||
itemToKey={item => item.id}
|
||||
defaultItem={
|
||||
files && selectedItemId
|
||||
? files.find(item => item.id === selectedItemId)
|
||||
: undefined
|
||||
}
|
||||
defaultText={
|
||||
noFiles
|
||||
? t('no_image_files_found')
|
||||
: t('select_image_from_project_files')
|
||||
}
|
||||
label="Image file"
|
||||
onSelectedItemChanged={item => {
|
||||
dispatch({
|
||||
getPath: item ? async () => `${item.path}${item.name}` : undefined,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<OLFormGroup>
|
||||
<Select
|
||||
items={files || []}
|
||||
itemToString={file => (file ? file.name : '')}
|
||||
itemToSubtitle={item => item?.path ?? ''}
|
||||
itemToKey={item => item.id}
|
||||
defaultItem={
|
||||
files && selectedItemId
|
||||
? files.find(item => item.id === selectedItemId)
|
||||
: undefined
|
||||
}
|
||||
defaultText={
|
||||
noFiles
|
||||
? t('no_image_files_found')
|
||||
: t('select_image_from_project_files')
|
||||
}
|
||||
label="Image file"
|
||||
onSelectedItemChanged={item => {
|
||||
dispatch({
|
||||
getPath: item ? async () => `${item.path}${item.name}` : undefined,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</OLFormGroup>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -11,12 +11,17 @@ import { refreshProjectMetadata } from '../../../../file-tree/util/api'
|
|||
import { useProjectContext } from '../../../../../shared/context/project-context'
|
||||
import Icon from '../../../../../shared/components/icon'
|
||||
import classNames from 'classnames'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { FileRelocator } from '../file-relocator'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCodeMirrorViewContext } from '../../codemirror-context'
|
||||
import { waitForFileTreeUpdate } from '../../../extensions/figure-modal'
|
||||
import getMeta from '@/utils/meta'
|
||||
import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { Spinner } from 'react-bootstrap-5'
|
||||
import { isBootstrap5 } from '@/features/utils/bootstrap-5'
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
export enum FileUploadStatus {
|
||||
|
@ -25,6 +30,7 @@ export enum FileUploadStatus {
|
|||
NOT_ATTEMPTED,
|
||||
UPLOADING,
|
||||
}
|
||||
|
||||
/* eslint-enable no-unused-vars */
|
||||
|
||||
export const FigureModalUploadFileSource: FC = () => {
|
||||
|
@ -199,50 +205,52 @@ export const FigureModalUploadFileSource: FC = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="figure-modal-upload">
|
||||
{file ? (
|
||||
<FileContainer
|
||||
name={file.name}
|
||||
size={file.size}
|
||||
status={
|
||||
uploading
|
||||
? FileUploadStatus.UPLOADING
|
||||
: uploadError
|
||||
? FileUploadStatus.ERROR
|
||||
: FileUploadStatus.NOT_ATTEMPTED
|
||||
}
|
||||
onDelete={() => {
|
||||
uppy.removeFile(file.id)
|
||||
setFile(null)
|
||||
const newName = nameDirty ? name : ''
|
||||
setName(newName)
|
||||
dispatchUploadAction(newName, null, folder)
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Dashboard
|
||||
uppy={uppy}
|
||||
showProgressDetails
|
||||
height={120}
|
||||
width="100%"
|
||||
showLinkToFileUploadResult={false}
|
||||
proudlyDisplayPoweredByUppy={false}
|
||||
showSelectedFiles={false}
|
||||
hideUploadButton
|
||||
locale={{
|
||||
strings: {
|
||||
// Text to show on the droppable area.
|
||||
// `%{browseFiles}` is replaced with a link that opens the system file selection dialog.
|
||||
dropPasteFiles: `${t(
|
||||
'drag_here_paste_an_image_or'
|
||||
)} %{browseFiles}`,
|
||||
// Used as the label for the link that opens the system file selection dialog.
|
||||
browseFiles: t('select_from_your_computer'),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<OLFormGroup>
|
||||
<div className="figure-modal-upload">
|
||||
{file ? (
|
||||
<FileContainer
|
||||
name={file.name}
|
||||
size={file.size}
|
||||
status={
|
||||
uploading
|
||||
? FileUploadStatus.UPLOADING
|
||||
: uploadError
|
||||
? FileUploadStatus.ERROR
|
||||
: FileUploadStatus.NOT_ATTEMPTED
|
||||
}
|
||||
onDelete={() => {
|
||||
uppy.removeFile(file.id)
|
||||
setFile(null)
|
||||
const newName = nameDirty ? name : ''
|
||||
setName(newName)
|
||||
dispatchUploadAction(newName, null, folder)
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Dashboard
|
||||
uppy={uppy}
|
||||
showProgressDetails
|
||||
height={120}
|
||||
width="100%"
|
||||
showLinkToFileUploadResult={false}
|
||||
proudlyDisplayPoweredByUppy={false}
|
||||
showSelectedFiles={false}
|
||||
hideUploadButton
|
||||
locale={{
|
||||
strings: {
|
||||
// Text to show on the droppable area.
|
||||
// `%{browseFiles}` is replaced with a link that opens the system file selection dialog.
|
||||
dropPasteFiles: `${t(
|
||||
'drag_here_paste_an_image_or'
|
||||
)} %{browseFiles}`,
|
||||
// Used as the label for the link that opens the system file selection dialog.
|
||||
browseFiles: t('select_from_your_computer'),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</OLFormGroup>
|
||||
<FileRelocator
|
||||
folder={folder}
|
||||
name={name}
|
||||
|
@ -266,50 +274,67 @@ export const FileContainer: FC<{
|
|||
onDelete?: () => any
|
||||
}> = ({ name, size, status, onDelete }) => {
|
||||
const { t } = useTranslation()
|
||||
let icon
|
||||
let icon = ''
|
||||
switch (status) {
|
||||
case FileUploadStatus.ERROR:
|
||||
icon = 'times-circle'
|
||||
icon = isBootstrap5() ? 'cancel' : 'times-circle'
|
||||
break
|
||||
case FileUploadStatus.SUCCESS:
|
||||
icon = 'check-circle'
|
||||
icon = isBootstrap5() ? 'check_circle' : 'check-circle'
|
||||
break
|
||||
case FileUploadStatus.NOT_ATTEMPTED:
|
||||
icon = 'picture-o'
|
||||
icon = isBootstrap5() ? 'imagesmode' : 'picture-o'
|
||||
break
|
||||
case FileUploadStatus.UPLOADING:
|
||||
icon = 'spinner'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="file-container">
|
||||
<div className="file-container-file">
|
||||
<Icon
|
||||
spin={status === FileUploadStatus.UPLOADING}
|
||||
type={icon}
|
||||
className={classNames(
|
||||
{
|
||||
'text-success': status === FileUploadStatus.SUCCESS,
|
||||
'text-danger': status === FileUploadStatus.ERROR,
|
||||
},
|
||||
'file-icon'
|
||||
<span
|
||||
className={classNames({
|
||||
'text-success': status === FileUploadStatus.SUCCESS,
|
||||
'text-danger': status === FileUploadStatus.ERROR,
|
||||
})}
|
||||
>
|
||||
{status === FileUploadStatus.UPLOADING ? (
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon spin type="spinner" className="file-icon" />}
|
||||
bs5={
|
||||
<Spinner
|
||||
animation="border"
|
||||
aria-hidden="true"
|
||||
as="span"
|
||||
role="status"
|
||||
size="sm"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type={icon} className="file-icon" />}
|
||||
bs5={<MaterialIcon type={icon} className="align-text-bottom" />}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
<div className="file-info">
|
||||
<span className="file-name" aria-label={t('file_name_figure_modal')}>
|
||||
{name}
|
||||
</span>
|
||||
{size !== undefined && (
|
||||
<FileSize size={size} className="text-small" />
|
||||
)}
|
||||
{size !== undefined && <FileSize size={size} />}
|
||||
</div>
|
||||
<Button
|
||||
bsStyle={null}
|
||||
className="btn btn-link p-0"
|
||||
<OLButton
|
||||
variant="link"
|
||||
className="p-0 text-decoration-none"
|
||||
aria-label={t('remove_or_replace_figure')}
|
||||
onClick={() => onDelete && onDelete()}
|
||||
>
|
||||
<Icon fw type="times-circle" className="file-action file-icon" />
|
||||
</Button>
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={
|
||||
<Icon fw type="times-circle" className="file-action file-icon" />
|
||||
}
|
||||
bs5={<MaterialIcon type="cancel" />}
|
||||
/>
|
||||
</OLButton>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -336,8 +361,8 @@ const FileSize: FC<{ size: number; className?: string }> = ({
|
|||
const [label, bytesPerUnit] = BYTE_UNITS[labelIndex]
|
||||
const sizeInUnits = Math.round(size / bytesPerUnit)
|
||||
return (
|
||||
<span aria-label={t('file_size')} className={className}>
|
||||
<small aria-label={t('file_size')} className={className}>
|
||||
{sizeInUnits} {label}
|
||||
</span>
|
||||
</small>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -9,6 +9,9 @@ import { useTranslation } from 'react-i18next'
|
|||
import { useCodeMirrorViewContext } from '../../codemirror-context'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { waitForFileTreeUpdate } from '../../../extensions/figure-modal'
|
||||
import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
|
||||
import OLFormControl from '@/features/ui/components/ol/ol-form-control'
|
||||
import OLFormLabel from '@/features/ui/components/ol/ol-form-label'
|
||||
|
||||
function generateLinkedFileFetcher(
|
||||
projectId: string,
|
||||
|
@ -72,30 +75,25 @@ export const FigureModalUrlSource: FC = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<label
|
||||
htmlFor="figure-modal-url-url"
|
||||
className="figure-modal-input-label"
|
||||
>
|
||||
{t('image_url')}
|
||||
</label>
|
||||
<input
|
||||
id="figure-modal-url-url"
|
||||
type="text"
|
||||
className="form-control figure-modal-input-field"
|
||||
placeholder={t('enter_image_url')}
|
||||
value={url}
|
||||
onChange={e => {
|
||||
setUrl(e.target.value)
|
||||
let newName = name
|
||||
if (!nameDirty) {
|
||||
// TODO: Improve this
|
||||
const parts = e.target.value.split('/')
|
||||
newName = parts[parts.length - 1] ?? ''
|
||||
setName(newName)
|
||||
}
|
||||
ensureButtonActivation(e.target.value, newName, folder)
|
||||
}}
|
||||
/>
|
||||
<OLFormGroup controlId="figure-modal-url-url">
|
||||
<OLFormLabel>{t('image_url')}</OLFormLabel>
|
||||
<OLFormControl
|
||||
type="text"
|
||||
placeholder={t('enter_image_url')}
|
||||
value={url}
|
||||
onChange={e => {
|
||||
setUrl(e.target.value)
|
||||
let newName = name
|
||||
if (!nameDirty) {
|
||||
// TODO: Improve this
|
||||
const parts = e.target.value.split('/')
|
||||
newName = parts[parts.length - 1] ?? ''
|
||||
setName(newName)
|
||||
}
|
||||
ensureButtonActivation(e.target.value, newName, folder)
|
||||
}}
|
||||
/>
|
||||
</OLFormGroup>
|
||||
<FileRelocator
|
||||
folder={folder}
|
||||
name={name}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import Icon from '../../../shared/components/icon'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import { useLayoutContext } from '../../../shared/context/layout-context'
|
||||
|
||||
function SwitchToPDFButton() {
|
||||
|
@ -21,15 +23,21 @@ function SwitchToPDFButton() {
|
|||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
bsStyle={null}
|
||||
bsSize="xs"
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={handleClick}
|
||||
className="switch-to-pdf-btn toolbar-btn-secondary btn-secondary"
|
||||
bs3Props={{
|
||||
bsSize: 'xsmall',
|
||||
className: 'switch-to-pdf-btn toolbar-btn-secondary',
|
||||
}}
|
||||
>
|
||||
<Icon type="file-pdf-o" className="toolbar-btn-secondary-icon" />
|
||||
<span className="toolbar-btn-secondary-text">{t('switch_to_pdf')}</span>
|
||||
</Button>
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="file-pdf-o" className="toolbar-btn-secondary-icon" />}
|
||||
bs5={<MaterialIcon type="picture_as_pdf" />}
|
||||
/>
|
||||
{t('switch_to_pdf')}
|
||||
</OLButton>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,37 +1,28 @@
|
|||
import { FC, memo, useRef } from 'react'
|
||||
import { Button, ListGroup, Overlay, Popover } from 'react-bootstrap'
|
||||
import Icon from '../../../../shared/components/icon'
|
||||
import useDropdown from '../../../../shared/hooks/use-dropdown'
|
||||
import Tooltip from '../../../../shared/components/tooltip'
|
||||
import OLListGroup from '@/features/ui/components/ol/ol-list-group'
|
||||
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
||||
import OLOverlay from '@/features/ui/components/ol/ol-overlay'
|
||||
import OLPopover from '@/features/ui/components/ol/ol-popover'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics'
|
||||
import { useCodeMirrorViewContext } from '../codemirror-context'
|
||||
import MaterialIcon from '../../../../shared/components/material-icon'
|
||||
|
||||
export const ToolbarButtonMenu: FC<{
|
||||
id: string
|
||||
label: string
|
||||
icon: string
|
||||
materialIcon?: boolean
|
||||
icon: React.ReactNode
|
||||
altCommand?: (view: EditorView) => void
|
||||
}> = memo(function ButtonMenu({
|
||||
icon,
|
||||
id,
|
||||
label,
|
||||
materialIcon,
|
||||
altCommand,
|
||||
children,
|
||||
}) {
|
||||
}> = memo(function ButtonMenu({ icon, id, label, altCommand, children }) {
|
||||
const target = useRef<any>(null)
|
||||
const { open, onToggle, ref } = useDropdown()
|
||||
const view = useCodeMirrorViewContext()
|
||||
|
||||
const button = (
|
||||
<Button
|
||||
<button
|
||||
type="button"
|
||||
className="ol-cm-toolbar-button"
|
||||
className="ol-cm-toolbar-button btn"
|
||||
aria-label={label}
|
||||
bsStyle={null}
|
||||
onMouseDown={event => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
@ -48,36 +39,36 @@ export const ToolbarButtonMenu: FC<{
|
|||
}}
|
||||
ref={target}
|
||||
>
|
||||
{materialIcon ? <MaterialIcon type={icon} /> : <Icon type={icon} fw />}
|
||||
</Button>
|
||||
{icon}
|
||||
</button>
|
||||
)
|
||||
|
||||
const overlay = (
|
||||
<Overlay
|
||||
<OLOverlay
|
||||
show={open}
|
||||
target={target.current}
|
||||
placement="bottom"
|
||||
container={view.dom}
|
||||
containerPadding={0}
|
||||
animation
|
||||
transition
|
||||
rootClose
|
||||
onHide={() => onToggle(false)}
|
||||
>
|
||||
<Popover
|
||||
<OLPopover
|
||||
id={`${id}-menu`}
|
||||
ref={ref}
|
||||
className="ol-cm-toolbar-button-menu-popover"
|
||||
>
|
||||
<ListGroup
|
||||
<OLListGroup
|
||||
role="menu"
|
||||
onClick={() => {
|
||||
onToggle(false)
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ListGroup>
|
||||
</Popover>
|
||||
</Overlay>
|
||||
</OLListGroup>
|
||||
</OLPopover>
|
||||
</OLOverlay>
|
||||
)
|
||||
|
||||
if (!label) {
|
||||
|
@ -91,14 +82,14 @@ export const ToolbarButtonMenu: FC<{
|
|||
|
||||
return (
|
||||
<>
|
||||
<Tooltip
|
||||
<OLTooltip
|
||||
hidden={open}
|
||||
id={id}
|
||||
description={<div>{label}</div>}
|
||||
overlayProps={{ placement: 'bottom' }}
|
||||
>
|
||||
{button}
|
||||
</Tooltip>
|
||||
</OLTooltip>
|
||||
{overlay}
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { ListGroupItem } from 'react-bootstrap'
|
||||
import { ToolbarButtonMenu } from './button-menu'
|
||||
import Icon from '../../../../shared/components/icon'
|
||||
import Icon from '@/shared/components/icon'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import OLListGroupItem from '@/features/ui/components/ol/ol-list-group-item'
|
||||
import { memo, useCallback } from 'react'
|
||||
import { FigureModalSource } from '../figure-modal/figure-modal-context'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
@ -32,44 +34,61 @@ export const InsertFigureDropdown = memo(function InsertFigureDropdown() {
|
|||
<ToolbarButtonMenu
|
||||
id="toolbar-figure"
|
||||
label={t('toolbar_insert_figure')}
|
||||
icon="picture-o"
|
||||
icon={
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon fw type="picture-o" />}
|
||||
bs5={<MaterialIcon type="imagesmode" />}
|
||||
/>
|
||||
}
|
||||
altCommand={insertFigure}
|
||||
>
|
||||
<ListGroupItem
|
||||
<OLListGroupItem
|
||||
onClick={() =>
|
||||
openFigureModal(FigureModalSource.FILE_UPLOAD, 'file-upload')
|
||||
}
|
||||
>
|
||||
<Icon type="upload" fw />
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="upload" fw />}
|
||||
bs5={<MaterialIcon type="upload" />}
|
||||
/>
|
||||
{t('upload_from_computer')}
|
||||
</ListGroupItem>
|
||||
<ListGroupItem
|
||||
</OLListGroupItem>
|
||||
<OLListGroupItem
|
||||
onClick={() =>
|
||||
openFigureModal(FigureModalSource.FILE_TREE, 'current-project')
|
||||
}
|
||||
>
|
||||
<Icon type="archive" fw />
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="archive" fw />}
|
||||
bs5={<MaterialIcon type="inbox" />}
|
||||
/>
|
||||
{t('from_project_files')}
|
||||
</ListGroupItem>
|
||||
</OLListGroupItem>
|
||||
{(hasLinkedProjectFileFeature || hasLinkedProjectOutputFileFeature) && (
|
||||
<ListGroupItem
|
||||
<OLListGroupItem
|
||||
onClick={() =>
|
||||
openFigureModal(FigureModalSource.OTHER_PROJECT, 'other-project')
|
||||
}
|
||||
>
|
||||
<Icon type="folder-open" fw />
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="folder-open" fw />}
|
||||
bs5={<MaterialIcon type="folder_open" />}
|
||||
/>
|
||||
{t('from_another_project')}
|
||||
</ListGroupItem>
|
||||
</OLListGroupItem>
|
||||
)}
|
||||
{hasLinkUrlFeature && (
|
||||
<ListGroupItem
|
||||
<OLListGroupItem
|
||||
onClick={() =>
|
||||
openFigureModal(FigureModalSource.FROM_URL, 'from-url')
|
||||
}
|
||||
>
|
||||
<Icon type="globe" fw />
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="globe" fw />}
|
||||
bs5={<MaterialIcon type="public" />}
|
||||
/>
|
||||
{t('from_url')}
|
||||
</ListGroupItem>
|
||||
</OLListGroupItem>
|
||||
)}
|
||||
</ToolbarButtonMenu>
|
||||
)
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { ListGroupItem } from 'react-bootstrap'
|
||||
import { ToolbarButtonMenu } from './button-menu'
|
||||
import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics'
|
||||
import MaterialIcon from '../../../../shared/components/material-icon'
|
||||
|
@ -9,6 +8,7 @@ import {
|
|||
wrapInInlineMath,
|
||||
} from '../../extensions/toolbar/commands'
|
||||
import { memo } from 'react'
|
||||
import OLListGroupItem from '@/features/ui/components/ol/ol-list-group-item'
|
||||
|
||||
export const MathDropdown = memo(function MathDropdown() {
|
||||
const { t } = useTranslation()
|
||||
|
@ -18,10 +18,9 @@ export const MathDropdown = memo(function MathDropdown() {
|
|||
<ToolbarButtonMenu
|
||||
id="toolbar-math"
|
||||
label={t('toolbar_insert_math')}
|
||||
icon="calculate"
|
||||
materialIcon
|
||||
icon={<MaterialIcon type="calculate" />}
|
||||
>
|
||||
<ListGroupItem
|
||||
<OLListGroupItem
|
||||
aria-label={t('toolbar_insert_inline_math')}
|
||||
onClick={event => {
|
||||
emitToolbarEvent(view, 'toolbar-inline-math')
|
||||
|
@ -32,8 +31,8 @@ export const MathDropdown = memo(function MathDropdown() {
|
|||
>
|
||||
<MaterialIcon type="123" />
|
||||
<span>{t('toolbar_insert_inline_math')}</span>
|
||||
</ListGroupItem>
|
||||
<ListGroupItem
|
||||
</OLListGroupItem>
|
||||
<OLListGroupItem
|
||||
aria-label={t('toolbar_insert_display_math')}
|
||||
onClick={event => {
|
||||
emitToolbarEvent(view, 'toolbar-display-math')
|
||||
|
@ -44,7 +43,7 @@ export const MathDropdown = memo(function MathDropdown() {
|
|||
>
|
||||
<MaterialIcon type="view_day" />
|
||||
<span>{t('toolbar_insert_display_math')}</span>
|
||||
</ListGroupItem>
|
||||
</OLListGroupItem>
|
||||
</ToolbarButtonMenu>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -1,21 +1,26 @@
|
|||
import { FC, LegacyRef, useRef } from 'react'
|
||||
import { Button, Overlay, Popover } from 'react-bootstrap'
|
||||
import { FC, useRef } from 'react'
|
||||
import classnames from 'classnames'
|
||||
import Icon from '../../../../shared/components/icon'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import { useCodeMirrorViewContext } from '../codemirror-context'
|
||||
import OLOverlay from '@/features/ui/components/ol/ol-overlay'
|
||||
import OLPopover from '@/features/ui/components/ol/ol-popover'
|
||||
import { bsVersion } from '@/features/utils/bootstrap-5'
|
||||
|
||||
export const ToolbarOverflow: FC<{
|
||||
overflowed: boolean
|
||||
overflowOpen: boolean
|
||||
setOverflowOpen: (open: boolean) => void
|
||||
overflowRef?: LegacyRef<Popover>
|
||||
overflowRef?: React.Ref<HTMLDivElement>
|
||||
}> = ({ overflowed, overflowOpen, setOverflowOpen, overflowRef, children }) => {
|
||||
const buttonRef = useRef<Button>(null)
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
const view = useCodeMirrorViewContext()
|
||||
|
||||
const className = classnames(
|
||||
'ol-cm-toolbar-button',
|
||||
'ol-cm-toolbar-overflow-toggle',
|
||||
bsVersion({ bs3: 'btn' }),
|
||||
{
|
||||
'ol-cm-toolbar-overflow-toggle-visible': overflowed,
|
||||
}
|
||||
|
@ -23,13 +28,12 @@ export const ToolbarOverflow: FC<{
|
|||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type="button"
|
||||
id="toolbar-more"
|
||||
className={className}
|
||||
aria-label="More"
|
||||
bsStyle={null}
|
||||
onMouseDown={event => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
@ -38,22 +42,26 @@ export const ToolbarOverflow: FC<{
|
|||
setOverflowOpen(!overflowOpen)
|
||||
}}
|
||||
>
|
||||
<Icon type="ellipsis-h" fw />
|
||||
</Button>
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="ellipsis-h" fw />}
|
||||
bs5={<MaterialIcon type="more_horiz" />}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<Overlay
|
||||
<OLOverlay
|
||||
show={overflowOpen}
|
||||
target={buttonRef.current ?? undefined}
|
||||
target={buttonRef.current}
|
||||
placement="bottom"
|
||||
container={view.dom}
|
||||
containerPadding={0}
|
||||
animation
|
||||
// containerPadding={0}
|
||||
transition
|
||||
rootClose
|
||||
onHide={() => setOverflowOpen(false)}
|
||||
>
|
||||
<Popover id="popover-toolbar-overflow" ref={overflowRef}>
|
||||
<OLPopover id="popover-toolbar-overflow" ref={overflowRef}>
|
||||
<div className="ol-cm-toolbar-overflow">{children}</div>
|
||||
</Popover>
|
||||
</Overlay>
|
||||
</OLPopover>
|
||||
</OLOverlay>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -8,7 +8,8 @@ import {
|
|||
setSectionHeadingLevel,
|
||||
} from '../../extensions/toolbar/sections'
|
||||
import { useCallback, useMemo, useRef } from 'react'
|
||||
import { Overlay, Popover } from 'react-bootstrap'
|
||||
import OLOverlay from '@/features/ui/components/ol/ol-overlay'
|
||||
import OLPopover from '@/features/ui/components/ol/ol-popover'
|
||||
import useEventListener from '../../../../shared/hooks/use-event-listener'
|
||||
import useDropdown from '../../../../shared/hooks/use-dropdown'
|
||||
import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics'
|
||||
|
@ -69,17 +70,27 @@ export const SectionHeadingDropdown = () => {
|
|||
</button>
|
||||
|
||||
{overflowOpen && (
|
||||
<Overlay
|
||||
<OLOverlay
|
||||
show
|
||||
onHide={() => setOverflowOpen(false)}
|
||||
animation={false}
|
||||
transition={false}
|
||||
container={view.dom}
|
||||
containerPadding={0}
|
||||
placement="bottom"
|
||||
rootClose
|
||||
target={toggleButtonRef.current ?? undefined}
|
||||
target={toggleButtonRef.current}
|
||||
popperConfig={{
|
||||
modifiers: [
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [0, 1],
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<Popover
|
||||
<OLPopover
|
||||
id="popover-toolbar-section-heading"
|
||||
className="ol-cm-toolbar-menu-popover"
|
||||
>
|
||||
|
@ -113,8 +124,8 @@ export const SectionHeadingDropdown = () => {
|
|||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Popover>
|
||||
</Overlay>
|
||||
</OLPopover>
|
||||
</OLOverlay>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -2,9 +2,10 @@ import { FC, memo, useCallback, useRef, useState } from 'react'
|
|||
import * as commands from '../../extensions/toolbar/commands'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useDropdown from '../../../../shared/hooks/use-dropdown'
|
||||
import { Button, Overlay, Popover } from 'react-bootstrap'
|
||||
import { useCodeMirrorViewContext } from '../codemirror-context'
|
||||
import Tooltip from '../../../../shared/components/tooltip'
|
||||
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
||||
import OLOverlay from '@/features/ui/components/ol/ol-overlay'
|
||||
import OLPopover from '@/features/ui/components/ol/ol-popover'
|
||||
import MaterialIcon from '../../../../shared/components/material-icon'
|
||||
import classNames from 'classnames'
|
||||
import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics'
|
||||
|
@ -27,17 +28,16 @@ export const TableInserterDropdown = memo(() => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Tooltip
|
||||
<OLTooltip
|
||||
hidden={open}
|
||||
id="toolbar-table"
|
||||
description={<div>{t('toolbar_insert_table')}</div>}
|
||||
overlayProps={{ placement: 'bottom' }}
|
||||
>
|
||||
<Button
|
||||
<button
|
||||
type="button"
|
||||
className="ol-cm-toolbar-button"
|
||||
className="ol-cm-toolbar-button btn"
|
||||
aria-label={t('toolbar_insert_table')}
|
||||
bsStyle={null}
|
||||
onMouseDown={event => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
@ -48,19 +48,19 @@ export const TableInserterDropdown = memo(() => {
|
|||
ref={target}
|
||||
>
|
||||
<MaterialIcon type="table_chart" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Overlay
|
||||
</button>
|
||||
</OLTooltip>
|
||||
<OLOverlay
|
||||
show={open}
|
||||
target={target.current}
|
||||
placement="bottom"
|
||||
container={view.dom}
|
||||
containerPadding={0}
|
||||
animation
|
||||
transition
|
||||
rootClose
|
||||
onHide={() => onToggle(false)}
|
||||
>
|
||||
<Popover
|
||||
<OLPopover
|
||||
id="toolbar-table-menu"
|
||||
ref={ref}
|
||||
className="ol-cm-toolbar-button-menu-popover ol-cm-toolbar-button-menu-popover-unstyled"
|
||||
|
@ -68,8 +68,8 @@ export const TableInserterDropdown = memo(() => {
|
|||
<div className="ol-cm-toolbar-table-grid-popover">
|
||||
<SizeGrid sizeX={10} sizeY={10} onSizeSelected={onSizeSelected} />
|
||||
</div>
|
||||
</Popover>
|
||||
</Overlay>
|
||||
</OLPopover>
|
||||
</OLOverlay>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { memo, useCallback } from 'react'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { useCodeMirrorViewContext } from '../codemirror-context'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import classnames from 'classnames'
|
||||
import Tooltip from '../../../../shared/components/tooltip'
|
||||
import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics'
|
||||
import Icon from '../../../../shared/components/icon'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import { bsVersion } from '@/features/utils/bootstrap-5'
|
||||
|
||||
export const ToolbarButton = memo<{
|
||||
id: string
|
||||
|
@ -49,18 +51,31 @@ export const ToolbarButton = memo<{
|
|||
)
|
||||
|
||||
const button = (
|
||||
<Button
|
||||
className={classnames('ol-cm-toolbar-button', className, { hidden })}
|
||||
<button
|
||||
className={classnames(
|
||||
'ol-cm-toolbar-button',
|
||||
bsVersion({ bs3: 'btn' }),
|
||||
className,
|
||||
{
|
||||
active,
|
||||
hidden,
|
||||
}
|
||||
)}
|
||||
aria-label={label}
|
||||
onMouseDown={handleMouseDown}
|
||||
onClick={!disabled ? handleClick : undefined}
|
||||
bsStyle={null}
|
||||
active={active}
|
||||
aria-disabled={disabled}
|
||||
type="button"
|
||||
>
|
||||
{textIcon ? icon : <Icon type={icon} fw accessibilityLabel={label} />}
|
||||
</Button>
|
||||
{textIcon ? (
|
||||
icon
|
||||
) : (
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type={icon} fw accessibilityLabel={label} />}
|
||||
bs5={<MaterialIcon type={icon} accessibilityLabel={label} />}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
|
||||
if (!label) {
|
||||
|
@ -75,12 +90,12 @@ export const ToolbarButton = memo<{
|
|||
)
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
<OLTooltip
|
||||
id={id}
|
||||
description={description}
|
||||
overlayProps={{ placement: 'bottom' }}
|
||||
>
|
||||
{button}
|
||||
</Tooltip>
|
||||
</OLTooltip>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -11,6 +11,7 @@ import { useTranslation } from 'react-i18next'
|
|||
import { MathDropdown } from './math-dropdown'
|
||||
import { TableInserterDropdown } from './table-inserter-dropdown'
|
||||
import { withinFormattingCommand } from '@/features/source-editor/utils/tree-operations/formatting'
|
||||
import { bsVersion } from '@/features/utils/bootstrap-5'
|
||||
|
||||
const isMac = /Mac/.test(window.navigator?.platform)
|
||||
|
||||
|
@ -37,19 +38,22 @@ export const ToolbarItems: FC<{
|
|||
return (
|
||||
<>
|
||||
{showGroup('group-history') && (
|
||||
<div className="ol-cm-toolbar-button-group">
|
||||
<div
|
||||
className="ol-cm-toolbar-button-group"
|
||||
aria-label={t('toolbar_undo_redo_actions')}
|
||||
>
|
||||
<ToolbarButton
|
||||
id="toolbar-undo"
|
||||
label={t('toolbar_undo')}
|
||||
command={undo}
|
||||
icon="undo"
|
||||
icon={bsVersion({ bs5: 'undo', bs3: 'undo' }) as string}
|
||||
shortcut={isMac ? '⌘Z' : 'Ctrl+Z'}
|
||||
/>
|
||||
<ToolbarButton
|
||||
id="toolbar-redo"
|
||||
label={t('toolbar_redo')}
|
||||
command={redo}
|
||||
icon="repeat"
|
||||
icon={bsVersion({ bs5: 'redo', bs3: 'repeat' }) as string}
|
||||
shortcut={isMac ? '⇧⌘Z' : 'Ctrl+Y'}
|
||||
/>
|
||||
</div>
|
||||
|
@ -60,18 +64,22 @@ export const ToolbarItems: FC<{
|
|||
<div
|
||||
className="ol-cm-toolbar-button-group"
|
||||
data-overflow="group-section"
|
||||
aria-label={t('toolbar_text_formatting')}
|
||||
>
|
||||
<SectionHeadingDropdown />
|
||||
</div>
|
||||
)}
|
||||
{showGroup('group-format') && (
|
||||
<div className="ol-cm-toolbar-button-group">
|
||||
<div
|
||||
className="ol-cm-toolbar-button-group"
|
||||
aria-label={t('toolbar_text_style')}
|
||||
>
|
||||
<ToolbarButton
|
||||
id="toolbar-format-bold"
|
||||
label={t('toolbar_format_bold')}
|
||||
command={commands.toggleBold}
|
||||
active={isActive('\\textbf')}
|
||||
icon="bold"
|
||||
icon={bsVersion({ bs5: 'format_bold', bs3: 'bold' }) as string}
|
||||
shortcut={isMac ? '⌘B' : 'Ctrl+B'}
|
||||
/>
|
||||
<ToolbarButton
|
||||
|
@ -79,7 +87,9 @@ export const ToolbarItems: FC<{
|
|||
label={t('toolbar_format_italic')}
|
||||
command={commands.toggleItalic}
|
||||
active={isActive('\\textit')}
|
||||
icon="italic"
|
||||
icon={
|
||||
bsVersion({ bs5: 'format_italic', bs3: 'italic' }) as string
|
||||
}
|
||||
shortcut={isMac ? '⌘I' : 'Ctrl+I'}
|
||||
/>
|
||||
</div>
|
||||
|
@ -88,6 +98,7 @@ export const ToolbarItems: FC<{
|
|||
<div
|
||||
className="ol-cm-toolbar-button-group"
|
||||
data-overflow="group-math"
|
||||
aria-label={t('toolbar_insert_math_and_symbols')}
|
||||
>
|
||||
<MathDropdown />
|
||||
{symbolPaletteAvailable && (
|
||||
|
@ -107,24 +118,25 @@ export const ToolbarItems: FC<{
|
|||
<div
|
||||
className="ol-cm-toolbar-button-group"
|
||||
data-overflow="group-misc"
|
||||
aria-label={t('toolbar_insert_misc')}
|
||||
>
|
||||
<ToolbarButton
|
||||
id="toolbar-href"
|
||||
label={t('toolbar_insert_link')}
|
||||
command={commands.wrapInHref}
|
||||
icon="link"
|
||||
icon={bsVersion({ bs5: 'link', bs3: 'link' }) as string}
|
||||
/>
|
||||
<ToolbarButton
|
||||
id="toolbar-ref"
|
||||
label={t('toolbar_insert_cross_reference')}
|
||||
command={commands.insertRef}
|
||||
icon="tag"
|
||||
icon={bsVersion({ bs5: 'sell', bs3: 'tag' }) as string}
|
||||
/>
|
||||
<ToolbarButton
|
||||
id="toolbar-cite"
|
||||
label={t('toolbar_insert_citation')}
|
||||
command={commands.insertCite}
|
||||
icon="book"
|
||||
icon={bsVersion({ bs5: 'menu_book', bs3: 'book' }) as string}
|
||||
/>
|
||||
<InsertFigureDropdown />
|
||||
<TableInserterDropdown />
|
||||
|
@ -134,24 +146,40 @@ export const ToolbarItems: FC<{
|
|||
<div
|
||||
className="ol-cm-toolbar-button-group"
|
||||
data-overflow="group-list"
|
||||
aria-label={t('toolbar_list_indentation')}
|
||||
>
|
||||
<ToolbarButton
|
||||
id="toolbar-bullet-list"
|
||||
label={t('toolbar_bullet_list')}
|
||||
command={commands.toggleBulletList}
|
||||
icon="list-ul"
|
||||
icon={
|
||||
bsVersion({
|
||||
bs5: 'format_list_bulleted',
|
||||
bs3: 'list-ul',
|
||||
}) as string
|
||||
}
|
||||
/>
|
||||
<ToolbarButton
|
||||
id="toolbar-numbered-list"
|
||||
label={t('toolbar_numbered_list')}
|
||||
command={commands.toggleNumberedList}
|
||||
icon="list-ol"
|
||||
icon={
|
||||
bsVersion({
|
||||
bs5: 'format_list_numbered',
|
||||
bs3: 'list-ol',
|
||||
}) as string
|
||||
}
|
||||
/>
|
||||
<ToolbarButton
|
||||
id="toolbar-format-indent-decrease"
|
||||
label={t('toolbar_decrease_indent')}
|
||||
command={commands.indentDecrease}
|
||||
icon="outdent"
|
||||
icon={
|
||||
bsVersion({
|
||||
bs5: 'format_indent_decrease',
|
||||
bs3: 'outdent',
|
||||
}) as string
|
||||
}
|
||||
shortcut={visual ? (isMac ? '⌘[' : 'Ctrl+[') : undefined}
|
||||
disabled={listDepth < 2}
|
||||
/>
|
||||
|
@ -159,7 +187,12 @@ export const ToolbarItems: FC<{
|
|||
id="toolbar-format-indent-increase"
|
||||
label={t('toolbar_increase_indent')}
|
||||
command={commands.indentIncrease}
|
||||
icon="indent"
|
||||
icon={
|
||||
bsVersion({
|
||||
bs5: 'format_indent_increase',
|
||||
bs3: 'indent',
|
||||
}) as string
|
||||
}
|
||||
shortcut={visual ? (isMac ? '⌘]' : 'Ctrl+]') : undefined}
|
||||
disabled={listDepth < 1}
|
||||
/>
|
||||
|
|
|
@ -43,10 +43,13 @@ const toolbarTheme = EditorView.theme({
|
|||
borderColor: 'rgba(125, 125, 125, 0.2)',
|
||||
backgroundColor: 'var(--editor-toolbar-bg)',
|
||||
color: 'var(--toolbar-btn-color)',
|
||||
'& .popover-content': {
|
||||
'& .popover-content, & .popover-body': {
|
||||
padding: 0,
|
||||
},
|
||||
'& .arrow': {
|
||||
'& .popover-body': {
|
||||
color: 'inherit',
|
||||
},
|
||||
'& .arrow, & .popover-arrow': {
|
||||
borderBottomColor: 'rgba(125, 125, 125, 0.2)',
|
||||
'&:after': {
|
||||
borderBottomColor: 'var(--editor-toolbar-bg)',
|
||||
|
@ -54,10 +57,12 @@ const toolbarTheme = EditorView.theme({
|
|||
},
|
||||
},
|
||||
'.ol-cm-toolbar-button-menu-popover': {
|
||||
'& > .popover-content': {
|
||||
backgroundColor: 'initial',
|
||||
'& > .popover-content, & > .popover-body': {
|
||||
padding: 0,
|
||||
color: 'initial',
|
||||
},
|
||||
'& .arrow': {
|
||||
'& .arrow, & .popover-arrow': {
|
||||
display: 'none',
|
||||
},
|
||||
'& .list-group': {
|
||||
|
@ -116,6 +121,7 @@ const toolbarTheme = EditorView.theme({
|
|||
width: '24px',
|
||||
height: '24px',
|
||||
overflow: 'hidden',
|
||||
color: 'inherit',
|
||||
'&:hover, &:focus, &:active, &.active': {
|
||||
backgroundColor: 'rgba(125, 125, 125, 0.1)',
|
||||
color: 'inherit',
|
||||
|
@ -204,11 +210,12 @@ const toolbarTheme = EditorView.theme({
|
|||
'&.top': {
|
||||
marginBottom: '1px',
|
||||
},
|
||||
'& .arrow': {
|
||||
'& .arrow, & .popover-arrow': {
|
||||
display: 'none',
|
||||
},
|
||||
'& .popover-content': {
|
||||
'& .popover-content, & > .popover-body': {
|
||||
padding: '0',
|
||||
color: 'inherit',
|
||||
},
|
||||
'& .ol-cm-toolbar-menu': {
|
||||
width: '120px',
|
||||
|
@ -225,6 +232,7 @@ const toolbarTheme = EditorView.theme({
|
|||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontWeight: 'bold',
|
||||
color: 'inherit',
|
||||
'&.ol-cm-toolbar-menu-item-active': {
|
||||
backgroundColor: 'rgba(125, 125, 125, 0.1)',
|
||||
},
|
||||
|
|
|
@ -27,7 +27,7 @@ const devToolsButton = ViewPlugin.define(view => {
|
|||
|
||||
const addButton = () => {
|
||||
const button = document.createElement('button')
|
||||
button.classList.add('btn', 'formatting-btn', 'formatting-btn--icon')
|
||||
button.classList.add('btn', 'formatting-btn', 'formatting-btn-icon')
|
||||
button.id = 'cm6-dev-tools-button'
|
||||
button.textContent = '🦧'
|
||||
button.style.border = 'none'
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
import {
|
||||
Children,
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
useState,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import {
|
||||
ToggleButtonGroup as BS3ToggleButtonGroup,
|
||||
ToggleButtonGroupProps as BS3ToggleButtonGroupProps,
|
||||
ToggleButtonProps as BS3ToggleButtonProps,
|
||||
} from 'react-bootstrap'
|
||||
|
||||
function ToggleButtonGroup<T extends string | number>({
|
||||
children,
|
||||
value,
|
||||
defaultValue,
|
||||
onChange,
|
||||
...props
|
||||
}: BS3ToggleButtonGroupProps) {
|
||||
const [selectedValue, setSelectedValue] = useState<T | T[] | null>(
|
||||
defaultValue || (props.type === 'checkbox' ? [] : null)
|
||||
)
|
||||
const isControlled = value !== undefined
|
||||
|
||||
useEffect(() => {
|
||||
if (isControlled) {
|
||||
if (props.type === 'radio') {
|
||||
setSelectedValue(value)
|
||||
} else {
|
||||
if (Array.isArray(value)) {
|
||||
setSelectedValue(Array.from(value))
|
||||
} else {
|
||||
setSelectedValue([value])
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [isControlled, value, props.type])
|
||||
|
||||
const handleButtonClick = (buttonValue: T) => {
|
||||
if (props.type === 'radio') {
|
||||
if (!isControlled) {
|
||||
setSelectedValue(buttonValue)
|
||||
}
|
||||
|
||||
onChange?.(buttonValue as any)
|
||||
} else if (props.type === 'checkbox') {
|
||||
const newValue = Array.isArray(selectedValue)
|
||||
? selectedValue.includes(buttonValue)
|
||||
? selectedValue.filter(val => val !== buttonValue) // Deselect
|
||||
: [...selectedValue, buttonValue] // Select
|
||||
: [buttonValue] // Initial selection if value is not array yet
|
||||
|
||||
if (!isControlled) {
|
||||
setSelectedValue(newValue)
|
||||
}
|
||||
|
||||
onChange?.(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
// Clone children and add custom onClick handlers
|
||||
const modifiedChildren = Children.map(children, child => {
|
||||
if (isValidElement(child)) {
|
||||
const childElement = child as React.ReactElement<
|
||||
BS3ToggleButtonProps & { active?: boolean }
|
||||
>
|
||||
|
||||
const isActive =
|
||||
props.type === 'radio'
|
||||
? selectedValue === childElement.props.value
|
||||
: Array.isArray(selectedValue) &&
|
||||
selectedValue.includes(childElement.props.value as T)
|
||||
|
||||
return cloneElement(childElement, {
|
||||
onClick: () => {
|
||||
handleButtonClick(childElement.props.value as T)
|
||||
},
|
||||
active: isActive,
|
||||
})
|
||||
}
|
||||
|
||||
return child
|
||||
})
|
||||
|
||||
return (
|
||||
<BS3ToggleButtonGroup
|
||||
{...props}
|
||||
value={isControlled ? value : undefined}
|
||||
defaultValue={defaultValue}
|
||||
// Ignore the broken onChange handler
|
||||
onChange={() => {}}
|
||||
>
|
||||
{modifiedChildren}
|
||||
</BS3ToggleButtonGroup>
|
||||
)
|
||||
}
|
||||
|
||||
export default ToggleButtonGroup
|
|
@ -17,6 +17,7 @@ import type {
|
|||
} from '@/features/ui/components/types/dropdown-menu-props'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { fixedForwardRef } from '@/utils/react'
|
||||
import classnames from 'classnames'
|
||||
|
||||
export function Dropdown({ ...props }: DropdownProps) {
|
||||
return <BS5Dropdown {...props} />
|
||||
|
@ -26,6 +27,7 @@ function DropdownItem(
|
|||
{
|
||||
active,
|
||||
children,
|
||||
className,
|
||||
description,
|
||||
leadingIcon,
|
||||
trailingIcon,
|
||||
|
@ -74,7 +76,9 @@ function DropdownItem(
|
|||
return (
|
||||
<BS5DropdownItem
|
||||
active={active}
|
||||
className={description ? 'dropdown-item-description-container' : ''}
|
||||
className={classnames(className, {
|
||||
'dropdown-item-description-container': description,
|
||||
})}
|
||||
role="menuitem"
|
||||
{...props}
|
||||
ref={ref}
|
||||
|
|
|
@ -13,6 +13,7 @@ export type OLButtonProps = ButtonProps & {
|
|||
loading?: React.ReactNode
|
||||
bsSize?: BS3ButtonSize
|
||||
block?: boolean
|
||||
className?: string
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,8 +44,8 @@ export const mapBsButtonSizes = (
|
|||
): 'sm' | 'lg' | undefined =>
|
||||
size === 'small' ? 'sm' : size === 'large' ? 'lg' : undefined
|
||||
|
||||
export default function OLButton(props: OLButtonProps) {
|
||||
const { bs3Props, ...rest } = props
|
||||
export default function OLButton({ bs3Props = {}, ...rest }: OLButtonProps) {
|
||||
const { className: _, ...restBs3Props } = bs3Props
|
||||
|
||||
// Get all `aria-*` and `data-*` attributes
|
||||
const extraProps = getAriaAndDataProps(rest)
|
||||
|
@ -52,7 +53,15 @@ export default function OLButton(props: OLButtonProps) {
|
|||
return (
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={
|
||||
<BS3Button {...bs3ButtonProps(rest)} {...bs3Props} {...extraProps}>
|
||||
<BS3Button
|
||||
{...bs3ButtonProps({
|
||||
...rest,
|
||||
// Override the `className` with bs3 specific className (if provided)
|
||||
className: bs3Props?.className ?? rest.className,
|
||||
})}
|
||||
{...restBs3Props}
|
||||
{...extraProps}
|
||||
>
|
||||
{bs3Props?.loading || rest.children}
|
||||
</BS3Button>
|
||||
}
|
||||
|
|
|
@ -20,6 +20,8 @@ function OLFormCheckbox(props: OLFormCheckboxProps) {
|
|||
inline: rest.inline,
|
||||
title: rest.title,
|
||||
autoComplete: rest.autoComplete,
|
||||
defaultChecked: rest.defaultChecked,
|
||||
className: rest.className,
|
||||
onChange: rest.onChange as (e: React.ChangeEvent<unknown>) => void,
|
||||
inputRef: node => {
|
||||
if (inputRef) {
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
import { ListGroupItem, ListGroupItemProps } from 'react-bootstrap-5'
|
||||
import {
|
||||
ListGroupItem as BS3ListGroupItem,
|
||||
ListGroupItemProps as BS3ListGroupItemProps,
|
||||
} from 'react-bootstrap'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import { getAriaAndDataProps } from '@/features/utils/bootstrap-5'
|
||||
|
||||
type OLListGroupItemProps = ListGroupItemProps & {
|
||||
bs3Props?: BS3ListGroupItemProps
|
||||
}
|
||||
|
||||
function OLListGroupItem(props: OLListGroupItemProps) {
|
||||
const { bs3Props, ...rest } = props
|
||||
|
||||
const bs3ListGroupItemProps: BS3ListGroupItemProps = {
|
||||
children: rest.children,
|
||||
active: rest.active,
|
||||
disabled: rest.disabled,
|
||||
href: rest.href,
|
||||
onClick: rest.onClick as BS3ListGroupItemProps['onClick'],
|
||||
...bs3Props,
|
||||
}
|
||||
|
||||
const extraProps = getAriaAndDataProps(rest)
|
||||
const as = rest.as ?? 'button'
|
||||
|
||||
return (
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<BS3ListGroupItem {...bs3ListGroupItemProps} {...extraProps} />}
|
||||
bs5={
|
||||
<ListGroupItem
|
||||
{...rest}
|
||||
as={as}
|
||||
type={as === 'button' ? 'button' : undefined}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default OLListGroupItem
|
|
@ -0,0 +1,34 @@
|
|||
import { ListGroup, ListGroupProps } from 'react-bootstrap-5'
|
||||
import {
|
||||
ListGroup as BS3ListGroup,
|
||||
ListGroupProps as BS3ListGroupProps,
|
||||
} from 'react-bootstrap'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import { getAriaAndDataProps } from '@/features/utils/bootstrap-5'
|
||||
|
||||
type OLListGroupProps = ListGroupProps & {
|
||||
bs3Props?: BS3ListGroupProps
|
||||
}
|
||||
|
||||
function OLListGroup(props: OLListGroupProps) {
|
||||
const { bs3Props, ...rest } = props
|
||||
|
||||
const bs3ListGroupProps: BS3ListGroupProps = {
|
||||
children: rest.children,
|
||||
role: rest.role,
|
||||
componentClass: rest.as,
|
||||
...bs3Props,
|
||||
}
|
||||
|
||||
const extraProps = getAriaAndDataProps(rest)
|
||||
const as = rest.as ?? 'div'
|
||||
|
||||
return (
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<BS3ListGroup {...bs3ListGroupProps} {...extraProps} />}
|
||||
bs5={<ListGroup {...rest} as={as} />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default OLListGroup
|
|
@ -0,0 +1,60 @@
|
|||
import { Overlay, OverlayProps } from 'react-bootstrap-5'
|
||||
import {
|
||||
Overlay as BS3Overlay,
|
||||
OverlayProps as BS3OverlayProps,
|
||||
} from 'react-bootstrap'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
|
||||
type OLOverlayProps = OverlayProps & {
|
||||
bs3Props?: BS3OverlayProps
|
||||
}
|
||||
|
||||
function OLOverlay(props: OLOverlayProps) {
|
||||
const { bs3Props, ...bs5Props } = props
|
||||
|
||||
let bs3OverlayProps: BS3OverlayProps = {
|
||||
children: bs5Props.children,
|
||||
target: bs5Props.target as BS3OverlayProps['target'],
|
||||
container: bs5Props.container,
|
||||
containerPadding: bs5Props.containerPadding,
|
||||
show: bs5Props.show,
|
||||
rootClose: bs5Props.rootClose,
|
||||
animation: bs5Props.transition,
|
||||
onHide: bs5Props.onHide as BS3OverlayProps['onHide'],
|
||||
onEnter: bs5Props.onEnter as BS3OverlayProps['onEnter'],
|
||||
onEntering: bs5Props.onEntering as BS3OverlayProps['onEntering'],
|
||||
onEntered: bs5Props.onEntered as BS3OverlayProps['onEntered'],
|
||||
onExit: bs5Props.onExit as BS3OverlayProps['onExit'],
|
||||
onExiting: bs5Props.onExiting as BS3OverlayProps['onExiting'],
|
||||
onExited: bs5Props.onExited as BS3OverlayProps['onExited'],
|
||||
}
|
||||
|
||||
if (bs5Props.placement) {
|
||||
const bs3PlacementOptions = [
|
||||
'top',
|
||||
'right',
|
||||
'bottom',
|
||||
'left',
|
||||
] satisfies Array<
|
||||
Extract<OverlayProps['placement'], BS3OverlayProps['placement']>
|
||||
>
|
||||
|
||||
for (const placement of bs3PlacementOptions) {
|
||||
if (placement === bs5Props.placement) {
|
||||
bs3OverlayProps.placement = bs5Props.placement
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bs3OverlayProps = { ...bs3OverlayProps, ...bs3Props }
|
||||
|
||||
return (
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<BS3Overlay {...bs3OverlayProps} />}
|
||||
bs5={<Overlay {...bs5Props} />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default OLOverlay
|
|
@ -0,0 +1,85 @@
|
|||
import { forwardRef } from 'react'
|
||||
import { Popover, PopoverProps } from 'react-bootstrap-5'
|
||||
import {
|
||||
Popover as BS3Popover,
|
||||
PopoverProps as BS3PopoverProps,
|
||||
} from 'react-bootstrap'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
|
||||
type OLPopoverProps = PopoverProps & {
|
||||
title?: React.ReactNode
|
||||
bs3Props?: BS3PopoverProps
|
||||
}
|
||||
|
||||
const OLPopover = forwardRef<HTMLDivElement, OLPopoverProps>((props, ref) => {
|
||||
// BS3 passes in some props automatically so the `props`
|
||||
// type should be adjusted to reflect the actual received object
|
||||
const propsCombinedWithAutoInjectedBs3Props = props as OLPopoverProps &
|
||||
Pick<
|
||||
BS3PopoverProps,
|
||||
'arrowOffsetLeft' | 'arrowOffsetTop' | 'positionLeft' | 'positionTop'
|
||||
>
|
||||
|
||||
const {
|
||||
bs3Props,
|
||||
title,
|
||||
children,
|
||||
arrowOffsetLeft,
|
||||
arrowOffsetTop,
|
||||
positionLeft,
|
||||
positionTop,
|
||||
...bs5Props
|
||||
} = propsCombinedWithAutoInjectedBs3Props
|
||||
|
||||
let bs3PopoverProps: BS3PopoverProps = {
|
||||
children,
|
||||
arrowOffsetLeft,
|
||||
arrowOffsetTop,
|
||||
positionLeft,
|
||||
positionTop,
|
||||
title,
|
||||
id: bs5Props.id,
|
||||
className: bs5Props.className,
|
||||
style: bs5Props.style,
|
||||
}
|
||||
|
||||
if (bs5Props.placement) {
|
||||
const bs3PlacementOptions = [
|
||||
'top',
|
||||
'right',
|
||||
'bottom',
|
||||
'left',
|
||||
] satisfies Array<
|
||||
Extract<PopoverProps['placement'], BS3PopoverProps['placement']>
|
||||
>
|
||||
|
||||
for (const placement of bs3PlacementOptions) {
|
||||
if (placement === bs5Props.placement) {
|
||||
bs3PopoverProps.placement = bs5Props.placement
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bs3PopoverProps = { ...bs3PopoverProps, ...bs3Props }
|
||||
|
||||
return (
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={
|
||||
<BS3Popover
|
||||
{...bs3PopoverProps}
|
||||
ref={ref as React.LegacyRef<BS3Popover>}
|
||||
/>
|
||||
}
|
||||
bs5={
|
||||
<Popover {...bs5Props} ref={ref}>
|
||||
{title && <Popover.Header>{title}</Popover.Header>}
|
||||
<Popover.Body>{children}</Popover.Body>
|
||||
</Popover>
|
||||
}
|
||||
/>
|
||||
)
|
||||
})
|
||||
OLPopover.displayName = 'OLPopover'
|
||||
|
||||
export default OLPopover
|
|
@ -0,0 +1,42 @@
|
|||
import { ToggleButtonGroup, ToggleButtonGroupProps } from 'react-bootstrap-5'
|
||||
import BS3ToggleButtonGroup from '@/features/ui/components/bootstrap-3/toggle-button-group'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import { getAriaAndDataProps } from '@/features/utils/bootstrap-5'
|
||||
|
||||
type BS3ToggleButtonGroupProps = React.ComponentProps<
|
||||
typeof BS3ToggleButtonGroup
|
||||
>
|
||||
|
||||
type OLToggleButtonGroupProps<T> = ToggleButtonGroupProps<T> & {
|
||||
bs3Props?: BS3ToggleButtonGroupProps
|
||||
}
|
||||
|
||||
function OLToggleButtonGroup<T>(props: OLToggleButtonGroupProps<T>) {
|
||||
const { bs3Props, ...rest } = props
|
||||
|
||||
const bs3ToggleButtonGroupProps = {
|
||||
name: rest.name,
|
||||
type: rest.type,
|
||||
value: rest.value,
|
||||
onChange: rest.onChange,
|
||||
children: rest.children,
|
||||
className: rest.className,
|
||||
defaultValue: rest.defaultValue,
|
||||
defaultChecked: rest.defaultChecked,
|
||||
...bs3Props,
|
||||
} as BS3ToggleButtonGroupProps
|
||||
|
||||
// Get all `aria-*` and `data-*` attributes
|
||||
const extraProps = getAriaAndDataProps(rest)
|
||||
|
||||
return (
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={
|
||||
<BS3ToggleButtonGroup {...extraProps} {...bs3ToggleButtonGroupProps} />
|
||||
}
|
||||
bs5={<ToggleButtonGroup {...rest} />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default OLToggleButtonGroup
|
|
@ -0,0 +1,48 @@
|
|||
import { ToggleButton, ToggleButtonProps } from 'react-bootstrap-5'
|
||||
import {
|
||||
ToggleButton as BS3ToggleButton,
|
||||
ToggleButtonProps as BS3ToggleButtonProps,
|
||||
} from 'react-bootstrap'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import { getAriaAndDataProps } from '@/features/utils/bootstrap-5'
|
||||
import classnames from 'classnames'
|
||||
|
||||
type OLToggleButtonProps = ToggleButtonProps & {
|
||||
bs3Props?: BS3ToggleButtonProps
|
||||
}
|
||||
|
||||
function OLToggleButton(props: OLToggleButtonProps) {
|
||||
const { bs3Props, ...rest } = props
|
||||
|
||||
const bs3ToggleButtonProps: BS3ToggleButtonProps & { active?: boolean } = {
|
||||
type: rest.type,
|
||||
name: rest.name,
|
||||
active: rest.active,
|
||||
checked: rest.checked,
|
||||
disabled: rest.disabled,
|
||||
onChange: rest.onChange as BS3ToggleButtonProps['onChange'],
|
||||
onClick: rest.onClick as BS3ToggleButtonProps['onClick'],
|
||||
value: rest.value as BS3ToggleButtonProps['value'],
|
||||
children: rest.children,
|
||||
className: classnames(`btn-${props.variant || 'primary'}`, rest.className),
|
||||
...bs3Props,
|
||||
}
|
||||
|
||||
// Get all `aria-*` and `data-*` attributes
|
||||
const extraProps = getAriaAndDataProps(rest)
|
||||
|
||||
return (
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={
|
||||
<BS3ToggleButton
|
||||
{...extraProps}
|
||||
{...bs3ToggleButtonProps}
|
||||
bsStyle={null}
|
||||
/>
|
||||
}
|
||||
bs5={<ToggleButton {...rest} />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default OLToggleButton
|
|
@ -1,13 +1,16 @@
|
|||
import type { FC, ReactNode } from 'react'
|
||||
import classnames from 'classnames'
|
||||
import Tooltip from './tooltip'
|
||||
import { OverlayTriggerProps } from 'react-bootstrap'
|
||||
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
|
||||
type TooltipProps = {
|
||||
id: string
|
||||
text: ReactNode
|
||||
placement?: OverlayTriggerProps['placement']
|
||||
className?: string
|
||||
placement?: NonNullable<
|
||||
React.ComponentProps<typeof OLTooltip>['overlayProps']
|
||||
>['placement']
|
||||
}
|
||||
|
||||
const BetaBadge: FC<{
|
||||
|
@ -15,7 +18,7 @@ const BetaBadge: FC<{
|
|||
url?: string
|
||||
phase?: string
|
||||
}> = ({ tooltip, url = '/beta/participate', phase = 'beta' }) => {
|
||||
let badgeClass
|
||||
let badgeClass: 'info-badge' | 'alpha-badge' | 'beta-badge'
|
||||
switch (phase) {
|
||||
case 'release':
|
||||
badgeClass = 'info-badge'
|
||||
|
@ -29,24 +32,28 @@ const BetaBadge: FC<{
|
|||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
<OLTooltip
|
||||
id={tooltip.id}
|
||||
description={tooltip.text}
|
||||
tooltipProps={{ className: tooltip.className }}
|
||||
overlayProps={{
|
||||
placement: tooltip.placement || 'bottom',
|
||||
delayHide: 100,
|
||||
delay: 100,
|
||||
}}
|
||||
>
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={classnames('badge', badgeClass)}
|
||||
>
|
||||
<a href={url} target="_blank" rel="noopener noreferrer">
|
||||
<span className="sr-only">{tooltip.text}</span>
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<span className={classnames('badge', badgeClass)} />}
|
||||
bs5={
|
||||
<MaterialIcon
|
||||
type="info"
|
||||
className={classnames('align-middle', badgeClass)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</a>
|
||||
</Tooltip>
|
||||
</OLTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,11 @@ import classNames from 'classnames'
|
|||
import { useSelect } from 'downshift'
|
||||
import Icon from './icon'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import { Form, Spinner } from 'react-bootstrap-5'
|
||||
import FormControl from '@/features/ui/components/bootstrap-5/form/form-control'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { DropdownItem } from '@/features/ui/components/bootstrap-5/dropdown-menu'
|
||||
|
||||
export type SelectProps<T> = {
|
||||
// The items rendered as dropdown options.
|
||||
|
@ -79,6 +84,7 @@ export const Select = <T,>({
|
|||
getItemProps,
|
||||
highlightedIndex,
|
||||
openMenu,
|
||||
closeMenu,
|
||||
} = useSelect({
|
||||
items: items ?? [],
|
||||
itemToString,
|
||||
|
@ -117,9 +123,16 @@ export const Select = <T,>({
|
|||
}
|
||||
}, [name, itemToString, selectedItem, defaultItem])
|
||||
|
||||
const handleMenuKeyDown = (event: React.KeyboardEvent<HTMLUListElement>) => {
|
||||
if (event.key === 'Escape' && isOpen) {
|
||||
event.stopPropagation()
|
||||
closeMenu()
|
||||
}
|
||||
}
|
||||
|
||||
const onKeyDown: KeyboardEventHandler<HTMLButtonElement> = useCallback(
|
||||
event => {
|
||||
if (event.key === 'Enter' && !isOpen) {
|
||||
if ((event.key === 'Enter' || event.key === ' ') && !isOpen) {
|
||||
event.preventDefault()
|
||||
;(event.nativeEvent as any).preventDownshiftDefault = true
|
||||
openMenu()
|
||||
|
@ -135,77 +148,148 @@ export const Select = <T,>({
|
|||
value = defaultText
|
||||
}
|
||||
return (
|
||||
<div className="select-wrapper" ref={rootRef}>
|
||||
<div>
|
||||
{label ? (
|
||||
<label {...getLabelProps()}>
|
||||
{label}{' '}
|
||||
{optionalLabel && (
|
||||
<span className="select-optional-label text-muted">
|
||||
({t('optional')})
|
||||
</span>
|
||||
)}{' '}
|
||||
{loading && <Icon data-testid="spinner" fw type="spinner" spin />}
|
||||
</label>
|
||||
) : null}
|
||||
<div
|
||||
className={classNames({ disabled }, 'select-trigger')}
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||
tabIndex={0}
|
||||
{...getToggleButtonProps({
|
||||
disabled,
|
||||
onKeyDown,
|
||||
})}
|
||||
>
|
||||
<div>{value}</div>
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={
|
||||
<div className="select-wrapper" ref={rootRef}>
|
||||
<div>
|
||||
{isOpen ? (
|
||||
<Icon type="chevron-up" fw />
|
||||
) : (
|
||||
<Icon type="chevron-down" fw />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul
|
||||
className={classNames({ hidden: !isOpen }, 'select-items')}
|
||||
{...getMenuProps({ disabled })}
|
||||
>
|
||||
{isOpen &&
|
||||
items?.map((item, index) => {
|
||||
const isDisabled = itemToDisabled && itemToDisabled(item)
|
||||
return (
|
||||
<li
|
||||
className={classNames({
|
||||
'select-highlighted': highlightedIndex === index,
|
||||
'selected-active': selectedItem === item,
|
||||
'select-icon': selectedIcon,
|
||||
'select-disabled': isDisabled,
|
||||
})}
|
||||
key={itemToKey(item)}
|
||||
{...getItemProps({ item, index, disabled: isDisabled })}
|
||||
>
|
||||
<span className="select-item-title">
|
||||
{selectedIcon && (
|
||||
<div className="select-item-icon">
|
||||
{(selectedItem === item ||
|
||||
(!selectedItem && defaultItem === item)) && (
|
||||
<Icon type="check" fw />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{itemToString(item)}
|
||||
</span>
|
||||
|
||||
{itemToSubtitle ? (
|
||||
<span className="text-muted select-item-subtitle">
|
||||
{itemToSubtitle(item)}
|
||||
{label ? (
|
||||
<label {...getLabelProps()}>
|
||||
{label}{' '}
|
||||
{optionalLabel && (
|
||||
<span className="select-optional-label text-muted">
|
||||
({t('optional')})
|
||||
</span>
|
||||
) : null}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)}{' '}
|
||||
{loading && (
|
||||
<Icon data-testid="spinner" fw type="spinner" spin />
|
||||
)}
|
||||
</label>
|
||||
) : null}
|
||||
<div
|
||||
className={classNames({ disabled }, 'select-trigger')}
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||
tabIndex={0}
|
||||
{...getToggleButtonProps({
|
||||
disabled,
|
||||
onKeyDown,
|
||||
})}
|
||||
>
|
||||
<div>{value}</div>
|
||||
<div>
|
||||
{isOpen ? (
|
||||
<Icon type="chevron-up" fw />
|
||||
) : (
|
||||
<Icon type="chevron-down" fw />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul
|
||||
className={classNames({ hidden: !isOpen }, 'select-items')}
|
||||
{...getMenuProps({ disabled, onKeyDown: handleMenuKeyDown })}
|
||||
>
|
||||
{isOpen &&
|
||||
items?.map((item, index) => {
|
||||
const isDisabled = itemToDisabled && itemToDisabled(item)
|
||||
return (
|
||||
<li
|
||||
className={classNames({
|
||||
'select-highlighted': highlightedIndex === index,
|
||||
'selected-active': selectedItem === item,
|
||||
'select-icon': selectedIcon,
|
||||
'select-disabled': isDisabled,
|
||||
})}
|
||||
key={itemToKey(item)}
|
||||
{...getItemProps({ item, index, disabled: isDisabled })}
|
||||
>
|
||||
<span className="select-item-title">
|
||||
{selectedIcon && (
|
||||
<div className="select-item-icon">
|
||||
{(selectedItem === item ||
|
||||
(!selectedItem && defaultItem === item)) && (
|
||||
<Icon type="check" fw />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{itemToString(item)}
|
||||
</span>
|
||||
|
||||
{itemToSubtitle ? (
|
||||
<span className="text-muted select-item-subtitle">
|
||||
{itemToSubtitle(item)}
|
||||
</span>
|
||||
) : null}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
bs5={
|
||||
<div className="select-wrapper" ref={rootRef}>
|
||||
{label ? (
|
||||
<Form.Label {...getLabelProps()}>
|
||||
{label}{' '}
|
||||
{optionalLabel && (
|
||||
<span className="fw-normal">({t('optional')})</span>
|
||||
)}{' '}
|
||||
{loading && (
|
||||
<span data-testid="spinner">
|
||||
<Spinner
|
||||
animation="border"
|
||||
aria-hidden="true"
|
||||
as="span"
|
||||
role="status"
|
||||
size="sm"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</Form.Label>
|
||||
) : null}
|
||||
<FormControl
|
||||
{...getToggleButtonProps({
|
||||
disabled,
|
||||
onKeyDown,
|
||||
className: 'select-trigger',
|
||||
})}
|
||||
value={value}
|
||||
readOnly
|
||||
append={
|
||||
<MaterialIcon
|
||||
type={isOpen ? 'keyboard_arrow_up' : 'keyboard_arrow_down'}
|
||||
className="align-text-bottom"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ul
|
||||
{...getMenuProps({ disabled, onKeyDown: handleMenuKeyDown })}
|
||||
className={classNames('dropdown-menu w-100', { show: isOpen })}
|
||||
>
|
||||
{isOpen &&
|
||||
items?.map((item, index) => {
|
||||
const isDisabled = itemToDisabled && itemToDisabled(item)
|
||||
return (
|
||||
<li role="none" key={itemToKey(item)}>
|
||||
<DropdownItem
|
||||
as="button"
|
||||
className={classNames({
|
||||
'select-highlighted': highlightedIndex === index,
|
||||
})}
|
||||
active={selectedItem === item}
|
||||
trailingIcon={selectedItem === item ? 'check' : undefined}
|
||||
description={
|
||||
itemToSubtitle ? itemToSubtitle(item) : undefined
|
||||
}
|
||||
{...getItemProps({ item, index, disabled: isDisabled })}
|
||||
>
|
||||
{itemToString(item)}
|
||||
</DropdownItem>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
import { FC, createContext, useContext } from 'react'
|
||||
|
||||
const SwitcherContext = createContext<
|
||||
| {
|
||||
name: string
|
||||
onChange?: (value: string) => any
|
||||
defaultValue?: string
|
||||
disabled: boolean
|
||||
}
|
||||
| undefined
|
||||
>(undefined)
|
||||
|
||||
export const Switcher: FC<{
|
||||
name: string
|
||||
onChange?: (value: string) => any
|
||||
defaultValue?: string
|
||||
disabled?: boolean
|
||||
}> = ({ name, children, onChange, defaultValue, disabled = false }) => {
|
||||
return (
|
||||
<SwitcherContext.Provider
|
||||
value={{ name, onChange, defaultValue, disabled }}
|
||||
>
|
||||
<fieldset>{children}</fieldset>
|
||||
</SwitcherContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const SwitcherItem: FC<{
|
||||
value: string
|
||||
label: string
|
||||
checked?: boolean
|
||||
}> = ({ value, label, checked = false }) => {
|
||||
const ctx = useContext(SwitcherContext)
|
||||
if (!ctx) {
|
||||
throw new Error('SwitcherItem must be a child of Switcher')
|
||||
}
|
||||
const { name, onChange, defaultValue, disabled } = ctx
|
||||
|
||||
const id = `${name}-option-${value.replace(/\W/g, '')}`
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="radio"
|
||||
value={value}
|
||||
id={id}
|
||||
className="switcher-input"
|
||||
name={name}
|
||||
defaultChecked={!disabled && (checked || defaultValue === value)}
|
||||
disabled={disabled}
|
||||
onChange={evt => {
|
||||
if (onChange) {
|
||||
onChange(evt.target.value)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<label htmlFor={id} className="switcher-label" aria-disabled={disabled}>
|
||||
<span>{label}</span>
|
||||
</label>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,28 +1,55 @@
|
|||
import { Switcher, SwitcherItem } from '../js/shared/components/switcher'
|
||||
import OLToggleButton from '@/features/ui/components/ol/ol-toggle-button'
|
||||
import OLToggleButtonGroup from '@/features/ui/components/ol/ol-toggle-button-group'
|
||||
|
||||
export const Base = () => {
|
||||
return (
|
||||
<Switcher name="figure-width" defaultValue="0.5">
|
||||
<SwitcherItem value="0.25" label="¼ width" />
|
||||
<SwitcherItem value="0.5" label="½ width" />
|
||||
<SwitcherItem value="0.75" label="¾ width" />
|
||||
<SwitcherItem value="1.0" label="Full width" />
|
||||
</Switcher>
|
||||
<OLToggleButtonGroup
|
||||
type="radio"
|
||||
name="figure-width"
|
||||
defaultValue="0.5"
|
||||
aria-label="Image width"
|
||||
>
|
||||
<OLToggleButton variant="secondary" id="width-25p" value="0.25">
|
||||
¼ width
|
||||
</OLToggleButton>
|
||||
<OLToggleButton variant="secondary" id="width-50p" value="0.5">
|
||||
½ width
|
||||
</OLToggleButton>
|
||||
<OLToggleButton variant="secondary" id="width-75p" value="0.75">
|
||||
¾ width
|
||||
</OLToggleButton>
|
||||
<OLToggleButton variant="secondary" id="width-100p" value="1.0">
|
||||
Full width
|
||||
</OLToggleButton>
|
||||
</OLToggleButtonGroup>
|
||||
)
|
||||
}
|
||||
|
||||
export const Disabled = () => {
|
||||
return (
|
||||
<Switcher name="figure-width" defaultValue="0.5" disabled>
|
||||
<SwitcherItem value="0.25" label="¼ width" />
|
||||
<SwitcherItem value="0.5" label="½ width" />
|
||||
<SwitcherItem value="0.75" label="¾ width" />
|
||||
<SwitcherItem value="1.0" label="Full width" />
|
||||
</Switcher>
|
||||
<OLToggleButtonGroup
|
||||
type="radio"
|
||||
name="figure-width"
|
||||
defaultValue="0.5"
|
||||
aria-label="Image width"
|
||||
>
|
||||
<OLToggleButton variant="secondary" id="width-25p" disabled value="0.25">
|
||||
¼ width
|
||||
</OLToggleButton>
|
||||
<OLToggleButton variant="secondary" id="width-50p" disabled value="0.5">
|
||||
½ width
|
||||
</OLToggleButton>
|
||||
<OLToggleButton variant="secondary" id="width-75p" disabled value="0.75">
|
||||
¾ width
|
||||
</OLToggleButton>
|
||||
<OLToggleButton variant="secondary" id="width-100p" disabled value="1.0">
|
||||
Full width
|
||||
</OLToggleButton>
|
||||
</OLToggleButtonGroup>
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Shared / Components / Switcher',
|
||||
component: Switcher,
|
||||
title: 'Shared / Components / Toggle Button Group',
|
||||
component: OLToggleButtonGroup,
|
||||
}
|
||||
|
|
|
@ -1,27 +1,3 @@
|
|||
.figure-modal-label {
|
||||
font-weight: normal;
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.figure-modal-form tr,
|
||||
.figure-modal-form td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.figure-modal-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.figure-modal-help-buttons,
|
||||
.figure-modal-actions {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.figure-modal-help-buttons {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.figure-modal-help-link,
|
||||
.figure-modal-help-link:hover,
|
||||
.figure-modal-help-link:focus,
|
||||
|
@ -34,29 +10,32 @@
|
|||
padding-left: 0;
|
||||
}
|
||||
|
||||
.figure-modal-switcher-input {
|
||||
display: flex;
|
||||
}
|
||||
.figure-modal-checkbox-input {
|
||||
label {
|
||||
display: inline-block;
|
||||
font-weight: normal;
|
||||
line-height: 100%;
|
||||
|
||||
.figure-modal-checkbox-input,
|
||||
.figure-modal-switcher-input {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.figure-modal-label-content {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.figure-modal-checkbox-input input[type='checkbox'] {
|
||||
vertical-align: top;
|
||||
margin-right: 12px;
|
||||
accent-color: @ol-green;
|
||||
input[type='checkbox'] {
|
||||
margin-right: 12px;
|
||||
margin-top: 2px;
|
||||
vertical-align: top;
|
||||
accent-color: @ol-green;
|
||||
}
|
||||
}
|
||||
|
||||
.figure-modal-switcher-input {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.figure-modal-input-field {
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.file-container {
|
||||
|
@ -71,6 +50,7 @@
|
|||
|
||||
.file-container-file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid @neutral-20;
|
||||
border-radius: 8px;
|
||||
background-color: white;
|
||||
|
@ -92,7 +72,6 @@
|
|||
|
||||
.file-icon {
|
||||
font-size: 20px;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.file-action {
|
||||
|
@ -129,17 +108,13 @@
|
|||
}
|
||||
|
||||
.figure-modal-source-button-icon {
|
||||
flex: 0 0 auto;
|
||||
&.source-icon {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.figure-modal-input-label:not(:first-child),
|
||||
.figure-modal .select-wrapper:not(:first-child) {
|
||||
margin-top: 16px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.figure-modal-upload .uppy-Dashboard-AddFiles-list {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.select-from-files-btn {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
|
|
@ -387,12 +387,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.formatting-btn--icon {
|
||||
.formatting-btn-icon {
|
||||
min-width: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.formatting-btn--icon:last-of-type {
|
||||
.formatting-btn-icon:last-of-type {
|
||||
border-right: 1px solid @formatting-btn-border;
|
||||
}
|
||||
|
||||
|
|
|
@ -175,6 +175,24 @@ $tooltip-border-radius: $border-radius-base;
|
|||
$tooltip-padding-y: $spacing-04;
|
||||
$tooltip-padding-x: $spacing-06;
|
||||
|
||||
// Popovers
|
||||
$popover-font-size: var(--font-size-02);
|
||||
$popover-bg: var(--bg-dark-primary);
|
||||
$popover-max-width: 320px;
|
||||
$popover-border-width: 0px; /* stylelint-disable-line length-zero-no-unit */
|
||||
$popover-border-radius: var(--border-radius-base);
|
||||
$popover-header-font-size: var(--font-size-02);
|
||||
$popover-header-bg: var(--bg-dark-primary);
|
||||
$popover-header-color: var(--content-primary-dark);
|
||||
$popover-header-padding-y: var(--spacing-04);
|
||||
$popover-header-padding-x: var(--spacing-06);
|
||||
$popover-body-color: var(--content-primary-dark);
|
||||
$popover-body-padding-y: var(--spacing-04);
|
||||
$popover-body-padding-x: var(--spacing-06);
|
||||
$popover-arrow-width: var(--spacing-04);
|
||||
$popover-arrow-height: var(--spacing-02);
|
||||
$popover-arrow-color: var(--bg-dark-primary);
|
||||
|
||||
// Links. Ideally we'd point these to CSS variables but Bootstrap performs
|
||||
// calculations on link color during compilation.
|
||||
$link-color: $link-ui;
|
||||
|
@ -228,3 +246,13 @@ $dropdown-padding-y: var(--spacing-02);
|
|||
$dropdown-item-padding-x: var(--spacing-04);
|
||||
$dropdown-item-padding-y: var(--spacing-05);
|
||||
$dropdown-header-color: var(--content-secondary);
|
||||
|
||||
// List group
|
||||
$list-group-color: var(--content-secondary);
|
||||
$list-group-border-width: 0;
|
||||
$list-group-border-radius: (--border-radius-base);
|
||||
$list-group-item-padding-y: var(--spacing-04);
|
||||
$list-group-item-padding-x: var(--spacing-05);
|
||||
$list-group-hover-bg: var(--bg-light-secondary);
|
||||
$list-group-disabled-color: var(--content-disabled);
|
||||
$list-group-disabled-bg: var(--bg-light-primary);
|
||||
|
|
|
@ -33,8 +33,10 @@
|
|||
@import 'bootstrap-5/scss/dropdown';
|
||||
@import 'bootstrap-5/scss/button-group';
|
||||
@import 'bootstrap-5/scss/badge';
|
||||
@import 'bootstrap-5/scss/list-group';
|
||||
@import 'bootstrap-5/scss/modal';
|
||||
@import 'bootstrap-5/scss/tooltip';
|
||||
@import 'bootstrap-5/scss/popover';
|
||||
@import 'bootstrap-5/scss/spinners';
|
||||
@import 'bootstrap-5/scss/card';
|
||||
@import 'bootstrap-5/scss/close';
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
@import 'notifications';
|
||||
@import 'system-messages';
|
||||
@import 'tooltip';
|
||||
@import 'popover';
|
||||
@import 'card';
|
||||
@import 'badge';
|
||||
@import 'form';
|
||||
|
@ -20,3 +21,6 @@
|
|||
@import 'table';
|
||||
@import 'blog-posts';
|
||||
@import 'tabs';
|
||||
@import 'beta-badges';
|
||||
@import 'list-group';
|
||||
@import 'select';
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
.info-badge {
|
||||
color: var(--blue-50);
|
||||
}
|
||||
|
||||
.alpha-badge {
|
||||
color: var(--green-50);
|
||||
}
|
||||
|
||||
.beta-badge {
|
||||
color: var(--yellow-40);
|
||||
}
|
|
@ -84,7 +84,7 @@
|
|||
}
|
||||
|
||||
.form-label {
|
||||
&:has(+ .form-control[disabled]) {
|
||||
&:has(+ .form-control[disabled], + .form-control-wrapper-disabled) {
|
||||
color: var(--content-disabled);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
.list-group-item {
|
||||
min-height: 48px;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
.popover {
|
||||
@include shadow-md;
|
||||
|
||||
line-height: var(--line-height-02);
|
||||
}
|
||||
|
||||
.popover-header {
|
||||
padding-bottom: 0;
|
||||
margin-bottom: var(--spacing-02);
|
||||
font-weight: 600;
|
||||
|
||||
& + .popover-body {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
.select-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.select-trigger {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.select-highlighted {
|
||||
background-color: var(--bg-light-secondary);
|
||||
}
|
|
@ -10,4 +10,5 @@
|
|||
@import 'editor/loading-screen';
|
||||
@import 'editor/outline';
|
||||
@import 'editor/file-tree';
|
||||
@import 'editor/figure-modal';
|
||||
@import 'website-redesign';
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
.figure-modal-help-link,
|
||||
.figure-modal-help-link:hover,
|
||||
.figure-modal-help-link:focus,
|
||||
.figure-modal-help-link:active {
|
||||
color: var(--neutral-90) !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.figure-modal-help-link {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.figure-modal-switcher-input {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.file-container {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
padding: var(--spacing-07) var(--spacing-08);
|
||||
border: 1px dashed #eaeaea;
|
||||
background-color: #fafafa;
|
||||
justify-content: space-between;
|
||||
border-radius: var(--border-radius-medium);
|
||||
}
|
||||
|
||||
.file-container-file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--neutral-20);
|
||||
border-radius: var(--border-radius-medium);
|
||||
background-color: white;
|
||||
height: 100%;
|
||||
padding: var(--spacing-06) var(--spacing-07);
|
||||
}
|
||||
|
||||
.file-info {
|
||||
margin-left: var(--spacing-07);
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.figure-modal-source-button-grid {
|
||||
display: grid;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
margin: 0 auto;
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.figure-modal-source-button {
|
||||
display: flex;
|
||||
flex: 1 1 0;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 4px 0 #1e253029;
|
||||
line-height: 44px;
|
||||
background-color: var(--white);
|
||||
border-radius: var(--border-radius-base);
|
||||
border: none;
|
||||
padding: 0 var(--spacing-04);
|
||||
|
||||
&-title {
|
||||
flex: 1 1 auto;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.figure-modal-source-button-icon {
|
||||
margin-right: var(--spacing-03);
|
||||
}
|
||||
|
||||
/* stylelint-disable selector-class-pattern */
|
||||
.figure-modal-upload .uppy-Dashboard-AddFiles-list {
|
||||
display: none;
|
||||
}
|
|
@ -79,7 +79,7 @@
|
|||
}
|
||||
|
||||
.ide-panel-group-resizing {
|
||||
background-color: white;
|
||||
background-color: var(--white);
|
||||
|
||||
// Hide panel contents while resizing
|
||||
.ide-react-editor-content,
|
||||
|
@ -188,3 +188,8 @@
|
|||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.full-size {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
|
|
@ -5,12 +5,18 @@
|
|||
--toolbar-btn-color: var(--white);
|
||||
--toolbar-btn-hover-bg-color: var(--neutral-80);
|
||||
--toolbar-btn-hover-color: var(--white);
|
||||
--toolbar-btn-active-color: var(--white);
|
||||
--toolbar-btn-active-bg-color: var(--green-50);
|
||||
--formatting-btn-color: var(--white);
|
||||
--formatting-btn-bg: var(--neutral-80);
|
||||
--formatting-btn-border: var(--neutral-70);
|
||||
--project-name-color: var(--neutral-40);
|
||||
--toolbar-filetree-bg-color: var(--neutral-80);
|
||||
--project-rename-link-color: var(--neutral-40);
|
||||
--project-rename-link-color-hover: var(--neutral-20);
|
||||
--editor-header-logo-background: url(../../../../../public/img/ol-brand/overleaf-o-white.svg)
|
||||
center / contain no-repeat;
|
||||
--editor-toolbar-bg: var(--neutral-80);
|
||||
}
|
||||
|
||||
@include theme('light') {
|
||||
|
@ -20,12 +26,17 @@
|
|||
--toolbar-btn-color: var(--neutral-70);
|
||||
--toolbar-btn-hover-bg-color: var(--neutral-10);
|
||||
--toolbar-btn-hover-color: var(--neutral-70);
|
||||
--toolbar-btn-active-bg-color: var(--green-50);
|
||||
--formatting-btn-color: var(--neutral-70);
|
||||
--formatting-btn-bg: transparent;
|
||||
--formatting-btn-border: var(--neutral-20);
|
||||
--project-name-color: var(--neutral-70);
|
||||
--toolbar-filetree-bg-color: var(--white);
|
||||
--project-rename-link-color: var(--neutral-70);
|
||||
--project-rename-link-color-hover: var(--neutral-70);
|
||||
--editor-header-logo-background: url(../../../../../public/img/ol-brand/overleaf-o.svg)
|
||||
center / contain no-repeat;
|
||||
--editor-toolbar-bg: var(--white);
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
|
@ -300,3 +311,156 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-editor {
|
||||
height: 32px;
|
||||
background-color: var(--editor-toolbar-bg);
|
||||
padding: 0 5px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: 10; // Prevent track changes showing over toolbar
|
||||
}
|
||||
|
||||
/**************************************
|
||||
Toggle Switch
|
||||
***************************************/
|
||||
|
||||
.toggle-switch {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 26px;
|
||||
margin-right: var(--spacing-03);
|
||||
border-radius: var(--border-radius-full);
|
||||
background-color: var(--neutral-20);
|
||||
padding: var(--spacing-01);
|
||||
}
|
||||
|
||||
.toggle-switch-label {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
font-weight: normal;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: var(--content-secondary);
|
||||
border-radius: var(--border-radius-full);
|
||||
transition:
|
||||
color 0.12s ease-out,
|
||||
background-color 0.12s ease-out,
|
||||
box-shadow 0.12s ease-out;
|
||||
overflow: hidden;
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0 var(--spacing-08);
|
||||
background-size: 200% 100%;
|
||||
background-position: 0 0;
|
||||
transition: background-position 0.12s ease-out;
|
||||
font-size: var(--font-size-02);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-switch-input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.toggle-switch-input:disabled + .toggle-switch-label {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.toggle-switch-input:checked + .toggle-switch-label {
|
||||
span {
|
||||
background-position: -100% 0;
|
||||
}
|
||||
|
||||
color: var(--white);
|
||||
background-color: var(--bg-accent-01);
|
||||
border-radius: var(--border-radius-full);
|
||||
box-shadow: 0 2px 4px rgb(30 37 48 / 16%);
|
||||
}
|
||||
|
||||
.toggle-switch-input:checked:nth-child(2) + .toggle-switch-label {
|
||||
span {
|
||||
background-position: 100% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-toggle-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
|
||||
.toggle-switch {
|
||||
margin-left: var(--spacing-03);
|
||||
}
|
||||
|
||||
.toggle-switch-label span {
|
||||
background: none;
|
||||
transition: background 0.12s ease-out;
|
||||
}
|
||||
|
||||
.toggle-switch-label:first-of-type span {
|
||||
padding-left: var(--spacing-04);
|
||||
}
|
||||
|
||||
.toggle-switch-label:last-of-type span {
|
||||
padding-right: var(--spacing-04);
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
/**************************************
|
||||
Formatting buttons
|
||||
***************************************/
|
||||
.formatting-btn {
|
||||
color: var(--formatting-btn-color);
|
||||
background-color: var(--formatting-btn-bg);
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
border-left: 1px solid var(--formatting-btn-border);
|
||||
border-radius: 0;
|
||||
|
||||
&:hover {
|
||||
color: var(--formatting-btn-color);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--white);
|
||||
background-color: var(--toolbar-btn-active-bg-color);
|
||||
box-shadow: none;
|
||||
|
||||
&:focus {
|
||||
color: var(--toolbar-btn-active-color);
|
||||
|
||||
&:not(:focus-visible) {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
color: var(--formatting-btn-color);
|
||||
}
|
||||
}
|
||||
|
||||
.formatting-btn-icon {
|
||||
min-width: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.formatting-btn-icon:last-of-type {
|
||||
border-right: 1px solid var(--formatting-btn-border);
|
||||
}
|
||||
|
|
|
@ -25,6 +25,20 @@
|
|||
}
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
.btn.active {
|
||||
background-color: @neutral-20;
|
||||
}
|
||||
|
||||
.btn[disabled] {
|
||||
cursor: not-allowed;
|
||||
background-color: @neutral-20;
|
||||
border-color: @neutral-20;
|
||||
opacity: 1;
|
||||
color: @neutral-90;
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent double borders when buttons are next to each other
|
||||
.btn-group {
|
||||
.btn + .btn,
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
.switcher-input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.switcher-label {
|
||||
border: 1px solid black;
|
||||
padding: 4px 16px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.switcher-label:first-of-type {
|
||||
border-top-left-radius: 999px;
|
||||
border-bottom-left-radius: 999px;
|
||||
}
|
||||
|
||||
.switcher-label:last-of-type {
|
||||
border-top-right-radius: 999px;
|
||||
border-bottom-right-radius: 999px;
|
||||
}
|
||||
|
||||
.switcher-label:not(:first-of-type) {
|
||||
border-left-width: 0px;
|
||||
}
|
||||
|
||||
.switcher-input:checked + .switcher-label {
|
||||
background-color: @neutral-20;
|
||||
}
|
||||
|
||||
.switcher-input:disabled + .switcher-label {
|
||||
cursor: not-allowed;
|
||||
background-color: @neutral-20;
|
||||
border-color: @neutral-20;
|
||||
opacity: 1;
|
||||
color: @neutral-90;
|
||||
}
|
|
@ -72,7 +72,6 @@
|
|||
@import 'components/list-group.less';
|
||||
@import 'components/select.less';
|
||||
@import 'components/switch.less';
|
||||
@import 'components/switcher.less';
|
||||
@import 'components/stepper.less';
|
||||
@import 'components/radio-chip.less';
|
||||
@import 'components/interstitial.less';
|
||||
|
|
|
@ -2143,7 +2143,9 @@
|
|||
"took_a_while": "That took a while...",
|
||||
"toolbar_bullet_list": "Bullet List",
|
||||
"toolbar_choose_section_heading_level": "Choose section heading level",
|
||||
"toolbar_code_visual_editor_switch": "Code and visual editor switch",
|
||||
"toolbar_decrease_indent": "Decrease Indent",
|
||||
"toolbar_editor": "Editor tools",
|
||||
"toolbar_format_bold": "Format Bold",
|
||||
"toolbar_format_italic": "Format Italic",
|
||||
"toolbar_increase_indent": "Increase Indent",
|
||||
|
@ -2154,7 +2156,10 @@
|
|||
"toolbar_insert_inline_math": "Insert Inline Math",
|
||||
"toolbar_insert_link": "Insert Link",
|
||||
"toolbar_insert_math": "Insert Math",
|
||||
"toolbar_insert_math_and_symbols": "Insert Math and Symbols",
|
||||
"toolbar_insert_misc": "Insert Misc (links, citations, cross-references, figures, tables)",
|
||||
"toolbar_insert_table": "Insert Table",
|
||||
"toolbar_list_indentation": "List and Indentation",
|
||||
"toolbar_numbered_list": "Numbered List",
|
||||
"toolbar_redo": "Redo",
|
||||
"toolbar_selected_projects": "Selected projects",
|
||||
|
@ -2163,8 +2168,12 @@
|
|||
"toolbar_selected_projects_restore": "Restore selected projects",
|
||||
"toolbar_table_insert_size_table": "Insert __size__ table",
|
||||
"toolbar_table_insert_table_lowercase": "Insert table",
|
||||
"toolbar_text_formatting": "Text formatting",
|
||||
"toolbar_text_style": "Text style",
|
||||
"toolbar_toggle_symbol_palette": "Toggle Symbol Palette",
|
||||
"toolbar_undo": "Undo",
|
||||
"toolbar_undo_redo_actions": "Undo/Redo actions",
|
||||
"toolbar_visibility": "Toolbar visibility",
|
||||
"tooltip_hide_filetree": "Click to hide the file tree",
|
||||
"tooltip_hide_pdf": "Click to hide the PDF",
|
||||
"tooltip_show_filetree": "Click to show the file tree",
|
||||
|
|
|
@ -28,9 +28,9 @@ describe('split test badge', function () {
|
|||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.get('a.badge.alpha-badge[href="/alpha/participate"]').contains(
|
||||
'This is an alpha feature'
|
||||
)
|
||||
cy.findByRole('link', { name: /this is an alpha feature/i })
|
||||
.should('have.attr', 'href', '/alpha/participate')
|
||||
.find('.badge.alpha-badge')
|
||||
})
|
||||
|
||||
it('does not render the alpha badge when user is not assigned to the variant', function () {
|
||||
|
@ -86,9 +86,9 @@ describe('split test badge', function () {
|
|||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.get('a.badge.beta-badge[href="/beta/participate"]').contains(
|
||||
'This is a beta feature'
|
||||
)
|
||||
cy.findByRole('link', { name: /this is a beta feature/i })
|
||||
.should('have.attr', 'href', '/beta/participate')
|
||||
.find('.badge.beta-badge')
|
||||
})
|
||||
|
||||
it('does not render the beta badge when user is not assigned to the variant', function () {
|
||||
|
@ -144,9 +144,9 @@ describe('split test badge', function () {
|
|||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.get('a.badge.info-badge[href="/feedback/form"]').contains(
|
||||
'This is a new feature'
|
||||
)
|
||||
cy.findByRole('link', { name: /this is a new feature/i })
|
||||
.should('have.attr', 'href', '/feedback/form')
|
||||
.find('.badge.info-badge')
|
||||
})
|
||||
|
||||
it('does not render the info badge when user is not assigned to the variant', function () {
|
||||
|
@ -218,8 +218,10 @@ describe('split test badge', function () {
|
|||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.get('a.badge.info-badge[href="/beta/participate"]')
|
||||
.contains('We are testing this new feature.')
|
||||
.contains('Click to give feedback')
|
||||
cy.findByRole('link', {
|
||||
name: /we are testing this new feature.*click to give feedback/i,
|
||||
})
|
||||
.should('have.attr', 'href', '/beta/participate')
|
||||
.find('.badge.info-badge')
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue