Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add new "Change Editor" option to note context menu #823

Merged
merged 32 commits into from
Jan 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6de0606
feat: add editor icon
amanharwara Jan 22, 2022
9432d41
refactor: remove 'any' type and format
amanharwara Jan 22, 2022
d09b5e0
refactor: move NotesOptions and add ChangeEditorOption
amanharwara Jan 22, 2022
723012e
refactor: fix type for using regular RefObject<T>
amanharwara Jan 22, 2022
73852b4
feat: add hide-if-last-child util class
amanharwara Jan 22, 2022
01ddb15
feat: add Change Editor option
amanharwara Jan 22, 2022
02a93ce
feat: make radio btn gray if not checked
amanharwara Jan 23, 2022
502fb6c
fix: accordion menu header and item sizing/spacing
amanharwara Jan 23, 2022
c578cfd
feat: add Escape key to KeyboardKey enum
amanharwara Jan 23, 2022
cc6b0e7
refactor: Remove Editor Menu
amanharwara Jan 23, 2022
8e19d34
feat: add editor select functionality
amanharwara Jan 23, 2022
dc52ed5
refactor: move plain editor name to constant
amanharwara Jan 23, 2022
340f712
feat: add premium editors with modal if no subscription
amanharwara Jan 23, 2022
73911f7
feat: show alert when switching to non-interchangeable editor
amanharwara Jan 24, 2022
6e7941e
fix: change editor menu going out of bounds
amanharwara Jan 24, 2022
8a1c2a0
feat: increase group header & editor item size
amanharwara Jan 24, 2022
52af361
fix: change editor menu close on blur
amanharwara Jan 24, 2022
f1bbbf2
refactor: Use KeyboardKey enum & remove else statement
amanharwara Jan 24, 2022
22b6846
feat: add keyboard navigation to change editor menu
amanharwara Jan 24, 2022
2e22c2a
fix: editor menu separators
amanharwara Jan 24, 2022
15e6293
feat: improve change editor menu sizing & spacing
amanharwara Jan 25, 2022
6ca9117
feat: show alert only if editor is not interchangeable
amanharwara Jan 26, 2022
7e6685f
feat: don't show alert when switching to/from plain editor
amanharwara Jan 26, 2022
267a360
chore: bump snjs version
amanharwara Jan 26, 2022
6ef47db
Merge branch 'develop' into feat/new-change-editor-flow
amanharwara Jan 26, 2022
c67f9f2
feat: temporarily remove change editor alert
amanharwara Jan 28, 2022
3f737e1
Merge branch 'develop' into feat/new-change-editor-flow
amanharwara Jan 28, 2022
d436e83
feat: dynamically get footer height
amanharwara Jan 28, 2022
1fed8c1
Merge branch 'feat/new-change-editor-flow' of github.com:standardnote…
amanharwara Jan 28, 2022
368b7ad
refactor: move magic number to const
amanharwara Jan 28, 2022
54b3413
refactor: move constants to constants file
amanharwara Jan 28, 2022
946ecc3
feat: use const instead of magic number
amanharwara Jan 28, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/assets/icons/ic-editor.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 0 additions & 2 deletions app/assets/javascripts/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ import {
} from './directives/functional';
import {
ActionsMenu,
EditorMenu,
HistoryMenu,
InputModal,
MenuRow,
Expand Down Expand Up @@ -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())
Expand Down
2 changes: 2 additions & 0 deletions app/assets/javascripts/components/Icon.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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,
Expand Down
19 changes: 7 additions & 12 deletions app/assets/javascripts/components/NotesContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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<HTMLDivElement>(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(() => {
Expand Down
288 changes: 288 additions & 0 deletions app/assets/javascripts/components/NotesOptions/ChangeEditorOption.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
};
Loading