refactor: lexical (#1954)

This commit is contained in:
Mo
2022-11-07 10:42:44 -06:00
committed by GitHub
parent 99bae83f8b
commit 2ed01a071c
182 changed files with 8525 additions and 1126 deletions

View File

@@ -19,7 +19,7 @@ declare global {
application?: WebApplication
mainApplicationGroup?: ApplicationGroup
MSStream?: Record<string, unknown>
MSStream?: unknown
}
}

View File

@@ -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>
)
}

View File

@@ -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
})
}
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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
}

View File

@@ -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,
}
}

View File

@@ -1,3 +0,0 @@
export const MaxBlockHeight: Record<string, number> = {
'org.standardnotes.standard-sheets': 300,
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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

View 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]
}