refactor: lexical (#1954)
This commit is contained in:
@@ -19,7 +19,7 @@ declare global {
|
||||
|
||||
application?: WebApplication
|
||||
mainApplicationGroup?: ApplicationGroup
|
||||
MSStream?: Record<string, unknown>
|
||||
MSStream?: unknown
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,36 +2,37 @@ import { WebApplication } from '@/Application/Application'
|
||||
import { SNNote } from '@standardnotes/snjs'
|
||||
import { FunctionComponent, useCallback, useRef } from 'react'
|
||||
import { BlockEditorController } from './BlockEditorController'
|
||||
import { AddBlockButton } from './BlockMenu/AddButton'
|
||||
import { MultiBlockRenderer } from './BlockRender/MultiBlockRenderer'
|
||||
import { BlockOption } from './BlockMenu/BlockOption'
|
||||
import { BlocksEditor } from '@standardnotes/blocks-editor'
|
||||
|
||||
const StringEllipses = '...'
|
||||
const NotePreviewCharLimit = 160
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
note: SNNote
|
||||
}
|
||||
|
||||
export const BlockEditor: FunctionComponent<Props> = ({ note, application }) => {
|
||||
const controller = useRef(new BlockEditorController(note, application))
|
||||
|
||||
const onSelectOption = useCallback(
|
||||
(option: BlockOption) => {
|
||||
void controller.current.addNewBlock(option)
|
||||
const handleChange = useCallback(
|
||||
(value: string) => {
|
||||
const content = value
|
||||
const truncate = content.length > NotePreviewCharLimit
|
||||
const substring = content.substring(0, NotePreviewCharLimit)
|
||||
const previewPlain = substring + (truncate ? StringEllipses : '')
|
||||
void controller.current.save({ text: content, previewPlain: previewPlain, previewHtml: undefined })
|
||||
},
|
||||
[controller],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<AddBlockButton application={application} onSelectOption={onSelectOption} />
|
||||
{note.blocksItem && (
|
||||
<MultiBlockRenderer
|
||||
controller={controller.current}
|
||||
key={note.uuid}
|
||||
blocksItem={note.blocksItem}
|
||||
note={note}
|
||||
application={application}
|
||||
/>
|
||||
)}
|
||||
<div className="relative h-full w-full p-5">
|
||||
<BlocksEditor
|
||||
onChange={handleChange}
|
||||
initialValue={note.content.text}
|
||||
className="relative relative resize-none text-base focus:shadow-none focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { NoteBlock, NoteMutator, SNComponent, SNNote } from '@standardnotes/snjs'
|
||||
import { BlockOption } from './BlockMenu/BlockOption'
|
||||
import { NoteMutator, SNNote } from '@standardnotes/snjs'
|
||||
|
||||
export class BlockEditorController {
|
||||
constructor(private note: SNNote, private application: WebApplication) {
|
||||
@@ -13,42 +12,11 @@ export class BlockEditorController {
|
||||
;(this.application as unknown) = undefined
|
||||
}
|
||||
|
||||
createBlockItem(editor: SNComponent): NoteBlock {
|
||||
const id = this.application.generateUuid()
|
||||
const block: NoteBlock = {
|
||||
id: id,
|
||||
editorIdentifier: editor.identifier,
|
||||
type: editor.noteType,
|
||||
content: '',
|
||||
}
|
||||
|
||||
return block
|
||||
}
|
||||
|
||||
async addNewBlock(option: BlockOption): Promise<void> {
|
||||
if (!option.component) {
|
||||
throw new Error('Non-component block options are not supported yet')
|
||||
}
|
||||
|
||||
const block = this.createBlockItem(option.component)
|
||||
async save(values: { text: string; previewPlain: string; previewHtml?: string }): Promise<void> {
|
||||
await this.application.mutator.changeAndSaveItem<NoteMutator>(this.note, (mutator) => {
|
||||
mutator.addBlock(block)
|
||||
})
|
||||
}
|
||||
|
||||
async removeBlock(block: NoteBlock): Promise<void> {
|
||||
await this.application.mutator.changeAndSaveItem<NoteMutator>(this.note, (mutator) => {
|
||||
mutator.removeBlock(block)
|
||||
})
|
||||
}
|
||||
|
||||
async saveBlockSize(block: NoteBlock, size: { width: number; height: number }): Promise<void> {
|
||||
if (block.size?.height === size.height) {
|
||||
return
|
||||
}
|
||||
|
||||
await this.application.mutator.changeAndSaveItem<NoteMutator>(this.note, (mutator) => {
|
||||
mutator.changeBlockSize(block.id, size)
|
||||
mutator.text = values.text
|
||||
mutator.preview_plain = values.previewPlain
|
||||
mutator.preview_html = values.previewHtml
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import { FunctionComponent, useCallback, useState } from 'react'
|
||||
import Icon from '../../Icon/Icon'
|
||||
import { BlockMenu } from './BlockMenu'
|
||||
import { BlockOption } from './BlockOption'
|
||||
|
||||
type AddButtonProps = {
|
||||
application: WebApplication
|
||||
onSelectOption: (option: BlockOption) => void
|
||||
}
|
||||
|
||||
export const AddBlockButton: FunctionComponent<AddButtonProps> = ({ application, onSelectOption }) => {
|
||||
const [showMenu, setShowMenu] = useState(false)
|
||||
|
||||
const toggleMenu = useCallback(() => {
|
||||
setShowMenu((prevValue) => !prevValue)
|
||||
}, [])
|
||||
|
||||
const handleSelection = useCallback(
|
||||
(option: BlockOption) => {
|
||||
onSelectOption(option)
|
||||
setShowMenu(false)
|
||||
},
|
||||
[onSelectOption],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="mt-2 flex flex-row flex-wrap">
|
||||
<button
|
||||
className={classNames(
|
||||
'fixed bottom-6 right-6 z-editor-title-bar ml-3 flex h-15 w-15 cursor-pointer items-center',
|
||||
`justify-center rounded-full border border-solid border-transparent ${'bg-info text-info-contrast'}`,
|
||||
'hover:brightness-125 md:static md:h-8 md:w-8',
|
||||
)}
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
<Icon type="add" size="custom" className="h-8 w-8 md:h-5 md:w-5" />
|
||||
</button>
|
||||
|
||||
{showMenu && <BlockMenu application={application} onSelectOption={handleSelection} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { ComponentArea } from '@standardnotes/snjs'
|
||||
import { FunctionComponent } from 'react'
|
||||
import { BlockMenuOption } from './BlockMenuOption'
|
||||
import { BlockOption } from './BlockOption'
|
||||
import { componentToBlockOption } from './componentToBlockOption'
|
||||
|
||||
type BlockMenuProps = {
|
||||
application: WebApplication
|
||||
onSelectOption: (row: BlockOption) => void
|
||||
}
|
||||
|
||||
export const BlockMenu: FunctionComponent<BlockMenuProps> = ({ application, onSelectOption }) => {
|
||||
const components = application.componentManager.componentsForArea(ComponentArea.Editor)
|
||||
const options = components.map((component) => componentToBlockOption(component, application.iconsController))
|
||||
|
||||
return (
|
||||
<div className="flex flex-row flex-wrap">
|
||||
{options.map((option) => {
|
||||
return <BlockMenuOption option={option} key={option.identifier} onSelect={onSelectOption} />
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { FunctionComponent } from 'react'
|
||||
import { BlockOption } from './BlockOption'
|
||||
|
||||
export type BlockMenuOptionProps = {
|
||||
option: BlockOption
|
||||
onSelect: (option: BlockOption) => void
|
||||
}
|
||||
|
||||
export const BlockMenuOption: FunctionComponent<BlockMenuOptionProps> = ({ option, onSelect }) => {
|
||||
return (
|
||||
<div
|
||||
className={'flex w-full cursor-pointer flex-row items-center border-[1px] border-b border-border p-4'}
|
||||
onClick={() => onSelect(option)}
|
||||
>
|
||||
<Icon type={option.icon} size={'large'} />
|
||||
<div className={'ml-3 text-base'}>{option.label}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { IconType, SNComponent } from '@standardnotes/snjs'
|
||||
|
||||
export type BlockOption = {
|
||||
editorIdentifier: string
|
||||
label: string
|
||||
identifier: string
|
||||
icon: IconType | string
|
||||
iconTint: number
|
||||
component?: SNComponent
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { IconsController, SNComponent } from '@standardnotes/snjs'
|
||||
import { BlockOption } from './BlockOption'
|
||||
|
||||
export function componentToBlockOption(component: SNComponent, iconsController: IconsController): BlockOption {
|
||||
const [iconType, tint] = iconsController.getIconAndTintForNoteType(component.package_info.note_type)
|
||||
|
||||
return {
|
||||
identifier: component.uuid,
|
||||
editorIdentifier: component.identifier,
|
||||
label: component.name,
|
||||
icon: iconType,
|
||||
iconTint: tint,
|
||||
component: component,
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export const MaxBlockHeight: Record<string, number> = {
|
||||
'org.standardnotes.standard-sheets': 300,
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { NoteBlocks, SNNote } from '@standardnotes/snjs'
|
||||
import { FunctionComponent } from 'react'
|
||||
import { BlockEditorController } from '../BlockEditorController'
|
||||
import { SingleBlockRenderer } from './SingleBlockRenderer'
|
||||
|
||||
type MultiBlockRendererProps = {
|
||||
application: WebApplication
|
||||
note: SNNote
|
||||
blocksItem: NoteBlocks
|
||||
controller: BlockEditorController
|
||||
}
|
||||
|
||||
export const MultiBlockRenderer: FunctionComponent<MultiBlockRendererProps> = ({
|
||||
blocksItem,
|
||||
controller,
|
||||
note,
|
||||
application,
|
||||
}) => {
|
||||
return (
|
||||
<div className="w-full">
|
||||
{blocksItem.blocks.map((block) => {
|
||||
return (
|
||||
<SingleBlockRenderer
|
||||
key={block.id}
|
||||
block={block}
|
||||
note={note}
|
||||
application={application}
|
||||
controller={controller}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { log, LoggingDomain } from '@/Logging'
|
||||
import { ComponentAction, NoteBlock, SNNote } from '@standardnotes/snjs'
|
||||
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import ComponentView from '../../ComponentView/ComponentView'
|
||||
import { MaxBlockHeight } from './MaxBlockHeight'
|
||||
import { BlockEditorController } from '../BlockEditorController'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
|
||||
type SingleBlockRendererProps = {
|
||||
application: WebApplication
|
||||
block: NoteBlock
|
||||
note: SNNote
|
||||
controller: BlockEditorController
|
||||
}
|
||||
|
||||
export const SingleBlockRenderer: FunctionComponent<SingleBlockRendererProps> = ({
|
||||
block,
|
||||
application,
|
||||
note,
|
||||
controller,
|
||||
}) => {
|
||||
const [height, setHeight] = useState<number | undefined>(block.size?.height)
|
||||
const [showCloseButton, setShowCloseButton] = useState(false)
|
||||
|
||||
const component = useMemo(
|
||||
() => application.componentManager.componentWithIdentifier(block.editorIdentifier),
|
||||
[block, application],
|
||||
)
|
||||
|
||||
const viewer = useMemo(
|
||||
() => component && application.componentManager.createBlockComponentViewer(component, note.uuid, block.id),
|
||||
[application, component, note.uuid, block.id],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const disposer = viewer?.addActionObserver((action, data) => {
|
||||
if (action === ComponentAction.SetSize) {
|
||||
if (data.height && data.height > 0) {
|
||||
const height = Math.min(Number(data.height), MaxBlockHeight[block.editorIdentifier] ?? Number(data.height))
|
||||
log(LoggingDomain.BlockEditor, `Received block height ${height}`)
|
||||
setHeight(height)
|
||||
void controller.saveBlockSize(block, { width: 0, height })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return disposer
|
||||
}, [viewer, block, controller])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (viewer) {
|
||||
application.componentManager.destroyComponentViewer(viewer)
|
||||
}
|
||||
}
|
||||
}, [application, viewer])
|
||||
|
||||
const onHoverEnter = useCallback(() => {
|
||||
setShowCloseButton(true)
|
||||
}, [])
|
||||
|
||||
const onHoverExit = useCallback(() => {
|
||||
setShowCloseButton(false)
|
||||
}, [])
|
||||
|
||||
const onRemoveBlock = useCallback(() => {
|
||||
void controller.removeBlock(block)
|
||||
}, [block, controller])
|
||||
|
||||
if (!component || !viewer) {
|
||||
return <div>Unable to find component {block.editorIdentifier}</div>
|
||||
}
|
||||
|
||||
const styles: Record<string, unknown> = {}
|
||||
if (height) {
|
||||
styles['height'] = height
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={onHoverEnter}
|
||||
onMouseLeave={onHoverExit}
|
||||
className="w-full border-info hover:border-[1px]"
|
||||
style={styles}
|
||||
>
|
||||
{showCloseButton && (
|
||||
<button
|
||||
className={classNames(
|
||||
'fixed bottom-6 right-6 z-editor-title-bar ml-3 flex h-15 w-15 cursor-pointer items-center',
|
||||
`justify-center rounded-full border border-solid border-transparent ${'bg-info text-info-contrast'}`,
|
||||
'hover:brightness-125 md:static md:h-8 md:w-8',
|
||||
)}
|
||||
onClick={onRemoveBlock}
|
||||
>
|
||||
<Icon type="close" size="custom" className="h-8 w-8 md:h-5 md:w-5" />
|
||||
</button>
|
||||
)}
|
||||
<ComponentView key={viewer.identifier} componentViewer={viewer} application={application} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
import { StringDeleteNote, STRING_DELETE_LOCKED_ATTEMPT, STRING_DELETE_PLACEHOLDER_ATTEMPT } from '@/Constants/Strings'
|
||||
import { featureTrunkEnabled, FeatureTrunkName } from '@/FeatureTrunk'
|
||||
import { log, LoggingDomain } from '@/Logging'
|
||||
import { debounce, isDesktopApplication, isMobileScreen, isTabletScreen } from '@/Utils'
|
||||
import { debounce, isDesktopApplication, isMobileScreen } from '@/Utils'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import {
|
||||
ApplicationEvent,
|
||||
@@ -40,6 +40,7 @@ import LinkedItemsButton from '../LinkedItems/LinkedItemsButton'
|
||||
import MobileItemsListButton from '../NoteGroupView/MobileItemsListButton'
|
||||
import EditingDisabledBanner from './EditingDisabledBanner'
|
||||
import { reloadFont } from './FontFunctions'
|
||||
import { getPlaintextFontSize } from '../../Utils/getPlaintextFontSize'
|
||||
import NoteStatusIndicator, { NoteStatus } from './NoteStatusIndicator'
|
||||
import NoteViewFileDropTarget from './NoteViewFileDropTarget'
|
||||
import { NoteViewProps } from './NoteViewProps'
|
||||
@@ -93,38 +94,6 @@ type State = {
|
||||
noteType?: NoteType
|
||||
}
|
||||
|
||||
const getPlaintextFontSize = (key: EditorFontSize): string => {
|
||||
const desktopMapping: Record<EditorFontSize, string> = {
|
||||
ExtraSmall: 'text-xs',
|
||||
Small: 'text-sm',
|
||||
Normal: 'text-editor',
|
||||
Medium: 'text-lg',
|
||||
Large: 'text-xl',
|
||||
}
|
||||
|
||||
const mobileMapping: Record<EditorFontSize, string> = {
|
||||
ExtraSmall: 'text-sm',
|
||||
Small: 'text-editor',
|
||||
Normal: 'text-lg',
|
||||
Medium: 'text-xl',
|
||||
Large: 'text-xl2',
|
||||
}
|
||||
|
||||
const tabletMapping: Record<EditorFontSize, string> = {
|
||||
ExtraSmall: 'text-sm',
|
||||
Small: 'text-editor',
|
||||
Normal: 'text-base',
|
||||
Medium: 'text-xl',
|
||||
Large: 'text-xl2',
|
||||
}
|
||||
|
||||
if (isTabletScreen()) {
|
||||
return tabletMapping[key]
|
||||
}
|
||||
|
||||
return isMobileScreen() ? mobileMapping[key] : desktopMapping[key]
|
||||
}
|
||||
|
||||
class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
readonly controller!: NoteViewController
|
||||
|
||||
|
||||
34
packages/web/src/javascripts/Utils/getPlaintextFontSize.tsx
Normal file
34
packages/web/src/javascripts/Utils/getPlaintextFontSize.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { isMobileScreen, isTabletScreen } from '@/Utils'
|
||||
import { EditorFontSize } from '@standardnotes/snjs'
|
||||
|
||||
export const getPlaintextFontSize = (key: EditorFontSize): string => {
|
||||
const desktopMapping: Record<EditorFontSize, string> = {
|
||||
ExtraSmall: 'text-xs',
|
||||
Small: 'text-sm',
|
||||
Normal: 'text-editor',
|
||||
Medium: 'text-lg',
|
||||
Large: 'text-xl',
|
||||
}
|
||||
|
||||
const mobileMapping: Record<EditorFontSize, string> = {
|
||||
ExtraSmall: 'text-sm',
|
||||
Small: 'text-editor',
|
||||
Normal: 'text-lg',
|
||||
Medium: 'text-xl',
|
||||
Large: 'text-xl2',
|
||||
}
|
||||
|
||||
const tabletMapping: Record<EditorFontSize, string> = {
|
||||
ExtraSmall: 'text-sm',
|
||||
Small: 'text-editor',
|
||||
Normal: 'text-base',
|
||||
Medium: 'text-xl',
|
||||
Large: 'text-xl2',
|
||||
}
|
||||
|
||||
if (isTabletScreen()) {
|
||||
return tabletMapping[key]
|
||||
}
|
||||
|
||||
return isMobileScreen() ? mobileMapping[key] : desktopMapping[key]
|
||||
}
|
||||
Reference in New Issue
Block a user