From b932e2a45e2eeb789fb03a53b3d5b0d2ad1e9ec4 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Sat, 29 Jan 2022 01:53:39 +0530 Subject: [PATCH] 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 * 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 --- app/assets/icons/ic-editor.svg | 3 + app/assets/javascripts/app.ts | 2 - app/assets/javascripts/components/Icon.tsx | 2 + .../components/NotesContextMenu.tsx | 19 +- .../NotesOptions/ChangeEditorOption.tsx | 288 ++++++++++++++++++ .../{ => NotesOptions}/NotesOptions.tsx | 114 ++++--- .../changeEditor/EditorAccordionMenu.tsx | 256 ++++++++++++++++ .../changeEditor/createEditorMenuGroups.ts | 128 ++++++++ .../components/NotesOptionsPanel.tsx | 143 +++++---- app/assets/javascripts/components/utils.ts | 2 +- .../directives/views/editorMenu.ts | 81 ----- .../javascripts/directives/views/index.ts | 1 - app/assets/javascripts/services/ioService.ts | 1 + .../ui_models/app_state/notes_state.ts | 42 +-- app/assets/javascripts/views/constants.ts | 2 + .../javascripts/views/note_view/note-view.pug | 14 - .../javascripts/views/note_view/note_view.ts | 160 +++------- app/assets/stylesheets/_sn.scss | 18 +- .../templates/directives/editor-menu.pug | 30 -- package.json | 2 +- yarn.lock | 8 +- 21 files changed, 932 insertions(+), 384 deletions(-) create mode 100644 app/assets/icons/ic-editor.svg create mode 100644 app/assets/javascripts/components/NotesOptions/ChangeEditorOption.tsx rename app/assets/javascripts/components/{ => NotesOptions}/NotesOptions.tsx (85%) create mode 100644 app/assets/javascripts/components/NotesOptions/changeEditor/EditorAccordionMenu.tsx create mode 100644 app/assets/javascripts/components/NotesOptions/changeEditor/createEditorMenuGroups.ts delete mode 100644 app/assets/javascripts/directives/views/editorMenu.ts delete mode 100644 app/assets/templates/directives/editor-menu.pug diff --git a/app/assets/icons/ic-editor.svg b/app/assets/icons/ic-editor.svg new file mode 100644 index 000000000..209be8f42 --- /dev/null +++ b/app/assets/icons/ic-editor.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/assets/javascripts/app.ts b/app/assets/javascripts/app.ts index 49fcb298c..f5d61825a 100644 --- a/app/assets/javascripts/app.ts +++ b/app/assets/javascripts/app.ts @@ -64,7 +64,6 @@ import { } from './directives/functional'; import { ActionsMenu, - EditorMenu, HistoryMenu, InputModal, MenuRow, @@ -160,7 +159,6 @@ const startApplication: StartApplication = async function startApplication( .directive('actionsMenu', () => new ActionsMenu()) .directive('challengeModal', () => new ChallengeModal()) .directive('componentView', ComponentViewDirective) - .directive('editorMenu', () => new EditorMenu()) .directive('inputModal', () => new InputModal()) .directive('menuRow', () => new MenuRow()) .directive('panelResizer', () => new PanelResizer()) diff --git a/app/assets/javascripts/components/Icon.tsx b/app/assets/javascripts/components/Icon.tsx index ef347b6b4..bbf20bbfa 100644 --- a/app/assets/javascripts/components/Icon.tsx +++ b/app/assets/javascripts/components/Icon.tsx @@ -1,3 +1,4 @@ +import EditorIcon from '../../icons/ic-editor.svg'; import PremiumFeatureIcon from '../../icons/ic-premium-feature.svg'; import PencilOffIcon from '../../icons/ic-pencil-off.svg'; import PlainTextIcon from '../../icons/ic-text-paragraph.svg'; @@ -68,6 +69,7 @@ import { toDirective } from './utils'; import { FunctionalComponent } from 'preact'; const ICONS = { + 'editor': EditorIcon, 'menu-arrow-down-alt': MenuArrowDownAlt, 'menu-arrow-right': MenuArrowRight, notes: NotesIcon, diff --git a/app/assets/javascripts/components/NotesContextMenu.tsx b/app/assets/javascripts/components/NotesContextMenu.tsx index 1c4c367f5..e24e54387 100644 --- a/app/assets/javascripts/components/NotesContextMenu.tsx +++ b/app/assets/javascripts/components/NotesContextMenu.tsx @@ -1,7 +1,7 @@ import { AppState } from '@/ui_models/app_state'; import { toDirective, useCloseOnBlur, useCloseOnClickOutside } from './utils'; import { observer } from 'mobx-react-lite'; -import { NotesOptions } from './NotesOptions'; +import { NotesOptions } from './NotesOptions/NotesOptions'; import { useCallback, useEffect, useRef } from 'preact/hooks'; import { WebApplication } from '@/ui_models/application'; @@ -11,21 +11,16 @@ type Props = { }; const NotesContextMenu = observer(({ application, appState }: Props) => { - const { - contextMenuOpen, - contextMenuPosition, - contextMenuMaxHeight, - } = appState.notes; + const { contextMenuOpen, contextMenuPosition, contextMenuMaxHeight } = + appState.notes; const contextMenuRef = useRef(null); - const [closeOnBlur] = useCloseOnBlur( - contextMenuRef as any, - (open: boolean) => appState.notes.setContextMenuOpen(open) + const [closeOnBlur] = useCloseOnBlur(contextMenuRef, (open: boolean) => + appState.notes.setContextMenuOpen(open) ); - useCloseOnClickOutside( - contextMenuRef as any, - (open: boolean) => appState.notes.setContextMenuOpen(open) + useCloseOnClickOutside(contextMenuRef, (open: boolean) => + appState.notes.setContextMenuOpen(open) ); const reloadContextMenuLayout = useCallback(() => { diff --git a/app/assets/javascripts/components/NotesOptions/ChangeEditorOption.tsx b/app/assets/javascripts/components/NotesOptions/ChangeEditorOption.tsx new file mode 100644 index 000000000..0aacc230a --- /dev/null +++ b/app/assets/javascripts/components/NotesOptions/ChangeEditorOption.tsx @@ -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 = { + icon?: IconType; + iconClassName?: string; + title: string; + items: Array; +}; + +export type EditorMenuItem = { + name: string; + component?: SNComponent; + isPremiumFeature?: boolean; +}; + +export type EditorMenuGroup = AccordionMenuGroup; + +export const ChangeEditorOption: FunctionComponent = ({ + 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(null); + const changeEditorButtonRef = useRef(null); + const [editors] = useState(() => + application.componentManager + .componentsForArea(ComponentArea.Editor) + .sort((a, b) => { + return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; + }) + ); + const [editorMenuGroups, setEditorMenuGroups] = useState( + [] + ); + 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 ( + + { + if (event.key === KeyboardKey.Escape) { + setChangeEditorMenuOpen(false); + } + }} + onBlur={closeOnBlur} + ref={changeEditorButtonRef} + className="sn-dropdown-item justify-between" + > +
+ + Change editor +
+ +
+ { + 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" + > + + + + +
+ ); +}; diff --git a/app/assets/javascripts/components/NotesOptions.tsx b/app/assets/javascripts/components/NotesOptions/NotesOptions.tsx similarity index 85% rename from app/assets/javascripts/components/NotesOptions.tsx rename to app/assets/javascripts/components/NotesOptions/NotesOptions.tsx index 0525d75c8..d83b6c5a2 100644 --- a/app/assets/javascripts/components/NotesOptions.tsx +++ b/app/assets/javascripts/components/NotesOptions/NotesOptions.tsx @@ -1,6 +1,6 @@ import { AppState } from '@/ui_models/app_state'; -import { Icon } from './Icon'; -import { Switch } from './Switch'; +import { Icon } from '../Icon'; +import { Switch } from '../Switch'; import { observer } from 'mobx-react-lite'; import { useRef, useState, useEffect, useMemo } from 'preact/hooks'; import { @@ -8,12 +8,17 @@ import { DisclosureButton, DisclosurePanel, } from '@reach/disclosure'; -import { SNApplication, SNNote } from '@standardnotes/snjs/dist/@types'; +import { SNApplication, SNNote } from '@standardnotes/snjs'; import { WebApplication } from '@/ui_models/application'; import { KeyboardModifier } from '@/services/ioService'; 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; appState: AppState; closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void; @@ -21,7 +26,7 @@ type Props = { }; type DeletePermanentlyButtonProps = { - closeOnBlur: Props['closeOnBlur']; + closeOnBlur: NotesOptionsProps['closeOnBlur']; onClick: () => void; }; @@ -86,7 +91,10 @@ const formatDate = (date: Date | undefined) => { 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( () => countNoteAttributes(note.text), [note.text] @@ -136,15 +144,19 @@ const NoteAttributes: FunctionComponent<{ application: SNApplication, note: SNNo }; const SpellcheckOptions: FunctionComponent<{ - appState: AppState, note: SNNote + appState: AppState; + note: SNNote; }> = ({ appState, note }) => { - const editor = appState.application.componentManager.editorForNote(note); const spellcheckControllable = Boolean( !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 (
@@ -157,19 +169,26 @@ const SpellcheckOptions: FunctionComponent<{ }} > - + Spellcheck {!spellcheckControllable && ( -

Spellcheck cannot be controlled for this editor.

+

+ Spellcheck cannot be controlled for this editor. +

)}
); }; export const NotesOptions = observer( - ({ application, appState, closeOnBlur, onSubmenuChange }: Props) => { + ({ + application, + appState, + closeOnBlur, + onSubmenuChange, + }: NotesOptionsProps) => { const [tagsMenuOpen, setTagsMenuOpen] = useState(false); const [tagsMenuPosition, setTagsMenuPosition] = useState<{ top: number; @@ -232,25 +251,39 @@ export const NotesOptions = observer( const defaultFontSize = window.getComputedStyle( document.documentElement ).fontSize; - const maxTagsMenuSize = parseFloat(defaultFontSize) * 30; + const maxTagsMenuSize = + parseFloat(defaultFontSize) * MAX_MENU_SIZE_MULTIPLIER; const { clientWidth, clientHeight } = document.documentElement; - const buttonRect = tagsButtonRef.current!.getBoundingClientRect(); - const footerHeight = 32; + const buttonRect = tagsButtonRef.current?.getBoundingClientRect(); + const footerElementRect = document + .getElementById('footer-bar') + ?.getBoundingClientRect(); + const footerHeightInPx = footerElementRect?.height; - if (buttonRect.top + maxTagsMenuSize > clientHeight - footerHeight) { - setTagsMenuMaxHeight(clientHeight - buttonRect.top - footerHeight - 2); - } + if (buttonRect && footerHeightInPx) { + if ( + buttonRect.top + maxTagsMenuSize > + clientHeight - footerHeightInPx + ) { + setTagsMenuMaxHeight( + clientHeight - + buttonRect.top - + footerHeightInPx - + MENU_MARGIN_FROM_APP_BORDER + ); + } - if (buttonRect.right + maxTagsMenuSize > clientWidth) { - setTagsMenuPosition({ - top: buttonRect.top, - right: clientWidth - buttonRect.left, - }); - } else { - setTagsMenuPosition({ - top: buttonRect.top, - left: buttonRect.right, - }); + if (buttonRect.right + maxTagsMenuSize > clientWidth) { + setTagsMenuPosition({ + top: buttonRect.top, + right: clientWidth - buttonRect.left, + }); + } else { + setTagsMenuPosition({ + top: buttonRect.top, + left: buttonRect.right, + }); + } } setTagsMenuOpen(!tagsMenuOpen); @@ -360,7 +393,7 @@ export const NotesOptions = observer( onKeyDown={(event) => { if (event.key === 'Escape') { setTagsMenuOpen(false); - tagsButtonRef.current!.focus(); + tagsButtonRef.current?.focus(); } }} style={{ @@ -383,9 +416,10 @@ export const NotesOptions = observer( > {tag.title} @@ -516,16 +550,18 @@ export const NotesOptions = observer( )} - - {notes.length === 1 ? ( <>
- - - + +
+
- ) : null} diff --git a/app/assets/javascripts/components/NotesOptions/changeEditor/EditorAccordionMenu.tsx b/app/assets/javascripts/components/NotesOptions/changeEditor/EditorAccordionMenu.tsx new file mode 100644 index 000000000..b18ffe129 --- /dev/null +++ b/app/assets/javascripts/components/NotesOptions/changeEditor/EditorAccordionMenu.tsx @@ -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; + 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(); + 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 ( + +
+

+ +

+
+
+ {group.items.map((item) => { + return ( + + ); + })} +
+
+
+
+
+ ); + })} + + ); +}; diff --git a/app/assets/javascripts/components/NotesOptions/changeEditor/createEditorMenuGroups.ts b/app/assets/javascripts/components/NotesOptions/changeEditor/createEditorMenuGroups.ts new file mode 100644 index 000000000..6df9d95c6 --- /dev/null +++ b/app/assets/javascripts/components/NotesOptions/changeEditor/createEditorMenuGroups.ts @@ -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 = { + 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; +}; diff --git a/app/assets/javascripts/components/NotesOptionsPanel.tsx b/app/assets/javascripts/components/NotesOptionsPanel.tsx index 923c2fb43..7b06fa2a1 100644 --- a/app/assets/javascripts/components/NotesOptionsPanel.tsx +++ b/app/assets/javascripts/components/NotesOptionsPanel.tsx @@ -9,7 +9,7 @@ import { } from '@reach/disclosure'; import { useRef, useState } from 'preact/hooks'; import { observer } from 'mobx-react-lite'; -import { NotesOptions } from './NotesOptions'; +import { NotesOptions } from './NotesOptions/NotesOptions'; import { WebApplication } from '@/ui_models/application'; type Props = { @@ -17,76 +17,85 @@ type Props = { appState: AppState; }; -export const NotesOptionsPanel = observer(({ application, appState }: Props) => { - const [open, setOpen] = useState(false); - const [position, setPosition] = useState({ - top: 0, - right: 0, - }); - const [maxHeight, setMaxHeight] = useState('auto'); - const buttonRef = useRef(null); - const panelRef = useRef(null); - const [closeOnBlur] = useCloseOnBlur(panelRef as any, setOpen); - const [submenuOpen, setSubmenuOpen] = useState(false); +export const NotesOptionsPanel = observer( + ({ application, appState }: Props) => { + const [open, setOpen] = useState(false); + const [position, setPosition] = useState({ + top: 0, + right: 0, + }); + const [maxHeight, setMaxHeight] = useState('auto'); + const buttonRef = useRef(null); + const panelRef = useRef(null); + const [closeOnBlur] = useCloseOnBlur(panelRef, setOpen); + const [submenuOpen, setSubmenuOpen] = useState(false); - const onSubmenuChange = (open: boolean) => { - setSubmenuOpen(open); - }; + const onSubmenuChange = (open: boolean) => { + setSubmenuOpen(open); + }; - return ( - { - const rect = buttonRef.current!.getBoundingClientRect(); - const { clientHeight } = document.documentElement; - const footerHeight = 32; - setMaxHeight(clientHeight - rect.bottom - footerHeight - 2); - setPosition({ - top: rect.bottom, - right: document.body.clientWidth - rect.right, - }); - setOpen(!open); - }} - > - { - if (event.key === 'Escape' && !submenuOpen) { - setOpen(false); + return ( + { + const rect = buttonRef.current?.getBoundingClientRect(); + if (rect) { + const { clientHeight } = document.documentElement; + const footerElementRect = document + .getElementById('footer-bar') + ?.getBoundingClientRect(); + const footerHeightInPx = footerElementRect?.height; + if (footerHeightInPx) { + setMaxHeight(clientHeight - rect.bottom - footerHeightInPx - 2); + } + setPosition({ + top: rect.bottom, + right: document.body.clientWidth - rect.right, + }); + setOpen(!open); } }} - onBlur={closeOnBlur} - ref={buttonRef} - className="sn-icon-button" > - Actions - - - { - if (event.key === 'Escape' && !submenuOpen) { - setOpen(false); - buttonRef.current!.focus(); - } - }} - ref={panelRef} - style={{ - ...position, - 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" - onBlur={closeOnBlur} - > - {open && ( - - )} - - - ); -}); + { + if (event.key === 'Escape' && !submenuOpen) { + setOpen(false); + } + }} + onBlur={closeOnBlur} + ref={buttonRef} + className="sn-icon-button" + > + Actions + + + { + if (event.key === 'Escape' && !submenuOpen) { + setOpen(false); + buttonRef.current?.focus(); + } + }} + ref={panelRef} + style={{ + ...position, + 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" + onBlur={closeOnBlur} + > + {open && ( + + )} + + + ); + } +); export const NotesOptionsPanelDirective = toDirective(NotesOptionsPanel); diff --git a/app/assets/javascripts/components/utils.ts b/app/assets/javascripts/components/utils.ts index 5c0d1adb4..5f2c29517 100644 --- a/app/assets/javascripts/components/utils.ts +++ b/app/assets/javascripts/components/utils.ts @@ -8,7 +8,7 @@ import { StateUpdater, useCallback, useState, useEffect } from 'preact/hooks'; * monitored. */ export function useCloseOnBlur( - container: { current?: HTMLDivElement }, + container: { current?: HTMLDivElement | null }, setOpen: (open: boolean) => void ): [ (event: { relatedTarget: EventTarget | null }) => void, diff --git a/app/assets/javascripts/directives/views/editorMenu.ts b/app/assets/javascripts/directives/views/editorMenu.ts deleted file mode 100644 index 646e943e9..000000000 --- a/app/assets/javascripts/directives/views/editorMenu.ts +++ /dev/null @@ -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: '=', - }; - } -} diff --git a/app/assets/javascripts/directives/views/index.ts b/app/assets/javascripts/directives/views/index.ts index c66459c2b..88c4d2818 100644 --- a/app/assets/javascripts/directives/views/index.ts +++ b/app/assets/javascripts/directives/views/index.ts @@ -1,5 +1,4 @@ export { ActionsMenu } from './actionsMenu'; -export { EditorMenu } from './editorMenu'; export { InputModal } from './inputModal'; export { MenuRow } from './menuRow'; export { PanelResizer } from './panelResizer'; diff --git a/app/assets/javascripts/services/ioService.ts b/app/assets/javascripts/services/ioService.ts index 315c892fa..c4885ec37 100644 --- a/app/assets/javascripts/services/ioService.ts +++ b/app/assets/javascripts/services/ioService.ts @@ -5,6 +5,7 @@ export enum KeyboardKey { Up = 'ArrowUp', Down = 'ArrowDown', Enter = 'Enter', + Escape = 'Escape', } export enum KeyboardModifier { diff --git a/app/assets/javascripts/ui_models/app_state/notes_state.ts b/app/assets/javascripts/ui_models/app_state/notes_state.ts index 6f141ad30..df192d03a 100644 --- a/app/assets/javascripts/ui_models/app_state/notes_state.ts +++ b/app/assets/javascripts/ui_models/app_state/notes_state.ts @@ -1,6 +1,7 @@ import { confirmDialog } from '@/services/alertService'; import { KeyboardModifier } from '@/services/ioService'; import { StringEmptyTrash, Strings, StringUtils } from '@/strings'; +import { MENU_MARGIN_FROM_APP_BORDER } from '@/views/constants'; import { UuidString, SNNote, @@ -205,32 +206,39 @@ export class NotesState { document.documentElement ).fontSize; 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 let openUpBottom = true; - const bottomSpace = - clientHeight - footerHeight - this.contextMenuClickLocation.y; - const upSpace = this.contextMenuClickLocation.y; + if (footerHeightInPx) { + const bottomSpace = + clientHeight - footerHeightInPx - this.contextMenuClickLocation.y; + const upSpace = this.contextMenuClickLocation.y; - // If not enough space to open up-bottom - if (maxContextMenuHeight > bottomSpace) { - // If there's enough space, open bottom-up - 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); + // If not enough space to open up-bottom + if (maxContextMenuHeight > bottomSpace) { + // If there's enough space, open bottom-up + 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 { - 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) { diff --git a/app/assets/javascripts/views/constants.ts b/app/assets/javascripts/views/constants.ts index a84c79f91..fc5422b31 100644 --- a/app/assets/javascripts/views/constants.ts +++ b/app/assets/javascripts/views/constants.ts @@ -2,3 +2,5 @@ export const PANEL_NAME_NOTES = 'notes'; export const PANEL_NAME_NAVIGATION = 'navigation'; 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])?)$/; +export const MENU_MARGIN_FROM_APP_BORDER = 5; +export const MAX_MENU_SIZE_MULTIPLIER = 30; diff --git a/app/assets/javascripts/views/note_view/note-view.pug b/app/assets/javascripts/views/note_view/note-view.pug index 1c851734f..a40f29e08 100644 --- a/app/assets/javascripts/views/note_view/note-view.pug +++ b/app/assets/javascripts/views/note_view/note-view.pug @@ -63,20 +63,6 @@ .sn-component(ng-if='self.note') #editor-menu-bar.sk-app-bar.no-edges .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( click-outside=`self.setMenuState('showActionsMenu', false)`, is-open='self.state.showActionsMenu', diff --git a/app/assets/javascripts/views/note_view/note_view.ts b/app/assets/javascripts/views/note_view/note_view.ts index ad3bdfdf1..62a5c1251 100644 --- a/app/assets/javascripts/views/note_view/note_view.ts +++ b/app/assets/javascripts/views/note_view/note_view.ts @@ -8,7 +8,6 @@ import { ContentType, SNComponent, SNNote, - NoteMutator, ComponentArea, PrefKey, ComponentMutator, @@ -28,7 +27,6 @@ import { EventSource } from '@/ui_models/app_state'; import { STRING_DELETE_PLACEHOLDER_ATTEMPT, STRING_DELETE_LOCKED_ATTEMPT, - STRING_EDIT_LOCKED_ATTEMPT, StringDeleteNote, } from '@/strings'; import { confirmDialog } from '@/services/alertService'; @@ -59,7 +57,6 @@ type EditorState = { isDesktop?: boolean; syncTakingTooLong: boolean; showActionsMenu: boolean; - showEditorMenu: boolean; showHistoryMenu: boolean; spellcheck: boolean; /** 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 { /** Passed through template */ readonly application!: WebApplication; @@ -114,7 +151,6 @@ export class NoteView extends PureViewCtrl { onReady: () => this.reloadPreferences(), }; - this.editorMenuOnSelect = this.editorMenuOnSelect.bind(this); this.onPanelResizeFinish = this.onPanelResizeFinish.bind(this); this.setScrollPosition = this.setScrollPosition.bind(this); this.resetScrollPosition = this.resetScrollPosition.bind(this); @@ -146,7 +182,6 @@ export class NoteView extends PureViewCtrl { this.onEditorComponentLoad = undefined; this.statusTimeout = undefined; (this.onPanelResizeFinish as unknown) = undefined; - (this.editorMenuOnSelect as unknown) = undefined; super.deinit(); } @@ -248,7 +283,6 @@ export class NoteView extends PureViewCtrl { spellcheck: true, syncTakingTooLong: false, showActionsMenu: false, - showEditorMenu: false, showHistoryMenu: false, noteStatus: undefined, textareaUnloading: false, @@ -441,7 +475,7 @@ export class NoteView extends PureViewCtrl { editorStateDidLoad: true, }); } - this.reloadFont(); + reloadFont(this.state.monospaceFont); } else { await this.setState({ editorStateDidLoad: true, @@ -462,7 +496,7 @@ export class NoteView extends PureViewCtrl { } closeAllMenus(exclude?: string) { - const allMenus = ['showEditorMenu', 'showActionsMenu', 'showHistoryMenu']; + const allMenus = ['showActionsMenu', 'showHistoryMenu']; const menuState: any = {}; for (const candidate of allMenus) { if (candidate !== exclude) { @@ -472,69 +506,6 @@ export class NoteView extends PureViewCtrl { 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() { return ( this.application.actionsManager.extensionsInContextOfItem(this.note) @@ -702,7 +673,7 @@ export class NoteView extends PureViewCtrl { if (spellcheck !== this.state.spellcheck) { await this.setState({ textareaUnloading: true }); await this.setState({ textareaUnloading: false }); - this.reloadFont(); + reloadFont(this.state.monospaceFont); await this.setState({ spellcheck, @@ -733,7 +704,7 @@ export class NoteView extends PureViewCtrl { return; } - this.reloadFont(); + reloadFont(this.state.monospaceFont); if ( this.state.marginResizersEnabled && @@ -753,19 +724,6 @@ export class NoteView extends PureViewCtrl { } } - 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 */ registerComponentManagerEventObserver() { @@ -844,42 +802,16 @@ export class NoteView extends PureViewCtrl { async disassociateComponentWithCurrentNote(component: SNComponent) { 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) { 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() { this.removeTrashKeyObserver = this.application.io.addKeyObserver({ key: KeyboardKey.Backspace, diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index 3cbcf3f1a..21b268ba4 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -249,6 +249,10 @@ margin-left: 0rem !important; } +.ml-0\.5 { + margin-left: 0.125rem; +} + .ml-3 { margin-left: 0.75rem; } @@ -374,6 +378,10 @@ min-width: 7.5rem; } +.min-w-68 { + min-width: 17rem; +} + .min-w-90 { min-width: 22.5rem; } @@ -687,9 +695,13 @@ @extend .h-4; @extend .border-2; @extend .border-solid; - @extend .border-info; @extend .rounded-full; @extend .relative; + border-color: var(--sn-stylekit-grey-1); + + &--checked { + @extend .border-info; + } } .pseudo-radio-btn--checked::after { @@ -819,3 +831,7 @@ cursor: default; pointer-events: none; } + +.hide-if-last-child:last-child { + display: none; +} diff --git a/app/assets/templates/directives/editor-menu.pug b/app/assets/templates/directives/editor-menu.pug deleted file mode 100644 index d537e81f3..000000000 --- a/app/assets/templates/directives/editor-menu.pug +++ /dev/null @@ -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'") diff --git a/package.json b/package.json index 26f3007c4..a08ec4db5 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "@reach/tooltip": "^0.16.2", "@standardnotes/components": "1.4.4", "@standardnotes/features": "1.26.1", - "@standardnotes/snjs": "2.41.0", + "@standardnotes/snjs": "2.42.0", "@standardnotes/settings": "^1.10.0", "@standardnotes/sncrypto-web": "1.6.0", "mobx": "^6.3.5", diff --git a/yarn.lock b/yarn.lock index ba3abb492..a6ff4252e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2657,10 +2657,10 @@ buffer "^6.0.3" libsodium-wrappers "^0.7.9" -"@standardnotes/snjs@2.41.0": - version "2.41.0" - resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.41.0.tgz#48d02e981780a9d7d823f6186497a1c6459bbd66" - integrity sha512-Rnl5wWbMKTQ+bQb7zGK/7HFt43bMBlP06G5zoEE0Vj/uxyneLxaEX+iPNXRUDqeHfxvedtTnIXsLGFEux/G1dg== +"@standardnotes/snjs@2.42.0": + version "2.42.0" + resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.42.0.tgz#11e28210fdab5cfd464438ee3d7ab93b5edbe9c7" + integrity sha512-XjevxZeru5Ryo4c7N4u9dhaqRd+ybk+guCbBT6sBfICD67B8/8qpS0GgKjeF5BlGTDvWvB/ECXyZo1bQVkKtgw== dependencies: "@standardnotes/auth" "^3.15.3" "@standardnotes/common" "^1.8.0"