first commit
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import { FC, memo, useRef } from 'react'
|
||||
import useDropdown from '../../../../shared/hooks/use-dropdown'
|
||||
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'
|
||||
|
||||
export const ToolbarButtonMenu: FC<{
|
||||
id: string
|
||||
label: string
|
||||
icon: React.ReactNode
|
||||
altCommand?: (view: EditorView) => void
|
||||
}> = memo(function ButtonMenu({ icon, id, label, altCommand, children }) {
|
||||
const target = useRef<any>(null)
|
||||
const { open, onToggle, ref } = useDropdown()
|
||||
const view = useCodeMirrorViewContext()
|
||||
|
||||
const button = (
|
||||
<button
|
||||
type="button"
|
||||
className="ol-cm-toolbar-button btn"
|
||||
aria-label={label}
|
||||
onMouseDown={event => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onClick={event => {
|
||||
if (event.altKey && altCommand && open === false) {
|
||||
emitToolbarEvent(view, id)
|
||||
event.preventDefault()
|
||||
altCommand(view)
|
||||
view.focus()
|
||||
} else {
|
||||
onToggle(!open)
|
||||
}
|
||||
}}
|
||||
ref={target}
|
||||
>
|
||||
{icon}
|
||||
</button>
|
||||
)
|
||||
|
||||
const overlay = (
|
||||
<OLOverlay
|
||||
show={open}
|
||||
target={target.current}
|
||||
placement="bottom"
|
||||
container={view.dom}
|
||||
containerPadding={0}
|
||||
transition
|
||||
rootClose
|
||||
onHide={() => onToggle(false)}
|
||||
>
|
||||
<OLPopover
|
||||
id={`${id}-menu`}
|
||||
ref={ref}
|
||||
className="ol-cm-toolbar-button-menu-popover"
|
||||
>
|
||||
<OLListGroup
|
||||
role="menu"
|
||||
onClick={() => {
|
||||
onToggle(false)
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</OLListGroup>
|
||||
</OLPopover>
|
||||
</OLOverlay>
|
||||
)
|
||||
|
||||
if (!label) {
|
||||
return (
|
||||
<>
|
||||
{button}
|
||||
{overlay}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<OLTooltip
|
||||
hidden={open}
|
||||
id={id}
|
||||
description={<div>{label}</div>}
|
||||
overlayProps={{ placement: 'bottom' }}
|
||||
>
|
||||
{button}
|
||||
</OLTooltip>
|
||||
{overlay}
|
||||
</>
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,93 @@
|
||||
import { ToolbarButtonMenu } from './button-menu'
|
||||
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'
|
||||
import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics'
|
||||
import { useCodeMirrorViewContext } from '../codemirror-context'
|
||||
import { insertFigure } from '../../extensions/toolbar/commands'
|
||||
import getMeta from '@/utils/meta'
|
||||
import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
|
||||
import { ToolbarButton } from './toolbar-button'
|
||||
|
||||
export const InsertFigureDropdown = memo(function InsertFigureDropdown() {
|
||||
const { t } = useTranslation()
|
||||
const view = useCodeMirrorViewContext()
|
||||
const { write } = usePermissionsContext()
|
||||
const openFigureModal = useCallback(
|
||||
(source: FigureModalSource, sourceName: string) => {
|
||||
emitToolbarEvent(view, `toolbar-figure-modal-${sourceName}`)
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('figure-modal:open', {
|
||||
detail: { source },
|
||||
})
|
||||
)
|
||||
},
|
||||
[view]
|
||||
)
|
||||
const {
|
||||
hasLinkedProjectFileFeature,
|
||||
hasLinkedProjectOutputFileFeature,
|
||||
hasLinkUrlFeature,
|
||||
} = getMeta('ol-ExposedSettings')
|
||||
|
||||
if (!write) {
|
||||
return (
|
||||
<ToolbarButton
|
||||
id="toolbar-figure"
|
||||
label={t('toolbar_insert_figure')}
|
||||
command={() =>
|
||||
openFigureModal(FigureModalSource.FILE_TREE, 'current-project')
|
||||
}
|
||||
icon="add_photo_alternate"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolbarButtonMenu
|
||||
id="toolbar-figure"
|
||||
label={t('toolbar_insert_figure')}
|
||||
icon={<MaterialIcon type="add_photo_alternate" />}
|
||||
altCommand={insertFigure}
|
||||
>
|
||||
<OLListGroupItem
|
||||
onClick={() =>
|
||||
openFigureModal(FigureModalSource.FILE_UPLOAD, 'file-upload')
|
||||
}
|
||||
>
|
||||
<MaterialIcon type="upload" />
|
||||
{t('upload_from_computer')}
|
||||
</OLListGroupItem>
|
||||
<OLListGroupItem
|
||||
onClick={() =>
|
||||
openFigureModal(FigureModalSource.FILE_TREE, 'current-project')
|
||||
}
|
||||
>
|
||||
<MaterialIcon type="inbox" />
|
||||
{t('from_project_files')}
|
||||
</OLListGroupItem>
|
||||
{(hasLinkedProjectFileFeature || hasLinkedProjectOutputFileFeature) && (
|
||||
<OLListGroupItem
|
||||
onClick={() =>
|
||||
openFigureModal(FigureModalSource.OTHER_PROJECT, 'other-project')
|
||||
}
|
||||
>
|
||||
<MaterialIcon type="folder_open" />
|
||||
{t('from_another_project')}
|
||||
</OLListGroupItem>
|
||||
)}
|
||||
{hasLinkUrlFeature && (
|
||||
<OLListGroupItem
|
||||
onClick={() =>
|
||||
openFigureModal(FigureModalSource.FROM_URL, 'from-url')
|
||||
}
|
||||
>
|
||||
<MaterialIcon type="public" />
|
||||
{t('from_url')}
|
||||
</OLListGroupItem>
|
||||
)}
|
||||
</ToolbarButtonMenu>
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,83 @@
|
||||
import { DropdownHeader } from '@/features/ui/components/bootstrap-5/dropdown-menu'
|
||||
import { ToolbarButtonMenu } from './button-menu'
|
||||
import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics'
|
||||
import MaterialIcon from '../../../../shared/components/material-icon'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCodeMirrorViewContext } from '../codemirror-context'
|
||||
import { useEditorContext } from '@/shared/context/editor-context'
|
||||
import {
|
||||
wrapInDisplayMath,
|
||||
wrapInInlineMath,
|
||||
} from '../../extensions/toolbar/commands'
|
||||
import { memo } from 'react'
|
||||
import OLListGroupItem from '@/features/ui/components/ol/ol-list-group-item'
|
||||
import sparkleWhite from '@/shared/svgs/sparkle-small-white.svg'
|
||||
import sparkle from '@/shared/svgs/ai-sparkle-text.svg'
|
||||
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
|
||||
|
||||
export const MathDropdown = memo(function MathDropdown() {
|
||||
const { t } = useTranslation()
|
||||
const view = useCodeMirrorViewContext()
|
||||
const { writefullInstance } = useEditorContext()
|
||||
|
||||
const wfRebrandEnabled = isSplitTestEnabled('wf-feature-rebrand')
|
||||
return (
|
||||
<ToolbarButtonMenu
|
||||
id="toolbar-math"
|
||||
label={t('toolbar_insert_math')}
|
||||
icon={<MaterialIcon type="calculate" />}
|
||||
>
|
||||
{wfRebrandEnabled && writefullInstance && (
|
||||
<>
|
||||
<DropdownHeader className="ol-cm-toolbar-header mx-2">
|
||||
{t('toolbar_insert_math_lowercase')}
|
||||
</DropdownHeader>
|
||||
<OLListGroupItem
|
||||
aria-label={t('toolbar_generate_math')}
|
||||
onClick={event => {
|
||||
writefullInstance?.openEquationGenerator()
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt="sparkle"
|
||||
className="ol-cm-toolbar-ai-sparkle-gradient"
|
||||
src={sparkle}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<img
|
||||
alt="sparkle"
|
||||
className="ol-cm-toolbar-ai-sparkle-white"
|
||||
src={sparkleWhite}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{t('generate_from_text_or_image')}</span>
|
||||
</OLListGroupItem>
|
||||
</>
|
||||
)}
|
||||
<OLListGroupItem
|
||||
aria-label={t('toolbar_insert_inline_math')}
|
||||
onClick={event => {
|
||||
emitToolbarEvent(view, 'toolbar-inline-math')
|
||||
event.preventDefault()
|
||||
wrapInInlineMath(view)
|
||||
view.focus()
|
||||
}}
|
||||
>
|
||||
<MaterialIcon type="123" />
|
||||
<span>{t('inline')}</span>
|
||||
</OLListGroupItem>
|
||||
<OLListGroupItem
|
||||
aria-label={t('toolbar_insert_display_math')}
|
||||
onClick={event => {
|
||||
emitToolbarEvent(view, 'toolbar-display-math')
|
||||
event.preventDefault()
|
||||
wrapInDisplayMath(view)
|
||||
view.focus()
|
||||
}}
|
||||
>
|
||||
<MaterialIcon type="view_day" />
|
||||
<span>{t('display')}</span>
|
||||
</OLListGroupItem>
|
||||
</ToolbarButtonMenu>
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,60 @@
|
||||
import { FC, useRef } from 'react'
|
||||
import classnames from 'classnames'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { useCodeMirrorViewContext } from '../codemirror-context'
|
||||
import OLOverlay from '@/features/ui/components/ol/ol-overlay'
|
||||
import OLPopover from '@/features/ui/components/ol/ol-popover'
|
||||
|
||||
export const ToolbarOverflow: FC<{
|
||||
overflowed: boolean
|
||||
overflowOpen: boolean
|
||||
setOverflowOpen: (open: boolean) => void
|
||||
overflowRef?: React.Ref<HTMLDivElement>
|
||||
}> = ({ overflowed, overflowOpen, setOverflowOpen, overflowRef, children }) => {
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
const view = useCodeMirrorViewContext()
|
||||
|
||||
const className = classnames(
|
||||
'ol-cm-toolbar-button',
|
||||
'ol-cm-toolbar-overflow-toggle',
|
||||
{
|
||||
'ol-cm-toolbar-overflow-toggle-visible': overflowed,
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type="button"
|
||||
id="toolbar-more"
|
||||
className={className}
|
||||
aria-label="More"
|
||||
onMouseDown={event => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onClick={() => {
|
||||
setOverflowOpen(!overflowOpen)
|
||||
}}
|
||||
>
|
||||
<MaterialIcon type="more_horiz" />
|
||||
</button>
|
||||
|
||||
<OLOverlay
|
||||
show={overflowOpen}
|
||||
target={buttonRef.current}
|
||||
placement="bottom"
|
||||
container={view.dom}
|
||||
// containerPadding={0}
|
||||
transition
|
||||
rootClose
|
||||
onHide={() => setOverflowOpen(false)}
|
||||
>
|
||||
<OLPopover id="popover-toolbar-overflow" ref={overflowRef}>
|
||||
<div className="ol-cm-toolbar-overflow">{children}</div>
|
||||
</OLPopover>
|
||||
</OLOverlay>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import classnames from 'classnames'
|
||||
import {
|
||||
useCodeMirrorStateContext,
|
||||
useCodeMirrorViewContext,
|
||||
} from '../codemirror-context'
|
||||
import {
|
||||
findCurrentSectionHeadingLevel,
|
||||
setSectionHeadingLevel,
|
||||
} from '../../extensions/toolbar/sections'
|
||||
import { useCallback, useMemo, useRef } from 'react'
|
||||
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'
|
||||
import Icon from '../../../../shared/components/icon'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const levels = new Map([
|
||||
['text', 'Normal text'],
|
||||
['section', 'Section'],
|
||||
['subsection', 'Subsection'],
|
||||
['subsubsection', 'Subsubsection'],
|
||||
['paragraph', 'Paragraph'],
|
||||
['subparagraph', 'Subparagraph'],
|
||||
])
|
||||
|
||||
const levelsEntries = [...levels.entries()]
|
||||
|
||||
export const SectionHeadingDropdown = () => {
|
||||
const state = useCodeMirrorStateContext()
|
||||
const view = useCodeMirrorViewContext()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { open: overflowOpen, onToggle: setOverflowOpen } = useDropdown()
|
||||
|
||||
useEventListener(
|
||||
'resize',
|
||||
useCallback(() => {
|
||||
setOverflowOpen(false)
|
||||
}, [setOverflowOpen])
|
||||
)
|
||||
|
||||
const toggleButtonRef = useRef<HTMLButtonElement | null>(null)
|
||||
|
||||
const currentLevel = useMemo(
|
||||
() => findCurrentSectionHeadingLevel(state),
|
||||
[state]
|
||||
)
|
||||
|
||||
const currentLabel = currentLevel
|
||||
? (levels.get(currentLevel.level) ?? currentLevel.level)
|
||||
: '---'
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
ref={toggleButtonRef}
|
||||
type="button"
|
||||
id="section-heading-menu-button"
|
||||
aria-haspopup="true"
|
||||
aria-controls="section-heading-menu"
|
||||
aria-label={t('toolbar_choose_section_heading_level')}
|
||||
className="ol-cm-toolbar-menu-toggle"
|
||||
onMouseDown={event => event.preventDefault()}
|
||||
onClick={() => setOverflowOpen(!overflowOpen)}
|
||||
>
|
||||
<span>{currentLabel}</span>
|
||||
<Icon type="caret-down" fw />
|
||||
</button>
|
||||
|
||||
{overflowOpen && (
|
||||
<OLOverlay
|
||||
show
|
||||
onHide={() => setOverflowOpen(false)}
|
||||
transition={false}
|
||||
container={view.dom}
|
||||
containerPadding={0}
|
||||
placement="bottom"
|
||||
rootClose
|
||||
target={toggleButtonRef.current}
|
||||
popperConfig={{
|
||||
modifiers: [
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [0, 1],
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<OLPopover
|
||||
id="popover-toolbar-section-heading"
|
||||
className="ol-cm-toolbar-menu-popover"
|
||||
>
|
||||
<div
|
||||
className="ol-cm-toolbar-menu"
|
||||
id="section-heading-menu"
|
||||
role="menu"
|
||||
aria-labelledby="section-heading-menu-button"
|
||||
>
|
||||
{levelsEntries.map(([level, label]) => (
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
key={level}
|
||||
onClick={() => {
|
||||
emitToolbarEvent(view, 'section-level-change')
|
||||
setSectionHeadingLevel(view, level)
|
||||
view.focus()
|
||||
setOverflowOpen(false)
|
||||
}}
|
||||
className={classnames(
|
||||
'ol-cm-toolbar-menu-item',
|
||||
`section-level-${level}`,
|
||||
{
|
||||
'ol-cm-toolbar-menu-item-active':
|
||||
level === currentLevel?.level,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</OLPopover>
|
||||
</OLOverlay>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { DropdownHeader } from '@/features/ui/components/bootstrap-5/dropdown-menu'
|
||||
import { ToolbarButtonMenu } from './button-menu'
|
||||
import MaterialIcon from '../../../../shared/components/material-icon'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEditorContext } from '@/shared/context/editor-context'
|
||||
import { memo, useRef, useCallback } from 'react'
|
||||
import OLListGroupItem from '@/features/ui/components/ol/ol-list-group-item'
|
||||
import sparkleWhite from '@/shared/svgs/sparkle-small-white.svg'
|
||||
import sparkle from '@/shared/svgs/ai-sparkle-text.svg'
|
||||
import { TableInserterDropdown } from './table-inserter-dropdown'
|
||||
import OLOverlay from '@/features/ui/components/ol/ol-overlay'
|
||||
import OLPopover from '@/features/ui/components/ol/ol-popover'
|
||||
import useDropdown from '../../../../shared/hooks/use-dropdown'
|
||||
import * as commands from '../../extensions/toolbar/commands'
|
||||
import { useCodeMirrorViewContext } from '../codemirror-context'
|
||||
import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics'
|
||||
|
||||
export const TableDropdown = memo(function TableDropdown() {
|
||||
const { t } = useTranslation()
|
||||
const { writefullInstance } = useEditorContext()
|
||||
const { open, onToggle, ref } = useDropdown()
|
||||
const target = useRef<any>(null)
|
||||
const view = useCodeMirrorViewContext()
|
||||
|
||||
const onSizeSelected = useCallback(
|
||||
(sizeX: number, sizeY: number) => {
|
||||
onToggle(false)
|
||||
commands.insertTable(view, sizeX, sizeY)
|
||||
emitToolbarEvent(view, 'table-generator-insert-table')
|
||||
view.focus()
|
||||
},
|
||||
[view, onToggle]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={target}>
|
||||
<ToolbarButtonMenu
|
||||
id="toolbar-table"
|
||||
label={t('toolbar_insert_table')}
|
||||
icon={<MaterialIcon type="table_chart" />}
|
||||
>
|
||||
<DropdownHeader className="ol-cm-toolbar-header mx-2">
|
||||
{t('toolbar_table_insert_table_lowercase')}
|
||||
</DropdownHeader>
|
||||
<OLListGroupItem
|
||||
aria-label={t('toolbar_generate_table')}
|
||||
onClick={event => {
|
||||
writefullInstance?.openTableGenerator()
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt="sparkle"
|
||||
className="ol-cm-toolbar-ai-sparkle-gradient"
|
||||
src={sparkle}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<img
|
||||
alt="sparkle"
|
||||
className="ol-cm-toolbar-ai-sparkle-white"
|
||||
src={sparkleWhite}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{t('generate_from_text_or_image')}</span>
|
||||
</OLListGroupItem>
|
||||
<div className="ol-cm-toolbar-dropdown-divider mx-2 my-0" />
|
||||
<OLListGroupItem
|
||||
aria-label={t('toolbar_insert_table')}
|
||||
onMouseDown={event => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onClick={() => {
|
||||
onToggle(!open)
|
||||
}}
|
||||
>
|
||||
<span>{t('select_size')}</span>
|
||||
</OLListGroupItem>
|
||||
</ToolbarButtonMenu>
|
||||
<OLOverlay
|
||||
show={open}
|
||||
target={target.current}
|
||||
placement="bottom"
|
||||
container={view.dom}
|
||||
containerPadding={0}
|
||||
transition
|
||||
rootClose
|
||||
onHide={() => onToggle(false)}
|
||||
>
|
||||
<OLPopover
|
||||
id="toolbar-table-menu"
|
||||
ref={ref}
|
||||
className="ol-cm-toolbar-button-menu-popover ol-cm-toolbar-button-menu-popover-unstyled"
|
||||
>
|
||||
<TableInserterDropdown onSizeSelected={onSizeSelected} />
|
||||
</OLPopover>
|
||||
</OLOverlay>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,128 @@
|
||||
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 { useCodeMirrorViewContext } from '../codemirror-context'
|
||||
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'
|
||||
|
||||
export const LegacyTableDropdown = memo(() => {
|
||||
const { t } = useTranslation()
|
||||
const { open, onToggle, ref } = useDropdown()
|
||||
const view = useCodeMirrorViewContext()
|
||||
const target = useRef<any>(null)
|
||||
|
||||
const onSizeSelected = useCallback(
|
||||
(sizeX: number, sizeY: number) => {
|
||||
onToggle(false)
|
||||
commands.insertTable(view, sizeX, sizeY)
|
||||
emitToolbarEvent(view, 'table-generator-insert-table')
|
||||
view.focus()
|
||||
},
|
||||
[view, onToggle]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<OLTooltip
|
||||
hidden={open}
|
||||
id="toolbar-table"
|
||||
description={<div>{t('toolbar_insert_table')}</div>}
|
||||
overlayProps={{ placement: 'bottom' }}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="ol-cm-toolbar-button btn"
|
||||
aria-label={t('toolbar_insert_table')}
|
||||
onMouseDown={event => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onClick={() => {
|
||||
onToggle(!open)
|
||||
}}
|
||||
ref={target}
|
||||
>
|
||||
<MaterialIcon type="table_chart" />
|
||||
</button>
|
||||
</OLTooltip>
|
||||
<OLOverlay
|
||||
show={open}
|
||||
target={target.current}
|
||||
placement="bottom"
|
||||
container={view.dom}
|
||||
containerPadding={0}
|
||||
transition
|
||||
rootClose
|
||||
onHide={() => onToggle(false)}
|
||||
>
|
||||
<OLPopover
|
||||
id="toolbar-table-menu"
|
||||
ref={ref}
|
||||
className="ol-cm-toolbar-button-menu-popover ol-cm-toolbar-button-menu-popover-unstyled"
|
||||
>
|
||||
<div className="ol-cm-toolbar-table-grid-popover">
|
||||
<SizeGrid sizeX={10} sizeY={10} onSizeSelected={onSizeSelected} />
|
||||
</div>
|
||||
</OLPopover>
|
||||
</OLOverlay>
|
||||
</>
|
||||
)
|
||||
})
|
||||
LegacyTableDropdown.displayName = 'TableInserterDropdown'
|
||||
|
||||
const range = (start: number, end: number) =>
|
||||
Array.from({ length: end - start + 1 }, (v, k) => k + start)
|
||||
|
||||
const SizeGrid: FC<{
|
||||
sizeX: number
|
||||
sizeY: number
|
||||
onSizeSelected: (sizeX: number, sizeY: number) => void
|
||||
}> = ({ sizeX, sizeY, onSizeSelected }) => {
|
||||
const [currentSize, setCurrentSize] = useState<{
|
||||
sizeX: number
|
||||
sizeY: number
|
||||
}>({ sizeX: 0, sizeY: 0 })
|
||||
const { t } = useTranslation()
|
||||
let label = t('toolbar_table_insert_table_lowercase')
|
||||
if (currentSize.sizeX > 0 && currentSize.sizeY > 0) {
|
||||
label = t('toolbar_table_insert_size_table', {
|
||||
size: `${currentSize.sizeY}×${currentSize.sizeX}`,
|
||||
})
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="ol-cm-toolbar-table-size-label">{label}</div>
|
||||
<table
|
||||
className="ol-cm-toolbar-table-grid"
|
||||
onMouseLeave={() => {
|
||||
setCurrentSize({ sizeX: 0, sizeY: 0 })
|
||||
}}
|
||||
>
|
||||
<tbody>
|
||||
{range(1, sizeY).map(y => (
|
||||
<tr key={y}>
|
||||
{range(1, sizeX).map(x => (
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
|
||||
<td
|
||||
className={classNames('ol-cm-toolbar-table-cell', {
|
||||
active: currentSize.sizeX >= x && currentSize.sizeY >= y,
|
||||
})}
|
||||
key={x}
|
||||
onMouseEnter={() => {
|
||||
setCurrentSize({ sizeX: x, sizeY: y })
|
||||
}}
|
||||
onMouseUp={() => onSizeSelected(x, y)}
|
||||
/>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { FC, useState } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export const TableInserterDropdown = ({
|
||||
onSizeSelected,
|
||||
}: {
|
||||
onSizeSelected: (sizeX: number, sizeY: number) => void
|
||||
}) => {
|
||||
return (
|
||||
<div className="ol-cm-toolbar-table-grid-popover">
|
||||
<SizeGrid sizeX={10} sizeY={10} onSizeSelected={onSizeSelected} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
TableInserterDropdown.displayName = 'TableInserterDropdown'
|
||||
|
||||
const range = (start: number, end: number) =>
|
||||
Array.from({ length: end - start + 1 }, (v, k) => k + start)
|
||||
|
||||
const SizeGrid: FC<{
|
||||
sizeX: number
|
||||
sizeY: number
|
||||
onSizeSelected: (sizeX: number, sizeY: number) => void
|
||||
}> = ({ sizeX, sizeY, onSizeSelected }) => {
|
||||
const [currentSize, setCurrentSize] = useState<{
|
||||
sizeX: number
|
||||
sizeY: number
|
||||
}>({ sizeX: 0, sizeY: 0 })
|
||||
const { t } = useTranslation()
|
||||
let label = t('toolbar_table_insert_table_lowercase')
|
||||
if (currentSize.sizeX > 0 && currentSize.sizeY > 0) {
|
||||
label = t('toolbar_table_insert_size_table', {
|
||||
size: `${currentSize.sizeY}×${currentSize.sizeX}`,
|
||||
})
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="ol-cm-toolbar-table-size-label">{label}</div>
|
||||
<table
|
||||
className="ol-cm-toolbar-table-grid"
|
||||
onMouseLeave={() => {
|
||||
setCurrentSize({ sizeX: 0, sizeY: 0 })
|
||||
}}
|
||||
>
|
||||
<tbody>
|
||||
{range(1, sizeY).map(y => (
|
||||
<tr key={y}>
|
||||
{range(1, sizeX).map(x => (
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
|
||||
<td
|
||||
className={classNames('ol-cm-toolbar-table-cell', {
|
||||
active: currentSize.sizeX >= x && currentSize.sizeY >= y,
|
||||
})}
|
||||
key={x}
|
||||
onMouseEnter={() => {
|
||||
setCurrentSize({ sizeX: x, sizeY: y })
|
||||
}}
|
||||
onMouseUp={() => onSizeSelected(x, y)}
|
||||
/>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { FC } from 'react'
|
||||
import * as commands from '@/features/source-editor/extensions/toolbar/commands'
|
||||
import { searchPanelOpen } from '@codemirror/search'
|
||||
import { ToolbarButton } from '@/features/source-editor/components/toolbar/toolbar-button'
|
||||
import { EditorState } from '@codemirror/state'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { isMac } from '@/shared/utils/os'
|
||||
|
||||
export const ToggleSearchButton: FC<{ state: EditorState }> = ({ state }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
id="toolbar-toggle-search"
|
||||
label={t('search_this_file')}
|
||||
command={commands.toggleSearch}
|
||||
active={searchPanelOpen(state)}
|
||||
icon="search"
|
||||
shortcut={isMac ? '⌘F' : 'Ctrl+F'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { memo, useCallback } from 'react'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { useCodeMirrorViewContext } from '../codemirror-context'
|
||||
import classnames from 'classnames'
|
||||
import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
||||
|
||||
export const ToolbarButton = memo<{
|
||||
id: string
|
||||
className?: string
|
||||
label: string
|
||||
command?: (view: EditorView) => void
|
||||
active?: boolean
|
||||
disabled?: boolean
|
||||
icon: string
|
||||
textIcon?: boolean
|
||||
hidden?: boolean
|
||||
shortcut?: string
|
||||
}>(function ToolbarButton({
|
||||
id,
|
||||
className,
|
||||
label,
|
||||
command,
|
||||
active = false,
|
||||
disabled,
|
||||
icon,
|
||||
textIcon = false,
|
||||
hidden = false,
|
||||
shortcut,
|
||||
}) {
|
||||
const view = useCodeMirrorViewContext()
|
||||
|
||||
const handleMouseDown = useCallback(event => {
|
||||
event.preventDefault()
|
||||
}, [])
|
||||
|
||||
const handleClick = useCallback(
|
||||
event => {
|
||||
emitToolbarEvent(view, id)
|
||||
if (command) {
|
||||
event.preventDefault()
|
||||
command(view)
|
||||
view.focus()
|
||||
}
|
||||
},
|
||||
[command, view, id]
|
||||
)
|
||||
|
||||
const button = (
|
||||
<button
|
||||
className={classnames('ol-cm-toolbar-button', className, {
|
||||
active,
|
||||
hidden,
|
||||
})}
|
||||
aria-label={label}
|
||||
onMouseDown={handleMouseDown}
|
||||
onClick={!disabled ? handleClick : undefined}
|
||||
aria-disabled={disabled}
|
||||
type="button"
|
||||
>
|
||||
{textIcon ? (
|
||||
icon
|
||||
) : (
|
||||
<MaterialIcon type={icon} accessibilityLabel={label} />
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
|
||||
if (!label) {
|
||||
return button
|
||||
}
|
||||
|
||||
const description = (
|
||||
<>
|
||||
<div>{label}</div>
|
||||
{shortcut && <div>{shortcut}</div>}
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<OLTooltip
|
||||
id={id}
|
||||
description={description}
|
||||
overlayProps={{ placement: 'bottom' }}
|
||||
>
|
||||
{button}
|
||||
</OLTooltip>
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,197 @@
|
||||
import { FC, memo } from 'react'
|
||||
import { EditorState } from '@codemirror/state'
|
||||
import { useEditorContext } from '../../../../shared/context/editor-context'
|
||||
import { ToolbarButton } from './toolbar-button'
|
||||
import { redo, undo } from '@codemirror/commands'
|
||||
import * as commands from '../../extensions/toolbar/commands'
|
||||
import { SectionHeadingDropdown } from './section-heading-dropdown'
|
||||
import getMeta from '../../../../utils/meta'
|
||||
import { InsertFigureDropdown } from './insert-figure-dropdown'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { MathDropdown } from './math-dropdown'
|
||||
import { TableDropdown } from './table-dropdown'
|
||||
import { LegacyTableDropdown } from './table-inserter-dropdown-legacy'
|
||||
import { withinFormattingCommand } from '@/features/source-editor/utils/tree-operations/formatting'
|
||||
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
|
||||
import { isMac } from '@/shared/utils/os'
|
||||
|
||||
export const ToolbarItems: FC<{
|
||||
state: EditorState
|
||||
overflowed?: Set<string>
|
||||
languageName?: string
|
||||
visual: boolean
|
||||
listDepth: number
|
||||
}> = memo(function ToolbarItems({
|
||||
state,
|
||||
overflowed,
|
||||
languageName,
|
||||
visual,
|
||||
listDepth,
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { toggleSymbolPalette, showSymbolPalette, writefullInstance } =
|
||||
useEditorContext()
|
||||
const isActive = withinFormattingCommand(state)
|
||||
|
||||
const symbolPaletteAvailable = getMeta('ol-symbolPaletteAvailable')
|
||||
const showGroup = (group: string) => !overflowed || overflowed.has(group)
|
||||
|
||||
const wfRebrandEnabled = isSplitTestEnabled('wf-feature-rebrand')
|
||||
|
||||
return (
|
||||
<>
|
||||
{showGroup('group-history') && (
|
||||
<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"
|
||||
shortcut={isMac ? '⌘Z' : 'Ctrl+Z'}
|
||||
/>
|
||||
<ToolbarButton
|
||||
id="toolbar-redo"
|
||||
label={t('toolbar_redo')}
|
||||
command={redo}
|
||||
icon="redo"
|
||||
shortcut={isMac ? '⇧⌘Z' : 'Ctrl+Y'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{languageName === 'latex' && (
|
||||
<>
|
||||
{showGroup('group-section') && (
|
||||
<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"
|
||||
aria-label={t('toolbar_text_style')}
|
||||
>
|
||||
<ToolbarButton
|
||||
id="toolbar-format-bold"
|
||||
label={t('toolbar_format_bold')}
|
||||
command={commands.toggleBold}
|
||||
active={isActive('\\textbf')}
|
||||
icon="format_bold"
|
||||
shortcut={isMac ? '⌘B' : 'Ctrl+B'}
|
||||
/>
|
||||
<ToolbarButton
|
||||
id="toolbar-format-italic"
|
||||
label={t('toolbar_format_italic')}
|
||||
command={commands.toggleItalic}
|
||||
active={isActive('\\textit')}
|
||||
icon="format_italic"
|
||||
shortcut={isMac ? '⌘I' : 'Ctrl+I'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{showGroup('group-math') && (
|
||||
<div
|
||||
className="ol-cm-toolbar-button-group"
|
||||
data-overflow="group-math"
|
||||
aria-label={t('toolbar_insert_math_and_symbols')}
|
||||
>
|
||||
<MathDropdown />
|
||||
{symbolPaletteAvailable && (
|
||||
<ToolbarButton
|
||||
id="toolbar-toggle-symbol-palette"
|
||||
label={t('toolbar_toggle_symbol_palette')}
|
||||
active={showSymbolPalette}
|
||||
command={toggleSymbolPalette}
|
||||
icon="Ω"
|
||||
textIcon
|
||||
className="ol-cm-toolbar-button-math"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{showGroup('group-misc') && (
|
||||
<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="add_link"
|
||||
/>
|
||||
<ToolbarButton
|
||||
id="toolbar-add-comment"
|
||||
label={t('add_comment')}
|
||||
disabled={state.selection.main.empty}
|
||||
command={commands.addComment}
|
||||
icon="add_comment"
|
||||
/>
|
||||
<ToolbarButton
|
||||
id="toolbar-ref"
|
||||
label={t('toolbar_insert_cross_reference')}
|
||||
command={commands.insertRef}
|
||||
icon="sell"
|
||||
/>
|
||||
<ToolbarButton
|
||||
id="toolbar-cite"
|
||||
label={t('toolbar_insert_citation')}
|
||||
command={commands.insertCite}
|
||||
icon="book_5"
|
||||
/>
|
||||
<InsertFigureDropdown />
|
||||
{wfRebrandEnabled && writefullInstance ? (
|
||||
<TableDropdown />
|
||||
) : (
|
||||
<LegacyTableDropdown />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{showGroup('group-list') && (
|
||||
<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="format_list_bulleted"
|
||||
/>
|
||||
<ToolbarButton
|
||||
id="toolbar-numbered-list"
|
||||
label={t('toolbar_numbered_list')}
|
||||
command={commands.toggleNumberedList}
|
||||
icon="format_list_numbered"
|
||||
/>
|
||||
<ToolbarButton
|
||||
id="toolbar-format-indent-decrease"
|
||||
label={t('toolbar_decrease_indent')}
|
||||
command={commands.indentDecrease}
|
||||
icon="format_indent_decrease"
|
||||
shortcut={visual ? (isMac ? '⌘[' : 'Ctrl+[') : undefined}
|
||||
disabled={listDepth < 2}
|
||||
/>
|
||||
<ToolbarButton
|
||||
id="toolbar-format-indent-increase"
|
||||
label={t('toolbar_increase_indent')}
|
||||
command={commands.indentIncrease}
|
||||
icon="format_indent_increase"
|
||||
shortcut={visual ? (isMac ? '⌘]' : 'Ctrl+]') : undefined}
|
||||
disabled={listDepth < 1}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user