Merge branch 'release/10.15.0'
This commit is contained in:
@@ -7,14 +7,11 @@ import {
|
||||
DisclosurePanel,
|
||||
} from '@reach/disclosure';
|
||||
import VisuallyHidden from '@reach/visually-hidden';
|
||||
import { ComponentArea, SNComponent } from '@standardnotes/snjs';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { useRef, useState } from 'preact/hooks';
|
||||
import { Icon } from './Icon';
|
||||
import { ChangeEditorMenu } from './NotesOptions/changeEditor/ChangeEditorMenu';
|
||||
import { createEditorMenuGroups } from './NotesOptions/changeEditor/createEditorMenuGroups';
|
||||
import { EditorMenuGroup } from './NotesOptions/ChangeEditorOption';
|
||||
import { useCloseOnBlur } from './utils';
|
||||
|
||||
type Props = {
|
||||
@@ -26,7 +23,8 @@ type Props = {
|
||||
export const ChangeEditorButton: FunctionComponent<Props> = observer(
|
||||
({ application, appState, onClickPreprocessing }) => {
|
||||
const note = Object.values(appState.notes.selectedNotes)[0];
|
||||
const [open, setOpen] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [position, setPosition] = useState({
|
||||
top: 0,
|
||||
right: 0,
|
||||
@@ -35,28 +33,7 @@ export const ChangeEditorButton: FunctionComponent<Props> = observer(
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [closeOnBlur] = useCloseOnBlur(containerRef, setOpen);
|
||||
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 [currentEditor, setCurrentEditor] = useState<SNComponent>();
|
||||
|
||||
useEffect(() => {
|
||||
setEditorMenuGroups(createEditorMenuGroups(application, editors));
|
||||
}, [application, editors]);
|
||||
|
||||
useEffect(() => {
|
||||
if (note) {
|
||||
setCurrentEditor(application.componentManager.editorForNote(note));
|
||||
}
|
||||
}, [application, note]);
|
||||
const [closeOnBlur] = useCloseOnBlur(containerRef, setIsOpen);
|
||||
|
||||
const toggleChangeEditorMenu = async () => {
|
||||
const rect = buttonRef.current?.getBoundingClientRect();
|
||||
@@ -81,22 +58,25 @@ export const ChangeEditorButton: FunctionComponent<Props> = observer(
|
||||
right: document.body.clientWidth - rect.right,
|
||||
});
|
||||
|
||||
const newOpenState = !open;
|
||||
const newOpenState = !isOpen;
|
||||
if (newOpenState && onClickPreprocessing) {
|
||||
await onClickPreprocessing();
|
||||
}
|
||||
|
||||
setOpen(newOpenState);
|
||||
setIsOpen(newOpenState);
|
||||
setTimeout(() => {
|
||||
setIsVisible(newOpenState);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
<Disclosure open={open} onChange={toggleChangeEditorMenu}>
|
||||
<Disclosure open={isOpen} onChange={toggleChangeEditorMenu}>
|
||||
<DisclosureButton
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setOpen(false);
|
||||
setIsOpen(false);
|
||||
}
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
@@ -109,7 +89,7 @@ export const ChangeEditorButton: FunctionComponent<Props> = observer(
|
||||
<DisclosurePanel
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setOpen(false);
|
||||
setIsOpen(false);
|
||||
buttonRef.current?.focus();
|
||||
}
|
||||
}}
|
||||
@@ -121,17 +101,14 @@ export const ChangeEditorButton: FunctionComponent<Props> = observer(
|
||||
className="sn-dropdown sn-dropdown--animated min-w-68 max-h-120 max-w-xs flex flex-col overflow-y-auto fixed"
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
{open && (
|
||||
{isOpen && (
|
||||
<ChangeEditorMenu
|
||||
closeOnBlur={closeOnBlur}
|
||||
application={application}
|
||||
isOpen={open}
|
||||
currentEditor={currentEditor}
|
||||
setSelectedEditor={setCurrentEditor}
|
||||
isVisible={isVisible}
|
||||
note={note}
|
||||
groups={editorMenuGroups}
|
||||
closeMenu={() => {
|
||||
setOpen(false);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -16,10 +16,6 @@ type Props = {
|
||||
|
||||
export const NotesListOptionsMenu: FunctionComponent<Props> = observer(
|
||||
({ closeDisplayOptionsMenu, closeOnBlur, application, isOpen }) => {
|
||||
const menuClassName =
|
||||
'sn-dropdown sn-dropdown--animated min-w-70 overflow-y-auto \
|
||||
border-1 border-solid border-main text-sm z-index-dropdown-menu \
|
||||
flex flex-col py-2 top-full bottom-0 left-2 absolute';
|
||||
const [sortBy, setSortBy] = useState(() =>
|
||||
application.getPreference(PrefKey.SortNotesBy, CollectionSort.CreatedAt)
|
||||
);
|
||||
@@ -117,154 +113,155 @@ flex flex-col py-2 top-full bottom-0 left-2 absolute';
|
||||
application.setPreference(PrefKey.NotesHideEditorIcon, !hideEditorIcon);
|
||||
};
|
||||
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<div ref={menuRef} className={menuClassName}>
|
||||
<Menu
|
||||
a11yLabel="Notes list options menu"
|
||||
closeMenu={closeDisplayOptionsMenu}
|
||||
isOpen={isOpen}
|
||||
<Menu
|
||||
className={
|
||||
'sn-dropdown sn-dropdown--animated min-w-70 max-h-120 overflow-y-auto \
|
||||
border-1 border-solid border-main text-sm z-index-dropdown-menu \
|
||||
flex flex-col py-2 top-full left-2 absolute'
|
||||
}
|
||||
a11yLabel="Notes list options menu"
|
||||
closeMenu={closeDisplayOptionsMenu}
|
||||
isOpen={isOpen}
|
||||
>
|
||||
<div className="px-3 my-1 text-xs font-semibold color-text uppercase">
|
||||
Sort by
|
||||
</div>
|
||||
<MenuItem
|
||||
className="py-2"
|
||||
type={MenuItemType.RadioButton}
|
||||
onClick={toggleSortByDateModified}
|
||||
checked={sortBy === CollectionSort.UpdatedAt}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<div className="px-3 my-1 text-xs font-semibold color-text uppercase">
|
||||
Sort by
|
||||
<div className="flex flex-grow items-center justify-between">
|
||||
<span>Date modified</span>
|
||||
{sortBy === CollectionSort.UpdatedAt ? (
|
||||
sortReverse ? (
|
||||
<Icon type="arrows-sort-up" className="color-neutral" />
|
||||
) : (
|
||||
<Icon type="arrows-sort-down" className="color-neutral" />
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
<MenuItem
|
||||
className="py-2"
|
||||
type={MenuItemType.RadioButton}
|
||||
onClick={toggleSortByDateModified}
|
||||
checked={sortBy === CollectionSort.UpdatedAt}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<div className="flex flex-grow items-center justify-between">
|
||||
<span>Date modified</span>
|
||||
{sortBy === CollectionSort.UpdatedAt ? (
|
||||
sortReverse ? (
|
||||
<Icon type="arrows-sort-up" className="color-neutral" />
|
||||
) : (
|
||||
<Icon type="arrows-sort-down" className="color-neutral" />
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="py-2"
|
||||
type={MenuItemType.RadioButton}
|
||||
onClick={toggleSortByCreationDate}
|
||||
checked={sortBy === CollectionSort.CreatedAt}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<div className="flex flex-grow items-center justify-between">
|
||||
<span>Creation date</span>
|
||||
{sortBy === CollectionSort.CreatedAt ? (
|
||||
sortReverse ? (
|
||||
<Icon type="arrows-sort-up" className="color-neutral" />
|
||||
) : (
|
||||
<Icon type="arrows-sort-down" className="color-neutral" />
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="py-2"
|
||||
type={MenuItemType.RadioButton}
|
||||
onClick={toggleSortByTitle}
|
||||
checked={sortBy === CollectionSort.Title}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<div className="flex flex-grow items-center justify-between">
|
||||
<span>Title</span>
|
||||
{sortBy === CollectionSort.Title ? (
|
||||
sortReverse ? (
|
||||
<Icon type="arrows-sort-up" className="color-neutral" />
|
||||
) : (
|
||||
<Icon type="arrows-sort-down" className="color-neutral" />
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItemSeparator />
|
||||
<div className="px-3 py-1 text-xs font-semibold color-text uppercase">
|
||||
View
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="py-2"
|
||||
type={MenuItemType.RadioButton}
|
||||
onClick={toggleSortByCreationDate}
|
||||
checked={sortBy === CollectionSort.CreatedAt}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<div className="flex flex-grow items-center justify-between">
|
||||
<span>Creation date</span>
|
||||
{sortBy === CollectionSort.CreatedAt ? (
|
||||
sortReverse ? (
|
||||
<Icon type="arrows-sort-up" className="color-neutral" />
|
||||
) : (
|
||||
<Icon type="arrows-sort-down" className="color-neutral" />
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hidePreview}
|
||||
onChange={toggleHidePreview}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<div className="flex flex-col max-w-3/4">Show note preview</div>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hideDate}
|
||||
onChange={toggleHideDate}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show date
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hideTags}
|
||||
onChange={toggleHideTags}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show tags
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hideEditorIcon}
|
||||
onChange={toggleEditorIcon}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show editor icon
|
||||
</MenuItem>
|
||||
<div className="h-1px my-2 bg-border"></div>
|
||||
<div className="px-3 py-1 text-xs font-semibold color-text uppercase">
|
||||
Other
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="py-2"
|
||||
type={MenuItemType.RadioButton}
|
||||
onClick={toggleSortByTitle}
|
||||
checked={sortBy === CollectionSort.Title}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<div className="flex flex-grow items-center justify-between">
|
||||
<span>Title</span>
|
||||
{sortBy === CollectionSort.Title ? (
|
||||
sortReverse ? (
|
||||
<Icon type="arrows-sort-up" className="color-neutral" />
|
||||
) : (
|
||||
<Icon type="arrows-sort-down" className="color-neutral" />
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hidePinned}
|
||||
onChange={toggleHidePinned}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show pinned notes
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hideProtected}
|
||||
onChange={toggleHideProtected}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show protected notes
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={showArchived}
|
||||
onChange={toggleShowArchived}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show archived notes
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={showTrashed}
|
||||
onChange={toggleShowTrashed}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show trashed notes
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItemSeparator />
|
||||
<div className="px-3 py-1 text-xs font-semibold color-text uppercase">
|
||||
View
|
||||
</div>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hidePreview}
|
||||
onChange={toggleHidePreview}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<div className="flex flex-col max-w-3/4">Show note preview</div>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hideDate}
|
||||
onChange={toggleHideDate}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show date
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hideTags}
|
||||
onChange={toggleHideTags}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show tags
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hideEditorIcon}
|
||||
onChange={toggleEditorIcon}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show editor icon
|
||||
</MenuItem>
|
||||
<div className="h-1px my-2 bg-border"></div>
|
||||
<div className="px-3 py-1 text-xs font-semibold color-text uppercase">
|
||||
Other
|
||||
</div>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hidePinned}
|
||||
onChange={toggleHidePinned}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show pinned notes
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hideProtected}
|
||||
onChange={toggleHideProtected}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show protected notes
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={showArchived}
|
||||
onChange={toggleShowArchived}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show archived notes
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={showTrashed}
|
||||
onChange={toggleShowTrashed}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show trashed notes
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
129
app/assets/javascripts/components/NotesOptions/AddTagOption.tsx
Normal file
129
app/assets/javascripts/components/NotesOptions/AddTagOption.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import {
|
||||
calculateSubmenuStyle,
|
||||
SubmenuStyle,
|
||||
} from '@/utils/calculateSubmenuStyle';
|
||||
import {
|
||||
Disclosure,
|
||||
DisclosureButton,
|
||||
DisclosurePanel,
|
||||
} from '@reach/disclosure';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { Icon } from '../Icon';
|
||||
import { useCloseOnBlur } from '../utils';
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
};
|
||||
|
||||
export const AddTagOption: FunctionComponent<Props> = observer(
|
||||
({ appState }) => {
|
||||
const menuContainerRef = useRef<HTMLDivElement>(null);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>({
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
maxHeight: 'auto',
|
||||
});
|
||||
|
||||
const [closeOnBlur] = useCloseOnBlur(menuContainerRef, setIsMenuOpen);
|
||||
|
||||
const toggleTagsMenu = () => {
|
||||
if (!isMenuOpen) {
|
||||
const menuPosition = calculateSubmenuStyle(menuButtonRef.current);
|
||||
if (menuPosition) {
|
||||
setMenuStyle(menuPosition);
|
||||
console.log(menuPosition);
|
||||
}
|
||||
}
|
||||
|
||||
setIsMenuOpen(!isMenuOpen);
|
||||
};
|
||||
|
||||
const recalculateMenuStyle = useCallback(() => {
|
||||
const newMenuPosition = calculateSubmenuStyle(
|
||||
menuButtonRef.current,
|
||||
menuRef.current
|
||||
);
|
||||
|
||||
if (newMenuPosition) {
|
||||
setMenuStyle(newMenuPosition);
|
||||
console.log(newMenuPosition);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isMenuOpen) {
|
||||
setTimeout(() => {
|
||||
recalculateMenuStyle();
|
||||
});
|
||||
}
|
||||
}, [isMenuOpen, recalculateMenuStyle]);
|
||||
|
||||
return (
|
||||
<div ref={menuContainerRef}>
|
||||
<Disclosure open={isMenuOpen} onChange={toggleTagsMenu}>
|
||||
<DisclosureButton
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setIsMenuOpen(false);
|
||||
}
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
ref={menuButtonRef}
|
||||
className="sn-dropdown-item justify-between"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Icon type="hashtag" className="mr-2 color-neutral" />
|
||||
Add tag
|
||||
</div>
|
||||
<Icon type="chevron-right" className="color-neutral" />
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel
|
||||
ref={menuRef}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setIsMenuOpen(false);
|
||||
menuButtonRef.current?.focus();
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
...menuStyle,
|
||||
position: 'fixed',
|
||||
}}
|
||||
className="sn-dropdown min-w-80 flex flex-col py-2 max-h-120 max-w-xs fixed overflow-y-auto"
|
||||
>
|
||||
{appState.tags.tags.map((tag) => (
|
||||
<button
|
||||
key={tag.title}
|
||||
className="sn-dropdown-item sn-dropdown-item--no-icon max-w-80"
|
||||
onBlur={closeOnBlur}
|
||||
onClick={() => {
|
||||
appState.notes.isTagInSelectedNotes(tag)
|
||||
? appState.notes.removeTagFromSelectedNotes(tag)
|
||||
: appState.notes.addTagToSelectedNotes(tag);
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={`whitespace-nowrap overflow-hidden overflow-ellipsis
|
||||
${
|
||||
appState.notes.isTagInSelectedNotes(tag)
|
||||
? 'font-bold'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
{appState.noteTags.getLongTitle(tag)}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -6,27 +6,21 @@ import {
|
||||
DisclosureButton,
|
||||
DisclosurePanel,
|
||||
} from '@reach/disclosure';
|
||||
import {
|
||||
ComponentArea,
|
||||
IconType,
|
||||
SNComponent,
|
||||
SNNote,
|
||||
} from '@standardnotes/snjs';
|
||||
import { IconType, SNComponent, SNNote } from '@standardnotes/snjs';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { Icon } from '../Icon';
|
||||
import { createEditorMenuGroups } from './changeEditor/createEditorMenuGroups';
|
||||
import { ChangeEditorMenu } from './changeEditor/ChangeEditorMenu';
|
||||
import {
|
||||
calculateSubmenuStyle,
|
||||
SubmenuStyle,
|
||||
} from '@/utils/calculateSubmenuStyle';
|
||||
import { useCloseOnBlur } from '../utils';
|
||||
|
||||
type ChangeEditorOptionProps = {
|
||||
appState: AppState;
|
||||
application: WebApplication;
|
||||
note: SNNote;
|
||||
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void;
|
||||
};
|
||||
|
||||
type AccordionMenuGroup<T> = {
|
||||
@@ -46,114 +40,97 @@ export type EditorMenuGroup = AccordionMenuGroup<EditorMenuItem>;
|
||||
|
||||
export const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({
|
||||
application,
|
||||
closeOnBlur,
|
||||
note,
|
||||
}) => {
|
||||
const [changeEditorMenuOpen, setChangeEditorMenuOpen] = useState(false);
|
||||
const [changeEditorMenuVisible, setChangeEditorMenuVisible] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>({
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
maxHeight: 'auto',
|
||||
});
|
||||
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)
|
||||
);
|
||||
const menuContainerRef = useRef<HTMLDivElement>(null);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setEditorMenuGroups(createEditorMenuGroups(application, editors));
|
||||
}, [application, editors]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedEditor(application.componentManager.editorForNote(note));
|
||||
}, [application, note]);
|
||||
const [closeOnBlur] = useCloseOnBlur(menuContainerRef, (open: boolean) => {
|
||||
setIsOpen(open);
|
||||
setIsVisible(open);
|
||||
});
|
||||
|
||||
const toggleChangeEditorMenu = () => {
|
||||
if (!changeEditorMenuOpen) {
|
||||
const menuStyle = calculateSubmenuStyle(changeEditorButtonRef.current);
|
||||
if (!isOpen) {
|
||||
const menuStyle = calculateSubmenuStyle(buttonRef.current);
|
||||
if (menuStyle) {
|
||||
setMenuStyle(menuStyle);
|
||||
}
|
||||
}
|
||||
|
||||
setChangeEditorMenuOpen(!changeEditorMenuOpen);
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (changeEditorMenuOpen) {
|
||||
if (isOpen) {
|
||||
setTimeout(() => {
|
||||
const newMenuStyle = calculateSubmenuStyle(
|
||||
changeEditorButtonRef.current,
|
||||
changeEditorMenuRef.current
|
||||
buttonRef.current,
|
||||
menuRef.current
|
||||
);
|
||||
|
||||
if (newMenuStyle) {
|
||||
setMenuStyle(newMenuStyle);
|
||||
setChangeEditorMenuVisible(true);
|
||||
setIsVisible(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [changeEditorMenuOpen]);
|
||||
}, [isOpen]);
|
||||
|
||||
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="dashboard" 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={{
|
||||
...menuStyle,
|
||||
position: 'fixed',
|
||||
}}
|
||||
className="sn-dropdown flex flex-col max-h-120 min-w-68 fixed overflow-y-auto"
|
||||
>
|
||||
{changeEditorMenuOpen && (
|
||||
<ChangeEditorMenu
|
||||
application={application}
|
||||
closeOnBlur={closeOnBlur}
|
||||
currentEditor={selectedEditor}
|
||||
setSelectedEditor={setSelectedEditor}
|
||||
note={note}
|
||||
groups={editorMenuGroups}
|
||||
isOpen={changeEditorMenuVisible}
|
||||
closeMenu={() => {
|
||||
setChangeEditorMenuOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
<div ref={menuContainerRef}>
|
||||
<Disclosure open={isOpen} onChange={toggleChangeEditorMenu}>
|
||||
<DisclosureButton
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === KeyboardKey.Escape) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
ref={buttonRef}
|
||||
className="sn-dropdown-item justify-between"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Icon type="dashboard" className="color-neutral mr-2" />
|
||||
Change editor
|
||||
</div>
|
||||
<Icon type="chevron-right" className="color-neutral" />
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel
|
||||
ref={menuRef}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === KeyboardKey.Escape) {
|
||||
setIsOpen(false);
|
||||
buttonRef.current?.focus();
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
...menuStyle,
|
||||
position: 'fixed',
|
||||
}}
|
||||
className="sn-dropdown flex flex-col max-h-120 min-w-68 fixed overflow-y-auto"
|
||||
>
|
||||
{isOpen && (
|
||||
<ChangeEditorMenu
|
||||
application={application}
|
||||
closeOnBlur={closeOnBlur}
|
||||
note={note}
|
||||
isVisible={isVisible}
|
||||
closeMenu={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,11 +12,11 @@ import { Action, ListedAccount, SNNote } from '@standardnotes/snjs';
|
||||
import { Fragment, FunctionComponent } from 'preact';
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { Icon } from '../Icon';
|
||||
import { useCloseOnBlur } from '../utils';
|
||||
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
note: SNNote;
|
||||
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void;
|
||||
};
|
||||
|
||||
type ListedMenuGroup = {
|
||||
@@ -230,8 +230,8 @@ const ListedActionsMenu: FunctionComponent<ListedActionsMenuProps> = ({
|
||||
export const ListedActionsOption: FunctionComponent<Props> = ({
|
||||
application,
|
||||
note,
|
||||
closeOnBlur,
|
||||
}) => {
|
||||
const menuContainerRef = useRef<HTMLDivElement>(null);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
@@ -242,6 +242,8 @@ export const ListedActionsOption: FunctionComponent<Props> = ({
|
||||
maxHeight: 'auto',
|
||||
});
|
||||
|
||||
const [closeOnBlur] = useCloseOnBlur(menuContainerRef, setIsMenuOpen);
|
||||
|
||||
const toggleListedMenu = () => {
|
||||
if (!isMenuOpen) {
|
||||
const menuPosition = calculateSubmenuStyle(menuButtonRef.current);
|
||||
@@ -273,34 +275,36 @@ export const ListedActionsOption: FunctionComponent<Props> = ({
|
||||
}, [isMenuOpen, recalculateMenuStyle]);
|
||||
|
||||
return (
|
||||
<Disclosure open={isMenuOpen} onChange={toggleListedMenu}>
|
||||
<DisclosureButton
|
||||
ref={menuButtonRef}
|
||||
onBlur={closeOnBlur}
|
||||
className="sn-dropdown-item justify-between"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Icon type="listed" className="color-neutral mr-2" />
|
||||
Listed actions
|
||||
</div>
|
||||
<Icon type="chevron-right" className="color-neutral" />
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel
|
||||
ref={menuRef}
|
||||
style={{
|
||||
...menuStyle,
|
||||
position: 'fixed',
|
||||
}}
|
||||
className="sn-dropdown flex flex-col max-h-120 min-w-68 pb-1 fixed overflow-y-auto"
|
||||
>
|
||||
{isMenuOpen && (
|
||||
<ListedActionsMenu
|
||||
application={application}
|
||||
note={note}
|
||||
recalculateMenuStyle={recalculateMenuStyle}
|
||||
/>
|
||||
)}
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
<div ref={menuContainerRef}>
|
||||
<Disclosure open={isMenuOpen} onChange={toggleListedMenu}>
|
||||
<DisclosureButton
|
||||
ref={menuButtonRef}
|
||||
onBlur={closeOnBlur}
|
||||
className="sn-dropdown-item justify-between"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Icon type="listed" className="color-neutral mr-2" />
|
||||
Listed actions
|
||||
</div>
|
||||
<Icon type="chevron-right" className="color-neutral" />
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel
|
||||
ref={menuRef}
|
||||
style={{
|
||||
...menuStyle,
|
||||
position: 'fixed',
|
||||
}}
|
||||
className="sn-dropdown flex flex-col max-h-120 min-w-68 pb-1 fixed overflow-y-auto"
|
||||
>
|
||||
{isMenuOpen && (
|
||||
<ListedActionsMenu
|
||||
application={application}
|
||||
note={note}
|
||||
recalculateMenuStyle={recalculateMenuStyle}
|
||||
/>
|
||||
)}
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,29 +2,20 @@ import { AppState } from '@/ui_models/app_state';
|
||||
import { Icon } from '../Icon';
|
||||
import { Switch } from '../Switch';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useRef, useState, useEffect, useMemo } from 'preact/hooks';
|
||||
import {
|
||||
Disclosure,
|
||||
DisclosureButton,
|
||||
DisclosurePanel,
|
||||
} from '@reach/disclosure';
|
||||
import { useState, useEffect, useMemo } from 'preact/hooks';
|
||||
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,
|
||||
BYTES_IN_ONE_MEGABYTE,
|
||||
} from '@/constants';
|
||||
import { BYTES_IN_ONE_MEGABYTE } from '@/constants';
|
||||
import { ListedActionsOption } from './ListedActionsOption';
|
||||
import { AddTagOption } from './AddTagOption';
|
||||
|
||||
export type NotesOptionsProps = {
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void;
|
||||
onSubmenuChange?: (submenuOpen: boolean) => void;
|
||||
};
|
||||
|
||||
type DeletePermanentlyButtonProps = {
|
||||
@@ -206,24 +197,7 @@ const NoteSizeWarning: FunctionComponent<{
|
||||
) : null;
|
||||
|
||||
export const NotesOptions = observer(
|
||||
({
|
||||
application,
|
||||
appState,
|
||||
closeOnBlur,
|
||||
onSubmenuChange,
|
||||
}: NotesOptionsProps) => {
|
||||
const [tagsMenuOpen, setTagsMenuOpen] = useState(false);
|
||||
const [tagsMenuPosition, setTagsMenuPosition] = useState<{
|
||||
top: number;
|
||||
right?: number;
|
||||
left?: number;
|
||||
}>({
|
||||
top: 0,
|
||||
right: 0,
|
||||
});
|
||||
const [tagsMenuMaxHeight, setTagsMenuMaxHeight] = useState<number | 'auto'>(
|
||||
'auto'
|
||||
);
|
||||
({ application, appState, closeOnBlur }: NotesOptionsProps) => {
|
||||
const [altKeyDown, setAltKeyDown] = useState(false);
|
||||
|
||||
const toggleOn = (condition: (note: SNNote) => boolean) => {
|
||||
@@ -246,14 +220,6 @@ export const NotesOptions = observer(
|
||||
const unpinned = notes.some((note) => !note.pinned);
|
||||
const errored = notes.some((note) => note.errorDecrypting);
|
||||
|
||||
const tagsButtonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (onSubmenuChange) {
|
||||
onSubmenuChange(tagsMenuOpen);
|
||||
}
|
||||
}, [tagsMenuOpen, onSubmenuChange]);
|
||||
|
||||
useEffect(() => {
|
||||
const removeAltKeyObserver = application.io.addKeyObserver({
|
||||
modifiers: [KeyboardModifier.Alt],
|
||||
@@ -270,48 +236,6 @@ export const NotesOptions = observer(
|
||||
};
|
||||
}, [application]);
|
||||
|
||||
const openTagsMenu = () => {
|
||||
const defaultFontSize = window.getComputedStyle(
|
||||
document.documentElement
|
||||
).fontSize;
|
||||
const maxTagsMenuSize =
|
||||
parseFloat(defaultFontSize) * MAX_MENU_SIZE_MULTIPLIER;
|
||||
const { clientWidth, clientHeight } = document.documentElement;
|
||||
const buttonRect = tagsButtonRef.current?.getBoundingClientRect();
|
||||
const footerElementRect = document
|
||||
.getElementById('footer-bar')
|
||||
?.getBoundingClientRect();
|
||||
const footerHeightInPx = footerElementRect?.height;
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setTagsMenuOpen(!tagsMenuOpen);
|
||||
};
|
||||
|
||||
const downloadSelectedItems = () => {
|
||||
notes.forEach((note) => {
|
||||
const editor = application.componentManager.editorForNote(note);
|
||||
@@ -416,70 +340,12 @@ export const NotesOptions = observer(
|
||||
<ChangeEditorOption
|
||||
appState={appState}
|
||||
application={application}
|
||||
closeOnBlur={closeOnBlur}
|
||||
note={notes[0]}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className="min-h-1px my-2 bg-border"></div>
|
||||
{appState.tags.tagsCount > 0 && (
|
||||
<Disclosure open={tagsMenuOpen} onChange={openTagsMenu}>
|
||||
<DisclosureButton
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setTagsMenuOpen(false);
|
||||
}
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
ref={tagsButtonRef}
|
||||
className="sn-dropdown-item justify-between"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Icon type="hashtag" className={iconClass} />
|
||||
{'Add tag'}
|
||||
</div>
|
||||
<Icon type="chevron-right" className="color-neutral" />
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setTagsMenuOpen(false);
|
||||
tagsButtonRef.current?.focus();
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
...tagsMenuPosition,
|
||||
maxHeight: tagsMenuMaxHeight,
|
||||
position: 'fixed',
|
||||
}}
|
||||
className="sn-dropdown min-w-80 flex flex-col py-2 max-h-120 max-w-xs fixed overflow-y-auto"
|
||||
>
|
||||
{appState.tags.tags.map((tag) => (
|
||||
<button
|
||||
key={tag.title}
|
||||
className="sn-dropdown-item sn-dropdown-item--no-icon max-w-80"
|
||||
onBlur={closeOnBlur}
|
||||
onClick={() => {
|
||||
appState.notes.isTagInSelectedNotes(tag)
|
||||
? appState.notes.removeTagFromSelectedNotes(tag)
|
||||
: appState.notes.addTagToSelectedNotes(tag);
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={`whitespace-nowrap overflow-hidden overflow-ellipsis
|
||||
${
|
||||
appState.notes.isTagInSelectedNotes(tag)
|
||||
? 'font-bold'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
{appState.noteTags.getLongTitle(tag)}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
)}
|
||||
{appState.tags.tagsCount > 0 && <AddTagOption appState={appState} />}
|
||||
{unpinned && (
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
@@ -604,11 +470,7 @@ export const NotesOptions = observer(
|
||||
{notes.length === 1 ? (
|
||||
<>
|
||||
<div className="min-h-1px my-2 bg-border"></div>
|
||||
<ListedActionsOption
|
||||
application={application}
|
||||
closeOnBlur={closeOnBlur}
|
||||
note={notes[0]}
|
||||
/>
|
||||
<ListedActionsOption application={application} 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>
|
||||
|
||||
@@ -19,19 +19,19 @@ import {
|
||||
TransactionalMutation,
|
||||
} from '@standardnotes/snjs';
|
||||
import { Fragment, FunctionComponent } from 'preact';
|
||||
import { StateUpdater, useCallback } from 'preact/hooks';
|
||||
import { useCallback, useEffect, useState } from 'preact/hooks';
|
||||
import { EditorMenuItem, EditorMenuGroup } from '../ChangeEditorOption';
|
||||
import { PLAIN_EDITOR_NAME } from './createEditorMenuGroups';
|
||||
import {
|
||||
createEditorMenuGroups,
|
||||
PLAIN_EDITOR_NAME,
|
||||
} from './createEditorMenuGroups';
|
||||
|
||||
type ChangeEditorMenuProps = {
|
||||
application: WebApplication;
|
||||
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void;
|
||||
closeMenu: () => void;
|
||||
groups: EditorMenuGroup[];
|
||||
isOpen: boolean;
|
||||
currentEditor: SNComponent | undefined;
|
||||
isVisible: boolean;
|
||||
note: SNNote;
|
||||
setSelectedEditor: StateUpdater<SNComponent | undefined>;
|
||||
};
|
||||
|
||||
const getGroupId = (group: EditorMenuGroup) =>
|
||||
@@ -41,12 +41,29 @@ export const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
||||
application,
|
||||
closeOnBlur,
|
||||
closeMenu,
|
||||
groups,
|
||||
isOpen,
|
||||
currentEditor,
|
||||
setSelectedEditor,
|
||||
isVisible,
|
||||
note,
|
||||
}) => {
|
||||
const [editors] = useState<SNComponent[]>(() =>
|
||||
application.componentManager
|
||||
.componentsForArea(ComponentArea.Editor)
|
||||
.sort((a, b) => {
|
||||
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
|
||||
})
|
||||
);
|
||||
const [groups, setGroups] = useState<EditorMenuGroup[]>([]);
|
||||
const [currentEditor, setCurrentEditor] = useState<SNComponent>();
|
||||
|
||||
useEffect(() => {
|
||||
setGroups(createEditorMenuGroups(application, editors));
|
||||
}, [application, editors]);
|
||||
|
||||
useEffect(() => {
|
||||
if (note) {
|
||||
setCurrentEditor(application.componentManager.editorForNote(note));
|
||||
}
|
||||
}, [application, note]);
|
||||
|
||||
const premiumModal = usePremiumModal();
|
||||
|
||||
const isSelectedEditor = useCallback(
|
||||
@@ -138,7 +155,7 @@ export const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
||||
/** Dirtying can happen above */
|
||||
application.sync();
|
||||
|
||||
setSelectedEditor(application.componentManager.editorForNote(note));
|
||||
setCurrentEditor(application.componentManager.editorForNote(note));
|
||||
};
|
||||
|
||||
const selectEditor = async (itemToBeSelected: EditorMenuItem) => {
|
||||
@@ -179,7 +196,7 @@ export const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
||||
<Menu
|
||||
className="pt-0.5 pb-1"
|
||||
a11yLabel="Change editor menu"
|
||||
isOpen={isOpen}
|
||||
isOpen={isVisible}
|
||||
>
|
||||
{groups
|
||||
.filter((group) => group.items && group.items.length)
|
||||
|
||||
@@ -30,11 +30,6 @@ export const NotesOptionsPanel = observer(
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
const [closeOnBlur] = useCloseOnBlur(panelRef, setOpen);
|
||||
const [submenuOpen, setSubmenuOpen] = useState(false);
|
||||
|
||||
const onSubmenuChange = (open: boolean) => {
|
||||
setSubmenuOpen(open);
|
||||
};
|
||||
|
||||
return (
|
||||
<Disclosure
|
||||
@@ -64,7 +59,7 @@ export const NotesOptionsPanel = observer(
|
||||
>
|
||||
<DisclosureButton
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape' && !submenuOpen) {
|
||||
if (event.key === 'Escape') {
|
||||
setOpen(false);
|
||||
}
|
||||
}}
|
||||
@@ -77,7 +72,7 @@ export const NotesOptionsPanel = observer(
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape' && !submenuOpen) {
|
||||
if (event.key === 'Escape') {
|
||||
setOpen(false);
|
||||
buttonRef.current?.focus();
|
||||
}
|
||||
@@ -96,7 +91,6 @@ export const NotesOptionsPanel = observer(
|
||||
application={application}
|
||||
appState={appState}
|
||||
closeOnBlur={closeOnBlur}
|
||||
onSubmenuChange={onSubmenuChange}
|
||||
/>
|
||||
)}
|
||||
</DisclosurePanel>
|
||||
|
||||
@@ -6,10 +6,11 @@ import {
|
||||
RefCallback,
|
||||
ComponentChild,
|
||||
} from 'preact';
|
||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { useEffect, useRef } from 'preact/hooks';
|
||||
import { JSXInternal } from 'preact/src/jsx';
|
||||
import { MenuItem, MenuItemListElement } from './MenuItem';
|
||||
import { KeyboardKey } from '@/services/ioService';
|
||||
import { useListKeyboardNavigation } from '../utils';
|
||||
|
||||
type MenuProps = {
|
||||
className?: string;
|
||||
@@ -28,7 +29,6 @@ export const Menu: FunctionComponent<MenuProps> = ({
|
||||
closeMenu,
|
||||
isOpen,
|
||||
}: MenuProps) => {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const menuItemRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
||||
|
||||
const menuElementRef = useRef<HTMLMenuElement>(null);
|
||||
@@ -40,44 +40,21 @@ export const Menu: FunctionComponent<MenuProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.key) {
|
||||
case KeyboardKey.Home:
|
||||
setCurrentIndex(0);
|
||||
break;
|
||||
case KeyboardKey.End:
|
||||
setCurrentIndex(
|
||||
menuItemRefs.current.length ? menuItemRefs.current.length - 1 : 0
|
||||
);
|
||||
break;
|
||||
case KeyboardKey.Down:
|
||||
setCurrentIndex((index) => {
|
||||
if (index + 1 < menuItemRefs.current.length) {
|
||||
return index + 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case KeyboardKey.Up:
|
||||
setCurrentIndex((index) => {
|
||||
if (index - 1 > -1) {
|
||||
return index - 1;
|
||||
} else {
|
||||
return menuItemRefs.current.length - 1;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case KeyboardKey.Escape:
|
||||
closeMenu?.();
|
||||
break;
|
||||
if (event.key === KeyboardKey.Escape) {
|
||||
closeMenu?.();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
useListKeyboardNavigation(menuElementRef);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && menuItemRefs.current[currentIndex]) {
|
||||
menuItemRefs.current[currentIndex]?.focus();
|
||||
if (isOpen && menuItemRefs.current.length > 0) {
|
||||
setTimeout(() => {
|
||||
menuElementRef.current?.focus();
|
||||
});
|
||||
}
|
||||
}, [currentIndex, isOpen]);
|
||||
}, [isOpen]);
|
||||
|
||||
const pushRefToArray: RefCallback<HTMLLIElement> = (instance) => {
|
||||
if (instance && instance.children) {
|
||||
@@ -128,7 +105,7 @@ export const Menu: FunctionComponent<MenuProps> = ({
|
||||
|
||||
return (
|
||||
<menu
|
||||
className={`m-0 p-0 list-style-none ${className}`}
|
||||
className={`m-0 p-0 list-style-none focus:shadow-none ${className}`}
|
||||
onKeyDown={handleKeyDown}
|
||||
ref={menuElementRef}
|
||||
style={style}
|
||||
|
||||
@@ -89,13 +89,17 @@ export const calculateDifferenceBetweenDatesInDays = (
|
||||
export const useListKeyboardNavigation = (
|
||||
container: Ref<HTMLElement | null>
|
||||
) => {
|
||||
const [listItems, setListItems] = useState<NodeListOf<HTMLButtonElement>>();
|
||||
const [listItems, setListItems] = useState<HTMLButtonElement[]>();
|
||||
const [focusedItemIndex, setFocusedItemIndex] = useState<number>(0);
|
||||
|
||||
const focusItemWithIndex = useCallback(
|
||||
(index: number) => {
|
||||
(index: number, items?: HTMLButtonElement[]) => {
|
||||
setFocusedItemIndex(index);
|
||||
listItems?.[index]?.focus();
|
||||
if (items && items.length > 0) {
|
||||
items[index]?.focus();
|
||||
} else {
|
||||
listItems?.[index]?.focus();
|
||||
}
|
||||
},
|
||||
[listItems]
|
||||
);
|
||||
@@ -103,7 +107,7 @@ export const useListKeyboardNavigation = (
|
||||
useEffect(() => {
|
||||
if (container.current) {
|
||||
container.current.tabIndex = FOCUSABLE_BUT_NOT_TABBABLE;
|
||||
setListItems(container.current.querySelectorAll('button'));
|
||||
setListItems(Array.from(container.current.querySelectorAll('button')));
|
||||
}
|
||||
}, [container]);
|
||||
|
||||
@@ -116,7 +120,13 @@ export const useListKeyboardNavigation = (
|
||||
}
|
||||
|
||||
if (!listItems?.length) {
|
||||
setListItems(container.current?.querySelectorAll('button'));
|
||||
setListItems(
|
||||
Array.from(
|
||||
container.current?.querySelectorAll(
|
||||
'button'
|
||||
) as NodeListOf<HTMLButtonElement>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (listItems) {
|
||||
@@ -143,16 +153,25 @@ export const useListKeyboardNavigation = (
|
||||
const FIRST_ITEM_FOCUS_TIMEOUT = 20;
|
||||
|
||||
const containerFocusHandler = useCallback(() => {
|
||||
if (listItems) {
|
||||
const selectedItemIndex = Array.from(listItems).findIndex(
|
||||
let temporaryItems = listItems && listItems?.length > 0 ? listItems : [];
|
||||
if (!temporaryItems.length) {
|
||||
temporaryItems = Array.from(
|
||||
container.current?.querySelectorAll(
|
||||
'button'
|
||||
) as NodeListOf<HTMLButtonElement>
|
||||
);
|
||||
setListItems(temporaryItems);
|
||||
}
|
||||
if (temporaryItems.length > 0) {
|
||||
const selectedItemIndex = Array.from(temporaryItems).findIndex(
|
||||
(item) => item.dataset.selected
|
||||
);
|
||||
const indexToFocus = selectedItemIndex > -1 ? selectedItemIndex : 0;
|
||||
setTimeout(() => {
|
||||
focusItemWithIndex(indexToFocus);
|
||||
focusItemWithIndex(indexToFocus, temporaryItems);
|
||||
}, FIRST_ITEM_FOCUS_TIMEOUT);
|
||||
}
|
||||
}, [focusItemWithIndex, listItems]);
|
||||
}, [container, focusItemWithIndex, listItems]);
|
||||
|
||||
useEffect(() => {
|
||||
const containerElement = container.current;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "standard-notes-web",
|
||||
"version": "3.12.0",
|
||||
"version": "3.12.1",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -82,7 +82,7 @@
|
||||
"@standardnotes/features": "1.34.1",
|
||||
"@standardnotes/settings": "^1.11.5",
|
||||
"@standardnotes/sncrypto-web": "1.7.3",
|
||||
"@standardnotes/snjs": "2.73.1",
|
||||
"@standardnotes/snjs": "2.73.2",
|
||||
"mobx": "^6.4.2",
|
||||
"mobx-react-lite": "^3.3.0",
|
||||
"preact": "^10.6.6",
|
||||
|
||||
@@ -2395,10 +2395,10 @@
|
||||
buffer "^6.0.3"
|
||||
libsodium-wrappers "^0.7.9"
|
||||
|
||||
"@standardnotes/snjs@2.73.1":
|
||||
version "2.73.1"
|
||||
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.73.1.tgz#4fe25a37cd3d0234aed9a5c5fc79221bcd785975"
|
||||
integrity sha512-xuPzS/PoSA9UTLr6jrynBSX9RPiao5tDJ8CBly9E4Fmnk38yOjgx/BaWs0V2cSTFvUvv9L2q8Pmb6arwl180Bw==
|
||||
"@standardnotes/snjs@2.73.2":
|
||||
version "2.73.2"
|
||||
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.73.2.tgz#5361d73c861b990d06821d5a822dcb0893830a43"
|
||||
integrity sha512-oGPZmzX+tNWQFxmvrQRtsqlofbW25YUOapyEPN8oVgfwV98r8EVzpUGXHWkog3ZW6oO3aO22Rhtk0D92d0CqtQ==
|
||||
dependencies:
|
||||
"@standardnotes/applications" "^1.1.3"
|
||||
"@standardnotes/auth" "^3.17.3"
|
||||
|
||||
Reference in New Issue
Block a user