feat: Dropdown component
feat: Add editor icons feat: Implement default editor selection
This commit is contained in:
@@ -1,7 +1,14 @@
|
|||||||
import { Listbox, ListboxOption } from '@reach/listbox';
|
import {
|
||||||
|
ListboxArrow,
|
||||||
|
ListboxButton,
|
||||||
|
ListboxInput,
|
||||||
|
ListboxList,
|
||||||
|
ListboxOption,
|
||||||
|
ListboxPopover,
|
||||||
|
} from '@reach/listbox';
|
||||||
import VisuallyHidden from '@reach/visually-hidden';
|
import VisuallyHidden from '@reach/visually-hidden';
|
||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
import { IconType } from './Icon';
|
import { IconType, Icon } from './Icon';
|
||||||
import '@reach/listbox/styles.css';
|
import '@reach/listbox/styles.css';
|
||||||
import { useState } from 'preact/hooks';
|
import { useState } from 'preact/hooks';
|
||||||
|
|
||||||
@@ -11,35 +18,85 @@ export type DropdownItem = {
|
|||||||
value: string;
|
value: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type DropdownProps = {
|
||||||
id: string;
|
id: string;
|
||||||
srLabel: string;
|
srLabel: string;
|
||||||
items: DropdownItem[];
|
items: DropdownItem[];
|
||||||
defaultValue: string;
|
defaultValue: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Dropdown: FunctionComponent<Props> = ({
|
type ListboxButtonProps = {
|
||||||
|
value: string | null;
|
||||||
|
label: string;
|
||||||
|
isExpanded: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const customDropdownButton: FunctionComponent<ListboxButtonProps> = ({
|
||||||
|
label,
|
||||||
|
isExpanded,
|
||||||
|
}) => (
|
||||||
|
<>
|
||||||
|
<div className="dropdown-selected-label">{label}</div>
|
||||||
|
<ListboxArrow
|
||||||
|
className={`sn-dropdown-arrow ${
|
||||||
|
isExpanded ? 'dropdown-arrow-rotated' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon type="menu-arrow-down" className="sn-icon--small color-grey-1" />
|
||||||
|
</ListboxArrow>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Dropdown: FunctionComponent<DropdownProps> = ({
|
||||||
id,
|
id,
|
||||||
srLabel,
|
srLabel,
|
||||||
items,
|
items,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
|
onChange,
|
||||||
}) => {
|
}) => {
|
||||||
const [value, setValue] = useState(defaultValue);
|
const [value, setValue] = useState(defaultValue);
|
||||||
|
|
||||||
const labelId = `${id}-label`;
|
const labelId = `${id}-label`;
|
||||||
|
|
||||||
|
const handleChange = (value: string) => {
|
||||||
|
setValue(value);
|
||||||
|
onChange(value);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<VisuallyHidden id={labelId}>{srLabel}</VisuallyHidden>
|
<VisuallyHidden id={labelId}>{srLabel}</VisuallyHidden>
|
||||||
<Listbox
|
<ListboxInput
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(value) => setValue(value)}
|
onChange={handleChange}
|
||||||
aria-labelledby={labelId}
|
aria-labelledby={labelId}
|
||||||
>
|
>
|
||||||
{items.map((item) => (
|
<ListboxButton
|
||||||
<ListboxOption value={item.value}>{item.label}</ListboxOption>
|
className="sn-dropdown-button"
|
||||||
))}
|
children={customDropdownButton}
|
||||||
</Listbox>
|
/>
|
||||||
|
<ListboxPopover className="sn-dropdown sn-dropdown-popover">
|
||||||
|
<div className="sn-component">
|
||||||
|
<ListboxList>
|
||||||
|
{items.map((item) => (
|
||||||
|
<ListboxOption
|
||||||
|
className="sn-dropdown-item"
|
||||||
|
value={item.value}
|
||||||
|
label={item.label}
|
||||||
|
>
|
||||||
|
{item.icon ? (
|
||||||
|
<div className="flex mr-3">
|
||||||
|
<Icon type={item.icon} className="sn-icon--small" />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="text-input">{item.label}</div>
|
||||||
|
</ListboxOption>
|
||||||
|
))}
|
||||||
|
</ListboxList>
|
||||||
|
</div>
|
||||||
|
</ListboxPopover>
|
||||||
|
</ListboxInput>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import PencilOffIcon from '../../icons/ic-pencil-off.svg';
|
import PencilOffIcon from '../../icons/ic-pencil-off.svg';
|
||||||
|
import PlainTextIcon from '../../icons/ic-text-paragraph.svg';
|
||||||
import RichTextIcon from '../../icons/ic-text-rich.svg';
|
import RichTextIcon from '../../icons/ic-text-rich.svg';
|
||||||
import TrashIcon from '../../icons/ic-trash.svg';
|
import TrashIcon from '../../icons/ic-trash.svg';
|
||||||
import PinIcon from '../../icons/ic-pin.svg';
|
import PinIcon from '../../icons/ic-pin.svg';
|
||||||
@@ -13,6 +14,12 @@ import PasswordIcon from '../../icons/ic-textbox-password.svg';
|
|||||||
import TrashSweepIcon from '../../icons/ic-trash-sweep.svg';
|
import TrashSweepIcon from '../../icons/ic-trash-sweep.svg';
|
||||||
import MoreIcon from '../../icons/ic-more.svg';
|
import MoreIcon from '../../icons/ic-more.svg';
|
||||||
import TuneIcon from '../../icons/ic-tune.svg';
|
import TuneIcon from '../../icons/ic-tune.svg';
|
||||||
|
import MenuArrowDownIcon from '../../icons/ic-menu-arrow-down.svg';
|
||||||
|
import AuthenticatorIcon from '../../icons/ic-authenticator.svg';
|
||||||
|
import SpreadsheetsIcon from '../../icons/ic-spreadsheets.svg';
|
||||||
|
import TasksIcon from '../../icons/ic-tasks.svg';
|
||||||
|
import MarkdownIcon from '../../icons/ic-markdown.svg';
|
||||||
|
import CodeIcon from '../../icons/ic-code.svg';
|
||||||
|
|
||||||
import AccessibilityIcon from '../../icons/ic-accessibility.svg';
|
import AccessibilityIcon from '../../icons/ic-accessibility.svg';
|
||||||
import HelpIcon from '../../icons/ic-help.svg';
|
import HelpIcon from '../../icons/ic-help.svg';
|
||||||
@@ -35,7 +42,13 @@ import { FunctionalComponent } from 'preact';
|
|||||||
|
|
||||||
const ICONS = {
|
const ICONS = {
|
||||||
'pencil-off': PencilOffIcon,
|
'pencil-off': PencilOffIcon,
|
||||||
|
'plain-text': PlainTextIcon,
|
||||||
'rich-text': RichTextIcon,
|
'rich-text': RichTextIcon,
|
||||||
|
code: CodeIcon,
|
||||||
|
markdown: MarkdownIcon,
|
||||||
|
authenticator: AuthenticatorIcon,
|
||||||
|
spreadsheets: SpreadsheetsIcon,
|
||||||
|
tasks: TasksIcon,
|
||||||
trash: TrashIcon,
|
trash: TrashIcon,
|
||||||
pin: PinIcon,
|
pin: PinIcon,
|
||||||
unpin: UnpinIcon,
|
unpin: UnpinIcon,
|
||||||
@@ -64,6 +77,7 @@ const ICONS = {
|
|||||||
check: CheckIcon,
|
check: CheckIcon,
|
||||||
'check-bold': CheckBoldIcon,
|
'check-bold': CheckBoldIcon,
|
||||||
'account-circle': AccountCircleIcon,
|
'account-circle': AccountCircleIcon,
|
||||||
|
'menu-arrow-down': MenuArrowDownIcon,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IconType = keyof typeof ICONS;
|
export type IconType = keyof typeof ICONS;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Dropdown, DropdownItem } from '@/components/Dropdown';
|
import { Dropdown, DropdownItem } from '@/components/Dropdown';
|
||||||
|
import { IconType } from '@/components/Icon';
|
||||||
import {
|
import {
|
||||||
PreferencesGroup,
|
PreferencesGroup,
|
||||||
PreferencesSegment,
|
PreferencesSegment,
|
||||||
@@ -7,7 +8,11 @@ import {
|
|||||||
Title,
|
Title,
|
||||||
} from '@/preferences/components';
|
} from '@/preferences/components';
|
||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
import { ComponentArea } from '@standardnotes/snjs';
|
import {
|
||||||
|
ComponentArea,
|
||||||
|
ComponentMutator,
|
||||||
|
SNComponent,
|
||||||
|
} from '@standardnotes/snjs';
|
||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
import { useEffect, useState } from 'preact/hooks';
|
import { useEffect, useState } from 'preact/hooks';
|
||||||
|
|
||||||
@@ -15,21 +20,85 @@ type Props = {
|
|||||||
application: WebApplication;
|
application: WebApplication;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getEditorIconType = (name: string): IconType | null => {
|
||||||
|
switch (name) {
|
||||||
|
case 'Bold Editor':
|
||||||
|
case 'Plus Editor':
|
||||||
|
return 'rich-text';
|
||||||
|
case 'TokenVault':
|
||||||
|
return 'authenticator';
|
||||||
|
case 'Secure Spreadsheets':
|
||||||
|
return 'spreadsheets';
|
||||||
|
case 'Task Editor':
|
||||||
|
return 'tasks';
|
||||||
|
case 'Code Editor':
|
||||||
|
return 'code';
|
||||||
|
}
|
||||||
|
if (name.includes('Markdown')) {
|
||||||
|
return 'markdown';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeEditorDefault = (
|
||||||
|
application: WebApplication,
|
||||||
|
component: SNComponent,
|
||||||
|
currentDefault: SNComponent
|
||||||
|
) => {
|
||||||
|
if (currentDefault) {
|
||||||
|
application.changeItem(currentDefault.uuid, (m) => {
|
||||||
|
const mutator = m as ComponentMutator;
|
||||||
|
mutator.defaultEditor = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
application.changeAndSaveItem(component.uuid, (m) => {
|
||||||
|
const mutator = m as ComponentMutator;
|
||||||
|
mutator.defaultEditor = true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeEditorDefault = (
|
||||||
|
application: WebApplication,
|
||||||
|
component: SNComponent
|
||||||
|
) => {
|
||||||
|
application.changeAndSaveItem(component.uuid, (m) => {
|
||||||
|
const mutator = m as ComponentMutator;
|
||||||
|
mutator.defaultEditor = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDefaultEditor = (application: WebApplication) => {
|
||||||
|
return application.componentManager
|
||||||
|
.componentsForArea(ComponentArea.Editor)
|
||||||
|
.filter((e) => e.isDefaultEditor())[0];
|
||||||
|
};
|
||||||
|
|
||||||
export const Defaults: FunctionComponent<Props> = ({ application }) => {
|
export const Defaults: FunctionComponent<Props> = ({ application }) => {
|
||||||
const [editorItems, setEditorItems] = useState<DropdownItem[]>([]);
|
const [editorItems, setEditorItems] = useState<DropdownItem[]>([]);
|
||||||
|
const [defaultEditorValue] = useState(
|
||||||
|
() =>
|
||||||
|
getDefaultEditor(application)?.package_info?.identifier || 'plain-editor'
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const editors = application.componentManager
|
const editors = application.componentManager
|
||||||
.componentsForArea(ComponentArea.Editor)
|
.componentsForArea(ComponentArea.Editor)
|
||||||
|
.sort((a, b) => {
|
||||||
|
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
|
||||||
|
})
|
||||||
.map((editor) => {
|
.map((editor) => {
|
||||||
|
const iconType = getEditorIconType(editor.name);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label: editor.name,
|
label: editor.name,
|
||||||
value: editor.package_info.identifier,
|
value: editor.package_info.identifier,
|
||||||
|
...(iconType ? { icon: iconType } : null),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
setEditorItems([
|
setEditorItems([
|
||||||
{
|
{
|
||||||
|
icon: 'plain-text',
|
||||||
label: 'Plain Editor',
|
label: 'Plain Editor',
|
||||||
value: 'plain-editor',
|
value: 'plain-editor',
|
||||||
},
|
},
|
||||||
@@ -37,6 +106,22 @@ export const Defaults: FunctionComponent<Props> = ({ application }) => {
|
|||||||
]);
|
]);
|
||||||
}, [application]);
|
}, [application]);
|
||||||
|
|
||||||
|
const setDefaultEditor = (value: string) => {
|
||||||
|
const editors = application.componentManager.componentsForArea(
|
||||||
|
ComponentArea.Editor
|
||||||
|
);
|
||||||
|
const currentDefault = getDefaultEditor(application);
|
||||||
|
|
||||||
|
if (value !== 'plain-editor') {
|
||||||
|
const editorComponent = editors.filter(
|
||||||
|
(e) => e.package_info.identifier === value
|
||||||
|
)[0];
|
||||||
|
makeEditorDefault(application, editorComponent, currentDefault);
|
||||||
|
} else {
|
||||||
|
removeEditorDefault(application, currentDefault);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PreferencesGroup>
|
<PreferencesGroup>
|
||||||
<PreferencesSegment>
|
<PreferencesSegment>
|
||||||
@@ -49,7 +134,8 @@ export const Defaults: FunctionComponent<Props> = ({ application }) => {
|
|||||||
id="def-editor-dropdown"
|
id="def-editor-dropdown"
|
||||||
srLabel="Select the default editor"
|
srLabel="Select the default editor"
|
||||||
items={editorItems}
|
items={editorItems}
|
||||||
defaultValue="plain-editor"
|
defaultValue={defaultEditorValue}
|
||||||
|
onChange={setDefaultEditor}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,14 +9,13 @@
|
|||||||
}
|
}
|
||||||
[data-reach-dialog-overlay]::before {
|
[data-reach-dialog-overlay]::before {
|
||||||
background-color: var(--sn-stylekit-contrast-background-color);
|
background-color: var(--sn-stylekit-contrast-background-color);
|
||||||
content: "";
|
content: '';
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
right: 0px;
|
right: 0px;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-reach-dialog-content] {
|
[data-reach-dialog-content] {
|
||||||
|
|||||||
@@ -53,6 +53,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sn-dropdown-popover {
|
||||||
|
z-index: 3001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sn-dropdown-button {
|
||||||
|
@extend .rounded;
|
||||||
|
@extend .px-3\.5;
|
||||||
|
@extend .py-1\.75;
|
||||||
|
@extend .fit-content;
|
||||||
|
@extend .bg-default;
|
||||||
|
@extend .text-input;
|
||||||
|
@extend .color-text;
|
||||||
|
@extend .border-neutral;
|
||||||
|
@extend .border-solid;
|
||||||
|
@extend .border-gray-300;
|
||||||
|
@extend .border-1;
|
||||||
|
@extend .min-w-42;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sn-dropdown-arrow {
|
||||||
|
@extend .flex;
|
||||||
|
}
|
||||||
|
|
||||||
/** Lesser specificity will give priority to reach's styles */
|
/** Lesser specificity will give priority to reach's styles */
|
||||||
[data-reach-custom-checkbox-container].sn-switch {
|
[data-reach-custom-checkbox-container].sn-switch {
|
||||||
@extend .duration-150;
|
@extend .duration-150;
|
||||||
@@ -114,6 +137,16 @@
|
|||||||
&.sn-dropdown-item--no-icon {
|
&.sn-dropdown-item--no-icon {
|
||||||
@extend .py-2;
|
@extend .py-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&[data-current-nav] {
|
||||||
|
@extend .bg-contrast;
|
||||||
|
@extend .hover\:color-text;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-current-selected] {
|
||||||
|
background-color: var(--sn-stylekit-info-backdrop-color);
|
||||||
|
@extend .color-info;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sn-tag {
|
.sn-tag {
|
||||||
@@ -278,4 +311,4 @@
|
|||||||
|
|
||||||
.select-none {
|
.select-none {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user