feat: Add new "Change Editor" option to note context menu (#823)
* feat: add editor icon * refactor: remove 'any' type and format * refactor: move NotesOptions and add ChangeEditorOption * refactor: fix type for using regular RefObject<T> * feat: add hide-if-last-child util class * feat: add Change Editor option * feat: make radio btn gray if not checked * fix: accordion menu header and item sizing/spacing * feat: add Escape key to KeyboardKey enum * refactor: Remove Editor Menu * feat: add editor select functionality * refactor: move plain editor name to constant * feat: add premium editors with modal if no subscription refactor: simplify menu group creation * feat: show alert when switching to non-interchangeable editor * fix: change editor menu going out of bounds * feat: increase group header & editor item size * fix: change editor menu close on blur * refactor: Use KeyboardKey enum & remove else statement * feat: add keyboard navigation to change editor menu * fix: editor menu separators * feat: improve change editor menu sizing & spacing * feat: show alert only if editor is not interchangeable * feat: don't show alert when switching to/from plain editor * chore: bump snjs version * feat: temporarily remove change editor alert * feat: dynamically get footer height * refactor: move magic number to const * refactor: move constants to constants file * feat: use const instead of magic number
This commit is contained in:
3
app/assets/icons/ic-editor.svg
Normal file
3
app/assets/icons/ic-editor.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M14.1667 3.60714V8.42857L12.5 6.82143L10.8333 8.42857V3.60714H7.5V16.4643H15.8333V3.60714H14.1667ZM2.5 6.01786V4.41071H4.16667V3.60714C4.16667 2.71518 4.91667 2 5.83333 2H15.8333C16.7083 2 17.5 2.76339 17.5 3.60714V16.4643C17.5 17.308 16.7083 18.0714 15.8333 18.0714H5.83333C4.95833 18.0714 4.16667 17.308 4.16667 16.4643V15.6607H2.5V14.0536H4.16667V10.8393H2.5V9.23214H4.16667V6.01786H2.5ZM4.16667 4.41071V6.01786H5.83333V4.41071H4.16667ZM4.16667 15.6607H5.83333V14.0536H4.16667V15.6607ZM4.16667 10.8393H5.83333V9.23214H4.16667V10.8393Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 632 B |
@@ -64,7 +64,6 @@ import {
|
|||||||
} from './directives/functional';
|
} from './directives/functional';
|
||||||
import {
|
import {
|
||||||
ActionsMenu,
|
ActionsMenu,
|
||||||
EditorMenu,
|
|
||||||
HistoryMenu,
|
HistoryMenu,
|
||||||
InputModal,
|
InputModal,
|
||||||
MenuRow,
|
MenuRow,
|
||||||
@@ -160,7 +159,6 @@ const startApplication: StartApplication = async function startApplication(
|
|||||||
.directive('actionsMenu', () => new ActionsMenu())
|
.directive('actionsMenu', () => new ActionsMenu())
|
||||||
.directive('challengeModal', () => new ChallengeModal())
|
.directive('challengeModal', () => new ChallengeModal())
|
||||||
.directive('componentView', ComponentViewDirective)
|
.directive('componentView', ComponentViewDirective)
|
||||||
.directive('editorMenu', () => new EditorMenu())
|
|
||||||
.directive('inputModal', () => new InputModal())
|
.directive('inputModal', () => new InputModal())
|
||||||
.directive('menuRow', () => new MenuRow())
|
.directive('menuRow', () => new MenuRow())
|
||||||
.directive('panelResizer', () => new PanelResizer())
|
.directive('panelResizer', () => new PanelResizer())
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import EditorIcon from '../../icons/ic-editor.svg';
|
||||||
import PremiumFeatureIcon from '../../icons/ic-premium-feature.svg';
|
import PremiumFeatureIcon from '../../icons/ic-premium-feature.svg';
|
||||||
import PencilOffIcon from '../../icons/ic-pencil-off.svg';
|
import PencilOffIcon from '../../icons/ic-pencil-off.svg';
|
||||||
import PlainTextIcon from '../../icons/ic-text-paragraph.svg';
|
import PlainTextIcon from '../../icons/ic-text-paragraph.svg';
|
||||||
@@ -68,6 +69,7 @@ import { toDirective } from './utils';
|
|||||||
import { FunctionalComponent } from 'preact';
|
import { FunctionalComponent } from 'preact';
|
||||||
|
|
||||||
const ICONS = {
|
const ICONS = {
|
||||||
|
'editor': EditorIcon,
|
||||||
'menu-arrow-down-alt': MenuArrowDownAlt,
|
'menu-arrow-down-alt': MenuArrowDownAlt,
|
||||||
'menu-arrow-right': MenuArrowRight,
|
'menu-arrow-right': MenuArrowRight,
|
||||||
notes: NotesIcon,
|
notes: NotesIcon,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { AppState } from '@/ui_models/app_state';
|
import { AppState } from '@/ui_models/app_state';
|
||||||
import { toDirective, useCloseOnBlur, useCloseOnClickOutside } from './utils';
|
import { toDirective, useCloseOnBlur, useCloseOnClickOutside } from './utils';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { NotesOptions } from './NotesOptions';
|
import { NotesOptions } from './NotesOptions/NotesOptions';
|
||||||
import { useCallback, useEffect, useRef } from 'preact/hooks';
|
import { useCallback, useEffect, useRef } from 'preact/hooks';
|
||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
|
|
||||||
@@ -11,21 +11,16 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const NotesContextMenu = observer(({ application, appState }: Props) => {
|
const NotesContextMenu = observer(({ application, appState }: Props) => {
|
||||||
const {
|
const { contextMenuOpen, contextMenuPosition, contextMenuMaxHeight } =
|
||||||
contextMenuOpen,
|
appState.notes;
|
||||||
contextMenuPosition,
|
|
||||||
contextMenuMaxHeight,
|
|
||||||
} = appState.notes;
|
|
||||||
|
|
||||||
const contextMenuRef = useRef<HTMLDivElement>(null);
|
const contextMenuRef = useRef<HTMLDivElement>(null);
|
||||||
const [closeOnBlur] = useCloseOnBlur(
|
const [closeOnBlur] = useCloseOnBlur(contextMenuRef, (open: boolean) =>
|
||||||
contextMenuRef as any,
|
appState.notes.setContextMenuOpen(open)
|
||||||
(open: boolean) => appState.notes.setContextMenuOpen(open)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useCloseOnClickOutside(
|
useCloseOnClickOutside(contextMenuRef, (open: boolean) =>
|
||||||
contextMenuRef as any,
|
appState.notes.setContextMenuOpen(open)
|
||||||
(open: boolean) => appState.notes.setContextMenuOpen(open)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const reloadContextMenuLayout = useCallback(() => {
|
const reloadContextMenuLayout = useCallback(() => {
|
||||||
|
|||||||
@@ -0,0 +1,288 @@
|
|||||||
|
import { KeyboardKey } from '@/services/ioService';
|
||||||
|
import { STRING_EDIT_LOCKED_ATTEMPT } from '@/strings';
|
||||||
|
import { WebApplication } from '@/ui_models/application';
|
||||||
|
import { AppState } from '@/ui_models/app_state';
|
||||||
|
import {
|
||||||
|
MENU_MARGIN_FROM_APP_BORDER,
|
||||||
|
MAX_MENU_SIZE_MULTIPLIER,
|
||||||
|
} from '@/views/constants';
|
||||||
|
import {
|
||||||
|
reloadFont,
|
||||||
|
transactionForAssociateComponentWithCurrentNote,
|
||||||
|
transactionForDisassociateComponentWithCurrentNote,
|
||||||
|
} from '@/views/note_view/note_view';
|
||||||
|
import {
|
||||||
|
Disclosure,
|
||||||
|
DisclosureButton,
|
||||||
|
DisclosurePanel,
|
||||||
|
} from '@reach/disclosure';
|
||||||
|
import {
|
||||||
|
ComponentArea,
|
||||||
|
ItemMutator,
|
||||||
|
NoteMutator,
|
||||||
|
PrefKey,
|
||||||
|
SNComponent,
|
||||||
|
SNNote,
|
||||||
|
TransactionalMutation,
|
||||||
|
} from '@standardnotes/snjs';
|
||||||
|
import { FunctionComponent } from 'preact';
|
||||||
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
|
import { Icon, IconType } from '../Icon';
|
||||||
|
import { PremiumModalProvider } from '../Premium';
|
||||||
|
import { createEditorMenuGroups } from './changeEditor/createEditorMenuGroups';
|
||||||
|
import { EditorAccordionMenu } from './changeEditor/EditorAccordionMenu';
|
||||||
|
|
||||||
|
type ChangeEditorOptionProps = {
|
||||||
|
appState: AppState;
|
||||||
|
application: WebApplication;
|
||||||
|
note: SNNote;
|
||||||
|
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AccordionMenuGroup<T> = {
|
||||||
|
icon?: IconType;
|
||||||
|
iconClassName?: string;
|
||||||
|
title: string;
|
||||||
|
items: Array<T>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EditorMenuItem = {
|
||||||
|
name: string;
|
||||||
|
component?: SNComponent;
|
||||||
|
isPremiumFeature?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EditorMenuGroup = AccordionMenuGroup<EditorMenuItem>;
|
||||||
|
|
||||||
|
export const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({
|
||||||
|
application,
|
||||||
|
appState,
|
||||||
|
closeOnBlur,
|
||||||
|
note,
|
||||||
|
}) => {
|
||||||
|
const [changeEditorMenuOpen, setChangeEditorMenuOpen] = useState(false);
|
||||||
|
const [changeEditorMenuPosition, setChangeEditorMenuPosition] = useState<{
|
||||||
|
top?: number | 'auto';
|
||||||
|
right?: number | 'auto';
|
||||||
|
bottom: number | 'auto';
|
||||||
|
left?: number | 'auto';
|
||||||
|
}>({
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
});
|
||||||
|
const changeEditorMenuRef = useRef<HTMLDivElement>(null);
|
||||||
|
const changeEditorButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const [editors] = useState<SNComponent[]>(() =>
|
||||||
|
application.componentManager
|
||||||
|
.componentsForArea(ComponentArea.Editor)
|
||||||
|
.sort((a, b) => {
|
||||||
|
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const [editorMenuGroups, setEditorMenuGroups] = useState<EditorMenuGroup[]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const [selectedEditor, setSelectedEditor] = useState(() =>
|
||||||
|
application.componentManager.editorForNote(note)
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setEditorMenuGroups(createEditorMenuGroups(editors));
|
||||||
|
}, [editors]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedEditor(application.componentManager.editorForNote(note));
|
||||||
|
}, [application, note]);
|
||||||
|
|
||||||
|
const toggleChangeEditorMenu = () => {
|
||||||
|
const defaultFontSize = window.getComputedStyle(
|
||||||
|
document.documentElement
|
||||||
|
).fontSize;
|
||||||
|
const maxChangeEditorMenuSize =
|
||||||
|
parseFloat(defaultFontSize) * MAX_MENU_SIZE_MULTIPLIER;
|
||||||
|
const { clientWidth, clientHeight } = document.documentElement;
|
||||||
|
const buttonRect = changeEditorButtonRef.current?.getBoundingClientRect();
|
||||||
|
const buttonParentRect =
|
||||||
|
changeEditorButtonRef.current?.parentElement?.getBoundingClientRect();
|
||||||
|
const footerElementRect = document
|
||||||
|
.getElementById('footer-bar')
|
||||||
|
?.getBoundingClientRect();
|
||||||
|
const footerHeightInPx = footerElementRect?.height;
|
||||||
|
|
||||||
|
if (buttonRect && buttonParentRect && footerHeightInPx) {
|
||||||
|
let positionBottom =
|
||||||
|
clientHeight - buttonRect.bottom - buttonRect.height / 2;
|
||||||
|
|
||||||
|
if (positionBottom < footerHeightInPx) {
|
||||||
|
positionBottom = footerHeightInPx + MENU_MARGIN_FROM_APP_BORDER;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buttonRect.right + maxChangeEditorMenuSize > clientWidth) {
|
||||||
|
setChangeEditorMenuPosition({
|
||||||
|
top: positionBottom - buttonParentRect.height / 2,
|
||||||
|
right: clientWidth - buttonRect.left,
|
||||||
|
bottom: 'auto',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setChangeEditorMenuPosition({
|
||||||
|
bottom: positionBottom,
|
||||||
|
left: buttonRect.right,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setChangeEditorMenuOpen(!changeEditorMenuOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (changeEditorMenuOpen) {
|
||||||
|
const defaultFontSize = window.getComputedStyle(
|
||||||
|
document.documentElement
|
||||||
|
).fontSize;
|
||||||
|
const maxChangeEditorMenuSize =
|
||||||
|
parseFloat(defaultFontSize) * MAX_MENU_SIZE_MULTIPLIER;
|
||||||
|
const changeEditorMenuBoundingRect =
|
||||||
|
changeEditorMenuRef.current?.getBoundingClientRect();
|
||||||
|
const buttonRect = changeEditorButtonRef.current?.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (changeEditorMenuBoundingRect && buttonRect) {
|
||||||
|
if (changeEditorMenuBoundingRect.y < MENU_MARGIN_FROM_APP_BORDER) {
|
||||||
|
if (
|
||||||
|
buttonRect.right + maxChangeEditorMenuSize >
|
||||||
|
document.documentElement.clientWidth
|
||||||
|
) {
|
||||||
|
setChangeEditorMenuPosition({
|
||||||
|
...changeEditorMenuPosition,
|
||||||
|
top: MENU_MARGIN_FROM_APP_BORDER + buttonRect.height,
|
||||||
|
bottom: 'auto',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setChangeEditorMenuPosition({
|
||||||
|
...changeEditorMenuPosition,
|
||||||
|
top: MENU_MARGIN_FROM_APP_BORDER,
|
||||||
|
bottom: 'auto',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [changeEditorMenuOpen, changeEditorMenuPosition]);
|
||||||
|
|
||||||
|
const selectComponent = async (component: SNComponent | null) => {
|
||||||
|
if (component) {
|
||||||
|
if (component.conflictOf) {
|
||||||
|
application.changeAndSaveItem(component.uuid, (mutator) => {
|
||||||
|
mutator.conflictOf = undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const transactions: TransactionalMutation[] = [];
|
||||||
|
|
||||||
|
if (appState.getActiveNoteController()?.isTemplateNote) {
|
||||||
|
await appState.getActiveNoteController().insertTemplatedNote();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note.locked) {
|
||||||
|
application.alertService.alert(STRING_EDIT_LOCKED_ATTEMPT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!component) {
|
||||||
|
if (!note.prefersPlainEditor) {
|
||||||
|
transactions.push({
|
||||||
|
itemUuid: note.uuid,
|
||||||
|
mutate: (m: ItemMutator) => {
|
||||||
|
const noteMutator = m as NoteMutator;
|
||||||
|
noteMutator.prefersPlainEditor = true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const currentEditor = application.componentManager.editorForNote(note);
|
||||||
|
if (currentEditor?.isExplicitlyEnabledForItem(note.uuid)) {
|
||||||
|
transactions.push(
|
||||||
|
transactionForDisassociateComponentWithCurrentNote(
|
||||||
|
currentEditor,
|
||||||
|
note
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
reloadFont(application.getPreference(PrefKey.EditorMonospaceEnabled));
|
||||||
|
} else if (component.area === ComponentArea.Editor) {
|
||||||
|
const currentEditor = application.componentManager.editorForNote(note);
|
||||||
|
if (currentEditor && component.uuid !== currentEditor.uuid) {
|
||||||
|
transactions.push(
|
||||||
|
transactionForDisassociateComponentWithCurrentNote(
|
||||||
|
currentEditor,
|
||||||
|
note
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const prefersPlain = note.prefersPlainEditor;
|
||||||
|
if (prefersPlain) {
|
||||||
|
transactions.push({
|
||||||
|
itemUuid: note.uuid,
|
||||||
|
mutate: (m: ItemMutator) => {
|
||||||
|
const noteMutator = m as NoteMutator;
|
||||||
|
noteMutator.prefersPlainEditor = false;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
transactions.push(
|
||||||
|
transactionForAssociateComponentWithCurrentNote(component, note)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await application.runTransactionalMutations(transactions);
|
||||||
|
/** Dirtying can happen above */
|
||||||
|
application.sync();
|
||||||
|
|
||||||
|
setSelectedEditor(application.componentManager.editorForNote(note));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Disclosure open={changeEditorMenuOpen} onChange={toggleChangeEditorMenu}>
|
||||||
|
<DisclosureButton
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === KeyboardKey.Escape) {
|
||||||
|
setChangeEditorMenuOpen(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={closeOnBlur}
|
||||||
|
ref={changeEditorButtonRef}
|
||||||
|
className="sn-dropdown-item justify-between"
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Icon type="editor" className="color-neutral mr-2" />
|
||||||
|
Change editor
|
||||||
|
</div>
|
||||||
|
<Icon type="chevron-right" className="color-neutral" />
|
||||||
|
</DisclosureButton>
|
||||||
|
<DisclosurePanel
|
||||||
|
ref={changeEditorMenuRef}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === KeyboardKey.Escape) {
|
||||||
|
setChangeEditorMenuOpen(false);
|
||||||
|
changeEditorButtonRef.current?.focus();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
...changeEditorMenuPosition,
|
||||||
|
position: 'fixed',
|
||||||
|
}}
|
||||||
|
className="sn-dropdown flex flex-col py-1 max-h-120 min-w-68 fixed overflow-y-auto"
|
||||||
|
>
|
||||||
|
<PremiumModalProvider state={appState.features}>
|
||||||
|
<EditorAccordionMenu
|
||||||
|
application={application}
|
||||||
|
closeOnBlur={closeOnBlur}
|
||||||
|
groups={editorMenuGroups}
|
||||||
|
isOpen={changeEditorMenuOpen}
|
||||||
|
selectComponent={selectComponent}
|
||||||
|
selectedEditor={selectedEditor}
|
||||||
|
/>
|
||||||
|
</PremiumModalProvider>
|
||||||
|
</DisclosurePanel>
|
||||||
|
</Disclosure>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AppState } from '@/ui_models/app_state';
|
import { AppState } from '@/ui_models/app_state';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from '../Icon';
|
||||||
import { Switch } from './Switch';
|
import { Switch } from '../Switch';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { useRef, useState, useEffect, useMemo } from 'preact/hooks';
|
import { useRef, useState, useEffect, useMemo } from 'preact/hooks';
|
||||||
import {
|
import {
|
||||||
@@ -8,12 +8,17 @@ import {
|
|||||||
DisclosureButton,
|
DisclosureButton,
|
||||||
DisclosurePanel,
|
DisclosurePanel,
|
||||||
} from '@reach/disclosure';
|
} from '@reach/disclosure';
|
||||||
import { SNApplication, SNNote } from '@standardnotes/snjs/dist/@types';
|
import { SNApplication, SNNote } from '@standardnotes/snjs';
|
||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
import { KeyboardModifier } from '@/services/ioService';
|
import { KeyboardModifier } from '@/services/ioService';
|
||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
|
import { ChangeEditorOption } from './ChangeEditorOption';
|
||||||
|
import {
|
||||||
|
MENU_MARGIN_FROM_APP_BORDER,
|
||||||
|
MAX_MENU_SIZE_MULTIPLIER,
|
||||||
|
} from '@/views/constants';
|
||||||
|
|
||||||
type Props = {
|
export type NotesOptionsProps = {
|
||||||
application: WebApplication;
|
application: WebApplication;
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void;
|
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void;
|
||||||
@@ -21,7 +26,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type DeletePermanentlyButtonProps = {
|
type DeletePermanentlyButtonProps = {
|
||||||
closeOnBlur: Props['closeOnBlur'];
|
closeOnBlur: NotesOptionsProps['closeOnBlur'];
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -86,7 +91,10 @@ const formatDate = (date: Date | undefined) => {
|
|||||||
return `${date.toDateString()} ${date.toLocaleTimeString()}`;
|
return `${date.toDateString()} ${date.toLocaleTimeString()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const NoteAttributes: FunctionComponent<{ application: SNApplication, note: SNNote }> = ({ application, note }) => {
|
const NoteAttributes: FunctionComponent<{
|
||||||
|
application: SNApplication;
|
||||||
|
note: SNNote;
|
||||||
|
}> = ({ application, note }) => {
|
||||||
const { words, characters, paragraphs } = useMemo(
|
const { words, characters, paragraphs } = useMemo(
|
||||||
() => countNoteAttributes(note.text),
|
() => countNoteAttributes(note.text),
|
||||||
[note.text]
|
[note.text]
|
||||||
@@ -136,15 +144,19 @@ const NoteAttributes: FunctionComponent<{ application: SNApplication, note: SNNo
|
|||||||
};
|
};
|
||||||
|
|
||||||
const SpellcheckOptions: FunctionComponent<{
|
const SpellcheckOptions: FunctionComponent<{
|
||||||
appState: AppState, note: SNNote
|
appState: AppState;
|
||||||
|
note: SNNote;
|
||||||
}> = ({ appState, note }) => {
|
}> = ({ appState, note }) => {
|
||||||
|
|
||||||
const editor = appState.application.componentManager.editorForNote(note);
|
const editor = appState.application.componentManager.editorForNote(note);
|
||||||
const spellcheckControllable = Boolean(
|
const spellcheckControllable = Boolean(
|
||||||
!editor ||
|
!editor ||
|
||||||
appState.application.getFeature(editor.identifier)?.spellcheckControl
|
appState.application.getFeature(editor.identifier)?.spellcheckControl
|
||||||
);
|
);
|
||||||
const noteSpellcheck = !spellcheckControllable ? true : note ? appState.notes.getSpellcheckStateForNote(note) : undefined;
|
const noteSpellcheck = !spellcheckControllable
|
||||||
|
? true
|
||||||
|
: note
|
||||||
|
? appState.notes.getSpellcheckStateForNote(note)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col px-3 py-1.5">
|
<div className="flex flex-col px-3 py-1.5">
|
||||||
@@ -157,19 +169,26 @@ const SpellcheckOptions: FunctionComponent<{
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="flex items-center">
|
<span className="flex items-center">
|
||||||
<Icon type='spellcheck' className={iconClass} />
|
<Icon type="spellcheck" className={iconClass} />
|
||||||
Spellcheck
|
Spellcheck
|
||||||
</span>
|
</span>
|
||||||
</Switch>
|
</Switch>
|
||||||
{!spellcheckControllable && (
|
{!spellcheckControllable && (
|
||||||
<p className="text-xs pt-1.5">Spellcheck cannot be controlled for this editor.</p>
|
<p className="text-xs pt-1.5">
|
||||||
|
Spellcheck cannot be controlled for this editor.
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NotesOptions = observer(
|
export const NotesOptions = observer(
|
||||||
({ application, appState, closeOnBlur, onSubmenuChange }: Props) => {
|
({
|
||||||
|
application,
|
||||||
|
appState,
|
||||||
|
closeOnBlur,
|
||||||
|
onSubmenuChange,
|
||||||
|
}: NotesOptionsProps) => {
|
||||||
const [tagsMenuOpen, setTagsMenuOpen] = useState(false);
|
const [tagsMenuOpen, setTagsMenuOpen] = useState(false);
|
||||||
const [tagsMenuPosition, setTagsMenuPosition] = useState<{
|
const [tagsMenuPosition, setTagsMenuPosition] = useState<{
|
||||||
top: number;
|
top: number;
|
||||||
@@ -232,25 +251,39 @@ export const NotesOptions = observer(
|
|||||||
const defaultFontSize = window.getComputedStyle(
|
const defaultFontSize = window.getComputedStyle(
|
||||||
document.documentElement
|
document.documentElement
|
||||||
).fontSize;
|
).fontSize;
|
||||||
const maxTagsMenuSize = parseFloat(defaultFontSize) * 30;
|
const maxTagsMenuSize =
|
||||||
|
parseFloat(defaultFontSize) * MAX_MENU_SIZE_MULTIPLIER;
|
||||||
const { clientWidth, clientHeight } = document.documentElement;
|
const { clientWidth, clientHeight } = document.documentElement;
|
||||||
const buttonRect = tagsButtonRef.current!.getBoundingClientRect();
|
const buttonRect = tagsButtonRef.current?.getBoundingClientRect();
|
||||||
const footerHeight = 32;
|
const footerElementRect = document
|
||||||
|
.getElementById('footer-bar')
|
||||||
|
?.getBoundingClientRect();
|
||||||
|
const footerHeightInPx = footerElementRect?.height;
|
||||||
|
|
||||||
if (buttonRect.top + maxTagsMenuSize > clientHeight - footerHeight) {
|
if (buttonRect && footerHeightInPx) {
|
||||||
setTagsMenuMaxHeight(clientHeight - buttonRect.top - footerHeight - 2);
|
if (
|
||||||
}
|
buttonRect.top + maxTagsMenuSize >
|
||||||
|
clientHeight - footerHeightInPx
|
||||||
|
) {
|
||||||
|
setTagsMenuMaxHeight(
|
||||||
|
clientHeight -
|
||||||
|
buttonRect.top -
|
||||||
|
footerHeightInPx -
|
||||||
|
MENU_MARGIN_FROM_APP_BORDER
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (buttonRect.right + maxTagsMenuSize > clientWidth) {
|
if (buttonRect.right + maxTagsMenuSize > clientWidth) {
|
||||||
setTagsMenuPosition({
|
setTagsMenuPosition({
|
||||||
top: buttonRect.top,
|
top: buttonRect.top,
|
||||||
right: clientWidth - buttonRect.left,
|
right: clientWidth - buttonRect.left,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setTagsMenuPosition({
|
setTagsMenuPosition({
|
||||||
top: buttonRect.top,
|
top: buttonRect.top,
|
||||||
left: buttonRect.right,
|
left: buttonRect.right,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setTagsMenuOpen(!tagsMenuOpen);
|
setTagsMenuOpen(!tagsMenuOpen);
|
||||||
@@ -360,7 +393,7 @@ export const NotesOptions = observer(
|
|||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
setTagsMenuOpen(false);
|
setTagsMenuOpen(false);
|
||||||
tagsButtonRef.current!.focus();
|
tagsButtonRef.current?.focus();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
@@ -383,9 +416,10 @@ export const NotesOptions = observer(
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`whitespace-nowrap overflow-hidden overflow-ellipsis
|
className={`whitespace-nowrap overflow-hidden overflow-ellipsis
|
||||||
${appState.notes.isTagInSelectedNotes(tag)
|
${
|
||||||
? 'font-bold'
|
appState.notes.isTagInSelectedNotes(tag)
|
||||||
: ''
|
? 'font-bold'
|
||||||
|
: ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{tag.title}
|
{tag.title}
|
||||||
@@ -516,16 +550,18 @@ export const NotesOptions = observer(
|
|||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{notes.length === 1 ? (
|
{notes.length === 1 ? (
|
||||||
<>
|
<>
|
||||||
<div className="min-h-1px my-2 bg-border"></div>
|
<div className="min-h-1px my-2 bg-border"></div>
|
||||||
|
<ChangeEditorOption
|
||||||
<SpellcheckOptions appState={appState} note={notes[0]} />
|
appState={appState}
|
||||||
|
application={application}
|
||||||
|
closeOnBlur={closeOnBlur}
|
||||||
|
note={notes[0]}
|
||||||
|
/>
|
||||||
|
<div className="min-h-1px my-2 bg-border"></div>
|
||||||
|
<SpellcheckOptions appState={appState} note={notes[0]} />
|
||||||
<div className="min-h-1px my-2 bg-border"></div>
|
<div className="min-h-1px my-2 bg-border"></div>
|
||||||
|
|
||||||
<NoteAttributes application={application} note={notes[0]} />
|
<NoteAttributes application={application} note={notes[0]} />
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
import { Icon } from '@/components/Icon';
|
||||||
|
import { usePremiumModal } from '@/components/Premium';
|
||||||
|
import { KeyboardKey } from '@/services/ioService';
|
||||||
|
import { WebApplication } from '@/ui_models/application';
|
||||||
|
import { SNComponent } from '@standardnotes/snjs';
|
||||||
|
import { Fragment, FunctionComponent } from 'preact';
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
||||||
|
import { EditorMenuItem, EditorMenuGroup } from '../ChangeEditorOption';
|
||||||
|
import { PLAIN_EDITOR_NAME } from './createEditorMenuGroups';
|
||||||
|
|
||||||
|
type EditorAccordionMenuProps = {
|
||||||
|
application: WebApplication;
|
||||||
|
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void;
|
||||||
|
groups: EditorMenuGroup[];
|
||||||
|
isOpen: boolean;
|
||||||
|
selectComponent: (component: SNComponent | null) => Promise<void>;
|
||||||
|
selectedEditor: SNComponent | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGroupId = (group: EditorMenuGroup) =>
|
||||||
|
group.title.toLowerCase().replace(/\s/, '-');
|
||||||
|
|
||||||
|
const getGroupBtnId = (groupId: string) => groupId + '-button';
|
||||||
|
|
||||||
|
export const EditorAccordionMenu: FunctionComponent<
|
||||||
|
EditorAccordionMenuProps
|
||||||
|
> = ({
|
||||||
|
application,
|
||||||
|
closeOnBlur,
|
||||||
|
groups,
|
||||||
|
isOpen,
|
||||||
|
selectComponent,
|
||||||
|
selectedEditor,
|
||||||
|
}) => {
|
||||||
|
const [activeGroupId, setActiveGroupId] = useState('');
|
||||||
|
const menuItemRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
||||||
|
const [focusedItemIndex, setFocusedItemIndex] = useState<number>();
|
||||||
|
const premiumModal = usePremiumModal();
|
||||||
|
|
||||||
|
const isSelectedEditor = useCallback(
|
||||||
|
(item: EditorMenuItem) => {
|
||||||
|
if (selectedEditor) {
|
||||||
|
if (item?.component?.identifier === selectedEditor.identifier) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (item.name === PLAIN_EDITOR_NAME) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
[selectedEditor]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const activeGroup = groups.find((group) => {
|
||||||
|
return group.items.some(isSelectedEditor);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (activeGroup) {
|
||||||
|
const newActiveGroupId = getGroupId(activeGroup);
|
||||||
|
setActiveGroupId(newActiveGroupId);
|
||||||
|
}
|
||||||
|
}, [groups, selectedEditor, isSelectedEditor]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
typeof focusedItemIndex === 'undefined' &&
|
||||||
|
activeGroupId.length &&
|
||||||
|
menuItemRefs.current.length
|
||||||
|
) {
|
||||||
|
const activeGroupIndex = menuItemRefs.current.findIndex(
|
||||||
|
(item) => item?.id === getGroupBtnId(activeGroupId)
|
||||||
|
);
|
||||||
|
setFocusedItemIndex(activeGroupIndex);
|
||||||
|
}
|
||||||
|
}, [activeGroupId, focusedItemIndex]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
typeof focusedItemIndex === 'number' &&
|
||||||
|
focusedItemIndex > -1 &&
|
||||||
|
isOpen
|
||||||
|
) {
|
||||||
|
const focusedItem = menuItemRefs.current[focusedItemIndex];
|
||||||
|
const containingGroupId = focusedItem?.closest(
|
||||||
|
'[data-accordion-group]'
|
||||||
|
)?.id;
|
||||||
|
if (
|
||||||
|
!focusedItem?.id &&
|
||||||
|
containingGroupId &&
|
||||||
|
containingGroupId !== activeGroupId
|
||||||
|
) {
|
||||||
|
setActiveGroupId(containingGroupId);
|
||||||
|
}
|
||||||
|
focusedItem?.focus();
|
||||||
|
}
|
||||||
|
}, [activeGroupId, focusedItemIndex, isOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
switch (e.key) {
|
||||||
|
case KeyboardKey.Up: {
|
||||||
|
if (
|
||||||
|
typeof focusedItemIndex === 'number' &&
|
||||||
|
menuItemRefs.current.length
|
||||||
|
) {
|
||||||
|
let previousItemIndex = focusedItemIndex - 1;
|
||||||
|
if (previousItemIndex < 0) {
|
||||||
|
previousItemIndex = menuItemRefs.current.length - 1;
|
||||||
|
}
|
||||||
|
setFocusedItemIndex(previousItemIndex);
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case KeyboardKey.Down: {
|
||||||
|
if (
|
||||||
|
typeof focusedItemIndex === 'number' &&
|
||||||
|
menuItemRefs.current.length
|
||||||
|
) {
|
||||||
|
let nextItemIndex = focusedItemIndex + 1;
|
||||||
|
if (nextItemIndex > menuItemRefs.current.length - 1) {
|
||||||
|
nextItemIndex = 0;
|
||||||
|
}
|
||||||
|
setFocusedItemIndex(nextItemIndex);
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [focusedItemIndex, groups]);
|
||||||
|
|
||||||
|
const selectEditor = (item: EditorMenuItem) => {
|
||||||
|
if (item.component) {
|
||||||
|
selectComponent(item.component);
|
||||||
|
} else if (item.isPremiumFeature) {
|
||||||
|
premiumModal.activate(item.name);
|
||||||
|
} else {
|
||||||
|
selectComponent(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{groups.map((group) => {
|
||||||
|
const groupId = getGroupId(group);
|
||||||
|
const buttonId = getGroupBtnId(groupId);
|
||||||
|
const contentId = `${groupId}-content`;
|
||||||
|
|
||||||
|
if (!group.items || !group.items.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment key={groupId}>
|
||||||
|
<div id={groupId} data-accordion-group>
|
||||||
|
<h3 className="m-0">
|
||||||
|
<button
|
||||||
|
aria-controls={contentId}
|
||||||
|
aria-expanded={activeGroupId === groupId}
|
||||||
|
className="sn-dropdown-item focus:bg-info-backdrop justify-between py-2.5"
|
||||||
|
id={buttonId}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
if (activeGroupId !== groupId) {
|
||||||
|
setActiveGroupId(groupId);
|
||||||
|
} else {
|
||||||
|
setActiveGroupId('');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={closeOnBlur}
|
||||||
|
ref={(button) => {
|
||||||
|
if (!menuItemRefs.current?.includes(button) && button) {
|
||||||
|
menuItemRefs.current.push(button);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
{group.icon && (
|
||||||
|
<Icon
|
||||||
|
type={group.icon}
|
||||||
|
className={`mr-2 ${group.iconClassName}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="font-semibold text-input">
|
||||||
|
{group.title}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Icon
|
||||||
|
type="chevron-down"
|
||||||
|
className={`sn-dropdown-arrow color-grey-1 ${
|
||||||
|
activeGroupId === groupId && 'sn-dropdown-arrow-flipped'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</h3>
|
||||||
|
<div
|
||||||
|
id={contentId}
|
||||||
|
aria-labelledby={buttonId}
|
||||||
|
className={activeGroupId !== groupId ? 'hidden' : ''}
|
||||||
|
>
|
||||||
|
<div role="radiogroup">
|
||||||
|
{group.items.map((item) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
role="radio"
|
||||||
|
onClick={() => {
|
||||||
|
selectEditor(item);
|
||||||
|
}}
|
||||||
|
className={`sn-dropdown-item py-2 text-input focus:bg-info-backdrop focus:shadow-none ${
|
||||||
|
item.isPremiumFeature && 'justify-between'
|
||||||
|
}`}
|
||||||
|
aria-checked={false}
|
||||||
|
onBlur={closeOnBlur}
|
||||||
|
ref={(button) => {
|
||||||
|
if (
|
||||||
|
!menuItemRefs.current?.includes(button) &&
|
||||||
|
button
|
||||||
|
) {
|
||||||
|
menuItemRefs.current.push(button);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div
|
||||||
|
className={`pseudo-radio-btn ${
|
||||||
|
isSelectedEditor(item)
|
||||||
|
? 'pseudo-radio-btn--checked'
|
||||||
|
: ''
|
||||||
|
} ml-0.5 mr-2`}
|
||||||
|
></div>
|
||||||
|
{item.name}
|
||||||
|
</div>
|
||||||
|
{item.isPremiumFeature && (
|
||||||
|
<Icon type="premium-feature" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="min-h-1px my-1 bg-border hide-if-last-child"></div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import {
|
||||||
|
ComponentArea,
|
||||||
|
FeatureDescription,
|
||||||
|
Features,
|
||||||
|
NoteType,
|
||||||
|
} from '@standardnotes/features';
|
||||||
|
import { ContentType, SNComponent } from '@standardnotes/snjs';
|
||||||
|
import { EditorMenuItem, EditorMenuGroup } from '../ChangeEditorOption';
|
||||||
|
|
||||||
|
/** @todo Implement interchangeable alert */
|
||||||
|
|
||||||
|
export const PLAIN_EDITOR_NAME = 'Plain Editor';
|
||||||
|
|
||||||
|
type EditorGroup = NoteType | 'plain' | 'others';
|
||||||
|
|
||||||
|
const getEditorGroup = (
|
||||||
|
featureDescription: FeatureDescription
|
||||||
|
): EditorGroup => {
|
||||||
|
if (featureDescription.note_type) {
|
||||||
|
return featureDescription.note_type;
|
||||||
|
} else if (featureDescription.file_type) {
|
||||||
|
switch (featureDescription.file_type) {
|
||||||
|
case 'txt':
|
||||||
|
return 'plain';
|
||||||
|
case 'html':
|
||||||
|
return NoteType.RichText;
|
||||||
|
case 'md':
|
||||||
|
return NoteType.Markdown;
|
||||||
|
default:
|
||||||
|
return 'others';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'others';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createEditorMenuGroups = (editors: SNComponent[]) => {
|
||||||
|
const editorItems: Record<EditorGroup, EditorMenuItem[]> = {
|
||||||
|
plain: [
|
||||||
|
{
|
||||||
|
name: PLAIN_EDITOR_NAME,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'rich-text': [],
|
||||||
|
markdown: [],
|
||||||
|
task: [],
|
||||||
|
code: [],
|
||||||
|
spreadsheet: [],
|
||||||
|
authentication: [],
|
||||||
|
others: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
Features.filter(
|
||||||
|
(feature) =>
|
||||||
|
feature.content_type === ContentType.Component &&
|
||||||
|
feature.area === ComponentArea.Editor
|
||||||
|
).forEach((editorFeature) => {
|
||||||
|
if (
|
||||||
|
!editors.find((editor) => editor.identifier === editorFeature.identifier)
|
||||||
|
) {
|
||||||
|
editorItems[getEditorGroup(editorFeature)].push({
|
||||||
|
name: editorFeature.name as string,
|
||||||
|
isPremiumFeature: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
editors.forEach((editor) => {
|
||||||
|
const editorItem: EditorMenuItem = {
|
||||||
|
name: editor.name,
|
||||||
|
component: editor,
|
||||||
|
};
|
||||||
|
|
||||||
|
editorItems[getEditorGroup(editor.package_info)].push(editorItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
const editorMenuGroups: EditorMenuGroup[] = [
|
||||||
|
{
|
||||||
|
icon: 'plain-text',
|
||||||
|
iconClassName: 'color-accessory-tint-1',
|
||||||
|
title: 'Plain text',
|
||||||
|
items: editorItems.plain,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'rich-text',
|
||||||
|
iconClassName: 'color-accessory-tint-1',
|
||||||
|
title: 'Rich text',
|
||||||
|
items: editorItems['rich-text'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'markdown',
|
||||||
|
iconClassName: 'color-accessory-tint-2',
|
||||||
|
title: 'Markdown text',
|
||||||
|
items: editorItems.markdown,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'tasks',
|
||||||
|
iconClassName: 'color-accessory-tint-3',
|
||||||
|
title: 'Todo',
|
||||||
|
items: editorItems.task,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'code',
|
||||||
|
iconClassName: 'color-accessory-tint-4',
|
||||||
|
title: 'Code',
|
||||||
|
items: editorItems.code,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'spreadsheets',
|
||||||
|
iconClassName: 'color-accessory-tint-5',
|
||||||
|
title: 'Spreadsheet',
|
||||||
|
items: editorItems.spreadsheet,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'authenticator',
|
||||||
|
iconClassName: 'color-accessory-tint-6',
|
||||||
|
title: 'Authentication',
|
||||||
|
items: editorItems.authentication,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'editor',
|
||||||
|
iconClassName: 'color-neutral',
|
||||||
|
title: 'Others',
|
||||||
|
items: editorItems.others,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return editorMenuGroups;
|
||||||
|
};
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from '@reach/disclosure';
|
} from '@reach/disclosure';
|
||||||
import { useRef, useState } from 'preact/hooks';
|
import { useRef, useState } from 'preact/hooks';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { NotesOptions } from './NotesOptions';
|
import { NotesOptions } from './NotesOptions/NotesOptions';
|
||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -17,76 +17,85 @@ type Props = {
|
|||||||
appState: AppState;
|
appState: AppState;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NotesOptionsPanel = observer(({ application, appState }: Props) => {
|
export const NotesOptionsPanel = observer(
|
||||||
const [open, setOpen] = useState(false);
|
({ application, appState }: Props) => {
|
||||||
const [position, setPosition] = useState({
|
const [open, setOpen] = useState(false);
|
||||||
top: 0,
|
const [position, setPosition] = useState({
|
||||||
right: 0,
|
top: 0,
|
||||||
});
|
right: 0,
|
||||||
const [maxHeight, setMaxHeight] = useState<number | 'auto'>('auto');
|
});
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
const [maxHeight, setMaxHeight] = useState<number | 'auto'>('auto');
|
||||||
const panelRef = useRef<HTMLDivElement>(null);
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
const [closeOnBlur] = useCloseOnBlur(panelRef as any, setOpen);
|
const panelRef = useRef<HTMLDivElement>(null);
|
||||||
const [submenuOpen, setSubmenuOpen] = useState(false);
|
const [closeOnBlur] = useCloseOnBlur(panelRef, setOpen);
|
||||||
|
const [submenuOpen, setSubmenuOpen] = useState(false);
|
||||||
|
|
||||||
const onSubmenuChange = (open: boolean) => {
|
const onSubmenuChange = (open: boolean) => {
|
||||||
setSubmenuOpen(open);
|
setSubmenuOpen(open);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Disclosure
|
<Disclosure
|
||||||
open={open}
|
open={open}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
const rect = buttonRef.current!.getBoundingClientRect();
|
const rect = buttonRef.current?.getBoundingClientRect();
|
||||||
const { clientHeight } = document.documentElement;
|
if (rect) {
|
||||||
const footerHeight = 32;
|
const { clientHeight } = document.documentElement;
|
||||||
setMaxHeight(clientHeight - rect.bottom - footerHeight - 2);
|
const footerElementRect = document
|
||||||
setPosition({
|
.getElementById('footer-bar')
|
||||||
top: rect.bottom,
|
?.getBoundingClientRect();
|
||||||
right: document.body.clientWidth - rect.right,
|
const footerHeightInPx = footerElementRect?.height;
|
||||||
});
|
if (footerHeightInPx) {
|
||||||
setOpen(!open);
|
setMaxHeight(clientHeight - rect.bottom - footerHeightInPx - 2);
|
||||||
}}
|
}
|
||||||
>
|
setPosition({
|
||||||
<DisclosureButton
|
top: rect.bottom,
|
||||||
onKeyDown={(event) => {
|
right: document.body.clientWidth - rect.right,
|
||||||
if (event.key === 'Escape' && !submenuOpen) {
|
});
|
||||||
setOpen(false);
|
setOpen(!open);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onBlur={closeOnBlur}
|
|
||||||
ref={buttonRef}
|
|
||||||
className="sn-icon-button"
|
|
||||||
>
|
>
|
||||||
<VisuallyHidden>Actions</VisuallyHidden>
|
<DisclosureButton
|
||||||
<Icon type="more" className="block" />
|
onKeyDown={(event) => {
|
||||||
</DisclosureButton>
|
if (event.key === 'Escape' && !submenuOpen) {
|
||||||
<DisclosurePanel
|
setOpen(false);
|
||||||
onKeyDown={(event) => {
|
}
|
||||||
if (event.key === 'Escape' && !submenuOpen) {
|
}}
|
||||||
setOpen(false);
|
onBlur={closeOnBlur}
|
||||||
buttonRef.current!.focus();
|
ref={buttonRef}
|
||||||
}
|
className="sn-icon-button"
|
||||||
}}
|
>
|
||||||
ref={panelRef}
|
<VisuallyHidden>Actions</VisuallyHidden>
|
||||||
style={{
|
<Icon type="more" className="block" />
|
||||||
...position,
|
</DisclosureButton>
|
||||||
maxHeight,
|
<DisclosurePanel
|
||||||
}}
|
onKeyDown={(event) => {
|
||||||
className="sn-dropdown sn-dropdown--animated min-w-80 max-h-120 max-w-xs flex flex-col py-2 overflow-y-auto fixed"
|
if (event.key === 'Escape' && !submenuOpen) {
|
||||||
onBlur={closeOnBlur}
|
setOpen(false);
|
||||||
>
|
buttonRef.current?.focus();
|
||||||
{open && (
|
}
|
||||||
<NotesOptions
|
}}
|
||||||
application={application}
|
ref={panelRef}
|
||||||
appState={appState}
|
style={{
|
||||||
closeOnBlur={closeOnBlur}
|
...position,
|
||||||
onSubmenuChange={onSubmenuChange}
|
maxHeight,
|
||||||
/>
|
}}
|
||||||
)}
|
className="sn-dropdown sn-dropdown--animated min-w-80 max-h-120 max-w-xs flex flex-col py-2 overflow-y-auto fixed"
|
||||||
</DisclosurePanel>
|
onBlur={closeOnBlur}
|
||||||
</Disclosure>
|
>
|
||||||
);
|
{open && (
|
||||||
});
|
<NotesOptions
|
||||||
|
application={application}
|
||||||
|
appState={appState}
|
||||||
|
closeOnBlur={closeOnBlur}
|
||||||
|
onSubmenuChange={onSubmenuChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DisclosurePanel>
|
||||||
|
</Disclosure>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const NotesOptionsPanelDirective = toDirective<Props>(NotesOptionsPanel);
|
export const NotesOptionsPanelDirective = toDirective<Props>(NotesOptionsPanel);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { StateUpdater, useCallback, useState, useEffect } from 'preact/hooks';
|
|||||||
* monitored.
|
* monitored.
|
||||||
*/
|
*/
|
||||||
export function useCloseOnBlur(
|
export function useCloseOnBlur(
|
||||||
container: { current?: HTMLDivElement },
|
container: { current?: HTMLDivElement | null },
|
||||||
setOpen: (open: boolean) => void
|
setOpen: (open: boolean) => void
|
||||||
): [
|
): [
|
||||||
(event: { relatedTarget: EventTarget | null }) => void,
|
(event: { relatedTarget: EventTarget | null }) => void,
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
import { WebDirective } from './../../types';
|
|
||||||
import { WebApplication } from '@/ui_models/application';
|
|
||||||
import { SNComponent, SNItem, ComponentArea } from '@standardnotes/snjs';
|
|
||||||
import { isDesktopApplication } from '@/utils';
|
|
||||||
import template from '%/directives/editor-menu.pug';
|
|
||||||
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
|
||||||
|
|
||||||
interface EditorMenuScope {
|
|
||||||
callback: (component: SNComponent) => void;
|
|
||||||
selectedEditorUuid: string;
|
|
||||||
currentItem: SNItem;
|
|
||||||
application: WebApplication;
|
|
||||||
}
|
|
||||||
|
|
||||||
class EditorMenuCtrl extends PureViewCtrl implements EditorMenuScope {
|
|
||||||
callback!: () => (component: SNComponent) => void;
|
|
||||||
selectedEditorUuid!: string;
|
|
||||||
currentItem!: SNItem;
|
|
||||||
application!: WebApplication;
|
|
||||||
|
|
||||||
/* @ngInject */
|
|
||||||
constructor($timeout: ng.ITimeoutService) {
|
|
||||||
super($timeout);
|
|
||||||
this.state = {
|
|
||||||
isDesktop: isDesktopApplication(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public isEditorSelected(editor: SNComponent) {
|
|
||||||
if (!this.selectedEditorUuid) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return this.selectedEditorUuid === editor.uuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
$onInit() {
|
|
||||||
super.$onInit();
|
|
||||||
const editors = this.application.componentManager
|
|
||||||
.componentsForArea(ComponentArea.Editor)
|
|
||||||
.sort((a, b) => {
|
|
||||||
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
|
|
||||||
});
|
|
||||||
this.setState({
|
|
||||||
editors: editors,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
selectComponent(component: SNComponent) {
|
|
||||||
if (component) {
|
|
||||||
if (component.conflictOf) {
|
|
||||||
this.application.changeAndSaveItem(component.uuid, (mutator) => {
|
|
||||||
mutator.conflictOf = undefined;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.$timeout(() => {
|
|
||||||
this.callback()(component);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
offlineAvailableForComponent(component: SNComponent) {
|
|
||||||
return component.local_url && this.state.isDesktop;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EditorMenu extends WebDirective {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.restrict = 'E';
|
|
||||||
this.template = template;
|
|
||||||
this.controller = EditorMenuCtrl;
|
|
||||||
this.controllerAs = 'self';
|
|
||||||
this.bindToController = true;
|
|
||||||
this.scope = {
|
|
||||||
callback: '&',
|
|
||||||
selectedEditorUuid: '=',
|
|
||||||
currentItem: '=',
|
|
||||||
application: '=',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
export { ActionsMenu } from './actionsMenu';
|
export { ActionsMenu } from './actionsMenu';
|
||||||
export { EditorMenu } from './editorMenu';
|
|
||||||
export { InputModal } from './inputModal';
|
export { InputModal } from './inputModal';
|
||||||
export { MenuRow } from './menuRow';
|
export { MenuRow } from './menuRow';
|
||||||
export { PanelResizer } from './panelResizer';
|
export { PanelResizer } from './panelResizer';
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export enum KeyboardKey {
|
|||||||
Up = 'ArrowUp',
|
Up = 'ArrowUp',
|
||||||
Down = 'ArrowDown',
|
Down = 'ArrowDown',
|
||||||
Enter = 'Enter',
|
Enter = 'Enter',
|
||||||
|
Escape = 'Escape',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum KeyboardModifier {
|
export enum KeyboardModifier {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { confirmDialog } from '@/services/alertService';
|
import { confirmDialog } from '@/services/alertService';
|
||||||
import { KeyboardModifier } from '@/services/ioService';
|
import { KeyboardModifier } from '@/services/ioService';
|
||||||
import { StringEmptyTrash, Strings, StringUtils } from '@/strings';
|
import { StringEmptyTrash, Strings, StringUtils } from '@/strings';
|
||||||
|
import { MENU_MARGIN_FROM_APP_BORDER } from '@/views/constants';
|
||||||
import {
|
import {
|
||||||
UuidString,
|
UuidString,
|
||||||
SNNote,
|
SNNote,
|
||||||
@@ -205,32 +206,39 @@ export class NotesState {
|
|||||||
document.documentElement
|
document.documentElement
|
||||||
).fontSize;
|
).fontSize;
|
||||||
const maxContextMenuHeight = parseFloat(defaultFontSize) * 30;
|
const maxContextMenuHeight = parseFloat(defaultFontSize) * 30;
|
||||||
const footerHeight = 32;
|
const footerElementRect = document
|
||||||
|
.getElementById('footer-bar')
|
||||||
|
?.getBoundingClientRect();
|
||||||
|
const footerHeightInPx = footerElementRect?.height;
|
||||||
|
|
||||||
// Open up-bottom is default behavior
|
// Open up-bottom is default behavior
|
||||||
let openUpBottom = true;
|
let openUpBottom = true;
|
||||||
|
|
||||||
const bottomSpace =
|
if (footerHeightInPx) {
|
||||||
clientHeight - footerHeight - this.contextMenuClickLocation.y;
|
const bottomSpace =
|
||||||
const upSpace = this.contextMenuClickLocation.y;
|
clientHeight - footerHeightInPx - this.contextMenuClickLocation.y;
|
||||||
|
const upSpace = this.contextMenuClickLocation.y;
|
||||||
|
|
||||||
// If not enough space to open up-bottom
|
// If not enough space to open up-bottom
|
||||||
if (maxContextMenuHeight > bottomSpace) {
|
if (maxContextMenuHeight > bottomSpace) {
|
||||||
// If there's enough space, open bottom-up
|
// If there's enough space, open bottom-up
|
||||||
if (upSpace > maxContextMenuHeight) {
|
if (upSpace > maxContextMenuHeight) {
|
||||||
openUpBottom = false;
|
|
||||||
this.setContextMenuMaxHeight('auto');
|
|
||||||
// Else, reduce max height (menu will be scrollable) and open in whichever direction there's more space
|
|
||||||
} else {
|
|
||||||
if (upSpace > bottomSpace) {
|
|
||||||
this.setContextMenuMaxHeight(upSpace - 2);
|
|
||||||
openUpBottom = false;
|
openUpBottom = false;
|
||||||
|
this.setContextMenuMaxHeight('auto');
|
||||||
|
// Else, reduce max height (menu will be scrollable) and open in whichever direction there's more space
|
||||||
} else {
|
} else {
|
||||||
this.setContextMenuMaxHeight(bottomSpace - 2);
|
if (upSpace > bottomSpace) {
|
||||||
|
this.setContextMenuMaxHeight(upSpace - MENU_MARGIN_FROM_APP_BORDER);
|
||||||
|
openUpBottom = false;
|
||||||
|
} else {
|
||||||
|
this.setContextMenuMaxHeight(
|
||||||
|
bottomSpace - MENU_MARGIN_FROM_APP_BORDER
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
this.setContextMenuMaxHeight('auto');
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
this.setContextMenuMaxHeight('auto');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (openUpBottom) {
|
if (openUpBottom) {
|
||||||
|
|||||||
@@ -2,3 +2,5 @@ export const PANEL_NAME_NOTES = 'notes';
|
|||||||
export const PANEL_NAME_NAVIGATION = 'navigation';
|
export const PANEL_NAME_NAVIGATION = 'navigation';
|
||||||
export const EMAIL_REGEX =
|
export const EMAIL_REGEX =
|
||||||
/^([a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?)$/;
|
/^([a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?)$/;
|
||||||
|
export const MENU_MARGIN_FROM_APP_BORDER = 5;
|
||||||
|
export const MAX_MENU_SIZE_MULTIPLIER = 30;
|
||||||
|
|||||||
@@ -63,20 +63,6 @@
|
|||||||
.sn-component(ng-if='self.note')
|
.sn-component(ng-if='self.note')
|
||||||
#editor-menu-bar.sk-app-bar.no-edges
|
#editor-menu-bar.sk-app-bar.no-edges
|
||||||
.left
|
.left
|
||||||
.sk-app-bar-item(
|
|
||||||
click-outside=`self.setMenuState('showEditorMenu', false)`
|
|
||||||
is-open='self.state.showEditorMenu',
|
|
||||||
ng-class="{'selected' : self.state.showEditorMenu}",
|
|
||||||
ng-click="self.toggleMenu('showEditorMenu')"
|
|
||||||
)
|
|
||||||
.sk-label Editor
|
|
||||||
editor-menu(
|
|
||||||
callback='self.editorMenuOnSelect',
|
|
||||||
current-item='self.note',
|
|
||||||
ng-if='self.state.showEditorMenu',
|
|
||||||
selected-editor-uuid='self.state.editorComponentViewer && self.state.editorComponentViewer.component.uuid',
|
|
||||||
application='self.application'
|
|
||||||
)
|
|
||||||
.sk-app-bar-item(
|
.sk-app-bar-item(
|
||||||
click-outside=`self.setMenuState('showActionsMenu', false)`,
|
click-outside=`self.setMenuState('showActionsMenu', false)`,
|
||||||
is-open='self.state.showActionsMenu',
|
is-open='self.state.showActionsMenu',
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
ContentType,
|
ContentType,
|
||||||
SNComponent,
|
SNComponent,
|
||||||
SNNote,
|
SNNote,
|
||||||
NoteMutator,
|
|
||||||
ComponentArea,
|
ComponentArea,
|
||||||
PrefKey,
|
PrefKey,
|
||||||
ComponentMutator,
|
ComponentMutator,
|
||||||
@@ -28,7 +27,6 @@ import { EventSource } from '@/ui_models/app_state';
|
|||||||
import {
|
import {
|
||||||
STRING_DELETE_PLACEHOLDER_ATTEMPT,
|
STRING_DELETE_PLACEHOLDER_ATTEMPT,
|
||||||
STRING_DELETE_LOCKED_ATTEMPT,
|
STRING_DELETE_LOCKED_ATTEMPT,
|
||||||
STRING_EDIT_LOCKED_ATTEMPT,
|
|
||||||
StringDeleteNote,
|
StringDeleteNote,
|
||||||
} from '@/strings';
|
} from '@/strings';
|
||||||
import { confirmDialog } from '@/services/alertService';
|
import { confirmDialog } from '@/services/alertService';
|
||||||
@@ -59,7 +57,6 @@ type EditorState = {
|
|||||||
isDesktop?: boolean;
|
isDesktop?: boolean;
|
||||||
syncTakingTooLong: boolean;
|
syncTakingTooLong: boolean;
|
||||||
showActionsMenu: boolean;
|
showActionsMenu: boolean;
|
||||||
showEditorMenu: boolean;
|
|
||||||
showHistoryMenu: boolean;
|
showHistoryMenu: boolean;
|
||||||
spellcheck: boolean;
|
spellcheck: boolean;
|
||||||
/** Setting to true then false will allow the main content textarea to be destroyed
|
/** Setting to true then false will allow the main content textarea to be destroyed
|
||||||
@@ -83,6 +80,46 @@ function sortAlphabetically(array: SNComponent[]): SNComponent[] {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const transactionForAssociateComponentWithCurrentNote = (
|
||||||
|
component: SNComponent,
|
||||||
|
note: SNNote
|
||||||
|
) => {
|
||||||
|
const transaction: TransactionalMutation = {
|
||||||
|
itemUuid: component.uuid,
|
||||||
|
mutate: (m: ItemMutator) => {
|
||||||
|
const mutator = m as ComponentMutator;
|
||||||
|
mutator.removeDisassociatedItemId(note.uuid);
|
||||||
|
mutator.associateWithItem(note.uuid);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return transaction;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const transactionForDisassociateComponentWithCurrentNote = (
|
||||||
|
component: SNComponent,
|
||||||
|
note: SNNote
|
||||||
|
) => {
|
||||||
|
const transaction: TransactionalMutation = {
|
||||||
|
itemUuid: component.uuid,
|
||||||
|
mutate: (m: ItemMutator) => {
|
||||||
|
const mutator = m as ComponentMutator;
|
||||||
|
mutator.removeAssociatedItemId(note.uuid);
|
||||||
|
mutator.disassociateWithItem(note.uuid);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return transaction;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reloadFont = (monospaceFont?: boolean) => {
|
||||||
|
const root = document.querySelector(':root') as HTMLElement;
|
||||||
|
const propertyName = '--sn-stylekit-editor-font-family';
|
||||||
|
if (monospaceFont) {
|
||||||
|
root.style.setProperty(propertyName, 'var(--sn-stylekit-monospace-font)');
|
||||||
|
} else {
|
||||||
|
root.style.setProperty(propertyName, 'var(--sn-stylekit-sans-serif-font)');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export class NoteView extends PureViewCtrl<unknown, EditorState> {
|
export class NoteView extends PureViewCtrl<unknown, EditorState> {
|
||||||
/** Passed through template */
|
/** Passed through template */
|
||||||
readonly application!: WebApplication;
|
readonly application!: WebApplication;
|
||||||
@@ -114,7 +151,6 @@ export class NoteView extends PureViewCtrl<unknown, EditorState> {
|
|||||||
onReady: () => this.reloadPreferences(),
|
onReady: () => this.reloadPreferences(),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.editorMenuOnSelect = this.editorMenuOnSelect.bind(this);
|
|
||||||
this.onPanelResizeFinish = this.onPanelResizeFinish.bind(this);
|
this.onPanelResizeFinish = this.onPanelResizeFinish.bind(this);
|
||||||
this.setScrollPosition = this.setScrollPosition.bind(this);
|
this.setScrollPosition = this.setScrollPosition.bind(this);
|
||||||
this.resetScrollPosition = this.resetScrollPosition.bind(this);
|
this.resetScrollPosition = this.resetScrollPosition.bind(this);
|
||||||
@@ -146,7 +182,6 @@ export class NoteView extends PureViewCtrl<unknown, EditorState> {
|
|||||||
this.onEditorComponentLoad = undefined;
|
this.onEditorComponentLoad = undefined;
|
||||||
this.statusTimeout = undefined;
|
this.statusTimeout = undefined;
|
||||||
(this.onPanelResizeFinish as unknown) = undefined;
|
(this.onPanelResizeFinish as unknown) = undefined;
|
||||||
(this.editorMenuOnSelect as unknown) = undefined;
|
|
||||||
super.deinit();
|
super.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +283,6 @@ export class NoteView extends PureViewCtrl<unknown, EditorState> {
|
|||||||
spellcheck: true,
|
spellcheck: true,
|
||||||
syncTakingTooLong: false,
|
syncTakingTooLong: false,
|
||||||
showActionsMenu: false,
|
showActionsMenu: false,
|
||||||
showEditorMenu: false,
|
|
||||||
showHistoryMenu: false,
|
showHistoryMenu: false,
|
||||||
noteStatus: undefined,
|
noteStatus: undefined,
|
||||||
textareaUnloading: false,
|
textareaUnloading: false,
|
||||||
@@ -441,7 +475,7 @@ export class NoteView extends PureViewCtrl<unknown, EditorState> {
|
|||||||
editorStateDidLoad: true,
|
editorStateDidLoad: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.reloadFont();
|
reloadFont(this.state.monospaceFont);
|
||||||
} else {
|
} else {
|
||||||
await this.setState({
|
await this.setState({
|
||||||
editorStateDidLoad: true,
|
editorStateDidLoad: true,
|
||||||
@@ -462,7 +496,7 @@ export class NoteView extends PureViewCtrl<unknown, EditorState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
closeAllMenus(exclude?: string) {
|
closeAllMenus(exclude?: string) {
|
||||||
const allMenus = ['showEditorMenu', 'showActionsMenu', 'showHistoryMenu'];
|
const allMenus = ['showActionsMenu', 'showHistoryMenu'];
|
||||||
const menuState: any = {};
|
const menuState: any = {};
|
||||||
for (const candidate of allMenus) {
|
for (const candidate of allMenus) {
|
||||||
if (candidate !== exclude) {
|
if (candidate !== exclude) {
|
||||||
@@ -472,69 +506,6 @@ export class NoteView extends PureViewCtrl<unknown, EditorState> {
|
|||||||
this.setState(menuState);
|
this.setState(menuState);
|
||||||
}
|
}
|
||||||
|
|
||||||
async editorMenuOnSelect(component?: SNComponent) {
|
|
||||||
const transactions: TransactionalMutation[] = [];
|
|
||||||
|
|
||||||
this.setMenuState('showEditorMenu', false);
|
|
||||||
|
|
||||||
if (this.appState.getActiveNoteController()?.isTemplateNote) {
|
|
||||||
await this.appState.getActiveNoteController().insertTemplatedNote();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.note.locked) {
|
|
||||||
this.application.alertService.alert(STRING_EDIT_LOCKED_ATTEMPT);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!component) {
|
|
||||||
if (!this.note.prefersPlainEditor) {
|
|
||||||
transactions.push({
|
|
||||||
itemUuid: this.note.uuid,
|
|
||||||
mutate: (m: ItemMutator) => {
|
|
||||||
const noteMutator = m as NoteMutator;
|
|
||||||
noteMutator.prefersPlainEditor = true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
this.state.editorComponentViewer?.component.isExplicitlyEnabledForItem(
|
|
||||||
this.note.uuid
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
transactions.push(
|
|
||||||
this.transactionForDisassociateComponentWithCurrentNote(
|
|
||||||
this.state.editorComponentViewer.component
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.reloadFont();
|
|
||||||
} else if (component.area === ComponentArea.Editor) {
|
|
||||||
const currentEditor = this.state.editorComponentViewer?.component;
|
|
||||||
if (currentEditor && component.uuid !== currentEditor.uuid) {
|
|
||||||
transactions.push(
|
|
||||||
this.transactionForDisassociateComponentWithCurrentNote(currentEditor)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const prefersPlain = this.note.prefersPlainEditor;
|
|
||||||
if (prefersPlain) {
|
|
||||||
transactions.push({
|
|
||||||
itemUuid: this.note.uuid,
|
|
||||||
mutate: (m: ItemMutator) => {
|
|
||||||
const noteMutator = m as NoteMutator;
|
|
||||||
noteMutator.prefersPlainEditor = false;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
transactions.push(
|
|
||||||
this.transactionForAssociateComponentWithCurrentNote(component)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.application.runTransactionalMutations(transactions);
|
|
||||||
/** Dirtying can happen above */
|
|
||||||
this.application.sync();
|
|
||||||
}
|
|
||||||
|
|
||||||
hasAvailableExtensions() {
|
hasAvailableExtensions() {
|
||||||
return (
|
return (
|
||||||
this.application.actionsManager.extensionsInContextOfItem(this.note)
|
this.application.actionsManager.extensionsInContextOfItem(this.note)
|
||||||
@@ -702,7 +673,7 @@ export class NoteView extends PureViewCtrl<unknown, EditorState> {
|
|||||||
if (spellcheck !== this.state.spellcheck) {
|
if (spellcheck !== this.state.spellcheck) {
|
||||||
await this.setState({ textareaUnloading: true });
|
await this.setState({ textareaUnloading: true });
|
||||||
await this.setState({ textareaUnloading: false });
|
await this.setState({ textareaUnloading: false });
|
||||||
this.reloadFont();
|
reloadFont(this.state.monospaceFont);
|
||||||
|
|
||||||
await this.setState({
|
await this.setState({
|
||||||
spellcheck,
|
spellcheck,
|
||||||
@@ -733,7 +704,7 @@ export class NoteView extends PureViewCtrl<unknown, EditorState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.reloadFont();
|
reloadFont(this.state.monospaceFont);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.state.marginResizersEnabled &&
|
this.state.marginResizersEnabled &&
|
||||||
@@ -753,19 +724,6 @@ export class NoteView extends PureViewCtrl<unknown, EditorState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadFont() {
|
|
||||||
const root = document.querySelector(':root') as HTMLElement;
|
|
||||||
const propertyName = '--sn-stylekit-editor-font-family';
|
|
||||||
if (this.state.monospaceFont) {
|
|
||||||
root.style.setProperty(propertyName, 'var(--sn-stylekit-monospace-font)');
|
|
||||||
} else {
|
|
||||||
root.style.setProperty(
|
|
||||||
propertyName,
|
|
||||||
'var(--sn-stylekit-sans-serif-font)'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @components */
|
/** @components */
|
||||||
|
|
||||||
registerComponentManagerEventObserver() {
|
registerComponentManagerEventObserver() {
|
||||||
@@ -844,42 +802,16 @@ export class NoteView extends PureViewCtrl<unknown, EditorState> {
|
|||||||
|
|
||||||
async disassociateComponentWithCurrentNote(component: SNComponent) {
|
async disassociateComponentWithCurrentNote(component: SNComponent) {
|
||||||
return this.application.runTransactionalMutation(
|
return this.application.runTransactionalMutation(
|
||||||
this.transactionForDisassociateComponentWithCurrentNote(component)
|
transactionForDisassociateComponentWithCurrentNote(component, this.note)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
transactionForDisassociateComponentWithCurrentNote(component: SNComponent) {
|
|
||||||
const note = this.note;
|
|
||||||
const transaction: TransactionalMutation = {
|
|
||||||
itemUuid: component.uuid,
|
|
||||||
mutate: (m: ItemMutator) => {
|
|
||||||
const mutator = m as ComponentMutator;
|
|
||||||
mutator.removeAssociatedItemId(note.uuid);
|
|
||||||
mutator.disassociateWithItem(note.uuid);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return transaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
async associateComponentWithCurrentNote(component: SNComponent) {
|
async associateComponentWithCurrentNote(component: SNComponent) {
|
||||||
return this.application.runTransactionalMutation(
|
return this.application.runTransactionalMutation(
|
||||||
this.transactionForAssociateComponentWithCurrentNote(component)
|
transactionForAssociateComponentWithCurrentNote(component, this.note)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
transactionForAssociateComponentWithCurrentNote(component: SNComponent) {
|
|
||||||
const note = this.note;
|
|
||||||
const transaction: TransactionalMutation = {
|
|
||||||
itemUuid: component.uuid,
|
|
||||||
mutate: (m: ItemMutator) => {
|
|
||||||
const mutator = m as ComponentMutator;
|
|
||||||
mutator.removeDisassociatedItemId(note.uuid);
|
|
||||||
mutator.associateWithItem(note.uuid);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return transaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
registerKeyboardShortcuts() {
|
registerKeyboardShortcuts() {
|
||||||
this.removeTrashKeyObserver = this.application.io.addKeyObserver({
|
this.removeTrashKeyObserver = this.application.io.addKeyObserver({
|
||||||
key: KeyboardKey.Backspace,
|
key: KeyboardKey.Backspace,
|
||||||
|
|||||||
@@ -249,6 +249,10 @@
|
|||||||
margin-left: 0rem !important;
|
margin-left: 0rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ml-0\.5 {
|
||||||
|
margin-left: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
.ml-3 {
|
.ml-3 {
|
||||||
margin-left: 0.75rem;
|
margin-left: 0.75rem;
|
||||||
}
|
}
|
||||||
@@ -374,6 +378,10 @@
|
|||||||
min-width: 7.5rem;
|
min-width: 7.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.min-w-68 {
|
||||||
|
min-width: 17rem;
|
||||||
|
}
|
||||||
|
|
||||||
.min-w-90 {
|
.min-w-90 {
|
||||||
min-width: 22.5rem;
|
min-width: 22.5rem;
|
||||||
}
|
}
|
||||||
@@ -687,9 +695,13 @@
|
|||||||
@extend .h-4;
|
@extend .h-4;
|
||||||
@extend .border-2;
|
@extend .border-2;
|
||||||
@extend .border-solid;
|
@extend .border-solid;
|
||||||
@extend .border-info;
|
|
||||||
@extend .rounded-full;
|
@extend .rounded-full;
|
||||||
@extend .relative;
|
@extend .relative;
|
||||||
|
border-color: var(--sn-stylekit-grey-1);
|
||||||
|
|
||||||
|
&--checked {
|
||||||
|
@extend .border-info;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pseudo-radio-btn--checked::after {
|
.pseudo-radio-btn--checked::after {
|
||||||
@@ -819,3 +831,7 @@
|
|||||||
cursor: default;
|
cursor: default;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hide-if-last-child:last-child {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
.sn-component
|
|
||||||
.sk-menu-panel.dropdown-menu
|
|
||||||
.sk-menu-panel-section
|
|
||||||
.sk-menu-panel-header
|
|
||||||
.sk-menu-panel-header-title Note Editor
|
|
||||||
menu-row(
|
|
||||||
action='self.selectComponent(null)',
|
|
||||||
circle="!self.selectedEditorUuid && 'success'",
|
|
||||||
label="'Plain Editor'"
|
|
||||||
)
|
|
||||||
menu-row(
|
|
||||||
ng-repeat='editor in self.state.editors track by editor.uuid'
|
|
||||||
action='self.selectComponent(editor)',
|
|
||||||
circle="self.isEditorSelected(editor) && 'success'",
|
|
||||||
label='editor.name',
|
|
||||||
subtitle="self.isEditorSelected(editor) && 'Version ' + editor.package_info.version",
|
|
||||||
)
|
|
||||||
.sk-menu-panel-column(
|
|
||||||
ng-if='editor.conflictOf'
|
|
||||||
)
|
|
||||||
.info(
|
|
||||||
ng-if='editor.conflictOf'
|
|
||||||
) Conflicted copy
|
|
||||||
a.no-decoration(
|
|
||||||
href='https://standardnotes.com/plans',
|
|
||||||
ng-if='self.state.editors.length == 0',
|
|
||||||
rel='noopener',
|
|
||||||
target='blank'
|
|
||||||
)
|
|
||||||
menu-row(label="'Download More Editors'")
|
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
"@reach/tooltip": "^0.16.2",
|
"@reach/tooltip": "^0.16.2",
|
||||||
"@standardnotes/components": "1.4.4",
|
"@standardnotes/components": "1.4.4",
|
||||||
"@standardnotes/features": "1.26.1",
|
"@standardnotes/features": "1.26.1",
|
||||||
"@standardnotes/snjs": "2.41.0",
|
"@standardnotes/snjs": "2.42.0",
|
||||||
"@standardnotes/settings": "^1.10.0",
|
"@standardnotes/settings": "^1.10.0",
|
||||||
"@standardnotes/sncrypto-web": "1.6.0",
|
"@standardnotes/sncrypto-web": "1.6.0",
|
||||||
"mobx": "^6.3.5",
|
"mobx": "^6.3.5",
|
||||||
|
|||||||
@@ -2657,10 +2657,10 @@
|
|||||||
buffer "^6.0.3"
|
buffer "^6.0.3"
|
||||||
libsodium-wrappers "^0.7.9"
|
libsodium-wrappers "^0.7.9"
|
||||||
|
|
||||||
"@standardnotes/snjs@2.41.0":
|
"@standardnotes/snjs@2.42.0":
|
||||||
version "2.41.0"
|
version "2.42.0"
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.41.0.tgz#48d02e981780a9d7d823f6186497a1c6459bbd66"
|
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.42.0.tgz#11e28210fdab5cfd464438ee3d7ab93b5edbe9c7"
|
||||||
integrity sha512-Rnl5wWbMKTQ+bQb7zGK/7HFt43bMBlP06G5zoEE0Vj/uxyneLxaEX+iPNXRUDqeHfxvedtTnIXsLGFEux/G1dg==
|
integrity sha512-XjevxZeru5Ryo4c7N4u9dhaqRd+ybk+guCbBT6sBfICD67B8/8qpS0GgKjeF5BlGTDvWvB/ECXyZo1bQVkKtgw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@standardnotes/auth" "^3.15.3"
|
"@standardnotes/auth" "^3.15.3"
|
||||||
"@standardnotes/common" "^1.8.0"
|
"@standardnotes/common" "^1.8.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user