feat(labs): super editor (#2001)
This commit is contained in:
@@ -39,6 +39,7 @@ type BlocksEditorProps = {
|
|||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
previewLength: number;
|
previewLength: number;
|
||||||
spellcheck?: boolean;
|
spellcheck?: boolean;
|
||||||
|
ignoreFirstChange?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
|
export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
|
||||||
@@ -47,11 +48,12 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
|
|||||||
children,
|
children,
|
||||||
previewLength,
|
previewLength,
|
||||||
spellcheck,
|
spellcheck,
|
||||||
|
ignoreFirstChange = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [didIgnoreFirstChange, setDidIgnoreFirstChange] = useState(false);
|
const [didIgnoreFirstChange, setDidIgnoreFirstChange] = useState(false);
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(editorState: EditorState, _editor: LexicalEditor) => {
|
(editorState: EditorState, _editor: LexicalEditor) => {
|
||||||
if (!didIgnoreFirstChange) {
|
if (ignoreFirstChange && !didIgnoreFirstChange) {
|
||||||
setDidIgnoreFirstChange(true);
|
setDidIgnoreFirstChange(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -88,7 +90,7 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
|
|||||||
{children}
|
{children}
|
||||||
<RichTextPlugin
|
<RichTextPlugin
|
||||||
contentEditable={
|
contentEditable={
|
||||||
<div id="blocks-editor" className="editor-scroller">
|
<div id="blocks-editor" className="editor-scroller h-full">
|
||||||
<div className="editor" ref={onRef}>
|
<div className="editor" ref={onRef}>
|
||||||
<ContentEditable
|
<ContentEditable
|
||||||
id={SuperEditorContentId}
|
id={SuperEditorContentId}
|
||||||
@@ -120,7 +122,6 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
|
|||||||
<CodeHighlightPlugin />
|
<CodeHighlightPlugin />
|
||||||
<LinkPlugin />
|
<LinkPlugin />
|
||||||
<HashtagPlugin />
|
<HashtagPlugin />
|
||||||
|
|
||||||
<AutoEmbedPlugin />
|
<AutoEmbedPlugin />
|
||||||
<TwitterPlugin />
|
<TwitterPlugin />
|
||||||
<YouTubePlugin />
|
<YouTubePlugin />
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { FeatureIdentifier } from './../Feature/FeatureIdentifier'
|
||||||
|
|
||||||
|
type ThirdPartyIdentifier = string
|
||||||
|
|
||||||
|
export type EditorIdentifier = FeatureIdentifier | ThirdPartyIdentifier
|
||||||
17
packages/features/src/Domain/Component/NoteType.spec.ts
Normal file
17
packages/features/src/Domain/Component/NoteType.spec.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { FeatureIdentifier } from '@standardnotes/features'
|
||||||
|
import { noteTypeForEditorIdentifier, NoteType } from './NoteType'
|
||||||
|
|
||||||
|
describe('note type', () => {
|
||||||
|
it('should return the correct note type for editor identifier', () => {
|
||||||
|
expect(noteTypeForEditorIdentifier(FeatureIdentifier.PlainEditor)).toEqual(NoteType.Plain)
|
||||||
|
expect(noteTypeForEditorIdentifier(FeatureIdentifier.SuperEditor)).toEqual(NoteType.Super)
|
||||||
|
expect(noteTypeForEditorIdentifier(FeatureIdentifier.MarkdownVisualEditor)).toEqual(NoteType.Markdown)
|
||||||
|
expect(noteTypeForEditorIdentifier(FeatureIdentifier.MarkdownProEditor)).toEqual(NoteType.Markdown)
|
||||||
|
expect(noteTypeForEditorIdentifier(FeatureIdentifier.PlusEditor)).toEqual(NoteType.RichText)
|
||||||
|
expect(noteTypeForEditorIdentifier(FeatureIdentifier.CodeEditor)).toEqual(NoteType.Code)
|
||||||
|
expect(noteTypeForEditorIdentifier(FeatureIdentifier.SheetsEditor)).toEqual(NoteType.Spreadsheet)
|
||||||
|
expect(noteTypeForEditorIdentifier(FeatureIdentifier.TaskEditor)).toEqual(NoteType.Task)
|
||||||
|
expect(noteTypeForEditorIdentifier(FeatureIdentifier.TokenVaultEditor)).toEqual(NoteType.Authentication)
|
||||||
|
expect(noteTypeForEditorIdentifier('org.third.party')).toEqual(NoteType.Unknown)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import { FindNativeFeature } from '../Feature/Features'
|
||||||
|
import { FeatureIdentifier } from './../Feature/FeatureIdentifier'
|
||||||
|
import { EditorIdentifier } from './EditorIdentifier'
|
||||||
|
|
||||||
export enum NoteType {
|
export enum NoteType {
|
||||||
Authentication = 'authentication',
|
Authentication = 'authentication',
|
||||||
Code = 'code',
|
Code = 'code',
|
||||||
@@ -6,6 +10,21 @@ export enum NoteType {
|
|||||||
Spreadsheet = 'spreadsheet',
|
Spreadsheet = 'spreadsheet',
|
||||||
Task = 'task',
|
Task = 'task',
|
||||||
Plain = 'plain-text',
|
Plain = 'plain-text',
|
||||||
Blocks = 'blocks',
|
Super = 'super',
|
||||||
Unknown = 'unknown',
|
Unknown = 'unknown',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function noteTypeForEditorIdentifier(identifier: EditorIdentifier): NoteType {
|
||||||
|
if (identifier === FeatureIdentifier.PlainEditor) {
|
||||||
|
return NoteType.Plain
|
||||||
|
} else if (identifier === FeatureIdentifier.SuperEditor) {
|
||||||
|
return NoteType.Super
|
||||||
|
}
|
||||||
|
|
||||||
|
const feature = FindNativeFeature(identifier as FeatureIdentifier)
|
||||||
|
if (feature && feature.note_type) {
|
||||||
|
return feature.note_type
|
||||||
|
}
|
||||||
|
|
||||||
|
return NoteType.Unknown
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type RoleFields = {
|
|||||||
|
|
||||||
/** Statically populated. Non-influencing; used as a reference by other static consumers (such as email service) */
|
/** Statically populated. Non-influencing; used as a reference by other static consumers (such as email service) */
|
||||||
availableInSubscriptions: SubscriptionName[]
|
availableInSubscriptions: SubscriptionName[]
|
||||||
|
availableInRoles?: RoleName[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BaseFeatureDescription = RoleFields & {
|
export type BaseFeatureDescription = RoleFields & {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ export enum FeatureIdentifier {
|
|||||||
DailyGDriveBackup = 'org.standardnotes.daily-gdrive-backup',
|
DailyGDriveBackup = 'org.standardnotes.daily-gdrive-backup',
|
||||||
DailyOneDriveBackup = 'org.standardnotes.daily-onedrive-backup',
|
DailyOneDriveBackup = 'org.standardnotes.daily-onedrive-backup',
|
||||||
Files = 'org.standardnotes.files',
|
Files = 'org.standardnotes.files',
|
||||||
FilesBeta = 'org.standardnotes.files-beta',
|
|
||||||
FilesLowStorageTier = 'org.standardnotes.files-low-storage-tier',
|
FilesLowStorageTier = 'org.standardnotes.files-low-storage-tier',
|
||||||
FilesMaximumStorageTier = 'org.standardnotes.files-max-storage-tier',
|
FilesMaximumStorageTier = 'org.standardnotes.files-max-storage-tier',
|
||||||
ListedCustomDomain = 'org.standardnotes.listed-custom-domain',
|
ListedCustomDomain = 'org.standardnotes.listed-custom-domain',
|
||||||
@@ -28,10 +27,12 @@ export enum FeatureIdentifier {
|
|||||||
SolarizedDarkTheme = 'org.standardnotes.theme-solarized-dark',
|
SolarizedDarkTheme = 'org.standardnotes.theme-solarized-dark',
|
||||||
TitaniumTheme = 'org.standardnotes.theme-titanium',
|
TitaniumTheme = 'org.standardnotes.theme-titanium',
|
||||||
|
|
||||||
|
PlainEditor = 'com.standardnotes.plain-text',
|
||||||
|
SuperEditor = 'com.standardnotes.super-editor',
|
||||||
|
|
||||||
CodeEditor = 'org.standardnotes.code-editor',
|
CodeEditor = 'org.standardnotes.code-editor',
|
||||||
MarkdownProEditor = 'org.standardnotes.advanced-markdown-editor',
|
MarkdownProEditor = 'org.standardnotes.advanced-markdown-editor',
|
||||||
MarkdownVisualEditor = 'org.standardnotes.markdown-visual-editor',
|
MarkdownVisualEditor = 'org.standardnotes.markdown-visual-editor',
|
||||||
PlainTextEditor = 'org.standardnotes.plain-text-editor',
|
|
||||||
PlusEditor = 'org.standardnotes.plus-editor',
|
PlusEditor = 'org.standardnotes.plus-editor',
|
||||||
SheetsEditor = 'org.standardnotes.standard-sheets',
|
SheetsEditor = 'org.standardnotes.standard-sheets',
|
||||||
TaskEditor = 'org.standardnotes.simple-task-editor',
|
TaskEditor = 'org.standardnotes.simple-task-editor',
|
||||||
@@ -50,4 +51,4 @@ export enum FeatureIdentifier {
|
|||||||
*/
|
*/
|
||||||
export const LegacyFileSafeIdentifier = 'org.standardnotes.legacy.file-safe'
|
export const LegacyFileSafeIdentifier = 'org.standardnotes.legacy.file-safe'
|
||||||
|
|
||||||
export const ExperimentalFeatures = []
|
export const ExperimentalFeatures = [FeatureIdentifier.SuperEditor]
|
||||||
|
|||||||
@@ -21,18 +21,11 @@ export function clientFeatures(): ClientFeatureDescription[] {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||||
name: 'Encrypted files (coming soon)',
|
name: 'Encrypted files',
|
||||||
identifier: FeatureIdentifier.Files,
|
identifier: FeatureIdentifier.Files,
|
||||||
permission_name: PermissionName.Files,
|
permission_name: PermissionName.Files,
|
||||||
description: '',
|
description: '',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
|
||||||
name: 'Encrypted files beta',
|
|
||||||
identifier: FeatureIdentifier.FilesBeta,
|
|
||||||
permission_name: PermissionName.FilesBeta,
|
|
||||||
description: '',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||||
name: 'Focus Mode',
|
name: 'Focus Mode',
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
|
import { FeatureIdentifier } from './../Feature/FeatureIdentifier'
|
||||||
|
import { RoleName, SubscriptionName } from '@standardnotes/common'
|
||||||
import { FeatureDescription } from '../Feature/FeatureDescription'
|
import { FeatureDescription } from '../Feature/FeatureDescription'
|
||||||
|
import { PermissionName } from '../Permission/PermissionName'
|
||||||
|
|
||||||
export function experimentalFeatures(): FeatureDescription[] {
|
export function experimentalFeatures(): FeatureDescription[] {
|
||||||
return []
|
const superEditor: FeatureDescription = {
|
||||||
|
name: 'Super Notes',
|
||||||
|
identifier: FeatureIdentifier.SuperEditor,
|
||||||
|
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||||
|
permission_name: PermissionName.SuperEditor,
|
||||||
|
description:
|
||||||
|
'A new way to edit notes. Type / to bring up the block selection menu, or @ to embed images or link other tags and notes. Type - then space to start a list, or [] then space to start a checklist. Drag and drop an image or file to embed it in your note.',
|
||||||
|
availableInRoles: [RoleName.PlusUser, RoleName.ProUser],
|
||||||
|
}
|
||||||
|
|
||||||
|
return [superEditor]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,4 +39,5 @@ export enum PermissionName {
|
|||||||
TokenVaultEditor = 'editor:token-vault',
|
TokenVaultEditor = 'editor:token-vault',
|
||||||
TwoFactorAuth = 'server:two-factor-auth',
|
TwoFactorAuth = 'server:two-factor-auth',
|
||||||
SubscriptionSharing = 'server:subscription-sharing',
|
SubscriptionSharing = 'server:subscription-sharing',
|
||||||
|
SuperEditor = 'editor:super-editor',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,3 +11,4 @@ export * from './Component/ComponentFlag'
|
|||||||
export * from './Component/ComponentPermission'
|
export * from './Component/ComponentPermission'
|
||||||
export * from './Component/NoteType'
|
export * from './Component/NoteType'
|
||||||
export * from './Component/ThemeDockIcon'
|
export * from './Component/ThemeDockIcon'
|
||||||
|
export * from './Component/EditorIdentifier'
|
||||||
|
|||||||
@@ -114,7 +114,8 @@ export class SNComponent extends DecryptedItem<ComponentContent> implements Comp
|
|||||||
return this.content_type === ContentType.Theme || this.area === ComponentArea.Themes
|
return this.content_type === ContentType.Theme || this.area === ComponentArea.Themes
|
||||||
}
|
}
|
||||||
|
|
||||||
public isDefaultEditor(): boolean {
|
/** @deprecated Use global application PrefKey.DefaultEditorIdentifier */
|
||||||
|
public legacyIsDefaultEditor(): boolean {
|
||||||
return this.getAppDomainValue(AppDataField.DefaultEditor) === true
|
return this.getAppDomainValue(AppDataField.DefaultEditor) === true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,6 @@ export class ComponentMutator extends DecryptedItemMutator<ComponentContent> {
|
|||||||
this.mutableContent.isMobileDefault = isMobileDefault
|
this.mutableContent.isMobileDefault = isMobileDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
set defaultEditor(defaultEditor: boolean) {
|
|
||||||
this.setAppDataItem(AppDataField.DefaultEditor, defaultEditor)
|
|
||||||
}
|
|
||||||
|
|
||||||
set componentData(componentData: Record<string, unknown>) {
|
set componentData(componentData: Record<string, unknown>) {
|
||||||
this.mutableContent.componentData = componentData
|
this.mutableContent.componentData = componentData
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FeatureIdentifier } from '@standardnotes/features'
|
import { EditorIdentifier } from '@standardnotes/features'
|
||||||
import { NewNoteTitleFormat } from '../UserPrefs'
|
import { NewNoteTitleFormat } from '../UserPrefs'
|
||||||
import { CollectionSortProperty } from './../../Runtime/Collection/CollectionSort'
|
import { CollectionSortProperty } from './../../Runtime/Collection/CollectionSort'
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ export interface TagPreferences {
|
|||||||
hideEditorIcon?: boolean
|
hideEditorIcon?: boolean
|
||||||
newNoteTitleFormat?: NewNoteTitleFormat
|
newNoteTitleFormat?: NewNoteTitleFormat
|
||||||
customNoteTitleFormat?: string
|
customNoteTitleFormat?: string
|
||||||
editorIdentifier?: FeatureIdentifier | string
|
editorIdentifier?: EditorIdentifier
|
||||||
entryMode?: 'normal' | 'daily'
|
entryMode?: 'normal' | 'daily'
|
||||||
panelWidth?: number
|
panelWidth?: number
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CollectionSortProperty } from '../../Runtime/Collection/CollectionSort'
|
import { CollectionSortProperty } from '../../Runtime/Collection/CollectionSort'
|
||||||
import { FeatureIdentifier } from '@standardnotes/features'
|
import { EditorIdentifier, FeatureIdentifier } from '@standardnotes/features'
|
||||||
|
|
||||||
export enum PrefKey {
|
export enum PrefKey {
|
||||||
TagsPanelWidth = 'tagsPanelWidth',
|
TagsPanelWidth = 'tagsPanelWidth',
|
||||||
@@ -38,6 +38,7 @@ export enum PrefKey {
|
|||||||
CustomNoteTitleFormat = 'customNoteTitleFormat',
|
CustomNoteTitleFormat = 'customNoteTitleFormat',
|
||||||
UpdateSavingStatusIndicator = 'updateSavingStatusIndicator',
|
UpdateSavingStatusIndicator = 'updateSavingStatusIndicator',
|
||||||
DarkMode = 'darkMode',
|
DarkMode = 'darkMode',
|
||||||
|
DefaultEditorIdentifier = 'defaultEditorIdentifier',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum NewNoteTitleFormat {
|
export enum NewNoteTitleFormat {
|
||||||
@@ -101,4 +102,5 @@ export type PrefValue = {
|
|||||||
[PrefKey.EditorFontSize]: EditorFontSize
|
[PrefKey.EditorFontSize]: EditorFontSize
|
||||||
[PrefKey.UpdateSavingStatusIndicator]: boolean
|
[PrefKey.UpdateSavingStatusIndicator]: boolean
|
||||||
[PrefKey.DarkMode]: boolean
|
[PrefKey.DarkMode]: boolean
|
||||||
|
[PrefKey.DefaultEditorIdentifier]: EditorIdentifier
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,6 @@ export interface ComponentManagerInterface {
|
|||||||
urlOverride?: string,
|
urlOverride?: string,
|
||||||
): ComponentViewerInterface
|
): ComponentViewerInterface
|
||||||
presentPermissionsDialog(_dialog: PermissionDialog): void
|
presentPermissionsDialog(_dialog: PermissionDialog): void
|
||||||
getDefaultEditor(): SNComponent | undefined
|
legacyGetDefaultEditor(): SNComponent | undefined
|
||||||
componentWithIdentifier(identifier: FeatureIdentifier | string): SNComponent | undefined
|
componentWithIdentifier(identifier: FeatureIdentifier | string): SNComponent | undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import { SNApplication } from './../Application/Application'
|
|
||||||
import { ContentType } from '@standardnotes/common'
|
|
||||||
import { MutatorService } from './../Services/Mutator/MutatorService'
|
|
||||||
import { SNComponentManager } from './../Services/ComponentManager/ComponentManager'
|
|
||||||
import { NoteType } from '@standardnotes/features'
|
|
||||||
import { NoteViewController } from './NoteViewController'
|
|
||||||
|
|
||||||
describe('note view controller', () => {
|
|
||||||
let application: SNApplication
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
application = {} as jest.Mocked<SNApplication>
|
|
||||||
application.streamItems = jest.fn()
|
|
||||||
|
|
||||||
const componentManager = {} as jest.Mocked<SNComponentManager>
|
|
||||||
componentManager.getDefaultEditor = jest.fn()
|
|
||||||
Object.defineProperty(application, 'componentManager', { value: componentManager })
|
|
||||||
|
|
||||||
const mutator = {} as jest.Mocked<MutatorService>
|
|
||||||
mutator.createTemplateItem = jest.fn()
|
|
||||||
Object.defineProperty(application, 'mutator', { value: mutator })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should create notes with plaintext note type', async () => {
|
|
||||||
const controller = new NoteViewController(application)
|
|
||||||
await controller.initialize(false)
|
|
||||||
|
|
||||||
expect(application.mutator.createTemplateItem).toHaveBeenCalledWith(
|
|
||||||
ContentType.Note,
|
|
||||||
expect.objectContaining({ noteType: NoteType.Plain }),
|
|
||||||
expect.anything(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,5 +1 @@
|
|||||||
export * from './NoteViewController'
|
|
||||||
export * from './FileViewController'
|
|
||||||
export * from './ItemGroupController'
|
|
||||||
export * from './ReactNativeToWebEvent'
|
export * from './ReactNativeToWebEvent'
|
||||||
export * from './TemplateNoteViewControllerOptions'
|
|
||||||
|
|||||||
@@ -619,7 +619,7 @@ export class SNComponentManager
|
|||||||
return editor
|
return editor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const defaultEditor = this.getDefaultEditor()
|
const defaultEditor = this.legacyGetDefaultEditor()
|
||||||
|
|
||||||
if (defaultEditor && !defaultEditor.isExplicitlyDisabledForItem(note.uuid)) {
|
if (defaultEditor && !defaultEditor.isExplicitlyDisabledForItem(note.uuid)) {
|
||||||
return defaultEditor
|
return defaultEditor
|
||||||
@@ -628,9 +628,9 @@ export class SNComponentManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getDefaultEditor(): SNComponent | undefined {
|
legacyGetDefaultEditor(): SNComponent | undefined {
|
||||||
const editors = this.componentsForArea(ComponentArea.Editor)
|
const editors = this.componentsForArea(ComponentArea.Editor)
|
||||||
return editors.filter((e) => e.isDefaultEditor())[0]
|
return editors.filter((e) => e.legacyIsDefaultEditor())[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
permissionsStringForPermissions(permissions: ComponentPermission[], component: SNComponent): string {
|
permissionsStringForPermissions(permissions: ComponentPermission[], component: SNComponent): string {
|
||||||
|
|||||||
@@ -173,24 +173,19 @@ export class SNFeaturesService
|
|||||||
|
|
||||||
public enableExperimentalFeature(identifier: FeaturesImports.FeatureIdentifier): void {
|
public enableExperimentalFeature(identifier: FeaturesImports.FeatureIdentifier): void {
|
||||||
const feature = this.getUserFeature(identifier)
|
const feature = this.getUserFeature(identifier)
|
||||||
if (!feature) {
|
|
||||||
throw Error('Attempting to enable a feature user does not have access to.')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.enabledExperimentalFeatures.push(identifier)
|
this.enabledExperimentalFeatures.push(identifier)
|
||||||
|
|
||||||
void this.storageService.setValue(StorageKey.ExperimentalFeatures, this.enabledExperimentalFeatures)
|
void this.storageService.setValue(StorageKey.ExperimentalFeatures, this.enabledExperimentalFeatures)
|
||||||
|
|
||||||
void this.mapRemoteNativeFeaturesToItems([feature])
|
if (feature) {
|
||||||
|
void this.mapRemoteNativeFeaturesToItems([feature])
|
||||||
|
}
|
||||||
|
|
||||||
void this.notifyEvent(FeaturesEvent.FeaturesUpdated)
|
void this.notifyEvent(FeaturesEvent.FeaturesUpdated)
|
||||||
}
|
}
|
||||||
|
|
||||||
public disableExperimentalFeature(identifier: FeaturesImports.FeatureIdentifier): void {
|
public disableExperimentalFeature(identifier: FeaturesImports.FeatureIdentifier): void {
|
||||||
const feature = this.getUserFeature(identifier)
|
|
||||||
if (!feature) {
|
|
||||||
throw Error('Attempting to disable a feature user does not have access to.')
|
|
||||||
}
|
|
||||||
|
|
||||||
removeFromArray(this.enabledExperimentalFeatures, identifier)
|
removeFromArray(this.enabledExperimentalFeatures, identifier)
|
||||||
|
|
||||||
void this.storageService.setValue(StorageKey.ExperimentalFeatures, this.enabledExperimentalFeatures)
|
void this.storageService.setValue(StorageKey.ExperimentalFeatures, this.enabledExperimentalFeatures)
|
||||||
@@ -486,6 +481,16 @@ export class SNFeaturesService
|
|||||||
return FeatureStatus.Entitled
|
return FeatureStatus.Entitled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isExperimentalFeature(featureId)) {
|
||||||
|
const nativeFeature = FeaturesImports.FindNativeFeature(featureId)
|
||||||
|
if (nativeFeature) {
|
||||||
|
const hasRole = this.roles.some((role) => nativeFeature.availableInRoles?.includes(role))
|
||||||
|
if (hasRole) {
|
||||||
|
return FeatureStatus.Entitled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const isDeprecated = this.isFeatureDeprecated(featureId)
|
const isDeprecated = this.isFeatureDeprecated(featureId)
|
||||||
if (isDeprecated) {
|
if (isDeprecated) {
|
||||||
if (this.hasPaidOnlineOrOfflineSubscription()) {
|
if (this.hasPaidOnlineOrOfflineSubscription()) {
|
||||||
|
|||||||
95
packages/web/src/javascripts/Application/Application.spec.ts
Normal file
95
packages/web/src/javascripts/Application/Application.spec.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
/**
|
||||||
|
* @jest-environment jsdom
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
Environment,
|
||||||
|
FeatureIdentifier,
|
||||||
|
namespacedKey,
|
||||||
|
Platform,
|
||||||
|
RawStorageKey,
|
||||||
|
SNComponent,
|
||||||
|
SNComponentManager,
|
||||||
|
SNLog,
|
||||||
|
SNTag,
|
||||||
|
} from '@standardnotes/snjs'
|
||||||
|
import { WebApplication } from '@/Application/Application'
|
||||||
|
import { WebOrDesktopDevice } from './Device/WebOrDesktopDevice'
|
||||||
|
|
||||||
|
describe('web application', () => {
|
||||||
|
let application: WebApplication
|
||||||
|
let componentManager: SNComponentManager
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
SNLog.onLog = console.log
|
||||||
|
SNLog.onError = console.error
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const identifier = '123'
|
||||||
|
|
||||||
|
window.matchMedia = jest.fn().mockReturnValue({ matches: true, addListener: jest.fn() })
|
||||||
|
|
||||||
|
const device = {
|
||||||
|
environment: Environment.Desktop,
|
||||||
|
appVersion: '1.2.3',
|
||||||
|
setApplication: jest.fn(),
|
||||||
|
openDatabase: jest.fn().mockReturnValue(Promise.resolve()),
|
||||||
|
getRawStorageValue: jest.fn().mockImplementation((key) => {
|
||||||
|
if (key === namespacedKey(identifier, RawStorageKey.SnjsVersion)) {
|
||||||
|
return '10.0.0'
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}),
|
||||||
|
setRawStorageValue: jest.fn(),
|
||||||
|
} as unknown as jest.Mocked<WebOrDesktopDevice>
|
||||||
|
|
||||||
|
application = new WebApplication(device, Platform.MacWeb, identifier, 'https://sync', 'https://socket')
|
||||||
|
|
||||||
|
componentManager = {} as jest.Mocked<SNComponentManager>
|
||||||
|
componentManager.legacyGetDefaultEditor = jest.fn()
|
||||||
|
Object.defineProperty(application, 'componentManager', { value: componentManager })
|
||||||
|
|
||||||
|
application.prepareForLaunch({ receiveChallenge: jest.fn() })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('geDefaultEditorIdentifier', () => {
|
||||||
|
it('should return plain editor if no default tag editor or component editor', () => {
|
||||||
|
const editorIdentifier = application.geDefaultEditorIdentifier()
|
||||||
|
|
||||||
|
expect(editorIdentifier).toEqual(FeatureIdentifier.PlainEditor)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return pref key based value if available', () => {
|
||||||
|
application.getPreference = jest.fn().mockReturnValue(FeatureIdentifier.SuperEditor)
|
||||||
|
|
||||||
|
const editorIdentifier = application.geDefaultEditorIdentifier()
|
||||||
|
|
||||||
|
expect(editorIdentifier).toEqual(FeatureIdentifier.SuperEditor)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return default tag identifier if tag supplied', () => {
|
||||||
|
const tag = {
|
||||||
|
preferences: {
|
||||||
|
editorIdentifier: FeatureIdentifier.SuperEditor,
|
||||||
|
},
|
||||||
|
} as jest.Mocked<SNTag>
|
||||||
|
|
||||||
|
const editorIdentifier = application.geDefaultEditorIdentifier(tag)
|
||||||
|
|
||||||
|
expect(editorIdentifier).toEqual(FeatureIdentifier.SuperEditor)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return legacy editor identifier', () => {
|
||||||
|
const editor = {
|
||||||
|
legacyIsDefaultEditor: jest.fn().mockReturnValue(true),
|
||||||
|
identifier: FeatureIdentifier.MarkdownProEditor,
|
||||||
|
} as unknown as jest.Mocked<SNComponent>
|
||||||
|
|
||||||
|
componentManager.legacyGetDefaultEditor = jest.fn().mockReturnValue(editor)
|
||||||
|
|
||||||
|
const editorIdentifier = application.geDefaultEditorIdentifier()
|
||||||
|
|
||||||
|
expect(editorIdentifier).toEqual(FeatureIdentifier.MarkdownProEditor)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
DeinitSource,
|
DeinitSource,
|
||||||
Platform,
|
Platform,
|
||||||
SNApplication,
|
SNApplication,
|
||||||
ItemGroupController,
|
|
||||||
removeFromArray,
|
removeFromArray,
|
||||||
DesktopDeviceInterface,
|
DesktopDeviceInterface,
|
||||||
isDesktopDevice,
|
isDesktopDevice,
|
||||||
@@ -20,6 +19,8 @@ import {
|
|||||||
MobileUnlockTiming,
|
MobileUnlockTiming,
|
||||||
InternalEventBus,
|
InternalEventBus,
|
||||||
DecryptedItem,
|
DecryptedItem,
|
||||||
|
EditorIdentifier,
|
||||||
|
FeatureIdentifier,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import { makeObservable, observable } from 'mobx'
|
import { makeObservable, observable } from 'mobx'
|
||||||
import { PanelResizedData } from '@/Types/PanelResizedData'
|
import { PanelResizedData } from '@/Types/PanelResizedData'
|
||||||
@@ -40,6 +41,8 @@ import { PrefDefaults } from '@/Constants/PrefDefaults'
|
|||||||
import { setCustomViewportHeight } from '@/setViewportHeightWithFallback'
|
import { setCustomViewportHeight } from '@/setViewportHeightWithFallback'
|
||||||
import { WebServices } from './WebServices'
|
import { WebServices } from './WebServices'
|
||||||
import { FeatureName } from '@/Controllers/FeatureName'
|
import { FeatureName } from '@/Controllers/FeatureName'
|
||||||
|
import { ItemGroupController } from '@/Components/NoteView/Controller/ItemGroupController'
|
||||||
|
import { VisibilityObserver } from './VisibilityObserver'
|
||||||
|
|
||||||
export type WebEventObserver = (event: WebAppEvent, data?: unknown) => void
|
export type WebEventObserver = (event: WebAppEvent, data?: unknown) => void
|
||||||
|
|
||||||
@@ -47,10 +50,10 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
|||||||
private webServices!: WebServices
|
private webServices!: WebServices
|
||||||
private webEventObservers: WebEventObserver[] = []
|
private webEventObservers: WebEventObserver[] = []
|
||||||
public itemControllerGroup: ItemGroupController
|
public itemControllerGroup: ItemGroupController
|
||||||
private onVisibilityChange: () => void
|
|
||||||
private mobileWebReceiver?: MobileWebReceiver
|
private mobileWebReceiver?: MobileWebReceiver
|
||||||
private androidBackHandler?: AndroidBackHandler
|
private androidBackHandler?: AndroidBackHandler
|
||||||
public readonly routeService: RouteServiceInterface
|
public readonly routeService: RouteServiceInterface
|
||||||
|
private visibilityObserver?: VisibilityObserver
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
deviceInterface: WebOrDesktopDevice,
|
deviceInterface: WebOrDesktopDevice,
|
||||||
@@ -106,14 +109,10 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.onVisibilityChange = () => {
|
|
||||||
const visible = document.visibilityState === 'visible'
|
|
||||||
const event = visible ? WebAppEvent.WindowDidFocus : WebAppEvent.WindowDidBlur
|
|
||||||
this.notifyWebEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isDesktopApplication()) {
|
if (!isDesktopApplication()) {
|
||||||
document.addEventListener('visibilitychange', this.onVisibilityChange)
|
this.visibilityObserver = new VisibilityObserver((event) => {
|
||||||
|
this.notifyWebEvent(event)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,8 +143,10 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
|||||||
|
|
||||||
this.webEventObservers.length = 0
|
this.webEventObservers.length = 0
|
||||||
|
|
||||||
document.removeEventListener('visibilitychange', this.onVisibilityChange)
|
if (this.visibilityObserver) {
|
||||||
;(this.onVisibilityChange as unknown) = undefined
|
this.visibilityObserver.deinit()
|
||||||
|
this.visibilityObserver = undefined
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error while deiniting application', error)
|
console.error('Error while deiniting application', error)
|
||||||
}
|
}
|
||||||
@@ -379,4 +380,13 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
|||||||
showAccountMenu(): void {
|
showAccountMenu(): void {
|
||||||
this.getViewControllerManager().accountMenuController.setShow(true)
|
this.getViewControllerManager().accountMenuController.setShow(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
geDefaultEditorIdentifier(currentTag?: SNTag): EditorIdentifier {
|
||||||
|
return (
|
||||||
|
currentTag?.preferences?.editorIdentifier ||
|
||||||
|
this.getPreference(PrefKey.DefaultEditorIdentifier) ||
|
||||||
|
this.componentManager.legacyGetDefaultEditor()?.identifier ||
|
||||||
|
FeatureIdentifier.PlainEditor
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { WebAppEvent } from '@standardnotes/snjs'
|
||||||
|
|
||||||
|
export class VisibilityObserver {
|
||||||
|
private raceTimeout?: ReturnType<typeof setTimeout>
|
||||||
|
|
||||||
|
constructor(private onEvent: (event: WebAppEvent) => void) {
|
||||||
|
/**
|
||||||
|
* Browsers may handle focus and visibilitychange events differently.
|
||||||
|
* Focus better handles window focus events but may not handle tab switching.
|
||||||
|
* We will listen for both and debouce notifying so that the most recent event wins.
|
||||||
|
*/
|
||||||
|
document.addEventListener('visibilitychange', this.onVisibilityChange)
|
||||||
|
window.addEventListener('focus', this.onFocusEvent, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
onVisibilityChange = () => {
|
||||||
|
const visible = document.visibilityState === 'visible'
|
||||||
|
const event = visible ? WebAppEvent.WindowDidFocus : WebAppEvent.WindowDidBlur
|
||||||
|
this.notifyEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
onFocusEvent = () => {
|
||||||
|
this.notifyEvent(WebAppEvent.WindowDidFocus)
|
||||||
|
}
|
||||||
|
|
||||||
|
private notifyEvent(event: WebAppEvent): void {
|
||||||
|
if (this.raceTimeout) {
|
||||||
|
clearTimeout(this.raceTimeout)
|
||||||
|
}
|
||||||
|
this.raceTimeout = setTimeout(() => {
|
||||||
|
this.onEvent(event)
|
||||||
|
}, 250)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit(): void {
|
||||||
|
document.removeEventListener('visibilitychange', this.onVisibilityChange)
|
||||||
|
window.removeEventListener('focus', this.onFocusEvent)
|
||||||
|
;(this.onEvent as unknown) = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { WebApplication } from '@/Application/Application'
|
|
||||||
import { NoteMutator, SNNote } from '@standardnotes/snjs'
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
async save(values: { text: string; previewPlain: string; previewHtml?: string }): Promise<void> {
|
|
||||||
await this.application.mutator.changeAndSaveItem<NoteMutator>(this.note, (mutator) => {
|
|
||||||
mutator.text = values.text
|
|
||||||
mutator.preview_plain = values.previewPlain
|
|
||||||
mutator.preview_html = values.previewHtml
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,7 @@ import { EditorMenuItem } from '@/Components/NotesOptions/EditorMenuItem'
|
|||||||
import { createEditorMenuGroups } from '../../Utils/createEditorMenuGroups'
|
import { createEditorMenuGroups } from '../../Utils/createEditorMenuGroups'
|
||||||
import { reloadFont } from '../NoteView/FontFunctions'
|
import { reloadFont } from '../NoteView/FontFunctions'
|
||||||
import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
|
import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
|
||||||
import { SuperNoteImporter } from '../BlockEditor/SuperNoteImporter'
|
import { SuperNoteImporter } from '../NoteView/SuperEditor/SuperNoteImporter'
|
||||||
|
|
||||||
type ChangeEditorMenuProps = {
|
type ChangeEditorMenuProps = {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
@@ -114,7 +114,7 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itemToBeSelected.noteType === NoteType.Blocks) {
|
if (itemToBeSelected.noteType === NoteType.Super) {
|
||||||
setPendingSuperItem(itemToBeSelected)
|
setPendingSuperItem(itemToBeSelected)
|
||||||
handleDisableClickoutsideRequest?.()
|
handleDisableClickoutsideRequest?.()
|
||||||
setShowSuperImporter(true)
|
setShowSuperImporter(true)
|
||||||
|
|||||||
@@ -1,12 +1,4 @@
|
|||||||
import {
|
import { FeatureIdentifier, NewNoteTitleFormat, PrefKey, EditorIdentifier, TagPreferences } from '@standardnotes/snjs'
|
||||||
ComponentArea,
|
|
||||||
ComponentMutator,
|
|
||||||
FeatureIdentifier,
|
|
||||||
NewNoteTitleFormat,
|
|
||||||
PrefKey,
|
|
||||||
SNComponent,
|
|
||||||
TagPreferences,
|
|
||||||
} from '@standardnotes/snjs'
|
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { ChangeEventHandler, FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
|
import { ChangeEventHandler, FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||||
@@ -16,31 +8,14 @@ import { WebApplication } from '@/Application/Application'
|
|||||||
import { AnyTag } from '@/Controllers/Navigation/AnyTagType'
|
import { AnyTag } from '@/Controllers/Navigation/AnyTagType'
|
||||||
import { PreferenceMode } from './PreferenceMode'
|
import { PreferenceMode } from './PreferenceMode'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { getDropdownItemsForAllEditors, PlainEditorType } from '@/Utils/DropdownItemsForEditors'
|
import { EditorOption, getDropdownItemsForAllEditors } from '@/Utils/DropdownItemsForEditors'
|
||||||
|
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||||
|
import { NoteTitleFormatOptions } from './NoteTitleFormatOptions'
|
||||||
|
|
||||||
const PrefChangeDebounceTimeInMs = 25
|
const PrefChangeDebounceTimeInMs = 25
|
||||||
|
|
||||||
const HelpPageUrl = 'https://day.js.org/docs/en/display/format#list-of-all-available-formats'
|
const HelpPageUrl = 'https://day.js.org/docs/en/display/format#list-of-all-available-formats'
|
||||||
|
|
||||||
const NoteTitleFormatOptions = [
|
|
||||||
{
|
|
||||||
label: 'Current date and time',
|
|
||||||
value: NewNoteTitleFormat.CurrentDateAndTime,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Current note count',
|
|
||||||
value: NewNoteTitleFormat.CurrentNoteCount,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Custom format',
|
|
||||||
value: NewNoteTitleFormat.CustomFormat,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Empty',
|
|
||||||
value: NewNoteTitleFormat.Empty,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
selectedTag: AnyTag
|
selectedTag: AnyTag
|
||||||
@@ -57,22 +32,24 @@ const NewNotePreferences: FunctionComponent<Props> = ({
|
|||||||
disabled,
|
disabled,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [editorItems, setEditorItems] = useState<DropdownItem[]>([])
|
const [editorItems, setEditorItems] = useState<DropdownItem[]>([])
|
||||||
const [defaultEditorIdentifier, setDefaultEditorIdentifier] = useState<string>(PlainEditorType)
|
const [defaultEditorIdentifier, setDefaultEditorIdentifier] = useState<EditorIdentifier>(
|
||||||
|
FeatureIdentifier.PlainEditor,
|
||||||
|
)
|
||||||
const [newNoteTitleFormat, setNewNoteTitleFormat] = useState<NewNoteTitleFormat>(
|
const [newNoteTitleFormat, setNewNoteTitleFormat] = useState<NewNoteTitleFormat>(
|
||||||
NewNoteTitleFormat.CurrentDateAndTime,
|
NewNoteTitleFormat.CurrentDateAndTime,
|
||||||
)
|
)
|
||||||
const [customNoteTitleFormat, setCustomNoteTitleFormat] = useState('')
|
const [customNoteTitleFormat, setCustomNoteTitleFormat] = useState('')
|
||||||
|
|
||||||
const getGlobalEditorDefault = useCallback((): SNComponent | undefined => {
|
const getGlobalEditorDefaultIdentifier = useCallback((): string => {
|
||||||
return application.componentManager.componentsForArea(ComponentArea.Editor).filter((e) => e.isDefaultEditor())[0]
|
return application.geDefaultEditorIdentifier()
|
||||||
}, [application])
|
}, [application])
|
||||||
|
|
||||||
const reloadPreferences = useCallback(() => {
|
const reloadPreferences = useCallback(() => {
|
||||||
if (mode === 'tag' && selectedTag.preferences?.editorIdentifier) {
|
if (mode === 'tag' && selectedTag.preferences?.editorIdentifier) {
|
||||||
setDefaultEditorIdentifier(selectedTag.preferences?.editorIdentifier)
|
setDefaultEditorIdentifier(selectedTag.preferences?.editorIdentifier)
|
||||||
} else {
|
} else {
|
||||||
const globalDefault = getGlobalEditorDefault()
|
const globalDefault = getGlobalEditorDefaultIdentifier()
|
||||||
setDefaultEditorIdentifier(globalDefault?.identifier || PlainEditorType)
|
setDefaultEditorIdentifier(globalDefault)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode === 'tag' && selectedTag.preferences?.newNoteTitleFormat) {
|
if (mode === 'tag' && selectedTag.preferences?.newNoteTitleFormat) {
|
||||||
@@ -82,7 +59,14 @@ const NewNotePreferences: FunctionComponent<Props> = ({
|
|||||||
application.getPreference(PrefKey.NewNoteTitleFormat, PrefDefaults[PrefKey.NewNoteTitleFormat]),
|
application.getPreference(PrefKey.NewNoteTitleFormat, PrefDefaults[PrefKey.NewNoteTitleFormat]),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, [mode, selectedTag, application, getGlobalEditorDefault, setDefaultEditorIdentifier, setNewNoteTitleFormat])
|
}, [
|
||||||
|
mode,
|
||||||
|
selectedTag,
|
||||||
|
application,
|
||||||
|
getGlobalEditorDefaultIdentifier,
|
||||||
|
setDefaultEditorIdentifier,
|
||||||
|
setNewNoteTitleFormat,
|
||||||
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mode === 'tag' && selectedTag.preferences?.customNoteTitleFormat) {
|
if (mode === 'tag' && selectedTag.preferences?.customNoteTitleFormat) {
|
||||||
@@ -107,52 +91,22 @@ const NewNotePreferences: FunctionComponent<Props> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeEditorGlobalDefault = (application: WebApplication, component: SNComponent) => {
|
|
||||||
application.mutator
|
|
||||||
.changeAndSaveItem(component, (m) => {
|
|
||||||
const mutator = m as ComponentMutator
|
|
||||||
mutator.defaultEditor = false
|
|
||||||
})
|
|
||||||
.catch(console.error)
|
|
||||||
}
|
|
||||||
|
|
||||||
const makeEditorGlobalDefault = (
|
|
||||||
application: WebApplication,
|
|
||||||
component: SNComponent,
|
|
||||||
currentDefault?: SNComponent,
|
|
||||||
) => {
|
|
||||||
if (currentDefault) {
|
|
||||||
removeEditorGlobalDefault(application, currentDefault)
|
|
||||||
}
|
|
||||||
application.mutator
|
|
||||||
.changeAndSaveItem(component, (m) => {
|
|
||||||
const mutator = m as ComponentMutator
|
|
||||||
mutator.defaultEditor = true
|
|
||||||
})
|
|
||||||
.catch(console.error)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setEditorItems(getDropdownItemsForAllEditors(application))
|
setEditorItems(getDropdownItemsForAllEditors(application))
|
||||||
}, [application])
|
}, [application])
|
||||||
|
|
||||||
const setDefaultEditor = (value: string) => {
|
const setDefaultEditor = useCallback(
|
||||||
setDefaultEditorIdentifier(value as FeatureIdentifier)
|
(value: EditorOption['value']) => {
|
||||||
|
setDefaultEditorIdentifier(value as FeatureIdentifier)
|
||||||
|
|
||||||
if (mode === 'global') {
|
if (mode === 'global') {
|
||||||
const editors = application.componentManager.componentsForArea(ComponentArea.Editor)
|
void application.setPreference(PrefKey.DefaultEditorIdentifier, value)
|
||||||
const currentDefault = getGlobalEditorDefault()
|
} else {
|
||||||
|
void changePreferencesCallback({ editorIdentifier: value })
|
||||||
if (value !== PlainEditorType) {
|
|
||||||
const editorComponent = editors.filter((e) => e.package_info.identifier === value)[0]
|
|
||||||
makeEditorGlobalDefault(application, editorComponent, currentDefault)
|
|
||||||
} else if (currentDefault) {
|
|
||||||
removeEditorGlobalDefault(application, currentDefault)
|
|
||||||
}
|
}
|
||||||
} else {
|
},
|
||||||
void changePreferencesCallback({ editorIdentifier: value })
|
[application, changePreferencesCallback, mode],
|
||||||
}
|
)
|
||||||
}
|
|
||||||
|
|
||||||
const debounceTimeoutRef = useRef<number>()
|
const debounceTimeoutRef = useRef<number>()
|
||||||
|
|
||||||
@@ -187,7 +141,7 @@ const NewNotePreferences: FunctionComponent<Props> = ({
|
|||||||
label="Select the default note type"
|
label="Select the default note type"
|
||||||
items={editorItems}
|
items={editorItems}
|
||||||
value={defaultEditorIdentifier}
|
value={defaultEditorIdentifier}
|
||||||
onChange={setDefaultEditor}
|
onChange={(value) => setDefaultEditor(value as EditorOption['value'])}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -211,7 +165,10 @@ const NewNotePreferences: FunctionComponent<Props> = ({
|
|||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<input
|
<input
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="w-full min-w-55 rounded border border-solid border-passive-3 bg-default px-2 py-1.5 text-sm focus-within:ring-2 focus-within:ring-info"
|
className={classNames(
|
||||||
|
'w-full min-w-55 rounded border border-solid border-passive-3 bg-default px-2 py-1.5 text-sm',
|
||||||
|
'focus-within:ring-2 focus-within:ring-info',
|
||||||
|
)}
|
||||||
placeholder="e.g. YYYY-MM-DD"
|
placeholder="e.g. YYYY-MM-DD"
|
||||||
value={customNoteTitleFormat}
|
value={customNoteTitleFormat}
|
||||||
onChange={handleCustomFormatInputChange}
|
onChange={handleCustomFormatInputChange}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { NewNoteTitleFormat } from '@standardnotes/snjs'
|
||||||
|
|
||||||
|
export const NoteTitleFormatOptions = [
|
||||||
|
{
|
||||||
|
label: 'Current date and time',
|
||||||
|
value: NewNoteTitleFormat.CurrentDateAndTime,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Current note count',
|
||||||
|
value: NewNoteTitleFormat.CurrentNoteCount,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Custom format',
|
||||||
|
value: NewNoteTitleFormat.CustomFormat,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Empty',
|
||||||
|
value: NewNoteTitleFormat.Empty,
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -25,9 +25,6 @@ const ListItemNotePreviewText: FunctionComponent<Props> = ({ item, hidePreview,
|
|||||||
{!item.preview_html && item.preview_plain && (
|
{!item.preview_html && item.preview_plain && (
|
||||||
<div className={`leading-1.3 line-clamp-${lineLimit} mt-1 overflow-hidden`}>{item.preview_plain}</div>
|
<div className={`leading-1.3 line-clamp-${lineLimit} mt-1 overflow-hidden`}>{item.preview_plain}</div>
|
||||||
)}
|
)}
|
||||||
{!item.preview_html && !item.preview_plain && item.text && (
|
|
||||||
<div className={`leading-1.3 line-clamp-${lineLimit} mt-1 overflow-hidden`}>{item.text}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { PLAIN_EDITOR_NAME } from '@/Constants/Constants'
|
|
||||||
import { isFile, SNNote } from '@standardnotes/snjs'
|
import { isFile, SNNote } from '@standardnotes/snjs'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { FunctionComponent, useCallback, useRef } from 'react'
|
import { FunctionComponent, useCallback, useRef } from 'react'
|
||||||
@@ -36,9 +35,8 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps<SNNote>> = ({
|
|||||||
|
|
||||||
const listItemRef = useRef<HTMLDivElement>(null)
|
const listItemRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const editorForNote = application.componentManager.editorForNote(item as SNNote)
|
const noteType = item.noteType || application.componentManager.editorForNote(item)?.package_info.note_type
|
||||||
const editorName = editorForNote?.name ?? PLAIN_EDITOR_NAME
|
const [icon, tint] = getIconAndTintForNoteType(noteType)
|
||||||
const [icon, tint] = getIconAndTintForNoteType(editorForNote?.package_info.note_type)
|
|
||||||
const hasFiles = application.items.itemsReferencingItem(item).filter(isFile).length > 0
|
const hasFiles = application.items.itemsReferencingItem(item).filter(isFile).length > 0
|
||||||
|
|
||||||
const openNoteContextMenu = (posX: number, posY: number) => {
|
const openNoteContextMenu = (posX: number, posY: number) => {
|
||||||
@@ -93,7 +91,7 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps<SNNote>> = ({
|
|||||||
>
|
>
|
||||||
{!hideIcon ? (
|
{!hideIcon ? (
|
||||||
<div className="mr-0 flex flex-col items-center justify-between p-4 pr-4">
|
<div className="mr-0 flex flex-col items-center justify-between p-4 pr-4">
|
||||||
<Icon ariaLabel={`Icon for ${editorName}`} type={icon} className={`text-accessory-tint-${tint}`} />
|
<Icon type={icon} className={`text-accessory-tint-${tint}`} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="pr-4" />
|
<div className="pr-4" />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FileItem, FileViewController, NoteViewController } from '@standardnotes/snjs'
|
import { FileItem } from '@standardnotes/snjs'
|
||||||
import { AbstractComponent } from '@/Components/Abstract/PureComponent'
|
import { AbstractComponent } from '@/Components/Abstract/PureComponent'
|
||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import MultipleSelectedNotes from '@/Components/MultipleSelectedNotes/MultipleSelectedNotes'
|
import MultipleSelectedNotes from '@/Components/MultipleSelectedNotes/MultipleSelectedNotes'
|
||||||
@@ -8,6 +8,8 @@ import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
|||||||
import ResponsivePaneContent from '../ResponsivePane/ResponsivePaneContent'
|
import ResponsivePaneContent from '../ResponsivePane/ResponsivePaneContent'
|
||||||
import FileView from '../FileView/FileView'
|
import FileView from '../FileView/FileView'
|
||||||
import NoteView from '../NoteView/NoteView'
|
import NoteView from '../NoteView/NoteView'
|
||||||
|
import { NoteViewController } from '../NoteView/Controller/NoteViewController'
|
||||||
|
import { FileViewController } from '../NoteView/Controller/FileViewController'
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
showMultipleSelectedNotes: boolean
|
showMultipleSelectedNotes: boolean
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export const EditorSaveTimeoutDebounce = {
|
||||||
|
Desktop: 350,
|
||||||
|
ImmediateChange: 100,
|
||||||
|
NativeMobileWeb: 700,
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { FileItem } from '@standardnotes/models'
|
import { FileItem } from '@standardnotes/models'
|
||||||
import { ContentType } from '@standardnotes/common'
|
import { ContentType } from '@standardnotes/common'
|
||||||
import { SNApplication } from '../Application/Application'
|
import { SNApplication } from '@standardnotes/snjs'
|
||||||
import { ItemViewControllerInterface } from './ItemViewControllerInterface'
|
import { ItemViewControllerInterface } from './ItemViewControllerInterface'
|
||||||
|
|
||||||
export class FileViewController implements ItemViewControllerInterface {
|
export class FileViewController implements ItemViewControllerInterface {
|
||||||
@@ -1,32 +1,18 @@
|
|||||||
import { FileItem, PrefKey, SNNote } from '@standardnotes/models'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import { removeFromArray } from '@standardnotes/utils'
|
import { removeFromArray } from '@standardnotes/utils'
|
||||||
import { ApplicationEvent } from '@standardnotes/services'
|
import { FileItem, SNNote } from '@standardnotes/snjs'
|
||||||
|
|
||||||
import { SNApplication } from '../Application/Application'
|
|
||||||
|
|
||||||
import { NoteViewController } from './NoteViewController'
|
import { NoteViewController } from './NoteViewController'
|
||||||
import { FileViewController } from './FileViewController'
|
import { FileViewController } from './FileViewController'
|
||||||
import { TemplateNoteViewControllerOptions } from './TemplateNoteViewControllerOptions'
|
import { TemplateNoteViewControllerOptions } from './TemplateNoteViewControllerOptions'
|
||||||
|
|
||||||
type ItemControllerGroupChangeCallback = (activeController: NoteViewController | FileViewController | undefined) => void
|
type ItemControllerGroupChangeCallback = (activeController: NoteViewController | FileViewController | undefined) => void
|
||||||
|
|
||||||
type CreateItemControllerOptions = FileItem | SNNote | TemplateNoteViewControllerOptions
|
|
||||||
|
|
||||||
export class ItemGroupController {
|
export class ItemGroupController {
|
||||||
public itemControllers: (NoteViewController | FileViewController)[] = []
|
public itemControllers: (NoteViewController | FileViewController)[] = []
|
||||||
private addTagHierarchy: boolean
|
|
||||||
changeObservers: ItemControllerGroupChangeCallback[] = []
|
changeObservers: ItemControllerGroupChangeCallback[] = []
|
||||||
eventObservers: (() => void)[] = []
|
eventObservers: (() => void)[] = []
|
||||||
|
|
||||||
constructor(private application: SNApplication) {
|
constructor(private application: WebApplication) {}
|
||||||
this.addTagHierarchy = application.getPreference(PrefKey.NoteAddToParentFolders, true)
|
|
||||||
|
|
||||||
this.eventObservers.push(
|
|
||||||
application.addSingleEventObserver(ApplicationEvent.PreferencesChanged, async () => {
|
|
||||||
this.addTagHierarchy = application.getPreference(PrefKey.NoteAddToParentFolders, true)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
public deinit(): void {
|
public deinit(): void {
|
||||||
;(this.application as unknown) = undefined
|
;(this.application as unknown) = undefined
|
||||||
@@ -44,26 +30,30 @@ export class ItemGroupController {
|
|||||||
this.itemControllers.length = 0
|
this.itemControllers.length = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
async createItemController(options: CreateItemControllerOptions): Promise<NoteViewController | FileViewController> {
|
async createItemController(context: {
|
||||||
|
file?: FileItem
|
||||||
|
note?: SNNote
|
||||||
|
templateOptions?: TemplateNoteViewControllerOptions
|
||||||
|
}): Promise<NoteViewController | FileViewController> {
|
||||||
if (this.activeItemViewController) {
|
if (this.activeItemViewController) {
|
||||||
this.closeItemController(this.activeItemViewController, { notify: false })
|
this.closeItemController(this.activeItemViewController, { notify: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
let controller!: NoteViewController | FileViewController
|
let controller!: NoteViewController | FileViewController
|
||||||
|
|
||||||
if (options instanceof FileItem) {
|
if (context.file) {
|
||||||
const file = options
|
controller = new FileViewController(this.application, context.file)
|
||||||
controller = new FileViewController(this.application, file)
|
} else if (context.note) {
|
||||||
} else if (options instanceof SNNote) {
|
controller = new NoteViewController(this.application, context.note)
|
||||||
const note = options
|
} else if (context.templateOptions) {
|
||||||
controller = new NoteViewController(this.application, note)
|
controller = new NoteViewController(this.application, undefined, context.templateOptions)
|
||||||
} else {
|
} else {
|
||||||
controller = new NoteViewController(this.application, undefined, options)
|
throw Error('Invalid input to createItemController')
|
||||||
}
|
}
|
||||||
|
|
||||||
this.itemControllers.push(controller)
|
this.itemControllers.push(controller)
|
||||||
|
|
||||||
await controller.initialize(this.addTagHierarchy)
|
await controller.initialize()
|
||||||
|
|
||||||
this.notifyObservers()
|
this.notifyObservers()
|
||||||
|
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { WebApplication } from '@/Application/Application'
|
||||||
|
import { ContentType } from '@standardnotes/common'
|
||||||
|
import {
|
||||||
|
MutatorService,
|
||||||
|
SNComponentManager,
|
||||||
|
SNComponent,
|
||||||
|
SNTag,
|
||||||
|
ItemsClientInterface,
|
||||||
|
SNNote,
|
||||||
|
} from '@standardnotes/snjs'
|
||||||
|
import { FeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||||
|
import { NoteViewController } from './NoteViewController'
|
||||||
|
|
||||||
|
describe('note view controller', () => {
|
||||||
|
let application: WebApplication
|
||||||
|
let componentManager: SNComponentManager
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
application = {} as jest.Mocked<WebApplication>
|
||||||
|
application.streamItems = jest.fn()
|
||||||
|
application.getPreference = jest.fn().mockReturnValue(true)
|
||||||
|
Object.defineProperty(application, 'items', { value: {} as jest.Mocked<ItemsClientInterface> })
|
||||||
|
|
||||||
|
componentManager = {} as jest.Mocked<SNComponentManager>
|
||||||
|
componentManager.legacyGetDefaultEditor = jest.fn()
|
||||||
|
Object.defineProperty(application, 'componentManager', { value: componentManager })
|
||||||
|
|
||||||
|
const mutator = {} as jest.Mocked<MutatorService>
|
||||||
|
mutator.createTemplateItem = jest.fn().mockReturnValue({} as SNNote)
|
||||||
|
Object.defineProperty(application, 'mutator', { value: mutator })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create notes with plaintext note type', async () => {
|
||||||
|
application.geDefaultEditorIdentifier = jest.fn().mockReturnValue(FeatureIdentifier.PlainEditor)
|
||||||
|
|
||||||
|
const controller = new NoteViewController(application)
|
||||||
|
await controller.initialize()
|
||||||
|
|
||||||
|
expect(application.mutator.createTemplateItem).toHaveBeenCalledWith(
|
||||||
|
ContentType.Note,
|
||||||
|
expect.objectContaining({ noteType: NoteType.Plain }),
|
||||||
|
expect.anything(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create notes with markdown note type', async () => {
|
||||||
|
componentManager.legacyGetDefaultEditor = jest.fn().mockReturnValue({
|
||||||
|
identifier: FeatureIdentifier.MarkdownProEditor,
|
||||||
|
} as SNComponent)
|
||||||
|
|
||||||
|
componentManager.componentWithIdentifier = jest.fn().mockReturnValue({
|
||||||
|
identifier: FeatureIdentifier.MarkdownProEditor,
|
||||||
|
} as SNComponent)
|
||||||
|
|
||||||
|
application.geDefaultEditorIdentifier = jest.fn().mockReturnValue(FeatureIdentifier.MarkdownProEditor)
|
||||||
|
|
||||||
|
const controller = new NoteViewController(application)
|
||||||
|
await controller.initialize()
|
||||||
|
|
||||||
|
expect(application.mutator.createTemplateItem).toHaveBeenCalledWith(
|
||||||
|
ContentType.Note,
|
||||||
|
expect.objectContaining({ noteType: NoteType.Markdown }),
|
||||||
|
expect.anything(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should add tag to note if default tag is set', async () => {
|
||||||
|
application.geDefaultEditorIdentifier = jest.fn().mockReturnValue(FeatureIdentifier.PlainEditor)
|
||||||
|
|
||||||
|
const tag = {
|
||||||
|
uuid: 'tag-uuid',
|
||||||
|
} as jest.Mocked<SNTag>
|
||||||
|
|
||||||
|
application.items.findItem = jest.fn().mockReturnValue(tag)
|
||||||
|
application.items.addTagToNote = jest.fn()
|
||||||
|
|
||||||
|
const controller = new NoteViewController(application, undefined, { tag: tag.uuid })
|
||||||
|
await controller.initialize()
|
||||||
|
|
||||||
|
expect(controller['defaultTag']).toEqual(tag)
|
||||||
|
expect(application.items.addTagToNote).toHaveBeenCalledWith(expect.anything(), tag, expect.anything())
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { NoteType } from '@standardnotes/features'
|
import { WebApplication } from '@/Application/Application'
|
||||||
|
import { noteTypeForEditorIdentifier } from '@standardnotes/features'
|
||||||
import { InfoStrings } from '@standardnotes/services'
|
import { InfoStrings } from '@standardnotes/services'
|
||||||
import {
|
import {
|
||||||
NoteMutator,
|
NoteMutator,
|
||||||
@@ -7,13 +8,15 @@ import {
|
|||||||
NoteContent,
|
NoteContent,
|
||||||
DecryptedItemInterface,
|
DecryptedItemInterface,
|
||||||
PayloadEmitSource,
|
PayloadEmitSource,
|
||||||
|
PrefKey,
|
||||||
} from '@standardnotes/models'
|
} from '@standardnotes/models'
|
||||||
|
import { UuidString } from '@standardnotes/snjs'
|
||||||
import { removeFromArray } from '@standardnotes/utils'
|
import { removeFromArray } from '@standardnotes/utils'
|
||||||
import { ContentType } from '@standardnotes/common'
|
import { ContentType } from '@standardnotes/common'
|
||||||
import { UuidString } from '@Lib/Types/UuidString'
|
|
||||||
import { SNApplication } from '../Application/Application'
|
|
||||||
import { ItemViewControllerInterface } from './ItemViewControllerInterface'
|
import { ItemViewControllerInterface } from './ItemViewControllerInterface'
|
||||||
import { TemplateNoteViewControllerOptions } from './TemplateNoteViewControllerOptions'
|
import { TemplateNoteViewControllerOptions } from './TemplateNoteViewControllerOptions'
|
||||||
|
import { EditorSaveTimeoutDebounce } from './EditorSaveTimeoutDebounce'
|
||||||
|
import { log, LoggingDomain } from '@/Logging'
|
||||||
|
|
||||||
export type EditorValues = {
|
export type EditorValues = {
|
||||||
title: string
|
title: string
|
||||||
@@ -23,25 +26,20 @@ export type EditorValues = {
|
|||||||
const StringEllipses = '...'
|
const StringEllipses = '...'
|
||||||
const NotePreviewCharLimit = 160
|
const NotePreviewCharLimit = 160
|
||||||
|
|
||||||
const SaveTimeoutDebounc = {
|
|
||||||
Desktop: 350,
|
|
||||||
ImmediateChange: 100,
|
|
||||||
NativeMobileWeb: 700,
|
|
||||||
}
|
|
||||||
|
|
||||||
export class NoteViewController implements ItemViewControllerInterface {
|
export class NoteViewController implements ItemViewControllerInterface {
|
||||||
public item!: SNNote
|
public item!: SNNote
|
||||||
public dealloced = false
|
public dealloced = false
|
||||||
private innerValueChangeObservers: ((note: SNNote, source: PayloadEmitSource) => void)[] = []
|
private innerValueChangeObservers: ((note: SNNote, source: PayloadEmitSource) => void)[] = []
|
||||||
private removeStreamObserver?: () => void
|
private disposers: (() => void)[] = []
|
||||||
public isTemplateNote = false
|
public isTemplateNote = false
|
||||||
private saveTimeout?: ReturnType<typeof setTimeout>
|
private saveTimeout?: ReturnType<typeof setTimeout>
|
||||||
private defaultTagUuid: UuidString | undefined
|
private defaultTagUuid: UuidString | undefined
|
||||||
private defaultTag?: SNTag
|
private defaultTag?: SNTag
|
||||||
public runtimeId = `${Math.random()}`
|
public runtimeId = `${Math.random()}`
|
||||||
|
public needsInit = true
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private application: SNApplication,
|
private application: WebApplication,
|
||||||
item?: SNNote,
|
item?: SNNote,
|
||||||
public templateNoteOptions?: TemplateNoteViewControllerOptions,
|
public templateNoteOptions?: TemplateNoteViewControllerOptions,
|
||||||
) {
|
) {
|
||||||
@@ -60,8 +58,10 @@ export class NoteViewController implements ItemViewControllerInterface {
|
|||||||
|
|
||||||
deinit(): void {
|
deinit(): void {
|
||||||
this.dealloced = true
|
this.dealloced = true
|
||||||
this.removeStreamObserver?.()
|
for (const disposer of this.disposers) {
|
||||||
;(this.removeStreamObserver as unknown) = undefined
|
disposer()
|
||||||
|
}
|
||||||
|
this.disposers.length = 0
|
||||||
;(this.application as unknown) = undefined
|
;(this.application as unknown) = undefined
|
||||||
;(this.item as unknown) = undefined
|
;(this.item as unknown) = undefined
|
||||||
|
|
||||||
@@ -70,22 +70,30 @@ export class NoteViewController implements ItemViewControllerInterface {
|
|||||||
this.saveTimeout = undefined
|
this.saveTimeout = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialize(addTagHierarchy: boolean): Promise<void> {
|
async initialize(): Promise<void> {
|
||||||
if (!this.item) {
|
if (!this.needsInit) {
|
||||||
const editorIdentifier =
|
throw Error('NoteViewController already initialized')
|
||||||
this.defaultTag?.preferences?.editorIdentifier ||
|
}
|
||||||
this.application.componentManager.getDefaultEditor()?.identifier
|
|
||||||
|
|
||||||
const defaultEditor = editorIdentifier
|
log(LoggingDomain.NoteView, 'Initializing NoteViewController')
|
||||||
? this.application.componentManager.componentWithIdentifier(editorIdentifier)
|
|
||||||
: undefined
|
this.needsInit = false
|
||||||
|
|
||||||
|
const addTagHierarchy = this.application.getPreference(PrefKey.NoteAddToParentFolders, true)
|
||||||
|
|
||||||
|
if (!this.item) {
|
||||||
|
log(LoggingDomain.NoteView, 'Initializing as template note')
|
||||||
|
|
||||||
|
const editorIdentifier = this.application.geDefaultEditorIdentifier(this.defaultTag)
|
||||||
|
|
||||||
|
const noteType = noteTypeForEditorIdentifier(editorIdentifier)
|
||||||
|
|
||||||
const note = this.application.mutator.createTemplateItem<NoteContent, SNNote>(
|
const note = this.application.mutator.createTemplateItem<NoteContent, SNNote>(
|
||||||
ContentType.Note,
|
ContentType.Note,
|
||||||
{
|
{
|
||||||
text: '',
|
text: '',
|
||||||
title: this.templateNoteOptions?.title || '',
|
title: this.templateNoteOptions?.title || '',
|
||||||
noteType: defaultEditor?.noteType || NoteType.Plain,
|
noteType: noteType,
|
||||||
editorIdentifier: editorIdentifier,
|
editorIdentifier: editorIdentifier,
|
||||||
references: [],
|
references: [],
|
||||||
},
|
},
|
||||||
@@ -119,9 +127,8 @@ export class NoteViewController implements ItemViewControllerInterface {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.removeStreamObserver = this.application.streamItems<SNNote>(
|
this.disposers.push(
|
||||||
ContentType.Note,
|
this.application.streamItems<SNNote>(ContentType.Note, ({ changed, inserted, source }) => {
|
||||||
({ changed, inserted, source }) => {
|
|
||||||
if (this.dealloced) {
|
if (this.dealloced) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -137,11 +144,12 @@ export class NoteViewController implements ItemViewControllerInterface {
|
|||||||
this.item = matchingNote
|
this.item = matchingNote
|
||||||
this.notifyObservers(matchingNote, source)
|
this.notifyObservers(matchingNote, source)
|
||||||
}
|
}
|
||||||
},
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public insertTemplatedNote(): Promise<DecryptedItemInterface> {
|
public insertTemplatedNote(): Promise<DecryptedItemInterface> {
|
||||||
|
log(LoggingDomain.NoteView, 'Inserting template note')
|
||||||
this.isTemplateNote = false
|
this.isTemplateNote = false
|
||||||
return this.application.mutator.insertItem(this.item)
|
return this.application.mutator.insertItem(this.item)
|
||||||
}
|
}
|
||||||
@@ -163,29 +171,55 @@ export class NoteViewController implements ItemViewControllerInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public async saveAndAwaitLocalPropagation(params: {
|
||||||
* @param bypassDebouncer Calling save will debounce by default. You can pass true to save
|
title?: string
|
||||||
* immediately.
|
text?: string
|
||||||
* @param isUserModified This field determines if the item will be saved as a user
|
isUserModified: boolean
|
||||||
* modification, thus updating the user modified date displayed in the UI
|
|
||||||
* @param dontUpdatePreviews Whether this change should update the note's plain and HTML
|
|
||||||
* preview.
|
|
||||||
* @param customMutate A custom mutator function.
|
|
||||||
*/
|
|
||||||
public async save(dto: {
|
|
||||||
editorValues: EditorValues
|
|
||||||
bypassDebouncer?: boolean
|
bypassDebouncer?: boolean
|
||||||
isUserModified?: boolean
|
dontGeneratePreviews?: boolean
|
||||||
dontUpdatePreviews?: boolean
|
previews?: { previewPlain: string; previewHtml?: string }
|
||||||
customMutate?: (mutator: NoteMutator) => void
|
customMutate?: (mutator: NoteMutator) => void
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const title = dto.editorValues.title
|
if (this.needsInit) {
|
||||||
const text = dto.editorValues.text
|
throw Error('NoteViewController not initialized')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.saveTimeout) {
|
||||||
|
clearTimeout(this.saveTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
const noDebounce = params.bypassDebouncer || this.application.noAccount()
|
||||||
|
|
||||||
|
const syncDebouceMs = noDebounce
|
||||||
|
? EditorSaveTimeoutDebounce.ImmediateChange
|
||||||
|
: this.application.isNativeMobileWeb()
|
||||||
|
? EditorSaveTimeoutDebounce.NativeMobileWeb
|
||||||
|
: EditorSaveTimeoutDebounce.Desktop
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.saveTimeout = setTimeout(() => {
|
||||||
|
void this.undebouncedSave({ ...params, onLocalPropagationComplete: resolve })
|
||||||
|
}, syncDebouceMs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private async undebouncedSave(params: {
|
||||||
|
title?: string
|
||||||
|
text?: string
|
||||||
|
bypassDebouncer?: boolean
|
||||||
|
isUserModified?: boolean
|
||||||
|
dontGeneratePreviews?: boolean
|
||||||
|
previews?: { previewPlain: string; previewHtml?: string }
|
||||||
|
customMutate?: (mutator: NoteMutator) => void
|
||||||
|
onLocalPropagationComplete?: () => void
|
||||||
|
onRemoteSyncComplete?: () => void
|
||||||
|
}): Promise<void> {
|
||||||
|
log(LoggingDomain.NoteView, 'Saving note', params)
|
||||||
|
|
||||||
const isTemplate = this.isTemplateNote
|
const isTemplate = this.isTemplateNote
|
||||||
|
|
||||||
if (typeof document !== 'undefined' && document.hidden) {
|
if (typeof document !== 'undefined' && document.hidden) {
|
||||||
void this.application.alertService.alert(InfoStrings.SavingWhileDocumentHidden)
|
void this.application.alertService.alert(InfoStrings.SavingWhileDocumentHidden)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTemplate) {
|
if (isTemplate) {
|
||||||
@@ -201,41 +235,37 @@ export class NoteViewController implements ItemViewControllerInterface {
|
|||||||
this.item,
|
this.item,
|
||||||
(mutator) => {
|
(mutator) => {
|
||||||
const noteMutator = mutator as NoteMutator
|
const noteMutator = mutator as NoteMutator
|
||||||
if (dto.customMutate) {
|
if (params.customMutate) {
|
||||||
dto.customMutate(noteMutator)
|
params.customMutate(noteMutator)
|
||||||
}
|
}
|
||||||
noteMutator.title = title
|
|
||||||
noteMutator.text = text
|
|
||||||
|
|
||||||
if (!dto.dontUpdatePreviews) {
|
if (params.title != undefined) {
|
||||||
const noteText = text || ''
|
noteMutator.title = params.title
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.text != undefined) {
|
||||||
|
noteMutator.text = params.text
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.previews) {
|
||||||
|
noteMutator.preview_plain = params.previews.previewPlain
|
||||||
|
noteMutator.preview_html = params.previews.previewHtml
|
||||||
|
} else if (!params.dontGeneratePreviews && params.text != undefined) {
|
||||||
|
const noteText = params.text || ''
|
||||||
const truncate = noteText.length > NotePreviewCharLimit
|
const truncate = noteText.length > NotePreviewCharLimit
|
||||||
const substring = noteText.substring(0, NotePreviewCharLimit)
|
const substring = noteText.substring(0, NotePreviewCharLimit)
|
||||||
const previewPlain = substring + (truncate ? StringEllipses : '')
|
const previewPlain = substring + (truncate ? StringEllipses : '')
|
||||||
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
noteMutator.preview_plain = previewPlain
|
noteMutator.preview_plain = previewPlain
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
noteMutator.preview_html = undefined
|
noteMutator.preview_html = undefined
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dto.isUserModified,
|
params.isUserModified,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (this.saveTimeout) {
|
params.onLocalPropagationComplete?.()
|
||||||
clearTimeout(this.saveTimeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
const noDebounce = dto.bypassDebouncer || this.application.noAccount()
|
void this.application.sync.sync().then(() => {
|
||||||
|
params.onRemoteSyncComplete?.()
|
||||||
const syncDebouceMs = noDebounce
|
})
|
||||||
? SaveTimeoutDebounc.ImmediateChange
|
|
||||||
: this.application.isNativeMobileWeb()
|
|
||||||
? SaveTimeoutDebounc.NativeMobileWeb
|
|
||||||
: SaveTimeoutDebounc.Desktop
|
|
||||||
|
|
||||||
this.saveTimeout = setTimeout(() => {
|
|
||||||
void this.application.sync.sync()
|
|
||||||
}, syncDebouceMs)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { UuidString } from '@Lib/Types/UuidString'
|
import { UuidString } from '@standardnotes/snjs'
|
||||||
|
|
||||||
export type TemplateNoteViewControllerOptions = {
|
export type TemplateNoteViewControllerOptions = {
|
||||||
title?: string
|
title?: string
|
||||||
@@ -8,12 +8,12 @@ import { NotesController } from '@/Controllers/NotesController'
|
|||||||
import {
|
import {
|
||||||
ApplicationEvent,
|
ApplicationEvent,
|
||||||
ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction,
|
ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction,
|
||||||
NoteViewController,
|
|
||||||
SNNote,
|
SNNote,
|
||||||
NoteType,
|
NoteType,
|
||||||
PayloadEmitSource,
|
PayloadEmitSource,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import NoteView from './NoteView'
|
import NoteView from './NoteView'
|
||||||
|
import { NoteViewController } from './Controller/NoteViewController'
|
||||||
|
|
||||||
describe('NoteView', () => {
|
describe('NoteView', () => {
|
||||||
let noteViewController: NoteViewController
|
let noteViewController: NoteViewController
|
||||||
|
|||||||
@@ -16,30 +16,24 @@ import {
|
|||||||
ComponentArea,
|
ComponentArea,
|
||||||
ComponentViewerInterface,
|
ComponentViewerInterface,
|
||||||
ContentType,
|
ContentType,
|
||||||
EditorFontSize,
|
|
||||||
EditorLineHeight,
|
|
||||||
isPayloadSourceInternalChange,
|
isPayloadSourceInternalChange,
|
||||||
isPayloadSourceRetrieved,
|
isPayloadSourceRetrieved,
|
||||||
NoteType,
|
NoteType,
|
||||||
NoteViewController,
|
|
||||||
PayloadEmitSource,
|
PayloadEmitSource,
|
||||||
PrefKey,
|
PrefKey,
|
||||||
ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction,
|
ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction,
|
||||||
SNComponent,
|
SNComponent,
|
||||||
SNNote,
|
SNNote,
|
||||||
WebAppEvent,
|
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import { confirmDialog, KeyboardKey, KeyboardModifier } from '@standardnotes/ui-services'
|
import { confirmDialog, KeyboardKey, KeyboardModifier } from '@standardnotes/ui-services'
|
||||||
import { ChangeEventHandler, createRef, KeyboardEventHandler, RefObject } from 'react'
|
import { ChangeEventHandler, createRef, KeyboardEventHandler, RefObject } from 'react'
|
||||||
import { EditorEventSource } from '../../Types/EditorEventSource'
|
import { SuperEditor } from './SuperEditor/SuperEditor'
|
||||||
import { BlockEditor } from '../BlockEditor/BlockEditorComponent'
|
|
||||||
import IndicatorCircle from '../IndicatorCircle/IndicatorCircle'
|
import IndicatorCircle from '../IndicatorCircle/IndicatorCircle'
|
||||||
import LinkedItemBubblesContainer from '../LinkedItems/LinkedItemBubblesContainer'
|
import LinkedItemBubblesContainer from '../LinkedItems/LinkedItemBubblesContainer'
|
||||||
import LinkedItemsButton from '../LinkedItems/LinkedItemsButton'
|
import LinkedItemsButton from '../LinkedItems/LinkedItemsButton'
|
||||||
import MobileItemsListButton from '../NoteGroupView/MobileItemsListButton'
|
import MobileItemsListButton from '../NoteGroupView/MobileItemsListButton'
|
||||||
import EditingDisabledBanner from './EditingDisabledBanner'
|
import EditingDisabledBanner from './EditingDisabledBanner'
|
||||||
import { reloadFont } from './FontFunctions'
|
import { reloadFont } from './FontFunctions'
|
||||||
import { getPlaintextFontSize } from '../../Utils/getPlaintextFontSize'
|
|
||||||
import NoteStatusIndicator, { NoteStatus } from './NoteStatusIndicator'
|
import NoteStatusIndicator, { NoteStatus } from './NoteStatusIndicator'
|
||||||
import NoteViewFileDropTarget from './NoteViewFileDropTarget'
|
import NoteViewFileDropTarget from './NoteViewFileDropTarget'
|
||||||
import { NoteViewProps } from './NoteViewProps'
|
import { NoteViewProps } from './NoteViewProps'
|
||||||
@@ -48,9 +42,10 @@ import {
|
|||||||
transactionForDisassociateComponentWithCurrentNote,
|
transactionForDisassociateComponentWithCurrentNote,
|
||||||
} from './TransactionFunctions'
|
} from './TransactionFunctions'
|
||||||
import { SuperEditorContentId } from '@standardnotes/blocks-editor'
|
import { SuperEditorContentId } from '@standardnotes/blocks-editor'
|
||||||
|
import { NoteViewController } from './Controller/NoteViewController'
|
||||||
|
import { PlainEditor, PlainEditorInterface } from './PlainEditor/PlainEditor'
|
||||||
|
|
||||||
const MinimumStatusDuration = 400
|
const MinimumStatusDuration = 400
|
||||||
const TextareaDebounce = 100
|
|
||||||
const NoteEditingDisabledText = 'Note editing disabled.'
|
const NoteEditingDisabledText = 'Note editing disabled.'
|
||||||
|
|
||||||
function sortAlphabetically(array: SNComponent[]): SNComponent[] {
|
function sortAlphabetically(array: SNComponent[]): SNComponent[] {
|
||||||
@@ -63,7 +58,6 @@ type State = {
|
|||||||
editorComponentViewerDidAlreadyReload?: boolean
|
editorComponentViewerDidAlreadyReload?: boolean
|
||||||
editorStateDidLoad: boolean
|
editorStateDidLoad: boolean
|
||||||
editorTitle: string
|
editorTitle: string
|
||||||
editorText: string
|
|
||||||
isDesktop?: boolean
|
isDesktop?: boolean
|
||||||
lockText: string
|
lockText: string
|
||||||
marginResizersEnabled?: boolean
|
marginResizersEnabled?: boolean
|
||||||
@@ -75,21 +69,14 @@ type State = {
|
|||||||
spellcheck: boolean
|
spellcheck: boolean
|
||||||
stackComponentViewers: ComponentViewerInterface[]
|
stackComponentViewers: ComponentViewerInterface[]
|
||||||
syncTakingTooLong: boolean
|
syncTakingTooLong: boolean
|
||||||
/** Setting to true then false will allow the main content textarea to be destroyed
|
monospaceFont?: boolean
|
||||||
* then re-initialized. Used when reloading spellcheck status. */
|
plainEditorFocused?: boolean
|
||||||
textareaUnloading: boolean
|
|
||||||
plaintextEditorFocused?: boolean
|
|
||||||
|
|
||||||
leftResizerWidth: number
|
leftResizerWidth: number
|
||||||
leftResizerOffset: number
|
leftResizerOffset: number
|
||||||
rightResizerWidth: number
|
rightResizerWidth: number
|
||||||
rightResizerOffset: number
|
rightResizerOffset: number
|
||||||
|
|
||||||
monospaceFont?: boolean
|
|
||||||
lineHeight?: EditorLineHeight
|
|
||||||
fontSize?: EditorFontSize
|
|
||||||
updateSavingIndicator?: boolean
|
updateSavingIndicator?: boolean
|
||||||
|
|
||||||
editorFeatureIdentifier?: string
|
editorFeatureIdentifier?: string
|
||||||
noteType?: NoteType
|
noteType?: NoteType
|
||||||
}
|
}
|
||||||
@@ -98,23 +85,17 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
readonly controller!: NoteViewController
|
readonly controller!: NoteViewController
|
||||||
|
|
||||||
private statusTimeout?: NodeJS.Timeout
|
private statusTimeout?: NodeJS.Timeout
|
||||||
private lastEditorFocusEventSource?: EditorEventSource
|
|
||||||
onEditorComponentLoad?: () => void
|
onEditorComponentLoad?: () => void
|
||||||
|
|
||||||
private removeTrashKeyObserver?: () => void
|
private removeTrashKeyObserver?: () => void
|
||||||
private removeTabObserver?: () => void
|
|
||||||
private removeComponentStreamObserver?: () => void
|
private removeComponentStreamObserver?: () => void
|
||||||
private removeComponentManagerObserver?: () => void
|
private removeComponentManagerObserver?: () => void
|
||||||
private removeInnerNoteObserver?: () => void
|
private removeInnerNoteObserver?: () => void
|
||||||
private removeWebAppEventObserver: () => void
|
|
||||||
|
|
||||||
private needsAdjustMobileCursor = false
|
|
||||||
private isAdjustingMobileCursor = false
|
|
||||||
|
|
||||||
private protectionTimeoutId: ReturnType<typeof setTimeout> | null = null
|
private protectionTimeoutId: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
private noteViewElementRef: RefObject<HTMLDivElement>
|
private noteViewElementRef: RefObject<HTMLDivElement>
|
||||||
private editorContentRef: RefObject<HTMLDivElement>
|
private editorContentRef: RefObject<HTMLDivElement>
|
||||||
|
private plainEditorRef?: RefObject<PlainEditorInterface>
|
||||||
|
|
||||||
constructor(props: NoteViewProps) {
|
constructor(props: NoteViewProps) {
|
||||||
super(props, props.application)
|
super(props, props.application)
|
||||||
@@ -130,18 +111,9 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
|
|
||||||
this.debounceReloadEditorComponent = debounce(this.debounceReloadEditorComponent.bind(this), 25)
|
this.debounceReloadEditorComponent = debounce(this.debounceReloadEditorComponent.bind(this), 25)
|
||||||
|
|
||||||
this.textAreaChangeDebounceSave = debounce(this.textAreaChangeDebounceSave, TextareaDebounce)
|
|
||||||
|
|
||||||
this.removeWebAppEventObserver = props.application.addWebEventObserver((event) => {
|
|
||||||
if (event === WebAppEvent.MobileKeyboardWillChangeFrame) {
|
|
||||||
this.scrollMobileCursorIntoViewAfterWebviewResize()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
availableStackComponents: [],
|
availableStackComponents: [],
|
||||||
editorStateDidLoad: false,
|
editorStateDidLoad: false,
|
||||||
editorText: '',
|
|
||||||
editorTitle: '',
|
editorTitle: '',
|
||||||
isDesktop: isDesktopApplication(),
|
isDesktop: isDesktopApplication(),
|
||||||
lockText: NoteEditingDisabledText,
|
lockText: NoteEditingDisabledText,
|
||||||
@@ -152,7 +124,6 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
spellcheck: true,
|
spellcheck: true,
|
||||||
stackComponentViewers: [],
|
stackComponentViewers: [],
|
||||||
syncTakingTooLong: false,
|
syncTakingTooLong: false,
|
||||||
textareaUnloading: false,
|
|
||||||
leftResizerWidth: 0,
|
leftResizerWidth: 0,
|
||||||
leftResizerOffset: 0,
|
leftResizerOffset: 0,
|
||||||
rightResizerWidth: 0,
|
rightResizerWidth: 0,
|
||||||
@@ -165,16 +136,6 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
this.editorContentRef = createRef<HTMLDivElement>()
|
this.editorContentRef = createRef<HTMLDivElement>()
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollMobileCursorIntoViewAfterWebviewResize() {
|
|
||||||
if (this.needsAdjustMobileCursor) {
|
|
||||||
this.needsAdjustMobileCursor = false
|
|
||||||
this.isAdjustingMobileCursor = true
|
|
||||||
document.getElementById('note-text-editor')?.blur()
|
|
||||||
document.getElementById('note-text-editor')?.focus()
|
|
||||||
this.isAdjustingMobileCursor = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override deinit() {
|
override deinit() {
|
||||||
super.deinit()
|
super.deinit()
|
||||||
;(this.controller as unknown) = undefined
|
;(this.controller as unknown) = undefined
|
||||||
@@ -194,28 +155,20 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
this.clearNoteProtectionInactivityTimer()
|
this.clearNoteProtectionInactivityTimer()
|
||||||
;(this.ensureNoteIsInsertedBeforeUIAction as unknown) = undefined
|
;(this.ensureNoteIsInsertedBeforeUIAction as unknown) = undefined
|
||||||
|
|
||||||
this.removeWebAppEventObserver?.()
|
|
||||||
;(this.removeWebAppEventObserver as unknown) = undefined
|
|
||||||
|
|
||||||
this.removeTabObserver?.()
|
|
||||||
this.removeTabObserver = undefined
|
|
||||||
this.onEditorComponentLoad = undefined
|
this.onEditorComponentLoad = undefined
|
||||||
|
|
||||||
this.statusTimeout = undefined
|
this.statusTimeout = undefined
|
||||||
;(this.onPanelResizeFinish as unknown) = undefined
|
;(this.onPanelResizeFinish as unknown) = undefined
|
||||||
;(this.authorizeAndDismissProtectedWarning as unknown) = undefined
|
;(this.authorizeAndDismissProtectedWarning as unknown) = undefined
|
||||||
;(this.editorComponentViewerRequestsReload as unknown) = undefined
|
;(this.editorComponentViewerRequestsReload as unknown) = undefined
|
||||||
;(this.onTextAreaChange as unknown) = undefined
|
|
||||||
;(this.onTitleEnter as unknown) = undefined
|
;(this.onTitleEnter as unknown) = undefined
|
||||||
;(this.onTitleChange as unknown) = undefined
|
;(this.onTitleChange as unknown) = undefined
|
||||||
;(this.onContentFocus as unknown) = undefined
|
|
||||||
;(this.onPanelResizeFinish as unknown) = undefined
|
;(this.onPanelResizeFinish as unknown) = undefined
|
||||||
;(this.stackComponentExpanded as unknown) = undefined
|
;(this.stackComponentExpanded as unknown) = undefined
|
||||||
;(this.toggleStackComponent as unknown) = undefined
|
;(this.toggleStackComponent as unknown) = undefined
|
||||||
;(this.onSystemEditorRef as unknown) = undefined
|
|
||||||
;(this.debounceReloadEditorComponent as unknown) = undefined
|
;(this.debounceReloadEditorComponent as unknown) = undefined
|
||||||
;(this.textAreaChangeDebounceSave as unknown) = undefined
|
|
||||||
;(this.editorContentRef as unknown) = undefined
|
;(this.editorContentRef as unknown) = undefined
|
||||||
|
;(this.plainEditorRef as unknown) = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
getState() {
|
getState() {
|
||||||
@@ -271,9 +224,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
|
|
||||||
if (this.controller.isTemplateNote) {
|
if (this.controller.isTemplateNote) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (this.controller.templateNoteOptions?.autofocusBehavior === 'editor') {
|
if (this.controller.templateNoteOptions?.autofocusBehavior === 'title') {
|
||||||
this.focusEditor()
|
|
||||||
} else {
|
|
||||||
this.focusTitle()
|
this.focusTitle()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -296,34 +247,22 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
throw Error('Editor received changes for non-current note')
|
throw Error('Editor received changes for non-current note')
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = this.state.editorTitle,
|
let title = this.state.editorTitle
|
||||||
text = this.state.editorText
|
|
||||||
|
|
||||||
if (isPayloadSourceRetrieved(source)) {
|
if (isPayloadSourceRetrieved(source)) {
|
||||||
title = note.title
|
title = note.title
|
||||||
text = note.text
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.state.editorTitle) {
|
if (!this.state.editorTitle) {
|
||||||
title = note.title
|
title = note.title
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.state.editorText) {
|
|
||||||
text = note.text
|
|
||||||
}
|
|
||||||
|
|
||||||
if (title !== this.state.editorTitle) {
|
if (title !== this.state.editorTitle) {
|
||||||
this.setState({
|
this.setState({
|
||||||
editorTitle: title,
|
editorTitle: title,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (text !== this.state.editorText) {
|
|
||||||
this.setState({
|
|
||||||
editorText: text,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (note.locked !== this.state.noteLocked) {
|
if (note.locked !== this.state.noteLocked) {
|
||||||
this.setState({
|
this.setState({
|
||||||
noteLocked: note.locked,
|
noteLocked: note.locked,
|
||||||
@@ -334,7 +273,6 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
this.setState({
|
this.setState({
|
||||||
editorFeatureIdentifier: note.editorIdentifier,
|
editorFeatureIdentifier: note.editorIdentifier,
|
||||||
noteType: note.noteType,
|
noteType: note.noteType,
|
||||||
editorText: note.text,
|
|
||||||
editorTitle: note.title,
|
editorTitle: note.title,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -625,36 +563,13 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onTextAreaChange: ChangeEventHandler<HTMLTextAreaElement> = ({ currentTarget }) => {
|
|
||||||
const text = currentTarget.value
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
editorText: text,
|
|
||||||
})
|
|
||||||
|
|
||||||
this.textAreaChangeDebounceSave()
|
|
||||||
}
|
|
||||||
|
|
||||||
textAreaChangeDebounceSave = () => {
|
|
||||||
log(LoggingDomain.NoteView, 'Performing save after debounce')
|
|
||||||
this.controller
|
|
||||||
.save({
|
|
||||||
editorValues: {
|
|
||||||
title: this.state.editorTitle,
|
|
||||||
text: this.state.editorText,
|
|
||||||
},
|
|
||||||
isUserModified: true,
|
|
||||||
})
|
|
||||||
.catch(console.error)
|
|
||||||
}
|
|
||||||
|
|
||||||
onTitleEnter: KeyboardEventHandler<HTMLInputElement> = ({ key, currentTarget }) => {
|
onTitleEnter: KeyboardEventHandler<HTMLInputElement> = ({ key, currentTarget }) => {
|
||||||
if (key !== KeyboardKey.Enter) {
|
if (key !== KeyboardKey.Enter) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
currentTarget.blur()
|
currentTarget.blur()
|
||||||
this.focusEditor()
|
this.plainEditorRef?.current?.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
onTitleChange: ChangeEventHandler<HTMLInputElement> = ({ currentTarget }) => {
|
onTitleChange: ChangeEventHandler<HTMLInputElement> = ({ currentTarget }) => {
|
||||||
@@ -667,49 +582,18 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.controller
|
this.controller
|
||||||
.save({
|
.saveAndAwaitLocalPropagation({
|
||||||
editorValues: {
|
title: title,
|
||||||
title: title,
|
|
||||||
text: this.state.editorText,
|
|
||||||
},
|
|
||||||
isUserModified: true,
|
isUserModified: true,
|
||||||
dontUpdatePreviews: true,
|
dontGeneratePreviews: true,
|
||||||
})
|
})
|
||||||
.catch(console.error)
|
.catch(console.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
focusEditor() {
|
|
||||||
const element = document.getElementById(ElementIds.NoteTextEditor)
|
|
||||||
if (element) {
|
|
||||||
this.lastEditorFocusEventSource = EditorEventSource.Script
|
|
||||||
element.focus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
focusTitle() {
|
focusTitle() {
|
||||||
document.getElementById(ElementIds.NoteTitleEditor)?.focus()
|
document.getElementById(ElementIds.NoteTitleEditor)?.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
onContentFocus = () => {
|
|
||||||
if (!this.isAdjustingMobileCursor) {
|
|
||||||
this.needsAdjustMobileCursor = true
|
|
||||||
}
|
|
||||||
if (this.lastEditorFocusEventSource) {
|
|
||||||
this.application.notifyWebEvent(WebAppEvent.EditorFocused, { eventSource: this.lastEditorFocusEventSource })
|
|
||||||
}
|
|
||||||
|
|
||||||
this.lastEditorFocusEventSource = undefined
|
|
||||||
this.setState({ plaintextEditorFocused: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
onContentBlur = () => {
|
|
||||||
if (this.lastEditorFocusEventSource) {
|
|
||||||
this.application.notifyWebEvent(WebAppEvent.EditorFocused, { eventSource: this.lastEditorFocusEventSource })
|
|
||||||
}
|
|
||||||
this.lastEditorFocusEventSource = undefined
|
|
||||||
this.setState({ plaintextEditorFocused: false })
|
|
||||||
}
|
|
||||||
|
|
||||||
setShowProtectedOverlay(show: boolean) {
|
setShowProtectedOverlay(show: boolean) {
|
||||||
this.viewControllerManager.notesController.setShowProtectedWarning(show)
|
this.viewControllerManager.notesController.setShowProtectedWarning(show)
|
||||||
}
|
}
|
||||||
@@ -737,13 +621,11 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
this.performNoteDeletion(this.note)
|
this.performNoteDeletion(this.note)
|
||||||
} else {
|
} else {
|
||||||
this.controller
|
this.controller
|
||||||
.save({
|
.saveAndAwaitLocalPropagation({
|
||||||
editorValues: {
|
title: this.state.editorTitle,
|
||||||
title: this.state.editorTitle,
|
|
||||||
text: this.state.editorText,
|
|
||||||
},
|
|
||||||
bypassDebouncer: true,
|
bypassDebouncer: true,
|
||||||
dontUpdatePreviews: true,
|
dontGeneratePreviews: true,
|
||||||
|
isUserModified: true,
|
||||||
customMutate: (mutator) => {
|
customMutate: (mutator) => {
|
||||||
mutator.trashed = true
|
mutator.trashed = true
|
||||||
},
|
},
|
||||||
@@ -773,15 +655,9 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
|
|
||||||
async reloadSpellcheck() {
|
async reloadSpellcheck() {
|
||||||
const spellcheck = this.viewControllerManager.notesController.getSpellcheckStateForNote(this.note)
|
const spellcheck = this.viewControllerManager.notesController.getSpellcheckStateForNote(this.note)
|
||||||
|
|
||||||
if (spellcheck !== this.state.spellcheck) {
|
if (spellcheck !== this.state.spellcheck) {
|
||||||
this.setState({ textareaUnloading: true })
|
|
||||||
this.setState({ textareaUnloading: false })
|
|
||||||
reloadFont(this.state.monospaceFont)
|
reloadFont(this.state.monospaceFont)
|
||||||
|
this.setState({ spellcheck })
|
||||||
this.setState({
|
|
||||||
spellcheck,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -797,10 +673,6 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
PrefDefaults[PrefKey.EditorResizersEnabled],
|
PrefDefaults[PrefKey.EditorResizersEnabled],
|
||||||
)
|
)
|
||||||
|
|
||||||
const lineHeight = this.application.getPreference(PrefKey.EditorLineHeight, PrefDefaults[PrefKey.EditorLineHeight])
|
|
||||||
|
|
||||||
const fontSize = this.application.getPreference(PrefKey.EditorFontSize, PrefDefaults[PrefKey.EditorFontSize])
|
|
||||||
|
|
||||||
const updateSavingIndicator = this.application.getPreference(
|
const updateSavingIndicator = this.application.getPreference(
|
||||||
PrefKey.UpdateSavingStatusIndicator,
|
PrefKey.UpdateSavingStatusIndicator,
|
||||||
PrefDefaults[PrefKey.UpdateSavingStatusIndicator],
|
PrefDefaults[PrefKey.UpdateSavingStatusIndicator],
|
||||||
@@ -811,8 +683,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
this.setState({
|
this.setState({
|
||||||
monospaceFont,
|
monospaceFont,
|
||||||
marginResizersEnabled,
|
marginResizersEnabled,
|
||||||
lineHeight,
|
|
||||||
fontSize,
|
|
||||||
updateSavingIndicator,
|
updateSavingIndicator,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -904,82 +775,20 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onSystemEditorRef = (ref: HTMLTextAreaElement | null) => {
|
|
||||||
if (this.removeTabObserver || !ref) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log(LoggingDomain.NoteView, 'On system editor ref')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert 4 spaces when a tab key is pressed,
|
|
||||||
* only used when inside of the text editor.
|
|
||||||
* If the shift key is pressed first, this event is
|
|
||||||
* not fired.
|
|
||||||
*/
|
|
||||||
const editor = document.getElementById(ElementIds.NoteTextEditor) as HTMLInputElement
|
|
||||||
|
|
||||||
if (!editor) {
|
|
||||||
console.error('Editor is not yet mounted; unable to add tab observer.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.removeTabObserver = this.application.io.addKeyObserver({
|
|
||||||
element: editor,
|
|
||||||
key: KeyboardKey.Tab,
|
|
||||||
onKeyDown: (event) => {
|
|
||||||
if (document.hidden || this.note.locked || event.shiftKey) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
event.preventDefault()
|
|
||||||
/** Using document.execCommand gives us undo support */
|
|
||||||
const insertSuccessful = document.execCommand('insertText', false, '\t')
|
|
||||||
if (!insertSuccessful) {
|
|
||||||
/** document.execCommand works great on Chrome/Safari but not Firefox */
|
|
||||||
const start = editor.selectionStart || 0
|
|
||||||
const end = editor.selectionEnd || 0
|
|
||||||
const spaces = ' '
|
|
||||||
/** Insert 4 spaces */
|
|
||||||
editor.value = editor.value.substring(0, start) + spaces + editor.value.substring(end)
|
|
||||||
/** Place cursor 4 spaces away from where the tab key was pressed */
|
|
||||||
editor.selectionStart = editor.selectionEnd = start + 4
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
editorText: editor.value,
|
|
||||||
})
|
|
||||||
|
|
||||||
this.controller
|
|
||||||
.save({
|
|
||||||
editorValues: {
|
|
||||||
title: this.state.editorTitle,
|
|
||||||
text: this.state.editorText,
|
|
||||||
},
|
|
||||||
bypassDebouncer: true,
|
|
||||||
})
|
|
||||||
.catch(console.error)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const observer = new MutationObserver((records) => {
|
|
||||||
for (const record of records) {
|
|
||||||
record.removedNodes.forEach((node) => {
|
|
||||||
if (node === editor) {
|
|
||||||
this.removeTabObserver?.()
|
|
||||||
this.removeTabObserver = undefined
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
observer.observe(editor.parentElement as HTMLElement, { childList: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureNoteIsInsertedBeforeUIAction = async () => {
|
ensureNoteIsInsertedBeforeUIAction = async () => {
|
||||||
if (this.controller.isTemplateNote) {
|
if (this.controller.isTemplateNote) {
|
||||||
await this.controller.insertTemplatedNote()
|
await this.controller.insertTemplatedNote()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onPlainFocus = () => {
|
||||||
|
this.setState({ plainEditorFocused: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
onPlainBlur = () => {
|
||||||
|
this.setState({ plainEditorFocused: false })
|
||||||
|
}
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
if (this.controller.dealloced) {
|
if (this.controller.dealloced) {
|
||||||
return null
|
return null
|
||||||
@@ -996,12 +805,12 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderHeaderOptions = isMobileScreen() ? !this.state.plaintextEditorFocused : true
|
const renderHeaderOptions = isMobileScreen() ? !this.state.plainEditorFocused : true
|
||||||
|
|
||||||
const editorMode =
|
const editorMode =
|
||||||
this.note.noteType === NoteType.Blocks
|
this.note.noteType === NoteType.Super
|
||||||
? 'blocks'
|
? 'super'
|
||||||
: this.state.editorStateDidLoad && !this.state.editorComponentViewer && !this.state.textareaUnloading
|
: this.state.editorStateDidLoad && !this.state.editorComponentViewer
|
||||||
? 'plain'
|
? 'plain'
|
||||||
: this.state.editorComponentViewer
|
: this.state.editorComponentViewer
|
||||||
? 'component'
|
? 'component'
|
||||||
@@ -1095,7 +904,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{editorMode !== 'blocks' && (
|
{editorMode !== 'super' && (
|
||||||
<LinkedItemBubblesContainer linkingController={this.viewControllerManager.linkingController} />
|
<LinkedItemBubblesContainer linkingController={this.viewControllerManager.linkingController} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1103,7 +912,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
id={ElementIds.EditorContent}
|
id={ElementIds.EditorContent}
|
||||||
className={`${ElementIds.EditorContent} z-editor-content overflow-scroll`}
|
className={`${ElementIds.EditorContent} z-editor-content overflow-auto`}
|
||||||
ref={this.editorContentRef}
|
ref={this.editorContentRef}
|
||||||
>
|
>
|
||||||
{this.state.marginResizersEnabled && this.editorContentRef.current ? (
|
{this.state.marginResizersEnabled && this.editorContentRef.current ? (
|
||||||
@@ -1133,34 +942,26 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{editorMode === 'plain' && (
|
{editorMode === 'plain' && (
|
||||||
<textarea
|
<PlainEditor
|
||||||
autoComplete="off"
|
application={this.application}
|
||||||
dir="auto"
|
spellcheck={this.state.spellcheck}
|
||||||
id={ElementIds.NoteTextEditor}
|
ref={this.plainEditorRef}
|
||||||
onChange={this.onTextAreaChange}
|
controller={this.controller}
|
||||||
onFocus={this.onContentFocus}
|
locked={this.state.noteLocked}
|
||||||
onBlur={this.onContentBlur}
|
onFocus={this.onPlainFocus}
|
||||||
readOnly={this.state.noteLocked}
|
onBlur={this.onPlainBlur}
|
||||||
ref={(ref) => ref && this.onSystemEditorRef(ref)}
|
/>
|
||||||
spellCheck={this.state.spellcheck}
|
|
||||||
value={this.state.editorText}
|
|
||||||
className={classNames(
|
|
||||||
'editable font-editor flex-grow',
|
|
||||||
this.state.lineHeight && `leading-${this.state.lineHeight.toLowerCase()}`,
|
|
||||||
this.state.fontSize && getPlaintextFontSize(this.state.fontSize),
|
|
||||||
)}
|
|
||||||
></textarea>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{editorMode === 'blocks' && (
|
{editorMode === 'super' && (
|
||||||
<div className={classNames('blocks-editor w-full flex-grow overflow-hidden overflow-y-scroll')}>
|
<div className={classNames('blocks-editor w-full flex-grow overflow-hidden overflow-y-scroll')}>
|
||||||
<BlockEditor
|
<SuperEditor
|
||||||
key={this.note.uuid}
|
key={this.note.uuid}
|
||||||
application={this.application}
|
application={this.application}
|
||||||
note={this.note}
|
|
||||||
linkingController={this.viewControllerManager.linkingController}
|
linkingController={this.viewControllerManager.linkingController}
|
||||||
filesController={this.viewControllerManager.filesController}
|
filesController={this.viewControllerManager.filesController}
|
||||||
spellcheck={this.state.spellcheck}
|
spellcheck={this.state.spellcheck}
|
||||||
|
controller={this.controller}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { NoteViewController } from '@standardnotes/snjs'
|
|
||||||
|
|
||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
|
import { NoteViewController } from './Controller/NoteViewController'
|
||||||
|
|
||||||
export interface NoteViewProps {
|
export interface NoteViewProps {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
|
|||||||
@@ -0,0 +1,253 @@
|
|||||||
|
import { WebApplication } from '@/Application/Application'
|
||||||
|
import { usePrevious } from '@/Components/ContentListView/Calendar/usePrevious'
|
||||||
|
import { ElementIds } from '@/Constants/ElementIDs'
|
||||||
|
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||||
|
import { log, LoggingDomain } from '@/Logging'
|
||||||
|
import { Disposer } from '@/Types/Disposer'
|
||||||
|
import { EditorEventSource } from '@/Types/EditorEventSource'
|
||||||
|
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||||
|
import { getPlaintextFontSize } from '@/Utils/getPlaintextFontSize'
|
||||||
|
import {
|
||||||
|
ApplicationEvent,
|
||||||
|
EditorFontSize,
|
||||||
|
EditorLineHeight,
|
||||||
|
isPayloadSourceRetrieved,
|
||||||
|
PrefKey,
|
||||||
|
WebAppEvent,
|
||||||
|
} from '@standardnotes/snjs'
|
||||||
|
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||||
|
import { ChangeEventHandler, forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'
|
||||||
|
import { NoteViewController } from '../Controller/NoteViewController'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
application: WebApplication
|
||||||
|
spellcheck: boolean
|
||||||
|
controller: NoteViewController
|
||||||
|
locked: boolean
|
||||||
|
onFocus: () => void
|
||||||
|
onBlur: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PlainEditorInterface = {
|
||||||
|
focus: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PlainEditor = forwardRef<PlainEditorInterface, Props>(
|
||||||
|
({ application, spellcheck, controller, locked, onFocus, onBlur }: Props, ref) => {
|
||||||
|
const [editorText, setEditorText] = useState<string | undefined>()
|
||||||
|
const [textareaUnloading, setTextareaUnloading] = useState(false)
|
||||||
|
const [lineHeight, setLineHeight] = useState<EditorLineHeight | undefined>()
|
||||||
|
const [fontSize, setFontSize] = useState<EditorFontSize | undefined>()
|
||||||
|
const previousSpellcheck = usePrevious(spellcheck)
|
||||||
|
|
||||||
|
const lastEditorFocusEventSource = useRef<EditorEventSource | undefined>()
|
||||||
|
const needsAdjustMobileCursor = useRef(false)
|
||||||
|
const isAdjustingMobileCursor = useRef(false)
|
||||||
|
const note = useRef(controller.item)
|
||||||
|
|
||||||
|
const tabObserverDisposer = useRef<Disposer>()
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
focus() {
|
||||||
|
focusEditor()
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const disposer = controller.addNoteInnerValueChangeObserver((updatedNote, source) => {
|
||||||
|
if (updatedNote.uuid !== note.current.uuid) {
|
||||||
|
throw Error('Editor received changes for non-current note')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isPayloadSourceRetrieved(source) ||
|
||||||
|
editorText == undefined ||
|
||||||
|
updatedNote.editorIdentifier !== note.current.editorIdentifier ||
|
||||||
|
updatedNote.noteType !== note.current.noteType
|
||||||
|
) {
|
||||||
|
setEditorText(updatedNote.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
note.current = updatedNote
|
||||||
|
})
|
||||||
|
|
||||||
|
return disposer
|
||||||
|
}, [controller, editorText, controller.item.uuid, controller.item.editorIdentifier, controller.item.noteType])
|
||||||
|
|
||||||
|
const onTextAreaChange: ChangeEventHandler<HTMLTextAreaElement> = ({ currentTarget }) => {
|
||||||
|
const text = currentTarget.value
|
||||||
|
|
||||||
|
setEditorText(text)
|
||||||
|
|
||||||
|
void controller.saveAndAwaitLocalPropagation({ text: text, isUserModified: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const onContentFocus = useCallback(() => {
|
||||||
|
if (!isAdjustingMobileCursor.current) {
|
||||||
|
needsAdjustMobileCursor.current = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastEditorFocusEventSource.current) {
|
||||||
|
application.notifyWebEvent(WebAppEvent.EditorFocused, { eventSource: lastEditorFocusEventSource })
|
||||||
|
}
|
||||||
|
|
||||||
|
lastEditorFocusEventSource.current = undefined
|
||||||
|
onFocus()
|
||||||
|
}, [application, isAdjustingMobileCursor, lastEditorFocusEventSource, onFocus])
|
||||||
|
|
||||||
|
const onContentBlur = useCallback(() => {
|
||||||
|
if (lastEditorFocusEventSource.current) {
|
||||||
|
application.notifyWebEvent(WebAppEvent.EditorFocused, { eventSource: lastEditorFocusEventSource })
|
||||||
|
}
|
||||||
|
lastEditorFocusEventSource.current = undefined
|
||||||
|
onBlur()
|
||||||
|
}, [application, lastEditorFocusEventSource, onBlur])
|
||||||
|
|
||||||
|
const scrollMobileCursorIntoViewAfterWebviewResize = useCallback(() => {
|
||||||
|
if (needsAdjustMobileCursor.current) {
|
||||||
|
needsAdjustMobileCursor.current = false
|
||||||
|
isAdjustingMobileCursor.current = true
|
||||||
|
document.getElementById('note-text-editor')?.blur()
|
||||||
|
document.getElementById('note-text-editor')?.focus()
|
||||||
|
isAdjustingMobileCursor.current = false
|
||||||
|
}
|
||||||
|
}, [needsAdjustMobileCursor])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const disposer = application.addWebEventObserver((event) => {
|
||||||
|
if (event === WebAppEvent.MobileKeyboardWillChangeFrame) {
|
||||||
|
scrollMobileCursorIntoViewAfterWebviewResize()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return disposer
|
||||||
|
}, [application, scrollMobileCursorIntoViewAfterWebviewResize])
|
||||||
|
|
||||||
|
const focusEditor = useCallback(() => {
|
||||||
|
const element = document.getElementById(ElementIds.NoteTextEditor)
|
||||||
|
if (element) {
|
||||||
|
lastEditorFocusEventSource.current = EditorEventSource.Script
|
||||||
|
element.focus()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (controller.isTemplateNote && controller.templateNoteOptions?.autofocusBehavior === 'editor') {
|
||||||
|
setTimeout(() => {
|
||||||
|
focusEditor()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [controller, focusEditor])
|
||||||
|
|
||||||
|
const reloadPreferences = useCallback(() => {
|
||||||
|
const lineHeight = application.getPreference(PrefKey.EditorLineHeight, PrefDefaults[PrefKey.EditorLineHeight])
|
||||||
|
const fontSize = application.getPreference(PrefKey.EditorFontSize, PrefDefaults[PrefKey.EditorFontSize])
|
||||||
|
|
||||||
|
setLineHeight(lineHeight)
|
||||||
|
setFontSize(fontSize)
|
||||||
|
}, [application])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reloadPreferences()
|
||||||
|
|
||||||
|
return application.addSingleEventObserver(ApplicationEvent.PreferencesChanged, async () => {
|
||||||
|
reloadPreferences()
|
||||||
|
})
|
||||||
|
}, [reloadPreferences, application])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (spellcheck !== previousSpellcheck) {
|
||||||
|
setTextareaUnloading(true)
|
||||||
|
setTimeout(() => {
|
||||||
|
setTextareaUnloading(false)
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
}, [spellcheck, previousSpellcheck])
|
||||||
|
|
||||||
|
const onRef = (ref: HTMLTextAreaElement | null) => {
|
||||||
|
if (tabObserverDisposer.current || !ref) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log(LoggingDomain.NoteView, 'On system editor ref')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert 4 spaces when a tab key is pressed, only used when inside of the text editor.
|
||||||
|
* If the shift key is pressed first, this event is not fired.
|
||||||
|
*/
|
||||||
|
const editor = document.getElementById(ElementIds.NoteTextEditor) as HTMLInputElement
|
||||||
|
|
||||||
|
if (!editor) {
|
||||||
|
console.error('Editor is not yet mounted; unable to add tab observer.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tabObserverDisposer.current = application.io.addKeyObserver({
|
||||||
|
element: editor,
|
||||||
|
key: KeyboardKey.Tab,
|
||||||
|
onKeyDown: (event) => {
|
||||||
|
if (document.hidden || note.current.locked || event.shiftKey) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
event.preventDefault()
|
||||||
|
/** Using document.execCommand gives us undo support */
|
||||||
|
const insertSuccessful = document.execCommand('insertText', false, '\t')
|
||||||
|
if (!insertSuccessful) {
|
||||||
|
/** document.execCommand works great on Chrome/Safari but not Firefox */
|
||||||
|
const start = editor.selectionStart || 0
|
||||||
|
const end = editor.selectionEnd || 0
|
||||||
|
const spaces = ' '
|
||||||
|
/** Insert 4 spaces */
|
||||||
|
editor.value = editor.value.substring(0, start) + spaces + editor.value.substring(end)
|
||||||
|
/** Place cursor 4 spaces away from where the tab key was pressed */
|
||||||
|
editor.selectionStart = editor.selectionEnd = start + 4
|
||||||
|
}
|
||||||
|
|
||||||
|
setEditorText(editor.value)
|
||||||
|
|
||||||
|
void controller.saveAndAwaitLocalPropagation({
|
||||||
|
text: editor.value,
|
||||||
|
bypassDebouncer: true,
|
||||||
|
isUserModified: true,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const observer = new MutationObserver((records) => {
|
||||||
|
for (const record of records) {
|
||||||
|
record.removedNodes.forEach((node) => {
|
||||||
|
if (node === editor) {
|
||||||
|
tabObserverDisposer.current?.()
|
||||||
|
tabObserverDisposer.current = undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
observer.observe(editor.parentElement as HTMLElement, { childList: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textareaUnloading) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
autoComplete="off"
|
||||||
|
dir="auto"
|
||||||
|
id={ElementIds.NoteTextEditor}
|
||||||
|
onChange={onTextAreaChange}
|
||||||
|
onFocus={onContentFocus}
|
||||||
|
onBlur={onContentBlur}
|
||||||
|
readOnly={locked}
|
||||||
|
ref={(ref) => ref && onRef(ref)}
|
||||||
|
spellCheck={spellcheck}
|
||||||
|
value={editorText}
|
||||||
|
className={classNames(
|
||||||
|
'editable font-editor flex-grow',
|
||||||
|
lineHeight && `leading-${lineHeight.toLowerCase()}`,
|
||||||
|
fontSize && getPlaintextFontSize(fontSize),
|
||||||
|
)}
|
||||||
|
></textarea>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
export type ChangeEditorFunction = (jsonContent: string) => void
|
||||||
|
type ChangeEditorFunctionProvider = (changeEditorFunction: ChangeEditorFunction) => void
|
||||||
|
|
||||||
|
export function ChangeContentCallbackPlugin({
|
||||||
|
providerCallback,
|
||||||
|
}: {
|
||||||
|
providerCallback: ChangeEditorFunctionProvider
|
||||||
|
}): JSX.Element | null {
|
||||||
|
const [editor] = useLexicalComposerContext()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const changeContents: ChangeEditorFunction = (jsonContent: string) => {
|
||||||
|
editor.update(() => {
|
||||||
|
const editorState = editor.parseEditorState(jsonContent)
|
||||||
|
editor.setEditorState(editorState)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
providerCallback(changeContents)
|
||||||
|
}, [editor, providerCallback])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { INSERT_FILE_COMMAND } from './../Commands'
|
import { INSERT_FILE_COMMAND } from '../Commands'
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
|
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
@@ -9,7 +9,7 @@ import { ContentType, SNNote } from '@standardnotes/snjs'
|
|||||||
import { getLinkingSearchResults } from '@/Utils/Items/Search/getSearchResults'
|
import { getLinkingSearchResults } from '@/Utils/Items/Search/getSearchResults'
|
||||||
import Popover from '@/Components/Popover/Popover'
|
import Popover from '@/Components/Popover/Popover'
|
||||||
import { INSERT_BUBBLE_COMMAND, INSERT_FILE_COMMAND } from '../Commands'
|
import { INSERT_BUBBLE_COMMAND, INSERT_FILE_COMMAND } from '../Commands'
|
||||||
import { useLinkingController } from '../../../../Controllers/LinkingControllerProvider'
|
import { useLinkingController } from '../../../../../Controllers/LinkingControllerProvider'
|
||||||
import { PopoverClassNames } from '../ClassNames'
|
import { PopoverClassNames } from '../ClassNames'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import { SNNote } from '@standardnotes/snjs'
|
import { isPayloadSourceRetrieved } from '@standardnotes/snjs'
|
||||||
import { FunctionComponent, useCallback, useRef } from 'react'
|
import { FunctionComponent, useCallback, useEffect, useRef } from 'react'
|
||||||
import { BlockEditorController } from './BlockEditorController'
|
|
||||||
import { BlocksEditor, BlocksEditorComposer } from '@standardnotes/blocks-editor'
|
import { BlocksEditor, BlocksEditorComposer } from '@standardnotes/blocks-editor'
|
||||||
import { ItemSelectionPlugin } from './Plugins/ItemSelectionPlugin/ItemSelectionPlugin'
|
import { ItemSelectionPlugin } from './Plugins/ItemSelectionPlugin/ItemSelectionPlugin'
|
||||||
import { FileNode } from './Plugins/EncryptedFilePlugin/Nodes/FileNode'
|
import { FileNode } from './Plugins/EncryptedFilePlugin/Nodes/FileNode'
|
||||||
@@ -9,7 +8,7 @@ import FilePlugin from './Plugins/EncryptedFilePlugin/FilePlugin'
|
|||||||
import BlockPickerMenuPlugin from './Plugins/BlockPickerPlugin/BlockPickerPlugin'
|
import BlockPickerMenuPlugin from './Plugins/BlockPickerPlugin/BlockPickerPlugin'
|
||||||
import { ErrorBoundary } from '@/Utils/ErrorBoundary'
|
import { ErrorBoundary } from '@/Utils/ErrorBoundary'
|
||||||
import { LinkingController } from '@/Controllers/LinkingController'
|
import { LinkingController } from '@/Controllers/LinkingController'
|
||||||
import LinkingControllerProvider from '../../Controllers/LinkingControllerProvider'
|
import LinkingControllerProvider from '../../../Controllers/LinkingControllerProvider'
|
||||||
import { BubbleNode } from './Plugins/ItemBubblePlugin/Nodes/BubbleNode'
|
import { BubbleNode } from './Plugins/ItemBubblePlugin/Nodes/BubbleNode'
|
||||||
import ItemBubblePlugin from './Plugins/ItemBubblePlugin/ItemBubblePlugin'
|
import ItemBubblePlugin from './Plugins/ItemBubblePlugin/ItemBubblePlugin'
|
||||||
import { NodeObserverPlugin } from './Plugins/NodeObserverPlugin/NodeObserverPlugin'
|
import { NodeObserverPlugin } from './Plugins/NodeObserverPlugin/NodeObserverPlugin'
|
||||||
@@ -17,29 +16,48 @@ import { FilesController } from '@/Controllers/FilesController'
|
|||||||
import FilesControllerProvider from '@/Controllers/FilesControllerProvider'
|
import FilesControllerProvider from '@/Controllers/FilesControllerProvider'
|
||||||
import DatetimePlugin from './Plugins/DateTimePlugin/DateTimePlugin'
|
import DatetimePlugin from './Plugins/DateTimePlugin/DateTimePlugin'
|
||||||
import AutoLinkPlugin from './Plugins/AutoLinkPlugin/AutoLinkPlugin'
|
import AutoLinkPlugin from './Plugins/AutoLinkPlugin/AutoLinkPlugin'
|
||||||
|
import { NoteViewController } from '../Controller/NoteViewController'
|
||||||
|
import {
|
||||||
|
ChangeContentCallbackPlugin,
|
||||||
|
ChangeEditorFunction,
|
||||||
|
} from './Plugins/ChangeContentCallback/ChangeContentCallback'
|
||||||
|
|
||||||
const NotePreviewCharLimit = 160
|
const NotePreviewCharLimit = 160
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
note: SNNote
|
controller: NoteViewController
|
||||||
linkingController: LinkingController
|
linkingController: LinkingController
|
||||||
filesController: FilesController
|
filesController: FilesController
|
||||||
spellcheck: boolean
|
spellcheck: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BlockEditor: FunctionComponent<Props> = ({
|
export const SuperEditor: FunctionComponent<Props> = ({
|
||||||
note,
|
|
||||||
application,
|
application,
|
||||||
linkingController,
|
linkingController,
|
||||||
filesController,
|
filesController,
|
||||||
spellcheck,
|
spellcheck,
|
||||||
|
controller,
|
||||||
}) => {
|
}) => {
|
||||||
const controller = useRef(new BlockEditorController(note, application))
|
const note = useRef(controller.item)
|
||||||
|
const changeEditorFunction = useRef<ChangeEditorFunction>()
|
||||||
|
const ignoreNextChange = useRef(false)
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(value: string, preview: string) => {
|
async (value: string, preview: string) => {
|
||||||
void controller.current.save({ text: value, previewPlain: preview, previewHtml: undefined })
|
if (ignoreNextChange.current === true) {
|
||||||
|
ignoreNextChange.current = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
void controller.saveAndAwaitLocalPropagation({
|
||||||
|
text: value,
|
||||||
|
isUserModified: true,
|
||||||
|
previews: {
|
||||||
|
previewPlain: preview,
|
||||||
|
previewHtml: undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
},
|
},
|
||||||
[controller],
|
[controller],
|
||||||
)
|
)
|
||||||
@@ -54,24 +72,49 @@ export const BlockEditor: FunctionComponent<Props> = ({
|
|||||||
[linkingController, application],
|
[linkingController, application],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const disposer = controller.addNoteInnerValueChangeObserver((updatedNote, source) => {
|
||||||
|
if (updatedNote.uuid !== note.current.uuid) {
|
||||||
|
throw Error('Editor received changes for non-current note')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPayloadSourceRetrieved(source)) {
|
||||||
|
ignoreNextChange.current = true
|
||||||
|
changeEditorFunction.current?.(updatedNote.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
note.current = updatedNote
|
||||||
|
})
|
||||||
|
|
||||||
|
return disposer
|
||||||
|
}, [controller, controller.item.uuid])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-full w-full px-5 py-4">
|
<div className="relative h-full w-full px-5 py-4">
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<LinkingControllerProvider controller={linkingController}>
|
<LinkingControllerProvider controller={linkingController}>
|
||||||
<FilesControllerProvider controller={filesController}>
|
<FilesControllerProvider controller={filesController}>
|
||||||
<BlocksEditorComposer readonly={note.locked} initialValue={note.text} nodes={[FileNode, BubbleNode]}>
|
<BlocksEditorComposer
|
||||||
|
readonly={note.current.locked}
|
||||||
|
initialValue={note.current.text}
|
||||||
|
nodes={[FileNode, BubbleNode]}
|
||||||
|
>
|
||||||
<BlocksEditor
|
<BlocksEditor
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="relative relative resize-none text-base focus:shadow-none focus:outline-none"
|
ignoreFirstChange={true}
|
||||||
|
className="relative relative h-full resize-none text-base focus:shadow-none focus:outline-none"
|
||||||
previewLength={NotePreviewCharLimit}
|
previewLength={NotePreviewCharLimit}
|
||||||
spellcheck={spellcheck}
|
spellcheck={spellcheck}
|
||||||
>
|
>
|
||||||
<ItemSelectionPlugin currentNote={note} />
|
<ItemSelectionPlugin currentNote={note.current} />
|
||||||
<FilePlugin />
|
<FilePlugin />
|
||||||
<ItemBubblePlugin />
|
<ItemBubblePlugin />
|
||||||
<BlockPickerMenuPlugin />
|
<BlockPickerMenuPlugin />
|
||||||
<DatetimePlugin />
|
<DatetimePlugin />
|
||||||
<AutoLinkPlugin />
|
<AutoLinkPlugin />
|
||||||
|
<ChangeContentCallbackPlugin
|
||||||
|
providerCallback={(callback) => (changeEditorFunction.current = callback)}
|
||||||
|
/>
|
||||||
<NodeObserverPlugin nodeType={BubbleNode} onRemove={handleBubbleRemove} />
|
<NodeObserverPlugin nodeType={BubbleNode} onRemove={handleBubbleRemove} />
|
||||||
<NodeObserverPlugin nodeType={FileNode} onRemove={handleBubbleRemove} />
|
<NodeObserverPlugin nodeType={FileNode} onRemove={handleBubbleRemove} />
|
||||||
</BlocksEditor>
|
</BlocksEditor>
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import { NoteType, SNNote } from '@standardnotes/snjs'
|
import { NoteType, SNNote } from '@standardnotes/snjs'
|
||||||
import { FunctionComponent, useCallback, useState } from 'react'
|
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
|
||||||
import { BlockEditorController } from './BlockEditorController'
|
|
||||||
import { BlocksEditor, BlocksEditorComposer } from '@standardnotes/blocks-editor'
|
import { BlocksEditor, BlocksEditorComposer } from '@standardnotes/blocks-editor'
|
||||||
import { ErrorBoundary } from '@/Utils/ErrorBoundary'
|
import { ErrorBoundary } from '@/Utils/ErrorBoundary'
|
||||||
import ModalDialog from '@/Components/Shared/ModalDialog'
|
import ModalDialog from '@/Components/Shared/ModalDialog'
|
||||||
@@ -10,6 +9,7 @@ import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription'
|
|||||||
import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
|
import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
|
||||||
import Button from '@/Components/Button/Button'
|
import Button from '@/Components/Button/Button'
|
||||||
import ImportPlugin from './Plugins/ImportPlugin/ImportPlugin'
|
import ImportPlugin from './Plugins/ImportPlugin/ImportPlugin'
|
||||||
|
import { NoteViewController } from '../Controller/NoteViewController'
|
||||||
|
|
||||||
export function spaceSeparatedStrings(...strings: string[]): string {
|
export function spaceSeparatedStrings(...strings: string[]): string {
|
||||||
return strings.join(' ')
|
return strings.join(' ')
|
||||||
@@ -36,12 +36,31 @@ export const SuperNoteImporter: FunctionComponent<Props> = ({ note, application,
|
|||||||
setLastValue({ text: value, previewPlain: preview })
|
setLastValue({ text: value, previewPlain: preview })
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const confirmConvert = useCallback(() => {
|
const performConvert = useCallback(
|
||||||
const controller = new BlockEditorController(note, application)
|
async (text: string, previewPlain: string) => {
|
||||||
void controller.save({ text: lastValue.text, previewPlain: lastValue.previewPlain, previewHtml: undefined })
|
const controller = new NoteViewController(application, note)
|
||||||
|
await controller.initialize()
|
||||||
|
await controller.saveAndAwaitLocalPropagation({
|
||||||
|
text: text,
|
||||||
|
previews: { previewPlain: previewPlain, previewHtml: undefined },
|
||||||
|
isUserModified: true,
|
||||||
|
bypassDebouncer: true,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[application, note],
|
||||||
|
)
|
||||||
|
|
||||||
|
const confirmConvert = useCallback(async () => {
|
||||||
|
await performConvert(lastValue.text, lastValue.previewPlain)
|
||||||
closeDialog()
|
closeDialog()
|
||||||
onConvertComplete()
|
onConvertComplete()
|
||||||
}, [closeDialog, application, lastValue, note, onConvertComplete])
|
}, [closeDialog, performConvert, onConvertComplete, lastValue])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (note.text.length === 0) {
|
||||||
|
void confirmConvert()
|
||||||
|
}
|
||||||
|
}, [note, confirmConvert])
|
||||||
|
|
||||||
const convertAsIs = useCallback(async () => {
|
const convertAsIs = useCallback(async () => {
|
||||||
const confirmed = await application.alertService.confirm(
|
const confirmed = await application.alertService.confirm(
|
||||||
@@ -56,11 +75,11 @@ export const SuperNoteImporter: FunctionComponent<Props> = ({ note, application,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const controller = new BlockEditorController(note, application)
|
await performConvert(note.text, note.preview_plain)
|
||||||
void controller.save({ text: note.text, previewPlain: note.preview_plain, previewHtml: undefined })
|
|
||||||
closeDialog()
|
closeDialog()
|
||||||
onConvertComplete()
|
onConvertComplete()
|
||||||
}, [closeDialog, application, note, onConvertComplete])
|
}, [closeDialog, application, note, onConvertComplete, performConvert])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalDialog>
|
<ModalDialog>
|
||||||
@@ -77,6 +96,7 @@ export const SuperNoteImporter: FunctionComponent<Props> = ({ note, application,
|
|||||||
<BlocksEditorComposer readonly initialValue={''}>
|
<BlocksEditorComposer readonly initialValue={''}>
|
||||||
<BlocksEditor
|
<BlocksEditor
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
ignoreFirstChange={false}
|
||||||
className="relative relative resize-none text-base focus:shadow-none focus:outline-none"
|
className="relative relative resize-none text-base focus:shadow-none focus:outline-none"
|
||||||
previewLength={NotePreviewCharLimit}
|
previewLength={NotePreviewCharLimit}
|
||||||
spellcheck={note.spellcheck}
|
spellcheck={note.spellcheck}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { PlainEditorType } from '@/Utils/DropdownItemsForEditors'
|
import { FeatureIdentifier } from '@standardnotes/features'
|
||||||
import { NoteType, PredicateCompoundOperator, PredicateJsonForm } from '@standardnotes/snjs'
|
import { NoteType, PredicateCompoundOperator, PredicateJsonForm } from '@standardnotes/snjs'
|
||||||
import { makeObservable, observable, action } from 'mobx'
|
import { makeObservable, observable, action } from 'mobx'
|
||||||
import { PredicateKeypath, PredicateKeypathTypes } from './PredicateKeypaths'
|
import { PredicateKeypath, PredicateKeypathTypes } from './PredicateKeypaths'
|
||||||
@@ -59,7 +59,7 @@ export class CompoundPredicateBuilderController {
|
|||||||
this.setPredicate(index, { value: Object.values(NoteType)[0] })
|
this.setPredicate(index, { value: Object.values(NoteType)[0] })
|
||||||
break
|
break
|
||||||
case 'editorIdentifier':
|
case 'editorIdentifier':
|
||||||
this.setPredicate(index, { value: PlainEditorType })
|
this.setPredicate(index, { value: FeatureIdentifier.PlainEditor })
|
||||||
break
|
break
|
||||||
case 'date':
|
case 'date':
|
||||||
this.setPredicate(index, { value: '1.days.ago' })
|
this.setPredicate(index, { value: '1.days.ago' })
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { IconType } from '@standardnotes/snjs'
|
||||||
|
|
||||||
export const PANEL_NAME_NOTES = 'notes'
|
export const PANEL_NAME_NOTES = 'notes'
|
||||||
export const PANEL_NAME_NAVIGATION = 'navigation'
|
export const PANEL_NAME_NAVIGATION = 'navigation'
|
||||||
|
|
||||||
@@ -21,8 +23,26 @@ export const TAG_FOLDERS_FEATURE_NAME = 'Tag folders'
|
|||||||
export const TAG_FOLDERS_FEATURE_TOOLTIP = 'A Plus or Pro plan is required to enable Tag folders.'
|
export const TAG_FOLDERS_FEATURE_TOOLTIP = 'A Plus or Pro plan is required to enable Tag folders.'
|
||||||
export const SMART_TAGS_FEATURE_NAME = 'Smart Tags'
|
export const SMART_TAGS_FEATURE_NAME = 'Smart Tags'
|
||||||
|
|
||||||
export const PLAIN_EDITOR_NAME = 'Plain Text'
|
|
||||||
export const BLOCKS_EDITOR_NAME = 'Super'
|
|
||||||
|
|
||||||
export const SYNC_TIMEOUT_DEBOUNCE = 350
|
export const SYNC_TIMEOUT_DEBOUNCE = 350
|
||||||
export const SYNC_TIMEOUT_NO_DEBOUNCE = 100
|
export const SYNC_TIMEOUT_NO_DEBOUNCE = 100
|
||||||
|
|
||||||
|
type EditorMetadata = {
|
||||||
|
name: string
|
||||||
|
icon: IconType
|
||||||
|
iconClassName: string
|
||||||
|
iconTintNumber: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SuperEditorMetadata: EditorMetadata = {
|
||||||
|
name: 'Super',
|
||||||
|
icon: 'file-doc',
|
||||||
|
iconClassName: 'text-accessory-tint-4',
|
||||||
|
iconTintNumber: 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PlainEditorMetadata: EditorMetadata = {
|
||||||
|
name: 'Plain Text',
|
||||||
|
icon: 'plain-text',
|
||||||
|
iconClassName: 'text-accessory-tint-1',
|
||||||
|
iconTintNumber: 1,
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
CollectionSort,
|
CollectionSort,
|
||||||
ContentType,
|
ContentType,
|
||||||
findInArray,
|
findInArray,
|
||||||
NoteViewController,
|
|
||||||
PrefKey,
|
PrefKey,
|
||||||
SmartView,
|
SmartView,
|
||||||
SNNote,
|
SNNote,
|
||||||
@@ -15,12 +14,10 @@ import {
|
|||||||
InternalEventBus,
|
InternalEventBus,
|
||||||
InternalEventHandlerInterface,
|
InternalEventHandlerInterface,
|
||||||
InternalEventInterface,
|
InternalEventInterface,
|
||||||
FileViewController,
|
|
||||||
FileItem,
|
FileItem,
|
||||||
WebAppEvent,
|
WebAppEvent,
|
||||||
NewNoteTitleFormat,
|
NewNoteTitleFormat,
|
||||||
useBoolean,
|
useBoolean,
|
||||||
TemplateNoteViewAutofocusBehavior,
|
|
||||||
isTag,
|
isTag,
|
||||||
isFile,
|
isFile,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
@@ -38,6 +35,9 @@ import dayjs from 'dayjs'
|
|||||||
import { LinkingController } from '../LinkingController'
|
import { LinkingController } from '../LinkingController'
|
||||||
import { AbstractViewController } from '../Abstract/AbstractViewController'
|
import { AbstractViewController } from '../Abstract/AbstractViewController'
|
||||||
import { log, LoggingDomain } from '@/Logging'
|
import { log, LoggingDomain } from '@/Logging'
|
||||||
|
import { NoteViewController } from '@/Components/NoteView/Controller/NoteViewController'
|
||||||
|
import { FileViewController } from '@/Components/NoteView/Controller/FileViewController'
|
||||||
|
import { TemplateNoteViewAutofocusBehavior } from '@/Components/NoteView/Controller/TemplateNoteViewControllerOptions'
|
||||||
|
|
||||||
const MinNoteCellHeight = 51.0
|
const MinNoteCellHeight = 51.0
|
||||||
const DefaultListNumNotes = 20
|
const DefaultListNumNotes = 20
|
||||||
@@ -255,7 +255,7 @@ export class ItemListController extends AbstractViewController implements Intern
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.application.itemControllerGroup.createItemController(note)
|
await this.application.itemControllerGroup.createItemController({ note })
|
||||||
|
|
||||||
this.linkingController.reloadAllLinks()
|
this.linkingController.reloadAllLinks()
|
||||||
|
|
||||||
@@ -273,7 +273,7 @@ export class ItemListController extends AbstractViewController implements Intern
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.application.itemControllerGroup.createItemController(file)
|
await this.application.itemControllerGroup.createItemController({ file })
|
||||||
|
|
||||||
this.linkingController.reloadAllLinks()
|
this.linkingController.reloadAllLinks()
|
||||||
}
|
}
|
||||||
@@ -624,10 +624,12 @@ export class ItemListController extends AbstractViewController implements Intern
|
|||||||
const activeRegularTagUuid = selectedTag instanceof SNTag ? selectedTag.uuid : undefined
|
const activeRegularTagUuid = selectedTag instanceof SNTag ? selectedTag.uuid : undefined
|
||||||
|
|
||||||
return this.application.itemControllerGroup.createItemController({
|
return this.application.itemControllerGroup.createItemController({
|
||||||
title,
|
templateOptions: {
|
||||||
tag: activeRegularTagUuid,
|
title,
|
||||||
createdAt,
|
tag: activeRegularTagUuid,
|
||||||
autofocusBehavior,
|
createdAt,
|
||||||
|
autofocusBehavior,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import { PopoverFileItemActionType } from '@/Components/AttachedFilesPopover/PopoverFileItemAction'
|
import { PopoverFileItemActionType } from '@/Components/AttachedFilesPopover/PopoverFileItemAction'
|
||||||
|
import { NoteViewController } from '@/Components/NoteView/Controller/NoteViewController'
|
||||||
import { AppPaneId } from '@/Components/ResponsivePane/AppPaneMetadata'
|
import { AppPaneId } from '@/Components/ResponsivePane/AppPaneMetadata'
|
||||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||||
import { createLinkFromItem } from '@/Utils/Items/Search/createLinkFromItem'
|
import { createLinkFromItem } from '@/Utils/Items/Search/createLinkFromItem'
|
||||||
@@ -11,7 +12,6 @@ import {
|
|||||||
FileItem,
|
FileItem,
|
||||||
InternalEventBus,
|
InternalEventBus,
|
||||||
naturalSort,
|
naturalSort,
|
||||||
NoteViewController,
|
|
||||||
PrefKey,
|
PrefKey,
|
||||||
SNNote,
|
SNNote,
|
||||||
SNTag,
|
SNTag,
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { isDev } from '@/Utils'
|
import { isDev } from '@/Utils'
|
||||||
|
|
||||||
export enum FeatureTrunkName {
|
export enum FeatureTrunkName {
|
||||||
Blocks,
|
Super,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FeatureTrunkStatus: Record<FeatureTrunkName, boolean> = {
|
export const FeatureTrunkStatus: Record<FeatureTrunkName, boolean> = {
|
||||||
[FeatureTrunkName.Blocks]: isDev && true,
|
[FeatureTrunkName.Super]: isDev && true,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function featureTrunkEnabled(trunk: FeatureTrunkName): boolean {
|
export function featureTrunkEnabled(trunk: FeatureTrunkName): boolean {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const LoggingStatus: Record<LoggingDomain, boolean> = {
|
|||||||
[LoggingDomain.Viewport]: false,
|
[LoggingDomain.Viewport]: false,
|
||||||
[LoggingDomain.Selection]: false,
|
[LoggingDomain.Selection]: false,
|
||||||
[LoggingDomain.BlockEditor]: false,
|
[LoggingDomain.BlockEditor]: false,
|
||||||
[LoggingDomain.Purchasing]: true,
|
[LoggingDomain.Purchasing]: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
|||||||
@@ -1,23 +1,34 @@
|
|||||||
import { ComponentArea, FeatureIdentifier } from '@standardnotes/features'
|
import { FeatureIdentifier } from '@standardnotes/snjs'
|
||||||
import { DropdownItem } from '@/Components/Dropdown/DropdownItem'
|
import { ComponentArea, NoteType } from '@standardnotes/features'
|
||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import { BLOCKS_EDITOR_NAME, PLAIN_EDITOR_NAME } from '@/Constants/Constants'
|
import { PlainEditorMetadata, SuperEditorMetadata } from '@/Constants/Constants'
|
||||||
import { featureTrunkEnabled, FeatureTrunkName } from '@/FeatureTrunk'
|
|
||||||
import { getIconAndTintForNoteType } from './Items/Icons/getIconAndTintForNoteType'
|
import { getIconAndTintForNoteType } from './Items/Icons/getIconAndTintForNoteType'
|
||||||
|
import { DropdownItem } from '@/Components/Dropdown/DropdownItem'
|
||||||
export const PlainEditorType = 'plain-editor'
|
|
||||||
export const BlocksType = 'blocks-editor'
|
|
||||||
|
|
||||||
export type EditorOption = DropdownItem & {
|
export type EditorOption = DropdownItem & {
|
||||||
value: FeatureIdentifier | typeof PlainEditorType | typeof BlocksType
|
value: FeatureIdentifier
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDropdownItemsForAllEditors(application: WebApplication) {
|
export function noteTypeForEditorOptionValue(value: EditorOption['value'], application: WebApplication): NoteType {
|
||||||
|
if (value === FeatureIdentifier.PlainEditor) {
|
||||||
|
return NoteType.Plain
|
||||||
|
} else if (value === FeatureIdentifier.SuperEditor) {
|
||||||
|
return NoteType.Super
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchingEditor = application.componentManager
|
||||||
|
.componentsForArea(ComponentArea.Editor)
|
||||||
|
.find((editor) => editor.identifier === value)
|
||||||
|
|
||||||
|
return matchingEditor ? matchingEditor.noteType : NoteType.Unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDropdownItemsForAllEditors(application: WebApplication): EditorOption[] {
|
||||||
const plaintextOption: EditorOption = {
|
const plaintextOption: EditorOption = {
|
||||||
icon: 'plain-text',
|
icon: PlainEditorMetadata.icon,
|
||||||
iconClassName: 'text-accessory-tint-1',
|
iconClassName: PlainEditorMetadata.iconClassName,
|
||||||
label: PLAIN_EDITOR_NAME,
|
label: PlainEditorMetadata.name,
|
||||||
value: PlainEditorType,
|
value: FeatureIdentifier.PlainEditor,
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = application.componentManager.componentsForArea(ComponentArea.Editor).map((editor): EditorOption => {
|
const options = application.componentManager.componentsForArea(ComponentArea.Editor).map((editor): EditorOption => {
|
||||||
@@ -34,12 +45,12 @@ export function getDropdownItemsForAllEditors(application: WebApplication) {
|
|||||||
|
|
||||||
options.push(plaintextOption)
|
options.push(plaintextOption)
|
||||||
|
|
||||||
if (featureTrunkEnabled(FeatureTrunkName.Blocks)) {
|
if (application.features.isExperimentalFeatureEnabled(FeatureIdentifier.SuperEditor)) {
|
||||||
options.push({
|
options.push({
|
||||||
icon: 'dashboard',
|
icon: SuperEditorMetadata.icon,
|
||||||
iconClassName: 'text-accessory-tint-1',
|
iconClassName: SuperEditorMetadata.iconClassName,
|
||||||
label: BLOCKS_EDITOR_NAME,
|
label: SuperEditorMetadata.name,
|
||||||
value: BlocksType,
|
value: FeatureIdentifier.SuperEditor,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { PlainEditorMetadata, SuperEditorMetadata } from '@/Constants/Constants'
|
||||||
import { NoteType } from '@standardnotes/features'
|
import { NoteType } from '@standardnotes/features'
|
||||||
import { IconType } from '@standardnotes/models'
|
import { IconType } from '@standardnotes/models'
|
||||||
|
|
||||||
@@ -15,7 +16,9 @@ export function getIconAndTintForNoteType(noteType?: NoteType): [IconType, numbe
|
|||||||
return ['tasks', 3]
|
return ['tasks', 3]
|
||||||
case NoteType.Code:
|
case NoteType.Code:
|
||||||
return ['code', 4]
|
return ['code', 4]
|
||||||
|
case NoteType.Super:
|
||||||
|
return [SuperEditorMetadata.icon, SuperEditorMetadata.iconTintNumber]
|
||||||
default:
|
default:
|
||||||
return ['plain-text', 1]
|
return [PlainEditorMetadata.icon, PlainEditorMetadata.iconTintNumber]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { featureTrunkEnabled, FeatureTrunkName } from '@/FeatureTrunk'
|
|
||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import {
|
import {
|
||||||
ContentType,
|
ContentType,
|
||||||
@@ -8,10 +7,11 @@ import {
|
|||||||
FeatureDescription,
|
FeatureDescription,
|
||||||
GetFeatures,
|
GetFeatures,
|
||||||
NoteType,
|
NoteType,
|
||||||
|
FeatureIdentifier,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import { EditorMenuGroup } from '@/Components/NotesOptions/EditorMenuGroup'
|
import { EditorMenuGroup } from '@/Components/NotesOptions/EditorMenuGroup'
|
||||||
import { EditorMenuItem } from '@/Components/NotesOptions/EditorMenuItem'
|
import { EditorMenuItem } from '@/Components/NotesOptions/EditorMenuItem'
|
||||||
import { BLOCKS_EDITOR_NAME, PLAIN_EDITOR_NAME } from '@/Constants/Constants'
|
import { PlainEditorMetadata, SuperEditorMetadata } from '@/Constants/Constants'
|
||||||
|
|
||||||
type NoteTypeToEditorRowsMap = Record<NoteType, EditorMenuItem[]>
|
type NoteTypeToEditorRowsMap = Record<NoteType, EditorMenuItem[]>
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ const insertInstalledComponentsInMap = (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const createGroupsFromMap = (map: NoteTypeToEditorRowsMap): EditorMenuGroup[] => {
|
const createGroupsFromMap = (map: NoteTypeToEditorRowsMap, application: WebApplication): EditorMenuGroup[] => {
|
||||||
const groups: EditorMenuGroup[] = [
|
const groups: EditorMenuGroup[] = [
|
||||||
{
|
{
|
||||||
icon: 'plain-text',
|
icon: 'plain-text',
|
||||||
@@ -124,12 +124,12 @@ const createGroupsFromMap = (map: NoteTypeToEditorRowsMap): EditorMenuGroup[] =>
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
if (featureTrunkEnabled(FeatureTrunkName.Blocks)) {
|
if (application.features.isExperimentalFeatureEnabled(FeatureIdentifier.SuperEditor)) {
|
||||||
groups.splice(1, 0, {
|
groups.splice(1, 0, {
|
||||||
icon: 'file-doc',
|
icon: SuperEditorMetadata.icon,
|
||||||
iconClassName: 'text-accessory-tint-4',
|
iconClassName: SuperEditorMetadata.iconClassName,
|
||||||
title: BLOCKS_EDITOR_NAME,
|
title: SuperEditorMetadata.name,
|
||||||
items: map[NoteType.Blocks],
|
items: map[NoteType.Super],
|
||||||
featured: true,
|
featured: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -137,16 +137,16 @@ const createGroupsFromMap = (map: NoteTypeToEditorRowsMap): EditorMenuGroup[] =>
|
|||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
|
|
||||||
const createBaselineMap = (): NoteTypeToEditorRowsMap => {
|
const createBaselineMap = (application: WebApplication): NoteTypeToEditorRowsMap => {
|
||||||
const map: NoteTypeToEditorRowsMap = {
|
const map: NoteTypeToEditorRowsMap = {
|
||||||
[NoteType.Plain]: [
|
[NoteType.Plain]: [
|
||||||
{
|
{
|
||||||
name: PLAIN_EDITOR_NAME,
|
name: PlainEditorMetadata.name,
|
||||||
isEntitled: true,
|
isEntitled: true,
|
||||||
noteType: NoteType.Plain,
|
noteType: NoteType.Plain,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[NoteType.Blocks]: [],
|
[NoteType.Super]: [],
|
||||||
[NoteType.RichText]: [],
|
[NoteType.RichText]: [],
|
||||||
[NoteType.Markdown]: [],
|
[NoteType.Markdown]: [],
|
||||||
[NoteType.Task]: [],
|
[NoteType.Task]: [],
|
||||||
@@ -156,11 +156,11 @@ const createBaselineMap = (): NoteTypeToEditorRowsMap => {
|
|||||||
[NoteType.Unknown]: [],
|
[NoteType.Unknown]: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
if (featureTrunkEnabled(FeatureTrunkName.Blocks)) {
|
if (application.features.isExperimentalFeatureEnabled(FeatureIdentifier.SuperEditor)) {
|
||||||
map[NoteType.Blocks].push({
|
map[NoteType.Super].push({
|
||||||
name: BLOCKS_EDITOR_NAME,
|
name: SuperEditorMetadata.name,
|
||||||
isEntitled: true,
|
isEntitled: true,
|
||||||
noteType: NoteType.Blocks,
|
noteType: NoteType.Super,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,11 +168,11 @@ const createBaselineMap = (): NoteTypeToEditorRowsMap => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const createEditorMenuGroups = (application: WebApplication, components: SNComponent[]) => {
|
export const createEditorMenuGroups = (application: WebApplication, components: SNComponent[]) => {
|
||||||
const map = createBaselineMap()
|
const map = createBaselineMap(application)
|
||||||
|
|
||||||
insertNonInstalledNativeComponentsInMap(map, components, application)
|
insertNonInstalledNativeComponentsInMap(map, components, application)
|
||||||
|
|
||||||
insertInstalledComponentsInMap(map, components, application)
|
insertInstalledComponentsInMap(map, components, application)
|
||||||
|
|
||||||
return createGroupsFromMap(map)
|
return createGroupsFromMap(map, application)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
class SNWebCrypto {
|
||||||
|
initialize() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { SNWebCrypto }
|
||||||
Reference in New Issue
Block a user