refactor: extract components to plugin repo (#1933)
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
dist
|
||||
node_modules
|
||||
web.webpack-defaults.js
|
||||
coverage
|
||||
coverage
|
||||
src/components
|
||||
src/favicon
|
||||
src/vendor
|
||||
@@ -20,9 +20,7 @@
|
||||
"tsc": "tsc --project tsconfig.json",
|
||||
"upgrade:snjs": "ncu -u '@standardnotes/*'",
|
||||
"watch": "webpack -w --config web.webpack.dev.js",
|
||||
"copy:components": "yarn copy:components-assets && yarn copy:components-zips",
|
||||
"copy:components-assets": "mkdir -p src/components/assets && cp -r ../../node_modules/@standardnotes/components-meta/dist/assets/. src/components/assets",
|
||||
"copy:components-zips": "mkdir -p src/components/zips && cp -r ../../node_modules/@standardnotes/components-meta/dist/zips/. ./src/components/zips"
|
||||
"copy:components": "node scripts/CopyComponents.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "*",
|
||||
@@ -39,12 +37,29 @@
|
||||
"@reach/listbox": "^0.17.0",
|
||||
"@reach/tooltip": "^0.17.0",
|
||||
"@reach/visually-hidden": "^0.17.0",
|
||||
"@standardnotes/components-meta": "workspace:*",
|
||||
"@standardnotes/authenticator": "^2.3.8",
|
||||
"@standardnotes/autobiography-theme": "^1.2.7",
|
||||
"@standardnotes/bold-editor": "^1.6.3",
|
||||
"@standardnotes/classic-code-editor": "^1.5.6",
|
||||
"@standardnotes/dynamic-theme": "^1.2.8",
|
||||
"@standardnotes/filepicker": "workspace:*",
|
||||
"@standardnotes/focus-theme": "^1.4.6",
|
||||
"@standardnotes/futura-theme": "^1.4.6",
|
||||
"@standardnotes/icons": "workspace:*",
|
||||
"@standardnotes/markdown-basic": "^1.6.1",
|
||||
"@standardnotes/markdown-hybrid": "^1.7.7",
|
||||
"@standardnotes/markdown-math": "^1.4.1",
|
||||
"@standardnotes/markdown-minimal": "^1.2.1",
|
||||
"@standardnotes/markdown-visual": "^1.0.7",
|
||||
"@standardnotes/midnight-theme": "^1.4.6",
|
||||
"@standardnotes/rich-text": "^1.8.7",
|
||||
"@standardnotes/simple-task-editor": "^1.3.10",
|
||||
"@standardnotes/sncrypto-web": "workspace:*",
|
||||
"@standardnotes/snjs": "workspace:*",
|
||||
"@standardnotes/solarized-dark-theme": "^1.4.6",
|
||||
"@standardnotes/spreadsheets": "^1.6.7",
|
||||
"@standardnotes/styles": "workspace:*",
|
||||
"@standardnotes/titanium-theme": "^1.4.6",
|
||||
"@standardnotes/toast": "workspace:*",
|
||||
"@standardnotes/ui-services": "workspace:^",
|
||||
"@types/jest": "^29.2.0",
|
||||
@@ -70,9 +85,10 @@
|
||||
"jest-environment-jsdom": "^29.2.1",
|
||||
"lint-staged": ">=12",
|
||||
"mini-css-extract-plugin": "^2.6.1",
|
||||
"minimatch": "^5.1.0",
|
||||
"mobx": "^6.6.2",
|
||||
"mobx-react-lite": "^3.4.0",
|
||||
"node-sass": "*",
|
||||
"node-sass": "^7.0.3",
|
||||
"npm-check-updates": "*",
|
||||
"postcss": "^8.4.16",
|
||||
"postcss-loader": "^7.0.1",
|
||||
|
||||
69
packages/web/scripts/CopyComponents.mjs
Normal file
69
packages/web/scripts/CopyComponents.mjs
Normal file
@@ -0,0 +1,69 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import {
|
||||
copyRecursiveSync,
|
||||
doesDirExist,
|
||||
emptyExistingDir,
|
||||
ensureDirExists,
|
||||
copyFileOrDir,
|
||||
} from '../../../scripts/ScriptUtils.mjs'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
const components = {
|
||||
'@standardnotes/authenticator': 'org.standardnotes.token-vault',
|
||||
'@standardnotes/autobiography-theme': 'org.standardnotes.theme-autobiography',
|
||||
'@standardnotes/classic-code-editor': 'org.standardnotes.code-editor',
|
||||
'@standardnotes/dynamic-theme': 'org.standardnotes.theme-dynamic',
|
||||
'@standardnotes/focus-theme': 'org.standardnotes.theme-focus',
|
||||
'@standardnotes/futura-theme': 'org.standardnotes.theme-futura',
|
||||
'@standardnotes/markdown-hybrid': 'org.standardnotes.advanced-markdown-editor',
|
||||
'@standardnotes/markdown-visual': 'org.standardnotes.markdown-visual-editor',
|
||||
'@standardnotes/midnight-theme': 'org.standardnotes.theme-midnight',
|
||||
'@standardnotes/rich-text': 'org.standardnotes.plus-editor',
|
||||
'@standardnotes/simple-task-editor': 'org.standardnotes.simple-task-editor',
|
||||
'@standardnotes/solarized-dark-theme': 'org.standardnotes.theme-solarized-dark',
|
||||
'@standardnotes/spreadsheets': 'org.standardnotes.standard-sheets',
|
||||
'@standardnotes/titanium-theme': 'org.standardnotes.theme-titanium',
|
||||
'@standardnotes/bold-editor': 'org.standardnotes.bold-editor',
|
||||
'@standardnotes/markdown-basic': 'org.standardnotes.simple-markdown-editor',
|
||||
'@standardnotes/markdown-math': 'org.standardnotes.fancy-markdown-editor',
|
||||
'@standardnotes/markdown-minimal': 'org.standardnotes.minimal-markdown-editor',
|
||||
}
|
||||
|
||||
const BasePath = path.join(__dirname, '../../../node_modules')
|
||||
|
||||
const FilesToCopy = ['index.html', 'dist', 'build', 'package.json']
|
||||
|
||||
const copyComponentAssets = async (srcComponentPath, destination, exludedFilesGlob) => {
|
||||
if (!doesDirExist(srcComponentPath)) {
|
||||
return false
|
||||
}
|
||||
|
||||
emptyExistingDir(destination)
|
||||
ensureDirExists(destination)
|
||||
|
||||
for (const file of FilesToCopy) {
|
||||
const srcFilePath = path.join(srcComponentPath, file)
|
||||
if (!fs.existsSync(srcFilePath)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const targetFilePath = path.join(destination, file)
|
||||
copyFileOrDir(srcFilePath, targetFilePath, exludedFilesGlob)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
for (const packageName of Object.keys(components)) {
|
||||
const identifier = components[packageName]
|
||||
const packagePath = `${BasePath}/${packageName}`
|
||||
|
||||
const assetsPath = `src/components/assets/${identifier}`
|
||||
fs.mkdirSync(assetsPath, { recursive: true })
|
||||
|
||||
await copyComponentAssets(packagePath, assetsPath, '**/package.json')
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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/BlockMenu'
|
||||
import { BlockOption } from './BlockMenu/BlockOption'
|
||||
|
||||
type AddButtonProps = {
|
||||
application: WebApplication
|
||||
onSelectOption: (option: BlockOption) => void
|
||||
}
|
||||
|
||||
export const AddBlockButton: FunctionComponent<AddButtonProps> = ({ application, onSelectOption }) => {
|
||||
const [showMenu, setShowMenu] = useState(true)
|
||||
|
||||
const toggleMenu = useCallback(() => {
|
||||
setShowMenu((prevValue) => !prevValue)
|
||||
}, [])
|
||||
|
||||
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={onSelectOption} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { SNNote } from '@standardnotes/snjs'
|
||||
import { FunctionComponent, useCallback, useRef } from 'react'
|
||||
import { BlockEditorController } from './BlockEditorController'
|
||||
import { AddBlockButton } from './AddButton'
|
||||
import { MultiBlockRenderer } from './BlockRender/MultiBlockRenderer'
|
||||
import { BlockOption } from './BlockMenu/BlockOption'
|
||||
|
||||
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)
|
||||
},
|
||||
[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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { NoteBlock, NoteMutator, SNComponent, SNNote } from '@standardnotes/snjs'
|
||||
import { BlockOption } from './BlockMenu/BlockOption'
|
||||
|
||||
export class BlockEditorController {
|
||||
constructor(private note: SNNote, private application: WebApplication) {
|
||||
this.note = note
|
||||
this.application = application
|
||||
}
|
||||
|
||||
deinit() {
|
||||
;(this.note as unknown) = undefined
|
||||
;(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)
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
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 flex-row items-center border-[1px] border-b border-border p-4'}
|
||||
onClick={() => onSelect}
|
||||
>
|
||||
<Icon type={option.icon} size={'large'} />
|
||||
<div className={'ml-3 text-base'}>{option.label}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { IconType, SNComponent } from '@standardnotes/snjs'
|
||||
|
||||
export type BlockOption = {
|
||||
editorIdentifier: string
|
||||
label: string
|
||||
identifier: string
|
||||
icon: IconType | string
|
||||
iconTint: number
|
||||
component?: SNComponent
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const MaxBlockHeight: Record<string, number> = {
|
||||
'org.standardnotes.standard-sheets': 300,
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
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="remove" size="custom" className="h-8 w-8 md:h-5 md:w-5" />
|
||||
</button>
|
||||
)}
|
||||
<ComponentView key={viewer.identifier} componentViewer={viewer} application={application} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -45,6 +45,7 @@ export const createEditorMenuGroups = (application: WebApplication, editors: SNC
|
||||
spreadsheet: [],
|
||||
authentication: [],
|
||||
others: [],
|
||||
blocks: [],
|
||||
}
|
||||
|
||||
GetFeatures()
|
||||
|
||||
@@ -200,6 +200,7 @@ const ComponentView: FunctionComponent<IProps> = ({ application, onLoad, compone
|
||||
{error === ComponentViewerError.MissingUrl && <UrlMissing componentName={component.displayName} />}
|
||||
{component.uuid && isComponentValid && (
|
||||
<iframe
|
||||
className="h-full w-full grow bg-transparent"
|
||||
ref={iframeRef}
|
||||
onLoad={onIframeLoad}
|
||||
data-component-viewer-id={componentViewer.identifier}
|
||||
|
||||
@@ -13,16 +13,10 @@ import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
import Dropdown from '@/Components/Dropdown/Dropdown'
|
||||
import { DropdownItem } from '@/Components/Dropdown/DropdownItem'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { PLAIN_EDITOR_NAME } from '@/Constants/Constants'
|
||||
import { AnyTag } from '@/Controllers/Navigation/AnyTagType'
|
||||
import { PreferenceMode } from './PreferenceMode'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const PlainEditorType = 'plain-editor'
|
||||
|
||||
type EditorOption = DropdownItem & {
|
||||
value: FeatureIdentifier | typeof PlainEditorType
|
||||
}
|
||||
import { getDropdownItemsForAllEditors, PlainEditorType } from '@/Utils/DropdownItemsForEditors'
|
||||
|
||||
const PrefChangeDebounceTimeInMs = 25
|
||||
|
||||
@@ -139,32 +133,7 @@ const NewNotePreferences: FunctionComponent<Props> = ({
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const editors = application.componentManager
|
||||
.componentsForArea(ComponentArea.Editor)
|
||||
.map((editor): EditorOption => {
|
||||
const identifier = editor.package_info.identifier
|
||||
const [iconType, tint] = application.iconsController.getIconAndTintForNoteType(editor.package_info.note_type)
|
||||
|
||||
return {
|
||||
label: editor.displayName,
|
||||
value: identifier,
|
||||
...(iconType ? { icon: iconType } : null),
|
||||
...(tint ? { iconClassName: `text-accessory-tint-${tint}` } : null),
|
||||
}
|
||||
})
|
||||
.concat([
|
||||
{
|
||||
icon: 'plain-text',
|
||||
iconClassName: 'text-accessory-tint-1',
|
||||
label: PLAIN_EDITOR_NAME,
|
||||
value: PlainEditorType,
|
||||
},
|
||||
])
|
||||
.sort((a, b) => {
|
||||
return a.label.toLowerCase() < b.label.toLowerCase() ? -1 : 1
|
||||
})
|
||||
|
||||
setEditorItems(editors)
|
||||
setEditorItems(getDropdownItemsForAllEditors(application))
|
||||
}, [application])
|
||||
|
||||
const setDefaultEditor = (value: string) => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
import { StringDeleteNote, STRING_DELETE_LOCKED_ATTEMPT, STRING_DELETE_PLACEHOLDER_ATTEMPT } from '@/Constants/Strings'
|
||||
import { log, LoggingDomain } from '@/Logging'
|
||||
import { debounce, isDesktopApplication, isMobileScreen, isTabletScreen } from '@/Utils'
|
||||
import { debounce, isDesktopApplication, isDev, isMobileScreen, isTabletScreen } from '@/Utils'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import {
|
||||
ApplicationEvent,
|
||||
@@ -32,6 +32,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 IndicatorCircle from '../IndicatorCircle/IndicatorCircle'
|
||||
import LinkedItemBubblesContainer from '../LinkedItems/LinkedItemBubblesContainer'
|
||||
import LinkedItemsButton from '../LinkedItems/LinkedItemsButton'
|
||||
@@ -54,6 +55,8 @@ function sortAlphabetically(array: SNComponent[]): SNComponent[] {
|
||||
return array.sort((a, b) => (a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1))
|
||||
}
|
||||
|
||||
const IsBlocksEnabled = isDev
|
||||
|
||||
type State = {
|
||||
availableStackComponents: SNComponent[]
|
||||
editorComponentViewer?: ComponentViewerInterface
|
||||
@@ -1024,6 +1027,15 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
|
||||
const renderHeaderOptions = isMobileScreen() ? !this.state.plaintextEditorFocused : true
|
||||
|
||||
const editorMode =
|
||||
IsBlocksEnabled && this.note.title.toLowerCase().includes('blocks')
|
||||
? 'blocks'
|
||||
: this.state.editorStateDidLoad && !this.state.editorComponentViewer && !this.state.textareaUnloading
|
||||
? 'plain'
|
||||
: this.state.editorComponentViewer
|
||||
? 'component'
|
||||
: 'plain'
|
||||
|
||||
return (
|
||||
<div aria-label="Note" className="section editor sn-component h-full md:max-h-full" ref={this.noteViewElementRef}>
|
||||
{this.note && (
|
||||
@@ -1117,7 +1129,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
|
||||
<div
|
||||
id={ElementIds.EditorContent}
|
||||
className={`${ElementIds.EditorContent} z-editor-content`}
|
||||
className={`${ElementIds.EditorContent} z-editor-content overflow-scroll`}
|
||||
ref={this.editorContentRef}
|
||||
>
|
||||
{this.state.marginResizersEnabled && this.editorContentRef.current ? (
|
||||
@@ -1134,7 +1146,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{this.state.editorComponentViewer && (
|
||||
{editorMode === 'component' && this.state.editorComponentViewer && (
|
||||
<div className="component-view">
|
||||
<ComponentView
|
||||
key={this.state.editorComponentViewer.identifier}
|
||||
@@ -1146,7 +1158,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{this.state.editorStateDidLoad && !this.state.editorComponentViewer && !this.state.textareaUnloading && (
|
||||
{editorMode === 'plain' && (
|
||||
<textarea
|
||||
autoComplete="off"
|
||||
dir="auto"
|
||||
@@ -1166,6 +1178,12 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
></textarea>
|
||||
)}
|
||||
|
||||
{editorMode === 'blocks' && (
|
||||
<div className={classNames('blocks-editor w-full flex-grow overflow-hidden overflow-y-scroll')}>
|
||||
<BlockEditor key={this.note.uuid} application={this.application} note={this.note} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{this.state.marginResizersEnabled && this.editorContentRef.current ? (
|
||||
<PanelResizer
|
||||
minWidth={300}
|
||||
|
||||
@@ -2,9 +2,6 @@ import { WebApplication } from '@/Application/Application'
|
||||
import { ClientDisplayableError, FeatureDescription } from '@standardnotes/snjs'
|
||||
import { makeAutoObservable, observable } from 'mobx'
|
||||
import { AnyPackageType } from '../Types/AnyPackageType'
|
||||
import { ComponentChecksumsType } from '@standardnotes/components-meta'
|
||||
import RawComponentChecksumsFile from '@standardnotes/components-meta/dist/zips/checksums.json'
|
||||
const ComponentChecksums = RawComponentChecksumsFile as ComponentChecksumsType
|
||||
|
||||
export class PackageProvider {
|
||||
static async load(application: WebApplication): Promise<PackageProvider | undefined> {
|
||||
@@ -38,6 +35,6 @@ function collectFeatures(features: FeatureDescription[] | undefined, versionMap:
|
||||
}
|
||||
|
||||
for (const feature of features) {
|
||||
versionMap.set(feature.identifier, ComponentChecksums[feature.identifier].version)
|
||||
versionMap.set(feature.identifier, 'Latest')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ export enum LoggingDomain {
|
||||
NavigationList,
|
||||
Viewport,
|
||||
Selection,
|
||||
BlockEditor,
|
||||
}
|
||||
|
||||
const LoggingStatus: Record<LoggingDomain, boolean> = {
|
||||
@@ -17,6 +18,7 @@ const LoggingStatus: Record<LoggingDomain, boolean> = {
|
||||
[LoggingDomain.NavigationList]: false,
|
||||
[LoggingDomain.Viewport]: false,
|
||||
[LoggingDomain.Selection]: false,
|
||||
[LoggingDomain.BlockEditor]: true,
|
||||
}
|
||||
|
||||
export function log(domain: LoggingDomain, ...args: any[]): void {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ComponentArea, FeatureIdentifier } from '@standardnotes/features'
|
||||
import { DropdownItem } from '@/Components/Dropdown/DropdownItem'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { PLAIN_EDITOR_NAME } from '@/Constants/Constants'
|
||||
|
||||
export const PlainEditorType = 'plain-editor'
|
||||
|
||||
export type EditorOption = DropdownItem & {
|
||||
value: FeatureIdentifier | typeof PlainEditorType
|
||||
}
|
||||
|
||||
export function getDropdownItemsForAllEditors(application: WebApplication) {
|
||||
const options = application.componentManager
|
||||
.componentsForArea(ComponentArea.Editor)
|
||||
.map((editor): EditorOption => {
|
||||
const identifier = editor.package_info.identifier
|
||||
const [iconType, tint] = application.iconsController.getIconAndTintForNoteType(editor.package_info.note_type)
|
||||
|
||||
return {
|
||||
label: editor.displayName,
|
||||
value: identifier,
|
||||
...(iconType ? { icon: iconType } : null),
|
||||
...(tint ? { iconClassName: `text-accessory-tint-${tint}` } : null),
|
||||
}
|
||||
})
|
||||
.concat([
|
||||
{
|
||||
icon: 'plain-text',
|
||||
iconClassName: 'text-accessory-tint-1',
|
||||
label: PLAIN_EDITOR_NAME,
|
||||
value: PlainEditorType,
|
||||
},
|
||||
])
|
||||
.sort((a, b) => {
|
||||
return a.label.toLowerCase() < b.label.toLowerCase() ? -1 : 1
|
||||
})
|
||||
|
||||
return options
|
||||
}
|
||||
@@ -112,14 +112,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Optionally use if .component-view container is not flex-based
|
||||
.component-view-container {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.component-view {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
@@ -147,16 +139,4 @@
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
iframe {
|
||||
// We're disabling flex: 1; because on Firefox, it causes weird sizing issues with component stack items.
|
||||
// Not sure yet if totally required.
|
||||
// Update: The extensions manager doesn't display correctly without it
|
||||
// flex-grow: 1 should fix that.
|
||||
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,5 +23,5 @@
|
||||
"@Controllers/*": ["src/javascripts/Controllers/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "coverage"]
|
||||
"exclude": ["node_modules", "dist", "coverage", "src/components", "src/vendor"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user