refactor: blocks plugins (#1956)
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import { ReactNode, createContext, useContext, memo } from 'react'
|
||||
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
|
||||
const ApplicationContext = createContext<WebApplication | undefined>(undefined)
|
||||
|
||||
export const useApplication = () => {
|
||||
const value = useContext(ApplicationContext)
|
||||
|
||||
if (!value) {
|
||||
throw new Error('Component must be a child of <ApplicationProvider />')
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
type ChildrenProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
type ProviderProps = {
|
||||
application: WebApplication
|
||||
} & ChildrenProps
|
||||
|
||||
const MemoizedChildren = memo(({ children }: ChildrenProps) => <>{children}</>)
|
||||
|
||||
const ApplicationProvider = ({ application, children }: ProviderProps) => {
|
||||
return (
|
||||
<ApplicationContext.Provider value={application}>
|
||||
<MemoizedChildren children={children} />
|
||||
</ApplicationContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(ApplicationProvider)
|
||||
@@ -28,6 +28,7 @@ import ResponsivePaneProvider from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import AndroidBackHandlerProvider from '@/NativeMobileWeb/useAndroidBackHandler'
|
||||
import ConfirmDeleteAccountContainer from '@/Components/ConfirmDeleteAccountModal/ConfirmDeleteAccountModal'
|
||||
import DarkModeHandler from '../DarkModeHandler/DarkModeHandler'
|
||||
import ApplicationProvider from './ApplicationProvider'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -193,80 +194,85 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
||||
}
|
||||
|
||||
return (
|
||||
<AndroidBackHandlerProvider application={application}>
|
||||
<DarkModeHandler application={application} />
|
||||
<ResponsivePaneProvider paneController={application.getViewControllerManager().paneController}>
|
||||
<PremiumModalProvider application={application} viewControllerManager={viewControllerManager}>
|
||||
<div className={platformString + ' main-ui-view sn-component h-full'}>
|
||||
<div id="app" className="app app-column-container" ref={appColumnContainerRef}>
|
||||
<FileDragNDropProvider
|
||||
application={application}
|
||||
featuresController={viewControllerManager.featuresController}
|
||||
filesController={viewControllerManager.filesController}
|
||||
>
|
||||
<Navigation application={application} />
|
||||
<ContentListView
|
||||
<ApplicationProvider application={application}>
|
||||
<AndroidBackHandlerProvider application={application}>
|
||||
<DarkModeHandler application={application} />
|
||||
<ResponsivePaneProvider paneController={application.getViewControllerManager().paneController}>
|
||||
<PremiumModalProvider application={application} viewControllerManager={viewControllerManager}>
|
||||
<div className={platformString + ' main-ui-view sn-component h-full'}>
|
||||
<div id="app" className="app app-column-container" ref={appColumnContainerRef}>
|
||||
<FileDragNDropProvider
|
||||
application={application}
|
||||
accountMenuController={viewControllerManager.accountMenuController}
|
||||
featuresController={viewControllerManager.featuresController}
|
||||
filesController={viewControllerManager.filesController}
|
||||
itemListController={viewControllerManager.itemListController}
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
noAccountWarningController={viewControllerManager.noAccountWarningController}
|
||||
>
|
||||
<Navigation application={application} />
|
||||
<ContentListView
|
||||
application={application}
|
||||
accountMenuController={viewControllerManager.accountMenuController}
|
||||
filesController={viewControllerManager.filesController}
|
||||
itemListController={viewControllerManager.itemListController}
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
noAccountWarningController={viewControllerManager.noAccountWarningController}
|
||||
notesController={viewControllerManager.notesController}
|
||||
selectionController={viewControllerManager.selectionController}
|
||||
searchOptionsController={viewControllerManager.searchOptionsController}
|
||||
linkingController={viewControllerManager.linkingController}
|
||||
/>
|
||||
<NoteGroupView application={application} />
|
||||
</FileDragNDropProvider>
|
||||
</div>
|
||||
|
||||
<>
|
||||
<Footer application={application} applicationGroup={mainApplicationGroup} />
|
||||
<SessionsModal application={application} viewControllerManager={viewControllerManager} />
|
||||
<PreferencesViewWrapper viewControllerManager={viewControllerManager} application={application} />
|
||||
<RevisionHistoryModal
|
||||
application={application}
|
||||
historyModalController={viewControllerManager.historyModalController}
|
||||
notesController={viewControllerManager.notesController}
|
||||
selectionController={viewControllerManager.selectionController}
|
||||
searchOptionsController={viewControllerManager.searchOptionsController}
|
||||
linkingController={viewControllerManager.linkingController}
|
||||
subscriptionController={viewControllerManager.subscriptionController}
|
||||
/>
|
||||
<NoteGroupView application={application} />
|
||||
</FileDragNDropProvider>
|
||||
</>
|
||||
|
||||
{renderChallenges()}
|
||||
|
||||
<>
|
||||
<NotesContextMenu
|
||||
application={application}
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
notesController={viewControllerManager.notesController}
|
||||
linkingController={viewControllerManager.linkingController}
|
||||
historyModalController={viewControllerManager.historyModalController}
|
||||
/>
|
||||
<TagContextMenuWrapper
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
featuresController={viewControllerManager.featuresController}
|
||||
/>
|
||||
<FileContextMenuWrapper
|
||||
filesController={viewControllerManager.filesController}
|
||||
selectionController={viewControllerManager.selectionController}
|
||||
/>
|
||||
<PurchaseFlowWrapper application={application} viewControllerManager={viewControllerManager} />
|
||||
<ConfirmSignoutContainer
|
||||
applicationGroup={mainApplicationGroup}
|
||||
viewControllerManager={viewControllerManager}
|
||||
application={application}
|
||||
/>
|
||||
<ToastContainer />
|
||||
<FilePreviewModalWrapper application={application} viewControllerManager={viewControllerManager} />
|
||||
<PermissionsModalWrapper application={application} />
|
||||
<ConfirmDeleteAccountContainer
|
||||
application={application}
|
||||
viewControllerManager={viewControllerManager}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
|
||||
<>
|
||||
<Footer application={application} applicationGroup={mainApplicationGroup} />
|
||||
<SessionsModal application={application} viewControllerManager={viewControllerManager} />
|
||||
<PreferencesViewWrapper viewControllerManager={viewControllerManager} application={application} />
|
||||
<RevisionHistoryModal
|
||||
application={application}
|
||||
historyModalController={viewControllerManager.historyModalController}
|
||||
notesController={viewControllerManager.notesController}
|
||||
selectionController={viewControllerManager.selectionController}
|
||||
subscriptionController={viewControllerManager.subscriptionController}
|
||||
/>
|
||||
</>
|
||||
|
||||
{renderChallenges()}
|
||||
|
||||
<>
|
||||
<NotesContextMenu
|
||||
application={application}
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
notesController={viewControllerManager.notesController}
|
||||
linkingController={viewControllerManager.linkingController}
|
||||
historyModalController={viewControllerManager.historyModalController}
|
||||
/>
|
||||
<TagContextMenuWrapper
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
featuresController={viewControllerManager.featuresController}
|
||||
/>
|
||||
<FileContextMenuWrapper
|
||||
filesController={viewControllerManager.filesController}
|
||||
selectionController={viewControllerManager.selectionController}
|
||||
/>
|
||||
<PurchaseFlowWrapper application={application} viewControllerManager={viewControllerManager} />
|
||||
<ConfirmSignoutContainer
|
||||
applicationGroup={mainApplicationGroup}
|
||||
viewControllerManager={viewControllerManager}
|
||||
application={application}
|
||||
/>
|
||||
<ToastContainer />
|
||||
<FilePreviewModalWrapper application={application} viewControllerManager={viewControllerManager} />
|
||||
<PermissionsModalWrapper application={application} />
|
||||
<ConfirmDeleteAccountContainer application={application} viewControllerManager={viewControllerManager} />
|
||||
</>
|
||||
</div>
|
||||
</PremiumModalProvider>
|
||||
</ResponsivePaneProvider>
|
||||
</AndroidBackHandlerProvider>
|
||||
</PremiumModalProvider>
|
||||
</ResponsivePaneProvider>
|
||||
</AndroidBackHandlerProvider>
|
||||
</ApplicationProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,11 @@ import { WebApplication } from '@/Application/Application'
|
||||
import { SNNote } from '@standardnotes/snjs'
|
||||
import { FunctionComponent, useCallback, useRef } from 'react'
|
||||
import { BlockEditorController } from './BlockEditorController'
|
||||
import { BlocksEditor } from '@standardnotes/blocks-editor'
|
||||
import { BlocksEditor, BlocksEditorComposer } from '@standardnotes/blocks-editor'
|
||||
import { ItemSelectionPlugin } from './Plugins/ItemSelectionPlugin/ItemSelectionPlugin'
|
||||
import { FileNode } from './Plugins/EncryptedFilePlugin/Nodes/FileNode'
|
||||
import FilePlugin from './Plugins/EncryptedFilePlugin/FilePlugin'
|
||||
import BlockPickerMenuPlugin from './Plugins/BlockPickerPlugin/BlockPickerPlugin'
|
||||
import { ErrorBoundary } from '@/Utils/ErrorBoundary'
|
||||
|
||||
const StringEllipses = '...'
|
||||
@@ -30,11 +34,16 @@ export const BlockEditor: FunctionComponent<Props> = ({ note, application }) =>
|
||||
return (
|
||||
<div className="relative h-full w-full p-5">
|
||||
<ErrorBoundary>
|
||||
<BlocksEditor
|
||||
onChange={handleChange}
|
||||
initialValue={note.content.text}
|
||||
className="relative relative resize-none text-base focus:shadow-none focus:outline-none"
|
||||
/>
|
||||
<BlocksEditorComposer initialValue={note.text} nodes={[FileNode]}>
|
||||
<BlocksEditor
|
||||
onChange={handleChange}
|
||||
className="relative relative resize-none text-base focus:shadow-none focus:outline-none"
|
||||
>
|
||||
<ItemSelectionPlugin currentNote={note} />
|
||||
<FilePlugin />
|
||||
<BlockPickerMenuPlugin />
|
||||
</BlocksEditor>
|
||||
</BlocksEditorComposer>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)
|
||||
@@ -0,0 +1,33 @@
|
||||
import { PopoverItemClassNames, PopoverItemSelectedClassNames } from '@standardnotes/blocks-editor'
|
||||
import { BlockPickerOption } from './BlockPickerOption'
|
||||
|
||||
export function BlockPickerMenuItem({
|
||||
index,
|
||||
isSelected,
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
option,
|
||||
}: {
|
||||
index: number
|
||||
isSelected: boolean
|
||||
onClick: () => void
|
||||
onMouseEnter: () => void
|
||||
option: BlockPickerOption
|
||||
}) {
|
||||
return (
|
||||
<li
|
||||
key={option.key}
|
||||
tabIndex={-1}
|
||||
className={`${PopoverItemClassNames} ${isSelected ? PopoverItemSelectedClassNames : ''}`}
|
||||
ref={option.setRefElement}
|
||||
role="option"
|
||||
aria-selected={isSelected}
|
||||
id={'typeahead-item-' + index}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onClick={onClick}
|
||||
>
|
||||
<i className={`icon ${option.iconName} mr-[8px] flex h-5 w-5 bg-contain fill-current text-center`} />
|
||||
<div className="">{option.title}</div>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { TypeaheadOption } from '@lexical/react/LexicalTypeaheadMenuPlugin'
|
||||
|
||||
export class BlockPickerOption extends TypeaheadOption {
|
||||
// What shows up in the editor
|
||||
title: string
|
||||
// Icon for display
|
||||
iconName?: string
|
||||
// For extra searching.
|
||||
keywords: Array<string>
|
||||
// TBD
|
||||
keyboardShortcut?: string
|
||||
// What happens when you select this option?
|
||||
onSelect: (queryString: string) => void
|
||||
|
||||
constructor(
|
||||
title: string,
|
||||
options: {
|
||||
iconName?: string
|
||||
keywords?: Array<string>
|
||||
keyboardShortcut?: string
|
||||
onSelect: (queryString: string) => void
|
||||
},
|
||||
) {
|
||||
super(title)
|
||||
this.title = title
|
||||
this.keywords = options.keywords || []
|
||||
this.iconName = options.iconName
|
||||
this.keyboardShortcut = options.keyboardShortcut
|
||||
this.onSelect = options.onSelect.bind(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { LexicalTypeaheadMenuPlugin, useBasicTypeaheadTriggerMatch } from '@lexical/react/LexicalTypeaheadMenuPlugin'
|
||||
import { TextNode } from 'lexical'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { PopoverClassNames } from '@standardnotes/blocks-editor'
|
||||
import useModal from '@standardnotes/blocks-editor/src/Lexical/Hooks/useModal'
|
||||
import { InsertTableDialog } from '@standardnotes/blocks-editor/src/Lexical/Plugins/TablePlugin'
|
||||
import { BlockPickerOption } from './BlockPickerOption'
|
||||
import { BlockPickerMenuItem } from './BlockPickerMenuItem'
|
||||
import { GetNumberedListBlock } from './Blocks/NumberedList'
|
||||
import { GetBulletedListBlock } from './Blocks/BulletedList'
|
||||
import { GetChecklistBlock } from './Blocks/Checklist'
|
||||
import { GetDividerBlock } from './Blocks/Divider'
|
||||
import { GetCollapsibleBlock } from './Blocks/Collapsible'
|
||||
import { GetParagraphBlock } from './Blocks/Paragraph'
|
||||
import { GetHeadingsBlocks } from './Blocks/Headings'
|
||||
import { GetQuoteBlock } from './Blocks/Quote'
|
||||
import { GetAlignmentBlocks } from './Blocks/Alignment'
|
||||
import { GetCodeBlock } from './Blocks/Code'
|
||||
import { GetEmbedsBlocks } from './Blocks/Embeds'
|
||||
import { GetDynamicTableBlocks, GetTableBlock } from './Blocks/Table'
|
||||
import Popover from '@/Components/Popover/Popover'
|
||||
|
||||
export default function BlockPickerMenuPlugin(): JSX.Element {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const [modal, showModal] = useModal()
|
||||
const [queryString, setQueryString] = useState<string | null>(null)
|
||||
|
||||
const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', {
|
||||
minLength: 0,
|
||||
})
|
||||
|
||||
const [popoverOpen, setPopoverOpen] = useState(true)
|
||||
|
||||
const options = useMemo(() => {
|
||||
const baseOptions = [
|
||||
GetParagraphBlock(editor),
|
||||
...GetHeadingsBlocks(editor),
|
||||
GetTableBlock(() =>
|
||||
showModal('Insert Table', (onClose) => <InsertTableDialog activeEditor={editor} onClose={onClose} />),
|
||||
),
|
||||
GetNumberedListBlock(editor),
|
||||
GetBulletedListBlock(editor),
|
||||
GetChecklistBlock(editor),
|
||||
GetQuoteBlock(editor),
|
||||
GetCodeBlock(editor),
|
||||
GetDividerBlock(editor),
|
||||
...GetAlignmentBlocks(editor),
|
||||
GetCollapsibleBlock(editor),
|
||||
...GetEmbedsBlocks(editor),
|
||||
]
|
||||
|
||||
const dynamicOptions = GetDynamicTableBlocks(editor, queryString || '')
|
||||
|
||||
return queryString
|
||||
? [
|
||||
...dynamicOptions,
|
||||
...baseOptions.filter((option) => {
|
||||
return new RegExp(queryString, 'gi').exec(option.title) || option.keywords != null
|
||||
? option.keywords.some((keyword) => new RegExp(queryString, 'gi').exec(keyword))
|
||||
: false
|
||||
}),
|
||||
]
|
||||
: baseOptions
|
||||
}, [editor, queryString, showModal])
|
||||
|
||||
const onSelectOption = useCallback(
|
||||
(
|
||||
selectedOption: BlockPickerOption,
|
||||
nodeToRemove: TextNode | null,
|
||||
closeMenu: () => void,
|
||||
matchingString: string,
|
||||
) => {
|
||||
editor.update(() => {
|
||||
if (nodeToRemove) {
|
||||
nodeToRemove.remove()
|
||||
}
|
||||
selectedOption.onSelect(matchingString)
|
||||
setPopoverOpen(false)
|
||||
closeMenu()
|
||||
})
|
||||
},
|
||||
[editor],
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{modal}
|
||||
<LexicalTypeaheadMenuPlugin<BlockPickerOption>
|
||||
onQueryChange={setQueryString}
|
||||
onSelectOption={onSelectOption}
|
||||
triggerFn={checkForTriggerMatch}
|
||||
options={options}
|
||||
onClose={() => {
|
||||
setPopoverOpen(false)
|
||||
}}
|
||||
onOpen={() => {
|
||||
setPopoverOpen(true)
|
||||
}}
|
||||
menuRenderFn={(anchorElementRef, { selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }) => {
|
||||
if (!anchorElementRef.current || !options.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
align="start"
|
||||
anchorPoint={{
|
||||
x: anchorElementRef.current.offsetLeft,
|
||||
y: anchorElementRef.current.offsetTop + anchorElementRef.current.offsetHeight,
|
||||
}}
|
||||
className={'min-h-80 h-80'}
|
||||
open={popoverOpen}
|
||||
togglePopover={() => {
|
||||
setPopoverOpen((prevValue) => !prevValue)
|
||||
}}
|
||||
>
|
||||
<div className={PopoverClassNames}>
|
||||
<ul>
|
||||
{options.map((option, i: number) => (
|
||||
<BlockPickerMenuItem
|
||||
index={i}
|
||||
isSelected={selectedIndex === i}
|
||||
onClick={() => {
|
||||
setHighlightedIndex(i)
|
||||
selectOptionAndCleanUp(option)
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setHighlightedIndex(i)
|
||||
}}
|
||||
key={option.key}
|
||||
option={option}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</Popover>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { BlockPickerOption } from '../BlockPickerOption'
|
||||
import { FORMAT_ELEMENT_COMMAND, LexicalEditor, ElementFormatType } from 'lexical'
|
||||
|
||||
export function GetAlignmentBlocks(editor: LexicalEditor) {
|
||||
return ['left', 'center', 'right', 'justify'].map(
|
||||
(alignment) =>
|
||||
new BlockPickerOption(`Align ${alignment}`, {
|
||||
iconName: `${alignment}-align`,
|
||||
keywords: ['align', 'justify', alignment],
|
||||
onSelect: () => editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, alignment as ElementFormatType),
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { BlockPickerOption } from '../BlockPickerOption'
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { INSERT_UNORDERED_LIST_COMMAND } from '@lexical/list'
|
||||
|
||||
export function GetBulletedListBlock(editor: LexicalEditor) {
|
||||
return new BlockPickerOption('Bulleted List', {
|
||||
iconName: 'bullet',
|
||||
keywords: ['bulleted list', 'unordered list', 'ul'],
|
||||
onSelect: () => editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { BlockPickerOption } from '../BlockPickerOption'
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { INSERT_CHECK_LIST_COMMAND } from '@lexical/list'
|
||||
|
||||
export function GetChecklistBlock(editor: LexicalEditor) {
|
||||
return new BlockPickerOption('Check List', {
|
||||
iconName: 'check',
|
||||
keywords: ['check list', 'todo list'],
|
||||
onSelect: () => editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { BlockPickerOption } from '../BlockPickerOption'
|
||||
import { $wrapNodes } from '@lexical/selection'
|
||||
import { $getSelection, $isRangeSelection, LexicalEditor } from 'lexical'
|
||||
import { $createCodeNode } from '@lexical/code'
|
||||
|
||||
export function GetCodeBlock(editor: LexicalEditor) {
|
||||
return new BlockPickerOption('Code', {
|
||||
iconName: 'code',
|
||||
keywords: ['javascript', 'python', 'js', 'codeblock'],
|
||||
onSelect: () =>
|
||||
editor.update(() => {
|
||||
const selection = $getSelection()
|
||||
if ($isRangeSelection(selection)) {
|
||||
if (selection.isCollapsed()) {
|
||||
$wrapNodes(selection, () => $createCodeNode())
|
||||
} else {
|
||||
const textContent = selection.getTextContent()
|
||||
const codeNode = $createCodeNode()
|
||||
selection.insertNodes([codeNode])
|
||||
selection.insertRawText(textContent)
|
||||
}
|
||||
}
|
||||
}),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { BlockPickerOption } from '../BlockPickerOption'
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { INSERT_COLLAPSIBLE_COMMAND } from '@standardnotes/blocks-editor/src/Lexical/Plugins/CollapsiblePlugin'
|
||||
|
||||
export function GetCollapsibleBlock(editor: LexicalEditor) {
|
||||
return new BlockPickerOption('Collapsible', {
|
||||
iconName: 'caret-right',
|
||||
keywords: ['collapse', 'collapsible', 'toggle'],
|
||||
onSelect: () => editor.dispatchCommand(INSERT_COLLAPSIBLE_COMMAND, undefined),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { BlockPickerOption } from '../BlockPickerOption'
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { INSERT_HORIZONTAL_RULE_COMMAND } from '@lexical/react/LexicalHorizontalRuleNode'
|
||||
|
||||
export function GetDividerBlock(editor: LexicalEditor) {
|
||||
return new BlockPickerOption('Divider', {
|
||||
iconName: 'horizontal-rule',
|
||||
keywords: ['horizontal rule', 'divider', 'hr'],
|
||||
onSelect: () => editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { BlockPickerOption } from '../BlockPickerOption'
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { INSERT_EMBED_COMMAND } from '@lexical/react/LexicalAutoEmbedPlugin'
|
||||
import { EmbedConfigs } from '@standardnotes/blocks-editor/src/Lexical/Plugins/AutoEmbedPlugin'
|
||||
|
||||
export function GetEmbedsBlocks(editor: LexicalEditor) {
|
||||
return EmbedConfigs.map(
|
||||
(embedConfig) =>
|
||||
new BlockPickerOption(`Embed ${embedConfig.contentName}`, {
|
||||
iconName: embedConfig.iconName,
|
||||
keywords: [...embedConfig.keywords, 'embed'],
|
||||
onSelect: () => editor.dispatchCommand(INSERT_EMBED_COMMAND, embedConfig.type),
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { BlockPickerOption } from '../BlockPickerOption'
|
||||
import { $wrapNodes } from '@lexical/selection'
|
||||
import { $getSelection, $isRangeSelection, LexicalEditor } from 'lexical'
|
||||
import { $createHeadingNode, HeadingTagType } from '@lexical/rich-text'
|
||||
|
||||
export function GetHeadingsBlocks(editor: LexicalEditor) {
|
||||
return Array.from({ length: 3 }, (_, i) => i + 1).map(
|
||||
(n) =>
|
||||
new BlockPickerOption(`Heading ${n}`, {
|
||||
iconName: `icon h${n}`,
|
||||
keywords: ['heading', 'header', `h${n}`],
|
||||
onSelect: () =>
|
||||
editor.update(() => {
|
||||
const selection = $getSelection()
|
||||
if ($isRangeSelection(selection)) {
|
||||
$wrapNodes(selection, () => $createHeadingNode(`h${n}` as HeadingTagType))
|
||||
}
|
||||
}),
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { BlockPickerOption } from '../BlockPickerOption'
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { INSERT_ORDERED_LIST_COMMAND } from '@lexical/list'
|
||||
|
||||
export function GetNumberedListBlock(editor: LexicalEditor) {
|
||||
return new BlockPickerOption('Numbered List', {
|
||||
iconName: 'number',
|
||||
keywords: ['numbered list', 'ordered list', 'ol'],
|
||||
onSelect: () => editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { BlockPickerOption } from '../BlockPickerOption'
|
||||
import { $wrapNodes } from '@lexical/selection'
|
||||
import { $createParagraphNode, $getSelection, $isRangeSelection, LexicalEditor } from 'lexical'
|
||||
|
||||
export function GetParagraphBlock(editor: LexicalEditor) {
|
||||
return new BlockPickerOption('Paragraph', {
|
||||
iconName: 'paragraph',
|
||||
keywords: ['normal', 'paragraph', 'p', 'text'],
|
||||
onSelect: () =>
|
||||
editor.update(() => {
|
||||
const selection = $getSelection()
|
||||
if ($isRangeSelection(selection)) {
|
||||
$wrapNodes(selection, () => $createParagraphNode())
|
||||
}
|
||||
}),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { BlockPickerOption } from '../BlockPickerOption'
|
||||
import { $wrapNodes } from '@lexical/selection'
|
||||
import { $getSelection, $isRangeSelection, LexicalEditor } from 'lexical'
|
||||
import { $createQuoteNode } from '@lexical/rich-text'
|
||||
|
||||
export function GetQuoteBlock(editor: LexicalEditor) {
|
||||
return new BlockPickerOption('Quote', {
|
||||
iconName: 'quote',
|
||||
keywords: ['block quote'],
|
||||
onSelect: () =>
|
||||
editor.update(() => {
|
||||
const selection = $getSelection()
|
||||
if ($isRangeSelection(selection)) {
|
||||
$wrapNodes(selection, () => $createQuoteNode())
|
||||
}
|
||||
}),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { BlockPickerOption } from '../BlockPickerOption'
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { INSERT_TABLE_COMMAND } from '@lexical/table'
|
||||
|
||||
export function GetTableBlock(onSelect: () => void) {
|
||||
return new BlockPickerOption('Table', {
|
||||
iconName: 'table',
|
||||
keywords: ['table', 'grid', 'spreadsheet', 'rows', 'columns'],
|
||||
onSelect,
|
||||
})
|
||||
}
|
||||
|
||||
export function GetDynamicTableBlocks(editor: LexicalEditor, queryString: string) {
|
||||
const options: Array<BlockPickerOption> = []
|
||||
|
||||
if (queryString == null) {
|
||||
return options
|
||||
}
|
||||
|
||||
const fullTableRegex = new RegExp(/^([1-9]|10)x([1-9]|10)$/)
|
||||
const partialTableRegex = new RegExp(/^([1-9]|10)x?$/)
|
||||
|
||||
const fullTableMatch = fullTableRegex.exec(queryString)
|
||||
const partialTableMatch = partialTableRegex.exec(queryString)
|
||||
|
||||
if (fullTableMatch) {
|
||||
const [rows, columns] = fullTableMatch[0].split('x').map((n: string) => parseInt(n, 10))
|
||||
|
||||
options.push(
|
||||
new BlockPickerOption(`${rows}x${columns} Table`, {
|
||||
iconName: 'table',
|
||||
keywords: ['table'],
|
||||
onSelect: () => editor.dispatchCommand(INSERT_TABLE_COMMAND, { columns: String(columns), rows: String(rows) }),
|
||||
}),
|
||||
)
|
||||
} else if (partialTableMatch) {
|
||||
const rows = parseInt(partialTableMatch[0], 10)
|
||||
|
||||
options.push(
|
||||
...Array.from({ length: 5 }, (_, i) => i + 1).map(
|
||||
(columns) =>
|
||||
new BlockPickerOption(`${rows}x${columns} Table`, {
|
||||
iconName: 'table',
|
||||
keywords: ['table'],
|
||||
onSelect: () =>
|
||||
editor.dispatchCommand(INSERT_TABLE_COMMAND, { columns: String(columns), rows: String(rows) }),
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { $insertNodeToNearestRoot } from '@lexical/utils'
|
||||
import { COMMAND_PRIORITY_EDITOR } from 'lexical'
|
||||
import { useEffect } from 'react'
|
||||
import { FileNode } from './Nodes/FileNode'
|
||||
import { $createFileNode } from './Nodes/FileUtils'
|
||||
import { INSERT_FILE_COMMAND } from '@standardnotes/blocks-editor'
|
||||
|
||||
export default function FilePlugin(): JSX.Element | null {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([FileNode])) {
|
||||
throw new Error('FilePlugin: FileNode not registered on editor')
|
||||
}
|
||||
|
||||
return editor.registerCommand<string>(
|
||||
INSERT_FILE_COMMAND,
|
||||
(payload) => {
|
||||
const fileNode = $createFileNode(payload)
|
||||
$insertNodeToNearestRoot(fileNode)
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
)
|
||||
}, [editor])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { BlockWithAlignableContents } from '@lexical/react/LexicalBlockWithAlignableContents'
|
||||
import { useMemo } from 'react'
|
||||
import { ElementFormatType, NodeKey } from 'lexical'
|
||||
import { useApplication } from '@/Components/ApplicationView/ApplicationProvider'
|
||||
import FilePreview from '@/Components/FilePreview/FilePreview'
|
||||
import { FileItem } from '@standardnotes/snjs'
|
||||
|
||||
export type FileComponentProps = Readonly<{
|
||||
className: Readonly<{
|
||||
base: string
|
||||
focus: string
|
||||
}>
|
||||
format: ElementFormatType | null
|
||||
nodeKey: NodeKey
|
||||
fileUuid: string
|
||||
}>
|
||||
|
||||
export function FileComponent({ className, format, nodeKey, fileUuid }: FileComponentProps) {
|
||||
const application = useApplication()
|
||||
const file = useMemo(() => application.items.findItem<FileItem>(fileUuid), [application, fileUuid])
|
||||
|
||||
if (!file) {
|
||||
return <div>Unable to find file {fileUuid}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<BlockWithAlignableContents className={className} format={format} nodeKey={nodeKey}>
|
||||
<FilePreview file={file} application={application} />
|
||||
</BlockWithAlignableContents>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { DOMConversionMap, DOMExportOutput, EditorConfig, ElementFormatType, LexicalEditor, NodeKey } from 'lexical'
|
||||
import { DecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
|
||||
import { $createFileNode, convertToFileElement } from './FileUtils'
|
||||
import { FileComponent } from './FileComponent'
|
||||
import { SerializedFileNode } from './SerializedFileNode'
|
||||
|
||||
export class FileNode extends DecoratorBlockNode {
|
||||
__id: string
|
||||
|
||||
static getType(): string {
|
||||
return 'snfile'
|
||||
}
|
||||
|
||||
static clone(node: FileNode): FileNode {
|
||||
return new FileNode(node.__id, node.__format, node.__key)
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedFileNode): FileNode {
|
||||
const node = $createFileNode(serializedNode.fileUuid)
|
||||
node.setFormat(serializedNode.format)
|
||||
return node
|
||||
}
|
||||
|
||||
exportJSON(): SerializedFileNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
fileUuid: this.getId(),
|
||||
version: 1,
|
||||
type: 'snfile',
|
||||
}
|
||||
}
|
||||
|
||||
static importDOM(): DOMConversionMap<HTMLDivElement> | null {
|
||||
return {
|
||||
div: (domNode: HTMLDivElement) => {
|
||||
if (!domNode.hasAttribute('data-lexical-file-uuid')) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
conversion: convertToFileElement,
|
||||
priority: 2,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
exportDOM(): DOMExportOutput {
|
||||
const element = document.createElement('div')
|
||||
element.setAttribute('data-lexical-file-uuid', this.__id)
|
||||
const text = document.createTextNode(this.getTextContent())
|
||||
element.append(text)
|
||||
return { element }
|
||||
}
|
||||
|
||||
constructor(id: string, format?: ElementFormatType, key?: NodeKey) {
|
||||
super(format, key)
|
||||
this.__id = id
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return this.__id
|
||||
}
|
||||
|
||||
getTextContent(_includeInert?: boolean | undefined, _includeDirectionless?: false | undefined): string {
|
||||
return `[File: ${this.__id}]`
|
||||
}
|
||||
|
||||
decorate(_editor: LexicalEditor, config: EditorConfig): JSX.Element {
|
||||
const embedBlockTheme = config.theme.embedBlock || {}
|
||||
const className = {
|
||||
base: embedBlockTheme.base || '',
|
||||
focus: embedBlockTheme.focus || '',
|
||||
}
|
||||
|
||||
return <FileComponent className={className} format={this.__format} nodeKey={this.getKey()} fileUuid={this.__id} />
|
||||
}
|
||||
|
||||
isInline(): false {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { DOMConversionOutput, LexicalNode } from 'lexical'
|
||||
|
||||
import { FileNode } from './FileNode'
|
||||
|
||||
export function convertToFileElement(domNode: HTMLDivElement): DOMConversionOutput | null {
|
||||
const fileUuid = domNode.getAttribute('data-lexical-file-uuid')
|
||||
if (fileUuid) {
|
||||
const node = $createFileNode(fileUuid)
|
||||
return { node }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function $createFileNode(fileUuid: string): FileNode {
|
||||
return new FileNode(fileUuid)
|
||||
}
|
||||
|
||||
export function $isFileNode(node: FileNode | LexicalNode | null | undefined): node is FileNode {
|
||||
return node instanceof FileNode
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Spread } from 'lexical'
|
||||
import { SerializedDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
|
||||
|
||||
export type SerializedFileNode = Spread<
|
||||
{
|
||||
fileUuid: string
|
||||
version: 1
|
||||
type: 'snfile'
|
||||
},
|
||||
SerializedDecoratorBlockNode
|
||||
>
|
||||
@@ -0,0 +1,18 @@
|
||||
import { FileItem } from '@standardnotes/snjs'
|
||||
import { TypeaheadOption } from '@lexical/react/LexicalTypeaheadMenuPlugin'
|
||||
|
||||
export class ItemOption extends TypeaheadOption {
|
||||
icon?: JSX.Element
|
||||
|
||||
constructor(
|
||||
public item: FileItem,
|
||||
public options: {
|
||||
keywords?: Array<string>
|
||||
keyboardShortcut?: string
|
||||
onSelect: (queryString: string) => void
|
||||
},
|
||||
) {
|
||||
super(item.title)
|
||||
this.key = item.uuid
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import LinkedItemMeta from '@/Components/LinkedItems/LinkedItemMeta'
|
||||
import { PopoverItemClassNames, PopoverItemSelectedClassNames } from '@standardnotes/blocks-editor'
|
||||
import { ItemOption } from './ItemOption'
|
||||
|
||||
type Props = {
|
||||
index: number
|
||||
isSelected: boolean
|
||||
onClick: () => void
|
||||
onMouseEnter: () => void
|
||||
option: ItemOption
|
||||
searchQuery: string
|
||||
}
|
||||
|
||||
export function ItemSelectionItemComponent({ index, isSelected, onClick, onMouseEnter, option, searchQuery }: Props) {
|
||||
return (
|
||||
<li
|
||||
key={option.key}
|
||||
tabIndex={-1}
|
||||
className={`${PopoverItemClassNames} ${isSelected ? PopoverItemSelectedClassNames : ''}`}
|
||||
ref={option.setRefElement}
|
||||
role="option"
|
||||
aria-selected={isSelected}
|
||||
id={'typeahead-item-' + index}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onClick={onClick}
|
||||
>
|
||||
<LinkedItemMeta item={option.item} searchQuery={searchQuery} />
|
||||
</li>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { LexicalTypeaheadMenuPlugin, useBasicTypeaheadTriggerMatch } from '@lexical/react/LexicalTypeaheadMenuPlugin'
|
||||
import { INSERT_FILE_COMMAND, PopoverClassNames } from '@standardnotes/blocks-editor'
|
||||
import { TextNode } from 'lexical'
|
||||
import { FunctionComponent, useCallback, useMemo, useState } from 'react'
|
||||
import { ItemSelectionItemComponent } from './ItemSelectionItemComponent'
|
||||
import { ItemOption } from './ItemOption'
|
||||
import { useApplication } from '@/Components/ApplicationView/ApplicationProvider'
|
||||
import { ContentType, FileItem, SNNote } from '@standardnotes/snjs'
|
||||
import { getLinkingSearchResults } from '@/Utils/Items/Search/getSearchResults'
|
||||
import Popover from '@/Components/Popover/Popover'
|
||||
|
||||
type Props = {
|
||||
currentNote: SNNote
|
||||
}
|
||||
|
||||
export const ItemSelectionPlugin: FunctionComponent<Props> = ({ currentNote }) => {
|
||||
const application = useApplication()
|
||||
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
const [queryString, setQueryString] = useState<string | null>('')
|
||||
|
||||
const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('@', {
|
||||
minLength: 0,
|
||||
})
|
||||
|
||||
const [popoverOpen, setPopoverOpen] = useState(true)
|
||||
|
||||
const onSelectOption = useCallback(
|
||||
(selectedOption: ItemOption, nodeToRemove: TextNode | null, closeMenu: () => void, matchingString: string) => {
|
||||
editor.update(() => {
|
||||
if (nodeToRemove) {
|
||||
nodeToRemove.remove()
|
||||
}
|
||||
selectedOption.options.onSelect(matchingString)
|
||||
setPopoverOpen(false)
|
||||
closeMenu()
|
||||
})
|
||||
},
|
||||
[editor],
|
||||
)
|
||||
|
||||
const options = useMemo(() => {
|
||||
const results = getLinkingSearchResults(queryString || '', application, currentNote, {
|
||||
contentType: ContentType.File,
|
||||
returnEmptyIfQueryEmpty: false,
|
||||
})
|
||||
const files = [...results.linkedItems, ...results.unlinkedItems] as FileItem[]
|
||||
return files.map((file) => {
|
||||
return new ItemOption(file, {
|
||||
onSelect: (_queryString: string) => {
|
||||
editor.dispatchCommand(INSERT_FILE_COMMAND, file.uuid)
|
||||
},
|
||||
})
|
||||
})
|
||||
}, [application, editor, currentNote, queryString])
|
||||
|
||||
return (
|
||||
<LexicalTypeaheadMenuPlugin<ItemOption>
|
||||
onQueryChange={setQueryString}
|
||||
onSelectOption={onSelectOption}
|
||||
triggerFn={checkForTriggerMatch}
|
||||
options={options}
|
||||
onClose={() => {
|
||||
setPopoverOpen(false)
|
||||
}}
|
||||
onOpen={() => {
|
||||
setPopoverOpen(true)
|
||||
}}
|
||||
menuRenderFn={(anchorElementRef, { selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }) => {
|
||||
if (!anchorElementRef.current || !options.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
align="start"
|
||||
anchorPoint={{
|
||||
x: anchorElementRef.current.offsetLeft,
|
||||
y: anchorElementRef.current.offsetTop + anchorElementRef.current.offsetHeight,
|
||||
}}
|
||||
className={'min-h-80 h-80'}
|
||||
open={popoverOpen}
|
||||
togglePopover={() => {
|
||||
setPopoverOpen((prevValue) => !prevValue)
|
||||
}}
|
||||
>
|
||||
<div className={PopoverClassNames}>
|
||||
<ul>
|
||||
{options.map((option, i: number) => (
|
||||
<ItemSelectionItemComponent
|
||||
searchQuery={queryString || ''}
|
||||
index={i}
|
||||
isSelected={selectedIndex === i}
|
||||
onClick={() => {
|
||||
setHighlightedIndex(i)
|
||||
selectOptionAndCleanUp(option)
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setHighlightedIndex(i)
|
||||
}}
|
||||
key={option.key}
|
||||
option={option}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</Popover>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { FunctionComponent, useCallback, useRef, useState } from 'react'
|
||||
import ChangeEditorMenu from './ChangeEditorMenu'
|
||||
import Popover from '../Popover/Popover'
|
||||
import RoundIconButton from '../Button/RoundIconButton'
|
||||
import { getIconAndTintForNoteType } from '@/Utils/Items/Icons/getIconAndTintForNoteType'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -24,9 +25,7 @@ const ChangeEditorButton: FunctionComponent<Props> = ({
|
||||
const [selectedEditor, setSelectedEditor] = useState(() => {
|
||||
return note ? application.componentManager.editorForNote(note) : undefined
|
||||
})
|
||||
const [selectedEditorIcon, selectedEditorIconTint] = application.iconsController.getIconAndTintForNoteType(
|
||||
selectedEditor?.package_info.note_type,
|
||||
)
|
||||
const [selectedEditorIcon, selectedEditorIconTint] = getIconAndTintForNoteType(selectedEditor?.package_info.note_type)
|
||||
|
||||
const toggleMenu = useCallback(async () => {
|
||||
const willMenuOpen = !isOpen
|
||||
|
||||
@@ -11,9 +11,9 @@ import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||
import { useContextMenuEvent } from '@/Hooks/useContextMenuEvent'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import { formatSizeToReadableString } from '@standardnotes/filepicker'
|
||||
import { getIconForFileType } from '@/Utils/Items/Icons/getIconForFileType'
|
||||
|
||||
const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
|
||||
application,
|
||||
filesController,
|
||||
hideDate,
|
||||
hideIcon,
|
||||
@@ -66,10 +66,7 @@ const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
|
||||
}, [item, onSelect, toggleAppPane])
|
||||
|
||||
const IconComponent = () =>
|
||||
getFileIconComponent(
|
||||
application.iconsController.getIconForFileType((item as FileItem).mimeType),
|
||||
'w-10 h-10 flex-shrink-0',
|
||||
)
|
||||
getFileIconComponent(getIconForFileType((item as FileItem).mimeType), 'w-10 h-10 flex-shrink-0')
|
||||
|
||||
useContextMenuEvent(listItemRef, openContextMenu)
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import ListItemNotePreviewText from './ListItemNotePreviewText'
|
||||
import { ListItemTitle } from './ListItemTitle'
|
||||
import { log, LoggingDomain } from '@/Logging'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import { getIconAndTintForNoteType } from '@/Utils/Items/Icons/getIconAndTintForNoteType'
|
||||
|
||||
const NoteListItem: FunctionComponent<DisplayableListItemProps<SNNote>> = ({
|
||||
application,
|
||||
@@ -37,7 +38,7 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps<SNNote>> = ({
|
||||
|
||||
const editorForNote = application.componentManager.editorForNote(item as SNNote)
|
||||
const editorName = editorForNote?.name ?? PLAIN_EDITOR_NAME
|
||||
const [icon, tint] = application.iconsController.getIconAndTintForNoteType(editorForNote?.package_info.note_type)
|
||||
const [icon, tint] = getIconAndTintForNoteType(editorForNote?.package_info.note_type)
|
||||
const hasFiles = application.items.itemsReferencingItem(item).filter(isFile).length > 0
|
||||
|
||||
const openNoteContextMenu = (posX: number, posY: number) => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { KeyboardKey } from '@standardnotes/ui-services'
|
||||
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import FilePreview from './FilePreview'
|
||||
import { getIconForFileType } from '@/Utils/Items/Icons/getIconForFileType'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -67,12 +68,8 @@ const FilePreviewModal: FunctionComponent<Props> = observer(({ application, view
|
||||
)
|
||||
|
||||
const IconComponent = useMemo(
|
||||
() =>
|
||||
getFileIconComponent(
|
||||
application.iconsController.getIconForFileType(currentFile.mimeType),
|
||||
'w-6 h-6 flex-shrink-0',
|
||||
),
|
||||
[application.iconsController, currentFile.mimeType],
|
||||
() => getFileIconComponent(getIconForFileType(currentFile.mimeType), 'w-6 h-6 flex-shrink-0'),
|
||||
[currentFile.mimeType],
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@@ -18,6 +18,8 @@ import { LinkingController } from '@/Controllers/LinkingController'
|
||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import Menu from '../Menu/Menu'
|
||||
import { getLinkingSearchResults } from '@/Utils/Items/Search/getSearchResults'
|
||||
import { useApplication } from '../ApplicationView/ApplicationProvider'
|
||||
|
||||
type Props = {
|
||||
linkingController: LinkingController
|
||||
@@ -27,18 +29,11 @@ type Props = {
|
||||
}
|
||||
|
||||
const ItemLinkAutocompleteInput = ({ linkingController, focusPreviousItem, focusedId, setFocusedId }: Props) => {
|
||||
const {
|
||||
tags,
|
||||
getTitleForLinkedTag,
|
||||
getLinkedItemIcon,
|
||||
getSearchResults,
|
||||
linkItemToSelectedItem,
|
||||
createAndAddNewTag,
|
||||
isEntitledToNoteLinking,
|
||||
} = linkingController
|
||||
const application = useApplication()
|
||||
const { tags, linkItemToSelectedItem, createAndAddNewTag, isEntitledToNoteLinking, activeItem } = linkingController
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const { unlinkedResults, shouldShowCreateTag } = getSearchResults(searchQuery)
|
||||
const { unlinkedItems, shouldShowCreateTag } = getLinkingSearchResults(searchQuery, application, activeItem)
|
||||
|
||||
const [dropdownVisible, setDropdownVisible] = useState(false)
|
||||
const [dropdownMaxHeight, setDropdownMaxHeight] = useState<number | 'auto'>('auto')
|
||||
@@ -105,7 +100,7 @@ const ItemLinkAutocompleteInput = ({ linkingController, focusPreviousItem, focus
|
||||
}
|
||||
}, [focusedId])
|
||||
|
||||
const areSearchResultsVisible = dropdownVisible && (unlinkedResults.length > 0 || shouldShowCreateTag)
|
||||
const areSearchResultsVisible = dropdownVisible && (unlinkedItems.length > 0 || shouldShowCreateTag)
|
||||
|
||||
const handleMenuKeyDown: KeyboardEventHandler<HTMLMenuElement> = useCallback((event) => {
|
||||
if (event.key === KeyboardKey.Escape) {
|
||||
@@ -155,10 +150,8 @@ const ItemLinkAutocompleteInput = ({ linkingController, focusPreviousItem, focus
|
||||
>
|
||||
<LinkedItemSearchResults
|
||||
createAndAddNewTag={createAndAddNewTag}
|
||||
getLinkedItemIcon={getLinkedItemIcon}
|
||||
getTitleForLinkedTag={getTitleForLinkedTag}
|
||||
linkItemToSelectedItem={linkItemToSelectedItem}
|
||||
results={unlinkedResults}
|
||||
results={unlinkedItems}
|
||||
searchQuery={searchQuery}
|
||||
shouldShowCreateTag={shouldShowCreateTag}
|
||||
onClickCallback={() => setSearchQuery('')}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { ItemLink, LinkableItem, LinkingController } from '@/Controllers/LinkingController'
|
||||
import { LinkingController } from '@/Controllers/LinkingController'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { KeyboardEventHandler, MouseEventHandler, useEffect, useRef, useState } from 'react'
|
||||
import { ContentType } from '@standardnotes/snjs'
|
||||
import Icon from '../Icon/Icon'
|
||||
import { ItemLink } from '@/Utils/Items/Search/ItemLink'
|
||||
import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
|
||||
import { getIconForItem } from '@/Utils/Items/Icons/getIconForItem'
|
||||
import { useApplication } from '../ApplicationView/ApplicationProvider'
|
||||
import { getTitleForLinkedTag } from '@/Utils/Items/Display/getTitleForLinkedTag'
|
||||
|
||||
type Props = {
|
||||
link: ItemLink
|
||||
getItemIcon: LinkingController['getLinkedItemIcon']
|
||||
getTitleForLinkedTag: LinkingController['getTitleForLinkedTag']
|
||||
activateItem: (item: LinkableItem) => Promise<void>
|
||||
unlinkItem: LinkingController['unlinkItemFromSelectedItem']
|
||||
focusPreviousItem: () => void
|
||||
@@ -21,8 +24,6 @@ type Props = {
|
||||
|
||||
const LinkedItemBubble = ({
|
||||
link,
|
||||
getItemIcon,
|
||||
getTitleForLinkedTag,
|
||||
activateItem,
|
||||
unlinkItem,
|
||||
focusPreviousItem,
|
||||
@@ -32,6 +33,7 @@ const LinkedItemBubble = ({
|
||||
isBidirectional,
|
||||
}: Props) => {
|
||||
const ref = useRef<HTMLButtonElement>(null)
|
||||
const application = useApplication()
|
||||
|
||||
const [showUnlinkButton, setShowUnlinkButton] = useState(false)
|
||||
const unlinkButtonRef = useRef<HTMLAnchorElement | null>(null)
|
||||
@@ -80,8 +82,8 @@ const LinkedItemBubble = ({
|
||||
}
|
||||
}
|
||||
|
||||
const [icon, iconClassName] = getItemIcon(link.item)
|
||||
const tagTitle = getTitleForLinkedTag(link.item)
|
||||
const [icon, iconClassName] = getIconForItem(link.item, application)
|
||||
const tagTitle = getTitleForLinkedTag(link.item, application)
|
||||
|
||||
useEffect(() => {
|
||||
if (link.id === focusedId) {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import ItemLinkAutocompleteInput from './ItemLinkAutocompleteInput'
|
||||
import { ItemLink, LinkableItem, LinkingController } from '@/Controllers/LinkingController'
|
||||
import { LinkingController } from '@/Controllers/LinkingController'
|
||||
import LinkedItemBubble from './LinkedItemBubble'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import { ContentType } from '@standardnotes/snjs'
|
||||
import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
|
||||
import { ItemLink } from '@/Utils/Items/Search/ItemLink'
|
||||
|
||||
type Props = {
|
||||
linkingController: LinkingController
|
||||
@@ -19,8 +21,6 @@ const LinkedItemBubblesContainer = ({ linkingController }: Props) => {
|
||||
notesLinkingToActiveItem,
|
||||
filesLinkingToActiveItem,
|
||||
unlinkItemFromSelectedItem: unlinkItem,
|
||||
getTitleForLinkedTag,
|
||||
getLinkedItemIcon: getItemIcon,
|
||||
activateItem,
|
||||
} = linkingController
|
||||
|
||||
@@ -86,8 +86,6 @@ const LinkedItemBubblesContainer = ({ linkingController }: Props) => {
|
||||
<LinkedItemBubble
|
||||
link={link}
|
||||
key={link.id}
|
||||
getItemIcon={getItemIcon}
|
||||
getTitleForLinkedTag={getTitleForLinkedTag}
|
||||
activateItem={activateItemAndTogglePane}
|
||||
unlinkItem={unlinkItem}
|
||||
focusPreviousItem={focusPreviousItem}
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import { LinkableItem, LinkingController } from '@/Controllers/LinkingController'
|
||||
import { splitQueryInString } from '@/Utils'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import { getTitleForLinkedTag } from '@/Utils/Items/Display/getTitleForLinkedTag'
|
||||
import { getIconForItem } from '@/Utils/Items/Icons/getIconForItem'
|
||||
import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useApplication } from '../ApplicationView/ApplicationProvider'
|
||||
import Icon from '../Icon/Icon'
|
||||
|
||||
const LinkedItemMeta = ({
|
||||
item,
|
||||
getItemIcon,
|
||||
getTitleForLinkedTag,
|
||||
searchQuery,
|
||||
}: {
|
||||
type Props = {
|
||||
item: LinkableItem
|
||||
getItemIcon: LinkingController['getLinkedItemIcon']
|
||||
getTitleForLinkedTag: LinkingController['getTitleForLinkedTag']
|
||||
searchQuery?: string
|
||||
}) => {
|
||||
const [icon, className] = getItemIcon(item)
|
||||
const tagTitle = getTitleForLinkedTag(item)
|
||||
}
|
||||
|
||||
const LinkedItemMeta = ({ item, searchQuery }: Props) => {
|
||||
const application = useApplication()
|
||||
const [icon, className] = getIconForItem(item, application)
|
||||
const tagTitle = getTitleForLinkedTag(item, application)
|
||||
const title = item.title ?? ''
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { LinkableItem, LinkingController } from '@/Controllers/LinkingController'
|
||||
import { LinkingController } from '@/Controllers/LinkingController'
|
||||
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { SNNote } from '@standardnotes/snjs'
|
||||
import Icon from '../Icon/Icon'
|
||||
import { PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
|
||||
import LinkedItemMeta from './LinkedItemMeta'
|
||||
import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
|
||||
|
||||
type Props = {
|
||||
createAndAddNewTag: LinkingController['createAndAddNewTag']
|
||||
getLinkedItemIcon: LinkingController['getLinkedItemIcon']
|
||||
getTitleForLinkedTag: LinkingController['getTitleForLinkedTag']
|
||||
linkItemToSelectedItem: LinkingController['linkItemToSelectedItem']
|
||||
results: LinkableItem[]
|
||||
searchQuery: string
|
||||
@@ -20,8 +19,6 @@ type Props = {
|
||||
|
||||
const LinkedItemSearchResults = ({
|
||||
createAndAddNewTag,
|
||||
getLinkedItemIcon,
|
||||
getTitleForLinkedTag,
|
||||
linkItemToSelectedItem,
|
||||
results,
|
||||
searchQuery,
|
||||
@@ -48,12 +45,7 @@ const LinkedItemSearchResults = ({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<LinkedItemMeta
|
||||
item={result}
|
||||
getItemIcon={getLinkedItemIcon}
|
||||
getTitleForLinkedTag={getTitleForLinkedTag}
|
||||
searchQuery={searchQuery}
|
||||
/>
|
||||
<LinkedItemMeta item={result} searchQuery={searchQuery} />
|
||||
{cannotLinkItem && <Icon type={PremiumFeatureIconName} className="ml-auto flex-shrink-0 text-info" />}
|
||||
</button>
|
||||
)
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { FeaturesController } from '@/Controllers/FeaturesController'
|
||||
import { FilesController } from '@/Controllers/FilesController'
|
||||
import { LinkableItem, LinkingController } from '@/Controllers/LinkingController'
|
||||
import { LinkingController } from '@/Controllers/LinkingController'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import { formatDateForContextMenu } from '@/Utils/DateUtils'
|
||||
import { getIconForItem } from '@/Utils/Items/Icons/getIconForItem'
|
||||
import { getLinkingSearchResults } from '@/Utils/Items/Search/getSearchResults'
|
||||
import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
|
||||
import { formatSizeToReadableString } from '@standardnotes/filepicker'
|
||||
import { FileItem } from '@standardnotes/snjs'
|
||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { ChangeEventHandler, useEffect, useRef, useState } from 'react'
|
||||
import { useApplication } from '../ApplicationView/ApplicationProvider'
|
||||
import { PopoverFileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction'
|
||||
import ClearInputButton from '../ClearInputButton/ClearInputButton'
|
||||
import Icon from '../Icon/Icon'
|
||||
@@ -22,29 +26,26 @@ import LinkedItemSearchResults from './LinkedItemSearchResults'
|
||||
|
||||
const LinkedItemsSectionItem = ({
|
||||
activateItem,
|
||||
getItemIcon,
|
||||
getTitleForLinkedTag,
|
||||
item,
|
||||
searchQuery,
|
||||
unlinkItem,
|
||||
handleFileAction,
|
||||
}: {
|
||||
activateItem: LinkingController['activateItem']
|
||||
getItemIcon: LinkingController['getLinkedItemIcon']
|
||||
getTitleForLinkedTag: LinkingController['getTitleForLinkedTag']
|
||||
item: LinkableItem
|
||||
searchQuery?: string
|
||||
unlinkItem: () => void
|
||||
handleFileAction: FilesController['handleFileAction']
|
||||
}) => {
|
||||
const menuButtonRef = useRef<HTMLButtonElement>(null)
|
||||
const application = useApplication()
|
||||
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||
const toggleMenu = () => setIsMenuOpen((open) => !open)
|
||||
|
||||
const [isRenamingFile, setIsRenamingFile] = useState(false)
|
||||
|
||||
const [icon, className] = getItemIcon(item)
|
||||
const [icon, className] = getIconForItem(item, application)
|
||||
const title = item.title ?? ''
|
||||
|
||||
const renameFile = async (name: string) => {
|
||||
@@ -93,12 +94,7 @@ const LinkedItemsSectionItem = ({
|
||||
toggleMenu()
|
||||
}}
|
||||
>
|
||||
<LinkedItemMeta
|
||||
item={item}
|
||||
getItemIcon={getItemIcon}
|
||||
getTitleForLinkedTag={getTitleForLinkedTag}
|
||||
searchQuery={searchQuery}
|
||||
/>
|
||||
<LinkedItemMeta item={item} searchQuery={searchQuery} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
@@ -174,23 +170,26 @@ const LinkedItemsPanel = ({
|
||||
notesLinkedToItem,
|
||||
notesLinkingToActiveItem,
|
||||
allItemLinks: allLinkedItems,
|
||||
getTitleForLinkedTag,
|
||||
getLinkedItemIcon,
|
||||
getSearchResults,
|
||||
linkItemToSelectedItem,
|
||||
unlinkItemFromSelectedItem,
|
||||
activateItem,
|
||||
createAndAddNewTag,
|
||||
isEntitledToNoteLinking,
|
||||
activeItem,
|
||||
} = linkingController
|
||||
|
||||
const { hasFiles } = featuresController
|
||||
const application = useApplication()
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null)
|
||||
const searchInputRef = useRef<HTMLInputElement | null>(null)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const isSearching = !!searchQuery.length
|
||||
const { linkedResults, unlinkedResults, shouldShowCreateTag } = getSearchResults(searchQuery)
|
||||
const { linkedResults, unlinkedItems, shouldShowCreateTag } = getLinkingSearchResults(
|
||||
searchQuery,
|
||||
application,
|
||||
activeItem,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && searchInputRef.current) {
|
||||
@@ -227,7 +226,7 @@ const LinkedItemsPanel = ({
|
||||
<form
|
||||
className={classNames(
|
||||
'sticky top-0 z-10 bg-default px-2.5 pt-2.5',
|
||||
allLinkedItems.length || linkedResults.length || unlinkedResults.length || notesLinkingToActiveItem.length
|
||||
allLinkedItems.length || linkedResults.length || unlinkedItems.length || notesLinkingToActiveItem.length
|
||||
? 'border-b border-border pb-2.5'
|
||||
: 'pb-1',
|
||||
)}
|
||||
@@ -254,15 +253,13 @@ const LinkedItemsPanel = ({
|
||||
<div className="divide-y divide-border">
|
||||
{isSearching ? (
|
||||
<>
|
||||
{(!!unlinkedResults.length || shouldShowCreateTag) && (
|
||||
{(!!unlinkedItems.length || shouldShowCreateTag) && (
|
||||
<div>
|
||||
<div className="mt-3 mb-1 px-3 text-menu-item font-semibold uppercase text-passive-0">Unlinked</div>
|
||||
<LinkedItemSearchResults
|
||||
createAndAddNewTag={createAndAddNewTag}
|
||||
getLinkedItemIcon={getLinkedItemIcon}
|
||||
getTitleForLinkedTag={getTitleForLinkedTag}
|
||||
linkItemToSelectedItem={linkItemToSelectedItem}
|
||||
results={unlinkedResults}
|
||||
results={unlinkedItems}
|
||||
searchQuery={searchQuery}
|
||||
shouldShowCreateTag={shouldShowCreateTag}
|
||||
isEntitledToNoteLinking={isEntitledToNoteLinking}
|
||||
@@ -281,8 +278,6 @@ const LinkedItemsPanel = ({
|
||||
<LinkedItemsSectionItem
|
||||
key={link.id}
|
||||
item={link.item}
|
||||
getItemIcon={getLinkedItemIcon}
|
||||
getTitleForLinkedTag={getTitleForLinkedTag}
|
||||
searchQuery={searchQuery}
|
||||
unlinkItem={() => unlinkItemFromSelectedItem(link)}
|
||||
activateItem={activateItem}
|
||||
@@ -303,8 +298,6 @@ const LinkedItemsPanel = ({
|
||||
<LinkedItemsSectionItem
|
||||
key={link.id}
|
||||
item={link.item}
|
||||
getItemIcon={getLinkedItemIcon}
|
||||
getTitleForLinkedTag={getTitleForLinkedTag}
|
||||
searchQuery={searchQuery}
|
||||
unlinkItem={() => unlinkItemFromSelectedItem(link)}
|
||||
activateItem={activateItem}
|
||||
@@ -336,8 +329,6 @@ const LinkedItemsPanel = ({
|
||||
<LinkedItemsSectionItem
|
||||
key={link.id}
|
||||
item={link.item}
|
||||
getItemIcon={getLinkedItemIcon}
|
||||
getTitleForLinkedTag={getTitleForLinkedTag}
|
||||
searchQuery={searchQuery}
|
||||
unlinkItem={() => unlinkItemFromSelectedItem(link)}
|
||||
activateItem={activateItem}
|
||||
@@ -357,8 +348,6 @@ const LinkedItemsPanel = ({
|
||||
<LinkedItemsSectionItem
|
||||
key={link.id}
|
||||
item={link.item}
|
||||
getItemIcon={getLinkedItemIcon}
|
||||
getTitleForLinkedTag={getTitleForLinkedTag}
|
||||
searchQuery={searchQuery}
|
||||
unlinkItem={() => unlinkItemFromSelectedItem(link)}
|
||||
activateItem={activateItem}
|
||||
@@ -376,8 +365,6 @@ const LinkedItemsPanel = ({
|
||||
<LinkedItemsSectionItem
|
||||
key={link.id}
|
||||
item={link.item}
|
||||
getItemIcon={getLinkedItemIcon}
|
||||
getTitleForLinkedTag={getTitleForLinkedTag}
|
||||
searchQuery={searchQuery}
|
||||
unlinkItem={() => unlinkItemFromSelectedItem(link)}
|
||||
activateItem={activateItem}
|
||||
@@ -397,8 +384,6 @@ const LinkedItemsPanel = ({
|
||||
<LinkedItemsSectionItem
|
||||
key={link.id}
|
||||
item={link.item}
|
||||
getItemIcon={getLinkedItemIcon}
|
||||
getTitleForLinkedTag={getTitleForLinkedTag}
|
||||
searchQuery={searchQuery}
|
||||
unlinkItem={() => unlinkItemFromSelectedItem(link)}
|
||||
activateItem={activateItem}
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
import { confirmDialog, KeyboardKey, KeyboardModifier } from '@standardnotes/ui-services'
|
||||
import { ChangeEventHandler, createRef, KeyboardEventHandler, RefObject } from 'react'
|
||||
import { EditorEventSource } from '../../Types/EditorEventSource'
|
||||
import { BlockEditor } from '../BlockEditor/BlockEditor'
|
||||
import { BlockEditor } from '../BlockEditor/BlockEditorComponent'
|
||||
import IndicatorCircle from '../IndicatorCircle/IndicatorCircle'
|
||||
import LinkedItemBubblesContainer from '../LinkedItems/LinkedItemBubblesContainer'
|
||||
import LinkedItemsButton from '../LinkedItems/LinkedItemsButton'
|
||||
|
||||
@@ -5,13 +5,13 @@ import { NavigationController } from '@/Controllers/Navigation/NavigationControl
|
||||
import { NotesController } from '@/Controllers/NotesController'
|
||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||
import Popover from '../Popover/Popover'
|
||||
import { LinkingController } from '@/Controllers/LinkingController'
|
||||
import { IconType } from '@standardnotes/snjs'
|
||||
import { getTitleForLinkedTag } from '@/Utils/Items/Display/getTitleForLinkedTag'
|
||||
import { useApplication } from '../ApplicationView/ApplicationProvider'
|
||||
|
||||
type Props = {
|
||||
navigationController: NavigationController
|
||||
notesController: NotesController
|
||||
linkingController: LinkingController
|
||||
className: string
|
||||
iconClassName: string
|
||||
}
|
||||
@@ -19,10 +19,10 @@ type Props = {
|
||||
const AddTagOption: FunctionComponent<Props> = ({
|
||||
navigationController,
|
||||
notesController,
|
||||
linkingController,
|
||||
className,
|
||||
iconClassName,
|
||||
}) => {
|
||||
const application = useApplication()
|
||||
const menuContainerRef = useRef<HTMLDivElement>(null)
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
@@ -79,7 +79,7 @@ const AddTagOption: FunctionComponent<Props> = ({
|
||||
className={`overflow-hidden overflow-ellipsis whitespace-nowrap
|
||||
${notesController.isTagInSelectedNotes(tag) ? 'font-bold' : ''}`}
|
||||
>
|
||||
{linkingController.getTitleForLinkedTag(tag)?.longTitle}
|
||||
{getTitleForLinkedTag(tag, application)?.longTitle}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
|
||||
@@ -28,7 +28,10 @@ type DeletePermanentlyButtonProps = {
|
||||
|
||||
const DeletePermanentlyButton = ({ onClick }: DeletePermanentlyButtonProps) => (
|
||||
<button
|
||||
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none md:text-menu-item"
|
||||
className={classNames(
|
||||
'flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-mobile-menu-item',
|
||||
'text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none md:text-menu-item',
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon type="close" className="mr-2 text-danger" />
|
||||
@@ -183,7 +186,6 @@ const NotesOptions = ({
|
||||
application,
|
||||
navigationController,
|
||||
notesController,
|
||||
linkingController,
|
||||
historyModalController,
|
||||
closeMenu,
|
||||
}: NotesOptionsProps) => {
|
||||
@@ -353,7 +355,6 @@ const NotesOptions = ({
|
||||
className={switchClassNames}
|
||||
navigationController={navigationController}
|
||||
notesController={notesController}
|
||||
linkingController={linkingController}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user