first commit
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
useCodeMirrorStateContext,
|
||||
useCodeMirrorViewContext,
|
||||
} from '../codemirror-context'
|
||||
import {
|
||||
closeCommandTooltip,
|
||||
resolveCommandNode,
|
||||
} from '../../extensions/command-tooltip'
|
||||
import {
|
||||
LiteralArgContent,
|
||||
ShortArg,
|
||||
ShortTextArgument,
|
||||
UrlArgument,
|
||||
} from '../../lezer-latex/latex.terms.mjs'
|
||||
import { EditorState } from '@codemirror/state'
|
||||
import { openURL } from '@/features/source-editor/utils/url'
|
||||
import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
|
||||
import OLFormLabel from '@/features/ui/components/ol/ol-form-label'
|
||||
import OLFormControl from '@/features/ui/components/ol/ol-form-control'
|
||||
import OLForm from '@/features/ui/components/ol/ol-form'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
|
||||
export const HrefTooltipContent: FC = () => {
|
||||
const state = useCodeMirrorStateContext()
|
||||
const view = useCodeMirrorViewContext()
|
||||
const [url, setUrl] = useState<string>(() => readUrl(state) ?? '')
|
||||
const { t } = useTranslation()
|
||||
|
||||
const inputRef = useRef<HTMLInputElement | null>(null)
|
||||
|
||||
// Update the URL if the argument value changes while not editing
|
||||
// TODO: on input blur, update the input value with this URL or read from the syntax tree?
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
const controller = new AbortController()
|
||||
|
||||
// update the input URL when it changes in the doc
|
||||
inputRef.current.addEventListener(
|
||||
'value-update',
|
||||
event => {
|
||||
setUrl((event as CustomEvent<string>).detail)
|
||||
},
|
||||
{ signal: controller.signal }
|
||||
)
|
||||
|
||||
// focus the URL input element when the tooltip opens, if the view is focused,
|
||||
// there is content selected in the doc, and no URL has been entered
|
||||
if (view.hasFocus && !view.state.selection.main.empty) {
|
||||
const currentUrl = readUrl(view.state)
|
||||
if (!currentUrl) {
|
||||
inputRef.current.focus()
|
||||
}
|
||||
}
|
||||
|
||||
inputRef.current?.addEventListener(
|
||||
'blur',
|
||||
() => {
|
||||
const currentUrl = readUrl(view.state)
|
||||
if (currentUrl) {
|
||||
setUrl(currentUrl)
|
||||
}
|
||||
},
|
||||
{ signal: controller.signal }
|
||||
)
|
||||
|
||||
return () => controller.abort()
|
||||
}
|
||||
}, [view])
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
event => {
|
||||
event.preventDefault()
|
||||
view.dispatch(closeCommandTooltip())
|
||||
view.focus()
|
||||
},
|
||||
[view]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="ol-cm-command-tooltip-content">
|
||||
<OLForm className="ol-cm-command-tooltip-form" onSubmit={handleSubmit}>
|
||||
<OLFormGroup controlId="link-tooltip-url-input">
|
||||
<OLFormLabel>URL</OLFormLabel>
|
||||
<OLFormControl
|
||||
type="url"
|
||||
size="sm"
|
||||
htmlSize={50}
|
||||
placeholder="https://…"
|
||||
value={url}
|
||||
ref={(element: HTMLInputElement) => {
|
||||
inputRef.current = element
|
||||
}}
|
||||
autoComplete="off"
|
||||
onChange={event => {
|
||||
const url = (event.target as HTMLInputElement).value
|
||||
setUrl(url)
|
||||
const spec = replaceUrl(state, url)
|
||||
if (spec) {
|
||||
view.dispatch(spec)
|
||||
}
|
||||
}}
|
||||
disabled={state.readOnly}
|
||||
/>
|
||||
</OLFormGroup>
|
||||
</OLForm>
|
||||
|
||||
<OLButton
|
||||
variant="link"
|
||||
type="button"
|
||||
className="ol-cm-command-tooltip-link justify-content-start"
|
||||
onClick={() => {
|
||||
// TODO: unescape content
|
||||
openURL(url)
|
||||
}}
|
||||
>
|
||||
<MaterialIcon type="open_in_new" />
|
||||
|
||||
{t('open_link')}
|
||||
</OLButton>
|
||||
|
||||
{!state.readOnly && (
|
||||
<OLButton
|
||||
variant="link"
|
||||
type="button"
|
||||
className="ol-cm-command-tooltip-link justify-content-start"
|
||||
onClick={() => {
|
||||
const spec = removeLink(state)
|
||||
if (spec) {
|
||||
view.dispatch(spec)
|
||||
view.focus()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MaterialIcon type="link_off" />
|
||||
|
||||
{t('remove_link')}
|
||||
</OLButton>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const readUrl = (state: EditorState) => {
|
||||
const commandNode = resolveCommandNode(state)
|
||||
const argumentNode = commandNode
|
||||
?.getChild(UrlArgument)
|
||||
?.getChild(LiteralArgContent)
|
||||
|
||||
if (argumentNode) {
|
||||
return state.sliceDoc(argumentNode.from, argumentNode.to)
|
||||
}
|
||||
}
|
||||
|
||||
const replaceUrl = (state: EditorState, url: string) => {
|
||||
const commandNode = resolveCommandNode(state)
|
||||
const argumentNode = commandNode
|
||||
?.getChild(UrlArgument)
|
||||
?.getChild(LiteralArgContent)
|
||||
|
||||
if (argumentNode) {
|
||||
return {
|
||||
changes: {
|
||||
from: argumentNode.from,
|
||||
to: argumentNode.to,
|
||||
insert: url,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const removeLink = (state: EditorState) => {
|
||||
const commandNode = resolveCommandNode(state)
|
||||
const contentNode = commandNode
|
||||
?.getChild(ShortTextArgument)
|
||||
?.getChild(ShortArg)
|
||||
|
||||
if (commandNode && contentNode) {
|
||||
const content = state.sliceDoc(contentNode.from, contentNode.to)
|
||||
return {
|
||||
changes: {
|
||||
from: commandNode.from,
|
||||
to: commandNode.to,
|
||||
insert: content,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useIncludedFile } from '@/features/source-editor/hooks/use-included-file'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
|
||||
export const IncludeTooltipContent: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { openIncludedFile } = useIncludedFile('IncludeArgument')
|
||||
|
||||
return (
|
||||
<div className="ol-cm-command-tooltip-content">
|
||||
<OLButton
|
||||
variant="link"
|
||||
type="button"
|
||||
className="ol-cm-command-tooltip-link"
|
||||
onClick={openIncludedFile}
|
||||
>
|
||||
<MaterialIcon type="edit" />
|
||||
{t('open_file')}
|
||||
</OLButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useIncludedFile } from '@/features/source-editor/hooks/use-included-file'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
|
||||
export const InputTooltipContent: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { openIncludedFile } = useIncludedFile('InputArgument')
|
||||
|
||||
return (
|
||||
<div className="ol-cm-command-tooltip-content">
|
||||
<OLButton
|
||||
variant="link"
|
||||
type="button"
|
||||
className="ol-cm-command-tooltip-link"
|
||||
onClick={openIncludedFile}
|
||||
>
|
||||
<MaterialIcon type="edit" />
|
||||
{t('open_file')}
|
||||
</OLButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
useCodeMirrorStateContext,
|
||||
useCodeMirrorViewContext,
|
||||
} from '../codemirror-context'
|
||||
import { resolveCommandNode } from '../../extensions/command-tooltip'
|
||||
import {
|
||||
LabelArgument,
|
||||
RefArgument,
|
||||
ShortArg,
|
||||
ShortTextArgument,
|
||||
} from '../../lezer-latex/latex.terms.mjs'
|
||||
import { SyntaxNode } from '@lezer/common'
|
||||
import { syntaxTree } from '@codemirror/language'
|
||||
import {
|
||||
EditorSelection,
|
||||
EditorState,
|
||||
TransactionSpec,
|
||||
} from '@codemirror/state'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
|
||||
export const RefTooltipContent: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const view = useCodeMirrorViewContext()
|
||||
const state = useCodeMirrorStateContext()
|
||||
|
||||
return (
|
||||
<div className="ol-cm-command-tooltip-content">
|
||||
<OLButton
|
||||
variant="link"
|
||||
type="button"
|
||||
className="ol-cm-command-tooltip-link"
|
||||
onClick={() => {
|
||||
const target = readTarget(state)
|
||||
if (target) {
|
||||
const labelNode = findTargetLabel(state, target)
|
||||
// TODO: handle label not found
|
||||
if (labelNode) {
|
||||
view.dispatch(selectNode(labelNode))
|
||||
view.focus()
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MaterialIcon type="link" />
|
||||
{t('open_target')}
|
||||
</OLButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const readTarget = (state: EditorState) => {
|
||||
const commandNode = resolveCommandNode(state)
|
||||
const argumentNode = commandNode
|
||||
?.getChild(RefArgument)
|
||||
?.getChild(ShortTextArgument)
|
||||
?.getChild(ShortArg)
|
||||
|
||||
if (argumentNode) {
|
||||
return state.sliceDoc(argumentNode.from, argumentNode.to)
|
||||
}
|
||||
}
|
||||
|
||||
const findTargetLabel = (state: EditorState, target: string) => {
|
||||
let labelNode: SyntaxNode | undefined
|
||||
|
||||
syntaxTree(state).iterate({
|
||||
enter(nodeRef) {
|
||||
if (labelNode) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (nodeRef.type.is(LabelArgument)) {
|
||||
const argumentNode = nodeRef.node
|
||||
.getChild('ShortTextArgument')
|
||||
?.getChild('ShortArg')
|
||||
|
||||
if (argumentNode) {
|
||||
const label = state.sliceDoc(argumentNode.from, argumentNode.to)
|
||||
if (label === target) {
|
||||
labelNode = argumentNode
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return labelNode
|
||||
}
|
||||
|
||||
const selectNode = (node: SyntaxNode): TransactionSpec => {
|
||||
const selection = EditorSelection.range(node.from, node.to)
|
||||
|
||||
return {
|
||||
selection,
|
||||
effects: EditorView.scrollIntoView(selection, {
|
||||
y: 'center',
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCodeMirrorStateContext } from '../codemirror-context'
|
||||
import { resolveCommandNode } from '../../extensions/command-tooltip'
|
||||
import {
|
||||
LiteralArgContent,
|
||||
UrlArgument,
|
||||
} from '../../lezer-latex/latex.terms.mjs'
|
||||
import { EditorState } from '@codemirror/state'
|
||||
import { openURL } from '@/features/source-editor/utils/url'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
|
||||
export const UrlTooltipContent: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const state = useCodeMirrorStateContext()
|
||||
|
||||
return (
|
||||
<div className="ol-cm-command-tooltip-content">
|
||||
<OLButton
|
||||
variant="link"
|
||||
type="button"
|
||||
className="ol-cm-command-tooltip-link"
|
||||
onClick={() => {
|
||||
const url = readUrl(state)
|
||||
if (url) {
|
||||
openURL(url)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MaterialIcon type="open_in_new" />
|
||||
{t('open_link')}
|
||||
</OLButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const readUrl = (state: EditorState) => {
|
||||
const commandNode = resolveCommandNode(state)
|
||||
const argumentNode = commandNode
|
||||
?.getChild(UrlArgument)
|
||||
?.getChild(LiteralArgContent)
|
||||
|
||||
if (argumentNode) {
|
||||
return state.sliceDoc(argumentNode.from, argumentNode.to)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user