mirror of
https://github.com/standardnotes/app.git
synced 2026-01-16 23:01:30 +00:00
215 lines
7 KiB
TypeScript
215 lines
7 KiB
TypeScript
import { Icon } from '@/Components/Icon'
|
|
import { Menu } from '@/Components/Menu/Menu'
|
|
import { MenuItem, MenuItemType } from '@/Components/Menu/MenuItem'
|
|
import {
|
|
reloadFont,
|
|
transactionForAssociateComponentWithCurrentNote,
|
|
transactionForDisassociateComponentWithCurrentNote,
|
|
} from '@/Components/NoteView/NoteView'
|
|
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
|
import { STRING_EDIT_LOCKED_ATTEMPT } from '@/Strings'
|
|
import { WebApplication } from '@/UIModels/Application'
|
|
import {
|
|
ComponentArea,
|
|
ItemMutator,
|
|
NoteMutator,
|
|
PrefKey,
|
|
SNComponent,
|
|
SNNote,
|
|
TransactionalMutation,
|
|
} from '@standardnotes/snjs'
|
|
import { Fragment, FunctionComponent } from 'preact'
|
|
import { useCallback, useEffect, useState } from 'preact/hooks'
|
|
import { EditorMenuItem, EditorMenuGroup } from '@/Components/NotesOptions/ChangeEditorOption'
|
|
import { createEditorMenuGroups, PLAIN_EDITOR_NAME } from './createEditorMenuGroups'
|
|
|
|
type ChangeEditorMenuProps = {
|
|
application: WebApplication
|
|
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
|
|
closeMenu: () => void
|
|
isVisible: boolean
|
|
note: SNNote
|
|
}
|
|
|
|
const getGroupId = (group: EditorMenuGroup) => group.title.toLowerCase().replace(/\s/, '-')
|
|
|
|
export const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
|
application,
|
|
closeOnBlur,
|
|
closeMenu,
|
|
isVisible,
|
|
note,
|
|
}) => {
|
|
const [editors] = useState<SNComponent[]>(() =>
|
|
application.componentManager.componentsForArea(ComponentArea.Editor).sort((a, b) => {
|
|
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1
|
|
}),
|
|
)
|
|
const [groups, setGroups] = useState<EditorMenuGroup[]>([])
|
|
const [currentEditor, setCurrentEditor] = useState<SNComponent>()
|
|
|
|
useEffect(() => {
|
|
setGroups(createEditorMenuGroups(application, editors))
|
|
}, [application, editors])
|
|
|
|
useEffect(() => {
|
|
if (note) {
|
|
setCurrentEditor(application.componentManager.editorForNote(note))
|
|
}
|
|
}, [application, note])
|
|
|
|
const premiumModal = usePremiumModal()
|
|
|
|
const isSelectedEditor = useCallback(
|
|
(item: EditorMenuItem) => {
|
|
if (currentEditor) {
|
|
if (item?.component?.identifier === currentEditor.identifier) {
|
|
return true
|
|
}
|
|
} else if (item.name === PLAIN_EDITOR_NAME) {
|
|
return true
|
|
}
|
|
return false
|
|
},
|
|
[currentEditor],
|
|
)
|
|
|
|
const selectComponent = async (component: SNComponent | null, note: SNNote) => {
|
|
if (component) {
|
|
if (component.conflictOf) {
|
|
application.mutator
|
|
.changeAndSaveItem(component, (mutator) => {
|
|
mutator.conflictOf = undefined
|
|
})
|
|
.catch(console.error)
|
|
}
|
|
}
|
|
|
|
const transactions: TransactionalMutation[] = []
|
|
|
|
if (application.getAppState().getActiveNoteController()?.isTemplateNote) {
|
|
await application.getAppState().getActiveNoteController().insertTemplatedNote()
|
|
}
|
|
|
|
if (note.locked) {
|
|
application.alertService.alert(STRING_EDIT_LOCKED_ATTEMPT).catch(console.error)
|
|
return
|
|
}
|
|
|
|
if (!component) {
|
|
if (!note.prefersPlainEditor) {
|
|
transactions.push({
|
|
itemUuid: note.uuid,
|
|
mutate: (m: ItemMutator) => {
|
|
const noteMutator = m as NoteMutator
|
|
noteMutator.prefersPlainEditor = true
|
|
},
|
|
})
|
|
}
|
|
const currentEditor = application.componentManager.editorForNote(note)
|
|
if (currentEditor?.isExplicitlyEnabledForItem(note.uuid)) {
|
|
transactions.push(transactionForDisassociateComponentWithCurrentNote(currentEditor, note))
|
|
}
|
|
reloadFont(application.getPreference(PrefKey.EditorMonospaceEnabled))
|
|
} else if (component.area === ComponentArea.Editor) {
|
|
const currentEditor = application.componentManager.editorForNote(note)
|
|
if (currentEditor && component.uuid !== currentEditor.uuid) {
|
|
transactions.push(transactionForDisassociateComponentWithCurrentNote(currentEditor, note))
|
|
}
|
|
const prefersPlain = note.prefersPlainEditor
|
|
if (prefersPlain) {
|
|
transactions.push({
|
|
itemUuid: note.uuid,
|
|
mutate: (m: ItemMutator) => {
|
|
const noteMutator = m as NoteMutator
|
|
noteMutator.prefersPlainEditor = false
|
|
},
|
|
})
|
|
}
|
|
transactions.push(transactionForAssociateComponentWithCurrentNote(component, note))
|
|
}
|
|
|
|
await application.mutator.runTransactionalMutations(transactions)
|
|
/** Dirtying can happen above */
|
|
application.sync.sync().catch(console.error)
|
|
|
|
setCurrentEditor(application.componentManager.editorForNote(note))
|
|
}
|
|
|
|
const selectEditor = async (itemToBeSelected: EditorMenuItem) => {
|
|
if (!itemToBeSelected.isEntitled) {
|
|
premiumModal.activate(itemToBeSelected.name)
|
|
return
|
|
}
|
|
|
|
const areBothEditorsPlain = !currentEditor && !itemToBeSelected.component
|
|
|
|
if (areBothEditorsPlain) {
|
|
return
|
|
}
|
|
|
|
let shouldSelectEditor = true
|
|
|
|
if (itemToBeSelected.component) {
|
|
const changeRequiresAlert = application.componentManager.doesEditorChangeRequireAlert(
|
|
currentEditor,
|
|
itemToBeSelected.component,
|
|
)
|
|
|
|
if (changeRequiresAlert) {
|
|
shouldSelectEditor = await application.componentManager.showEditorChangeAlert()
|
|
}
|
|
}
|
|
|
|
if (shouldSelectEditor) {
|
|
selectComponent(itemToBeSelected.component ?? null, note).catch(console.error)
|
|
}
|
|
|
|
closeMenu()
|
|
}
|
|
|
|
return (
|
|
<Menu className="pt-0.5 pb-1" a11yLabel="Change editor menu" isOpen={isVisible}>
|
|
{groups
|
|
.filter((group) => group.items && group.items.length)
|
|
.map((group, index) => {
|
|
const groupId = getGroupId(group)
|
|
|
|
return (
|
|
<Fragment key={groupId}>
|
|
<div
|
|
className={`flex items-center px-2.5 py-2 text-xs font-semibold color-text border-0 border-y-1px border-solid border-main ${
|
|
index === 0 ? 'border-t-0 mb-2' : 'my-2'
|
|
}`}
|
|
>
|
|
{group.icon && <Icon type={group.icon} className={`mr-2 ${group.iconClassName}`} />}
|
|
<div className="font-semibold text-input">{group.title}</div>
|
|
</div>
|
|
{group.items.map((item) => {
|
|
const onClickEditorItem = () => {
|
|
selectEditor(item).catch(console.error)
|
|
}
|
|
|
|
return (
|
|
<MenuItem
|
|
type={MenuItemType.RadioButton}
|
|
onClick={onClickEditorItem}
|
|
className={
|
|
'sn-dropdown-item py-2 text-input focus:bg-info-backdrop focus:shadow-none'
|
|
}
|
|
onBlur={closeOnBlur}
|
|
checked={isSelectedEditor(item)}
|
|
>
|
|
<div className="flex flex-grow items-center justify-between">
|
|
{item.name}
|
|
{!item.isEntitled && <Icon type="premium-feature" />}
|
|
</div>
|
|
</MenuItem>
|
|
)
|
|
})}
|
|
</Fragment>
|
|
)
|
|
})}
|
|
</Menu>
|
|
)
|
|
}
|