Merge branch 'develop' into feature/remove-batch-manager
This commit is contained in:
@@ -64,6 +64,7 @@ import { MultipleSelectedNotesDirective } from './components/MultipleSelectedNot
|
||||
import { NotesContextMenuDirective } from './components/NotesContextMenu';
|
||||
import { NotesOptionsPanelDirective } from './components/NotesOptionsPanel';
|
||||
import { IconDirective } from './components/Icon';
|
||||
import { NoteTagsContainerDirective } from './components/NoteTagsContainer';
|
||||
|
||||
function reloadHiddenFirefoxTab(): boolean {
|
||||
/**
|
||||
@@ -159,7 +160,8 @@ const startApplication: StartApplication = async function startApplication(
|
||||
.directive('multipleSelectedNotesPanel', MultipleSelectedNotesDirective)
|
||||
.directive('notesContextMenu', NotesContextMenuDirective)
|
||||
.directive('notesOptionsPanel', NotesOptionsPanelDirective)
|
||||
.directive('icon', IconDirective);
|
||||
.directive('icon', IconDirective)
|
||||
.directive('noteTagsContainer', NoteTagsContainerDirective);
|
||||
|
||||
// Filters
|
||||
angular.module('app').filter('trusted', ['$sce', trusted]);
|
||||
|
||||
79
app/assets/javascripts/components/AutocompleteTagHint.tsx
Normal file
79
app/assets/javascripts/components/AutocompleteTagHint.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useRef, useEffect } from 'preact/hooks';
|
||||
import { Icon } from './Icon';
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void;
|
||||
};
|
||||
|
||||
export const AutocompleteTagHint = observer(
|
||||
({ appState, closeOnBlur }: Props) => {
|
||||
const { autocompleteTagHintFocused } = appState.noteTags;
|
||||
|
||||
const hintRef = useRef<HTMLButtonElement>();
|
||||
|
||||
const { autocompleteSearchQuery, autocompleteTagResults } =
|
||||
appState.noteTags;
|
||||
|
||||
const onTagHintClick = async () => {
|
||||
await appState.noteTags.createAndAddNewTag();
|
||||
};
|
||||
|
||||
const onFocus = () => {
|
||||
appState.noteTags.setAutocompleteTagHintFocused(true);
|
||||
};
|
||||
|
||||
const onBlur = (event: FocusEvent) => {
|
||||
closeOnBlur(event);
|
||||
appState.noteTags.setAutocompleteTagHintFocused(false);
|
||||
};
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'ArrowUp') {
|
||||
if (autocompleteTagResults.length > 0) {
|
||||
const lastTagResult =
|
||||
autocompleteTagResults[autocompleteTagResults.length - 1];
|
||||
appState.noteTags.setFocusedTagResultUuid(lastTagResult.uuid);
|
||||
} else {
|
||||
appState.noteTags.setAutocompleteInputFocused(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (autocompleteTagHintFocused) {
|
||||
hintRef.current.focus();
|
||||
}
|
||||
}, [appState.noteTags, autocompleteTagHintFocused]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{autocompleteTagResults.length > 0 && (
|
||||
<div className="h-1px my-2 bg-border"></div>
|
||||
)}
|
||||
<button
|
||||
ref={hintRef}
|
||||
type="button"
|
||||
className="sn-dropdown-item"
|
||||
onClick={onTagHintClick}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
<span>Create new tag:</span>
|
||||
<span className="bg-contrast rounded text-xs color-text py-1 pl-1 pr-2 flex items-center ml-2">
|
||||
<Icon
|
||||
type="hashtag"
|
||||
className="sn-icon--small color-neutral mr-1"
|
||||
/>
|
||||
<span className="max-w-40 whitespace-nowrap overflow-hidden overflow-ellipsis">
|
||||
{autocompleteSearchQuery}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
140
app/assets/javascripts/components/AutocompleteTagInput.tsx
Normal file
140
app/assets/javascripts/components/AutocompleteTagInput.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { Disclosure, DisclosurePanel } from '@reach/disclosure';
|
||||
import { useCloseOnBlur } from './utils';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { AutocompleteTagResult } from './AutocompleteTagResult';
|
||||
import { AutocompleteTagHint } from './AutocompleteTagHint';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
};
|
||||
|
||||
export const AutocompleteTagInput = observer(({ appState }: Props) => {
|
||||
const {
|
||||
autocompleteInputFocused,
|
||||
autocompleteSearchQuery,
|
||||
autocompleteTagHintVisible,
|
||||
autocompleteTagResults,
|
||||
tags,
|
||||
tagsContainerMaxWidth,
|
||||
} = appState.noteTags;
|
||||
|
||||
const [dropdownVisible, setDropdownVisible] = useState(false);
|
||||
const [dropdownMaxHeight, setDropdownMaxHeight] =
|
||||
useState<number | 'auto'>('auto');
|
||||
|
||||
const dropdownRef = useRef<HTMLDivElement>();
|
||||
const inputRef = useRef<HTMLInputElement>();
|
||||
|
||||
const [closeOnBlur] = useCloseOnBlur(dropdownRef, (visible: boolean) => {
|
||||
setDropdownVisible(visible);
|
||||
appState.noteTags.clearAutocompleteSearch();
|
||||
});
|
||||
|
||||
const showDropdown = () => {
|
||||
const { clientHeight } = document.documentElement;
|
||||
const inputRect = inputRef.current.getBoundingClientRect();
|
||||
setDropdownMaxHeight(clientHeight - inputRect.bottom - 32 * 2);
|
||||
setDropdownVisible(true);
|
||||
};
|
||||
|
||||
const onSearchQueryChange = (event: Event) => {
|
||||
const query = (event.target as HTMLInputElement).value;
|
||||
appState.noteTags.setAutocompleteSearchQuery(query);
|
||||
appState.noteTags.searchActiveNoteAutocompleteTags();
|
||||
};
|
||||
|
||||
const onFormSubmit = async (event: Event) => {
|
||||
event.preventDefault();
|
||||
await appState.noteTags.createAndAddNewTag();
|
||||
};
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
switch (event.key) {
|
||||
case 'Backspace':
|
||||
case 'ArrowLeft':
|
||||
if (autocompleteSearchQuery === '' && tags.length > 0) {
|
||||
appState.noteTags.setFocusedTagUuid(tags[tags.length - 1].uuid);
|
||||
}
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
if (autocompleteTagResults.length > 0) {
|
||||
appState.noteTags.setFocusedTagResultUuid(autocompleteTagResults[0].uuid);
|
||||
} else if (autocompleteTagHintVisible) {
|
||||
appState.noteTags.setAutocompleteTagHintFocused(true);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const onFocus = () => {
|
||||
showDropdown();
|
||||
appState.noteTags.setAutocompleteInputFocused(true);
|
||||
};
|
||||
|
||||
const onBlur = (event: FocusEvent) => {
|
||||
closeOnBlur(event);
|
||||
appState.noteTags.setAutocompleteInputFocused(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
appState.noteTags.searchActiveNoteAutocompleteTags();
|
||||
}, [appState.noteTags]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autocompleteInputFocused) {
|
||||
inputRef.current.focus();
|
||||
appState.noteTags.setAutocompleteInputFocused(false);
|
||||
}
|
||||
}, [appState.noteTags, autocompleteInputFocused]);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={onFormSubmit}
|
||||
className={`${tags.length > 0 ? 'mt-2' : ''}`}
|
||||
>
|
||||
<Disclosure open={dropdownVisible} onChange={showDropdown}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
className={`${tags.length > 0 ? 'w-80' : 'w-70 mr-10'} bg-default text-xs
|
||||
color-text no-border h-7 focus:outline-none focus:shadow-none focus:border-bottom`}
|
||||
value={autocompleteSearchQuery}
|
||||
onChange={onSearchQueryChange}
|
||||
type="text"
|
||||
placeholder="Add tag"
|
||||
onBlur={onBlur}
|
||||
onFocus={onFocus}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
{dropdownVisible && (autocompleteTagResults.length > 0 || autocompleteTagHintVisible) && (
|
||||
<DisclosurePanel
|
||||
ref={dropdownRef}
|
||||
className={`${tags.length > 0 ? 'w-80' : 'w-70 mr-10'} sn-dropdown flex flex-col py-2 absolute`}
|
||||
style={{ maxHeight: dropdownMaxHeight, maxWidth: tagsContainerMaxWidth }}
|
||||
>
|
||||
<div className="overflow-y-scroll">
|
||||
{autocompleteTagResults.map((tagResult) => (
|
||||
<AutocompleteTagResult
|
||||
key={tagResult.uuid}
|
||||
appState={appState}
|
||||
tagResult={tagResult}
|
||||
closeOnBlur={closeOnBlur}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{autocompleteTagHintVisible && (
|
||||
<AutocompleteTagHint
|
||||
appState={appState}
|
||||
closeOnBlur={closeOnBlur}
|
||||
/>
|
||||
)}
|
||||
</DisclosurePanel>
|
||||
)}
|
||||
</Disclosure>
|
||||
</form>
|
||||
);
|
||||
});
|
||||
109
app/assets/javascripts/components/AutocompleteTagResult.tsx
Normal file
109
app/assets/javascripts/components/AutocompleteTagResult.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { SNTag } from '@standardnotes/snjs';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useEffect, useRef } from 'preact/hooks';
|
||||
import { Icon } from './Icon';
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
tagResult: SNTag;
|
||||
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void;
|
||||
};
|
||||
|
||||
export const AutocompleteTagResult = observer(
|
||||
({ appState, tagResult, closeOnBlur }: Props) => {
|
||||
const {
|
||||
autocompleteSearchQuery,
|
||||
autocompleteTagHintVisible,
|
||||
autocompleteTagResults,
|
||||
focusedTagResultUuid,
|
||||
} = appState.noteTags;
|
||||
|
||||
const tagResultRef = useRef<HTMLButtonElement>();
|
||||
|
||||
const onTagOptionClick = async (tag: SNTag) => {
|
||||
await appState.noteTags.addTagToActiveNote(tag);
|
||||
appState.noteTags.clearAutocompleteSearch();
|
||||
appState.noteTags.setAutocompleteInputFocused(true);
|
||||
};
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
const tagResultIndex = appState.noteTags.getTagIndex(
|
||||
tagResult,
|
||||
autocompleteTagResults
|
||||
);
|
||||
switch (event.key) {
|
||||
case 'ArrowUp':
|
||||
event.preventDefault();
|
||||
if (tagResultIndex === 0) {
|
||||
appState.noteTags.setAutocompleteInputFocused(true);
|
||||
} else {
|
||||
appState.noteTags.focusPreviousTagResult(tagResult);
|
||||
}
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
if (
|
||||
tagResultIndex === autocompleteTagResults.length - 1 &&
|
||||
autocompleteTagHintVisible
|
||||
) {
|
||||
appState.noteTags.setAutocompleteTagHintFocused(true);
|
||||
} else {
|
||||
appState.noteTags.focusNextTagResult(tagResult);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const onFocus = () => {
|
||||
appState.noteTags.setFocusedTagResultUuid(tagResult.uuid);
|
||||
};
|
||||
|
||||
const onBlur = (event: FocusEvent) => {
|
||||
closeOnBlur(event);
|
||||
appState.noteTags.setFocusedTagResultUuid(undefined);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (focusedTagResultUuid === tagResult.uuid) {
|
||||
tagResultRef.current.focus();
|
||||
appState.noteTags.setFocusedTagResultUuid(undefined);
|
||||
}
|
||||
}, [appState.noteTags, focusedTagResultUuid, tagResult]);
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={tagResultRef}
|
||||
type="button"
|
||||
className="sn-dropdown-item"
|
||||
onClick={() => onTagOptionClick(tagResult)}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
<Icon type="hashtag" className="color-neutral mr-2 min-h-5 min-w-5" />
|
||||
<span className="whitespace-nowrap overflow-hidden overflow-ellipsis">
|
||||
{autocompleteSearchQuery === ''
|
||||
? tagResult.title
|
||||
: tagResult.title
|
||||
.split(new RegExp(`(${autocompleteSearchQuery})`, 'gi'))
|
||||
.map((substring, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className={`${
|
||||
substring.toLowerCase() ===
|
||||
autocompleteSearchQuery.toLowerCase()
|
||||
? 'font-bold whitespace-pre-wrap'
|
||||
: 'whitespace-pre-wrap '
|
||||
}`}
|
||||
>
|
||||
{substring}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
112
app/assets/javascripts/components/NoteTag.tsx
Normal file
112
app/assets/javascripts/components/NoteTag.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { Icon } from './Icon';
|
||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { SNTag } from '@standardnotes/snjs/dist/@types';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
tag: SNTag;
|
||||
};
|
||||
|
||||
export const NoteTag = observer(({ appState, tag }: Props) => {
|
||||
const { focusedTagUuid, tags } = appState.noteTags;
|
||||
|
||||
const [showDeleteButton, setShowDeleteButton] = useState(false);
|
||||
const [tagClicked, setTagClicked] = useState(false);
|
||||
const deleteTagRef = useRef<HTMLButtonElement>();
|
||||
|
||||
const tagRef = useRef<HTMLButtonElement>();
|
||||
|
||||
const deleteTag = () => {
|
||||
appState.noteTags.focusPreviousTag(tag);
|
||||
appState.noteTags.removeTagFromActiveNote(tag);
|
||||
};
|
||||
|
||||
const onDeleteTagClick = (event: MouseEvent) => {
|
||||
event.stopImmediatePropagation();
|
||||
event.stopPropagation();
|
||||
deleteTag();
|
||||
};
|
||||
|
||||
const onTagClick = (event: MouseEvent) => {
|
||||
if (tagClicked && event.target !== deleteTagRef.current) {
|
||||
setTagClicked(false);
|
||||
appState.setSelectedTag(tag);
|
||||
} else {
|
||||
setTagClicked(true);
|
||||
}
|
||||
};
|
||||
|
||||
const onFocus = () => {
|
||||
appState.noteTags.setFocusedTagUuid(tag.uuid);
|
||||
setShowDeleteButton(true);
|
||||
};
|
||||
|
||||
const onBlur = (event: FocusEvent) => {
|
||||
const relatedTarget = event.relatedTarget as Node;
|
||||
if (relatedTarget !== deleteTagRef.current) {
|
||||
appState.noteTags.setFocusedTagUuid(undefined);
|
||||
setShowDeleteButton(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
const tagIndex = appState.noteTags.getTagIndex(tag, tags);
|
||||
switch (event.key) {
|
||||
case 'Backspace':
|
||||
deleteTag();
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
appState.noteTags.focusPreviousTag(tag);
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
if (tagIndex === tags.length - 1) {
|
||||
appState.noteTags.setAutocompleteInputFocused(true);
|
||||
} else {
|
||||
appState.noteTags.focusNextTag(tag);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (focusedTagUuid === tag.uuid) {
|
||||
tagRef.current.focus();
|
||||
appState.noteTags.setFocusedTagUuid(undefined);
|
||||
}
|
||||
}, [appState.noteTags, focusedTagUuid, tag]);
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={tagRef}
|
||||
className="sn-tag pl-1 pr-2 mr-2"
|
||||
onClick={onTagClick}
|
||||
onKeyDown={onKeyDown}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
>
|
||||
<Icon type="hashtag" className="sn-icon--small color-info mr-1" />
|
||||
<span className="whitespace-nowrap overflow-hidden overflow-ellipsis max-w-290px">
|
||||
{tag.title}
|
||||
</span>
|
||||
{showDeleteButton && (
|
||||
<button
|
||||
ref={deleteTagRef}
|
||||
type="button"
|
||||
className="ml-2 -mr-1 border-0 p-0 bg-transparent cursor-pointer flex"
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
onClick={onDeleteTagClick}
|
||||
>
|
||||
<Icon
|
||||
type="close"
|
||||
className="sn-icon--small color-neutral hover:color-info"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
41
app/assets/javascripts/components/NoteTagsContainer.tsx
Normal file
41
app/assets/javascripts/components/NoteTagsContainer.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { toDirective } from './utils';
|
||||
import { AutocompleteTagInput } from './AutocompleteTagInput';
|
||||
import { NoteTag } from './NoteTag';
|
||||
import { useEffect } from 'preact/hooks';
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
};
|
||||
|
||||
const NoteTagsContainer = observer(({ appState }: Props) => {
|
||||
const {
|
||||
tags,
|
||||
tagsContainerMaxWidth,
|
||||
} = appState.noteTags;
|
||||
|
||||
useEffect(() => {
|
||||
appState.noteTags.reloadTagsContainerMaxWidth();
|
||||
}, [appState.noteTags]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-default flex flex-wrap min-w-80 -mt-1 -mr-2"
|
||||
style={{
|
||||
maxWidth: tagsContainerMaxWidth,
|
||||
}}
|
||||
>
|
||||
{tags.map((tag) => (
|
||||
<NoteTag
|
||||
key={tag.uuid}
|
||||
appState={appState}
|
||||
tag={tag}
|
||||
/>
|
||||
))}
|
||||
<AutocompleteTagInput appState={appState} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const NoteTagsContainerDirective = toDirective<Props>(NoteTagsContainer);
|
||||
@@ -1,8 +1,8 @@
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { toDirective, useCloseOnBlur } from './utils';
|
||||
import { toDirective, useCloseOnBlur, useCloseOnClickOutside } from './utils';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { NotesOptions } from './NotesOptions';
|
||||
import { useCallback, useEffect, useRef } from 'preact/hooks';
|
||||
import { useRef } from 'preact/hooks';
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
@@ -15,23 +15,15 @@ const NotesContextMenu = observer(({ appState }: Props) => {
|
||||
(open: boolean) => appState.notes.setContextMenuOpen(open)
|
||||
);
|
||||
|
||||
const closeOnClickOutside = useCallback((event: MouseEvent) => {
|
||||
if (!contextMenuRef.current?.contains(event.target as Node)) {
|
||||
appState.notes.setContextMenuOpen(false);
|
||||
}
|
||||
}, [appState]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('click', closeOnClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('click', closeOnClickOutside);
|
||||
};
|
||||
}, [closeOnClickOutside]);
|
||||
useCloseOnClickOutside(
|
||||
contextMenuRef,
|
||||
(open: boolean) => appState.notes.setContextMenuOpen(open)
|
||||
);
|
||||
|
||||
return appState.notes.contextMenuOpen ? (
|
||||
<div
|
||||
ref={contextMenuRef}
|
||||
className="sn-dropdown max-h-120 max-w-80 flex flex-col py-2 overflow-y-scroll fixed"
|
||||
className="sn-dropdown max-h-120 max-w-xs flex flex-col py-2 overflow-y-scroll fixed"
|
||||
style={{
|
||||
...appState.notes.contextMenuPosition,
|
||||
maxHeight: appState.notes.contextMenuMaxHeight,
|
||||
|
||||
@@ -52,10 +52,6 @@ export const NotesOptions = observer(
|
||||
const tagsButtonRef = useRef<HTMLButtonElement>();
|
||||
|
||||
const iconClass = 'color-neutral mr-2';
|
||||
const buttonClass =
|
||||
'flex items-center border-0 focus:inner-ring-info ' +
|
||||
'cursor-pointer hover:bg-contrast color-text bg-transparent px-3 ' +
|
||||
'text-left';
|
||||
|
||||
useEffect(() => {
|
||||
if (onSubmenuChange) {
|
||||
@@ -136,14 +132,14 @@ export const NotesOptions = observer(
|
||||
{appState.tags.tagsCount > 0 && (
|
||||
<Disclosure open={tagsMenuOpen} onChange={openTagsMenu}>
|
||||
<DisclosureButton
|
||||
onKeyUp={(event) => {
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setTagsMenuOpen(false);
|
||||
}
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
ref={tagsButtonRef}
|
||||
className={`${buttonClass} py-1.5 justify-between`}
|
||||
className="sn-dropdown-item justify-between"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Icon type="hashtag" className={iconClass} />
|
||||
@@ -152,7 +148,7 @@ export const NotesOptions = observer(
|
||||
<Icon type="chevron-right" className="color-neutral" />
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel
|
||||
onKeyUp={(event) => {
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setTagsMenuOpen(false);
|
||||
tagsButtonRef.current.focus();
|
||||
@@ -163,12 +159,12 @@ export const NotesOptions = observer(
|
||||
maxHeight: tagsMenuMaxHeight,
|
||||
position: 'fixed',
|
||||
}}
|
||||
className="sn-dropdown flex flex-col py-2 max-h-120 max-w-80 fixed overflow-y-scroll"
|
||||
className="sn-dropdown flex flex-col py-2 max-h-120 max-w-xs fixed overflow-y-scroll"
|
||||
>
|
||||
{appState.tags.tags.map((tag) => (
|
||||
<button
|
||||
key={tag.title}
|
||||
className={`${buttonClass} py-2 max-w-80`}
|
||||
className="sn-dropdown-item sn-dropdown-item--no-icon max-w-80"
|
||||
onBlur={closeOnBlur}
|
||||
onClick={() => {
|
||||
appState.notes.isTagInSelectedNotes(tag)
|
||||
@@ -194,7 +190,7 @@ export const NotesOptions = observer(
|
||||
{unpinned && (
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className={`${buttonClass} py-1.5`}
|
||||
className="sn-dropdown-item"
|
||||
onClick={() => {
|
||||
appState.notes.setPinSelectedNotes(true);
|
||||
}}
|
||||
@@ -206,7 +202,7 @@ export const NotesOptions = observer(
|
||||
{pinned && (
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className={`${buttonClass} py-1.5`}
|
||||
className="sn-dropdown-item"
|
||||
onClick={() => {
|
||||
appState.notes.setPinSelectedNotes(false);
|
||||
}}
|
||||
@@ -218,7 +214,7 @@ export const NotesOptions = observer(
|
||||
{unarchived && (
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className={`${buttonClass} py-1.5`}
|
||||
className="sn-dropdown-item"
|
||||
onClick={() => {
|
||||
appState.notes.setArchiveSelectedNotes(true);
|
||||
}}
|
||||
@@ -230,7 +226,7 @@ export const NotesOptions = observer(
|
||||
{archived && (
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className={`${buttonClass} py-1.5`}
|
||||
className="sn-dropdown-item"
|
||||
onClick={() => {
|
||||
appState.notes.setArchiveSelectedNotes(false);
|
||||
}}
|
||||
@@ -242,7 +238,7 @@ export const NotesOptions = observer(
|
||||
{notTrashed && (
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className={`${buttonClass} py-1.5`}
|
||||
className="sn-dropdown-item"
|
||||
onClick={async () => {
|
||||
await appState.notes.setTrashSelectedNotes(true);
|
||||
}}
|
||||
@@ -255,7 +251,7 @@ export const NotesOptions = observer(
|
||||
<>
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className={`${buttonClass} py-1.5`}
|
||||
className="sn-dropdown-item"
|
||||
onClick={async () => {
|
||||
await appState.notes.setTrashSelectedNotes(false);
|
||||
}}
|
||||
@@ -265,7 +261,7 @@ export const NotesOptions = observer(
|
||||
</button>
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className={`${buttonClass} py-1.5`}
|
||||
className="sn-dropdown-item"
|
||||
onClick={async () => {
|
||||
await appState.notes.deleteNotesPermanently();
|
||||
}}
|
||||
@@ -275,7 +271,7 @@ export const NotesOptions = observer(
|
||||
</button>
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className={`${buttonClass} py-1.5`}
|
||||
className="sn-dropdown-item"
|
||||
onClick={async () => {
|
||||
await appState.notes.emptyTrash();
|
||||
}}
|
||||
|
||||
@@ -47,7 +47,7 @@ export const NotesOptionsPanel = observer(({ appState }: Props) => {
|
||||
}}
|
||||
>
|
||||
<DisclosureButton
|
||||
onKeyUp={(event) => {
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape' && !submenuOpen) {
|
||||
setOpen(false);
|
||||
}
|
||||
@@ -59,28 +59,28 @@ export const NotesOptionsPanel = observer(({ appState }: Props) => {
|
||||
<VisuallyHidden>Actions</VisuallyHidden>
|
||||
<Icon type="more" className="block" />
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel
|
||||
onKeyUp={(event) => {
|
||||
if (event.key === 'Escape' && !submenuOpen) {
|
||||
setOpen(false);
|
||||
buttonRef.current.focus();
|
||||
}
|
||||
}}
|
||||
ref={panelRef}
|
||||
style={{
|
||||
...position,
|
||||
maxHeight
|
||||
}}
|
||||
className="sn-dropdown sn-dropdown--animated max-h-120 max-w-80 flex flex-col py-2 overflow-y-scroll fixed"
|
||||
>
|
||||
{open && (
|
||||
<NotesOptions
|
||||
appState={appState}
|
||||
closeOnBlur={closeOnBlur}
|
||||
onSubmenuChange={onSubmenuChange}
|
||||
/>
|
||||
)}
|
||||
</DisclosurePanel>
|
||||
<DisclosurePanel
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape' && !submenuOpen) {
|
||||
setOpen(false);
|
||||
buttonRef.current.focus();
|
||||
}
|
||||
}}
|
||||
ref={panelRef}
|
||||
style={{
|
||||
...position,
|
||||
maxHeight,
|
||||
}}
|
||||
className="sn-dropdown sn-dropdown--animated max-h-120 max-w-xs flex flex-col py-2 overflow-y-scroll fixed"
|
||||
>
|
||||
{open && (
|
||||
<NotesOptions
|
||||
appState={appState}
|
||||
closeOnBlur={closeOnBlur}
|
||||
onSubmenuChange={onSubmenuChange}
|
||||
/>
|
||||
)}
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -42,7 +42,7 @@ export const Switch: FunctionalComponent<SwitchProps> = (
|
||||
<span
|
||||
aria-hidden
|
||||
className={`sn-switch-handle ${
|
||||
checked ? 'sn-switch-handle-right' : ''
|
||||
checked ? 'sn-switch-handle--right' : ''
|
||||
}`}
|
||||
/>
|
||||
</CustomCheckboxContainer>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FunctionComponent, h, render } from 'preact';
|
||||
import { StateUpdater, useCallback, useState } from 'preact/hooks';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* @returns a callback that will close a dropdown if none of its children has
|
||||
@@ -30,6 +31,26 @@ export function useCloseOnBlur(
|
||||
];
|
||||
}
|
||||
|
||||
export function useCloseOnClickOutside(
|
||||
container: { current: HTMLDivElement },
|
||||
setOpen: (open: boolean) => void
|
||||
): void {
|
||||
const closeOnClickOutside = useCallback((event: { target: EventTarget | null }) => {
|
||||
if (
|
||||
!container.current?.contains(event.target as Node)
|
||||
) {
|
||||
setOpen(false);
|
||||
}
|
||||
}, [container, setOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('click', closeOnClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('click', closeOnClickOutside);
|
||||
};
|
||||
}, [closeOnClickOutside]);
|
||||
}
|
||||
|
||||
export function toDirective<Props>(
|
||||
component: FunctionComponent<Props>,
|
||||
scope: Record<string, '=' | '&' | '@'> = {}
|
||||
|
||||
@@ -38,6 +38,7 @@ interface PanelResizerScope {
|
||||
index: number
|
||||
minWidth: number
|
||||
onResizeFinish: () => ResizeFinishCallback
|
||||
onWidthEvent?: () => void
|
||||
panelId: string
|
||||
property: PanelSide
|
||||
}
|
||||
@@ -53,6 +54,7 @@ class PanelResizerCtrl implements PanelResizerScope {
|
||||
index!: number
|
||||
minWidth!: number
|
||||
onResizeFinish!: () => ResizeFinishCallback
|
||||
onWidthEvent?: () => () => void
|
||||
panelId!: string
|
||||
property!: PanelSide
|
||||
|
||||
@@ -102,6 +104,7 @@ class PanelResizerCtrl implements PanelResizerScope {
|
||||
|
||||
$onDestroy() {
|
||||
(this.onResizeFinish as any) = undefined;
|
||||
(this.onWidthEvent as any) = undefined;
|
||||
(this.control as any) = undefined;
|
||||
window.removeEventListener(WINDOW_EVENT_RESIZE, this.handleResize);
|
||||
document.removeEventListener(MouseEventType.Move, this.onMouseMove);
|
||||
@@ -250,6 +253,9 @@ class PanelResizerCtrl implements PanelResizerScope {
|
||||
}
|
||||
|
||||
handleWidthEvent(event?: MouseEvent) {
|
||||
if (this.onWidthEvent && this.onWidthEvent()) {
|
||||
this.onWidthEvent()();
|
||||
}
|
||||
let x;
|
||||
if (event) {
|
||||
x = event!.clientX;
|
||||
@@ -387,6 +393,7 @@ export class PanelResizer extends WebDirective {
|
||||
index: '=',
|
||||
minWidth: '=',
|
||||
onResizeFinish: '&',
|
||||
onWidthEvent: '&',
|
||||
panelId: '=',
|
||||
property: '='
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ import { Bridge } from '@/services/bridge';
|
||||
import { storage, StorageKey } from '@/services/localStorage';
|
||||
import { AccountMenuState } from './account_menu_state';
|
||||
import { ActionsMenuState } from './actions_menu_state';
|
||||
import { NoteTagsState } from './note_tags_state';
|
||||
import { NoAccountWarningState } from './no_account_warning_state';
|
||||
import { SyncState } from './sync_state';
|
||||
import { SearchOptionsState } from './search_options_state';
|
||||
@@ -63,6 +64,7 @@ export class AppState {
|
||||
readonly accountMenu = new AccountMenuState();
|
||||
readonly actionsMenu = new ActionsMenuState();
|
||||
readonly noAccountWarning: NoAccountWarningState;
|
||||
readonly noteTags: NoteTagsState;
|
||||
readonly sync = new SyncState();
|
||||
readonly searchOptions: SearchOptionsState;
|
||||
readonly notes: NotesState;
|
||||
@@ -82,12 +84,18 @@ export class AppState {
|
||||
this.$rootScope = $rootScope;
|
||||
this.application = application;
|
||||
this.notes = new NotesState(
|
||||
this.application,
|
||||
application,
|
||||
this,
|
||||
async () => {
|
||||
await this.notifyEvent(AppStateEvent.ActiveEditorChanged);
|
||||
},
|
||||
this.appEventObserverRemovers,
|
||||
);
|
||||
this.noteTags = new NoteTagsState(
|
||||
application,
|
||||
this,
|
||||
this.appEventObserverRemovers
|
||||
);
|
||||
this.tags = new TagsState(
|
||||
application,
|
||||
this.appEventObserverRemovers,
|
||||
|
||||
208
app/assets/javascripts/ui_models/app_state/note_tags_state.ts
Normal file
208
app/assets/javascripts/ui_models/app_state/note_tags_state.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import { SNNote, ContentType, SNTag, UuidString } from '@standardnotes/snjs';
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import { WebApplication } from '../application';
|
||||
import { AppState } from './app_state';
|
||||
|
||||
export class NoteTagsState {
|
||||
autocompleteInputFocused = false;
|
||||
autocompleteSearchQuery = '';
|
||||
autocompleteTagHintFocused = false;
|
||||
autocompleteTagResults: SNTag[] = [];
|
||||
focusedTagResultUuid: UuidString | undefined = undefined;
|
||||
focusedTagUuid: UuidString | undefined = undefined;
|
||||
tags: SNTag[] = [];
|
||||
tagsContainerMaxWidth: number | 'auto' = 0;
|
||||
|
||||
constructor(
|
||||
private application: WebApplication,
|
||||
private appState: AppState,
|
||||
appEventListeners: (() => void)[]
|
||||
) {
|
||||
makeObservable(this, {
|
||||
autocompleteInputFocused: observable,
|
||||
autocompleteSearchQuery: observable,
|
||||
autocompleteTagHintFocused: observable,
|
||||
autocompleteTagResults: observable,
|
||||
focusedTagUuid: observable,
|
||||
focusedTagResultUuid: observable,
|
||||
tags: observable,
|
||||
tagsContainerMaxWidth: observable,
|
||||
|
||||
autocompleteTagHintVisible: computed,
|
||||
|
||||
clearAutocompleteSearch: action,
|
||||
focusNextTag: action,
|
||||
focusPreviousTag: action,
|
||||
setAutocompleteInputFocused: action,
|
||||
setAutocompleteSearchQuery: action,
|
||||
setAutocompleteTagHintFocused: action,
|
||||
setAutocompleteTagResults: action,
|
||||
setFocusedTagResultUuid: action,
|
||||
setFocusedTagUuid: action,
|
||||
setTags: action,
|
||||
setTagsContainerMaxWidth: action,
|
||||
reloadTags: action,
|
||||
});
|
||||
|
||||
appEventListeners.push(
|
||||
application.streamItems(ContentType.Tag, () => {
|
||||
this.reloadTags();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
get activeNote(): SNNote | undefined {
|
||||
return this.appState.notes.activeEditor?.note;
|
||||
}
|
||||
|
||||
get autocompleteTagHintVisible(): boolean {
|
||||
return (
|
||||
this.autocompleteSearchQuery !== '' &&
|
||||
!this.autocompleteTagResults.some(
|
||||
(tagResult) => tagResult.title === this.autocompleteSearchQuery
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
setAutocompleteInputFocused(focused: boolean): void {
|
||||
this.autocompleteInputFocused = focused;
|
||||
}
|
||||
|
||||
setAutocompleteSearchQuery(query: string): void {
|
||||
this.autocompleteSearchQuery = query;
|
||||
}
|
||||
|
||||
setAutocompleteTagHintFocused(focused: boolean): void {
|
||||
this.autocompleteTagHintFocused = focused;
|
||||
}
|
||||
|
||||
setAutocompleteTagResults(results: SNTag[]): void {
|
||||
this.autocompleteTagResults = results;
|
||||
}
|
||||
|
||||
setFocusedTagUuid(tagUuid: UuidString | undefined): void {
|
||||
this.focusedTagUuid = tagUuid;
|
||||
}
|
||||
|
||||
setFocusedTagResultUuid(tagUuid: UuidString | undefined): void {
|
||||
this.focusedTagResultUuid = tagUuid;
|
||||
}
|
||||
|
||||
setTags(tags: SNTag[]): void {
|
||||
this.tags = tags;
|
||||
}
|
||||
|
||||
setTagsContainerMaxWidth(width: number): void {
|
||||
this.tagsContainerMaxWidth = width;
|
||||
}
|
||||
|
||||
clearAutocompleteSearch(): void {
|
||||
this.setAutocompleteSearchQuery('');
|
||||
this.searchActiveNoteAutocompleteTags();
|
||||
}
|
||||
|
||||
async createAndAddNewTag(): Promise<void> {
|
||||
const newTag = await this.application.findOrCreateTag(
|
||||
this.autocompleteSearchQuery
|
||||
);
|
||||
await this.addTagToActiveNote(newTag);
|
||||
this.clearAutocompleteSearch();
|
||||
}
|
||||
|
||||
focusNextTag(tag: SNTag): void {
|
||||
const nextTagIndex = this.getTagIndex(tag, this.tags) + 1;
|
||||
if (nextTagIndex > -1 && this.tags.length > nextTagIndex) {
|
||||
const nextTag = this.tags[nextTagIndex];
|
||||
this.setFocusedTagUuid(nextTag.uuid);
|
||||
}
|
||||
}
|
||||
|
||||
focusNextTagResult(tagResult: SNTag): void {
|
||||
const nextTagResultIndex =
|
||||
this.getTagIndex(tagResult, this.autocompleteTagResults) + 1;
|
||||
if (
|
||||
nextTagResultIndex > -1 &&
|
||||
this.autocompleteTagResults.length > nextTagResultIndex
|
||||
) {
|
||||
const nextTagResult = this.autocompleteTagResults[nextTagResultIndex];
|
||||
this.setFocusedTagResultUuid(nextTagResult.uuid);
|
||||
}
|
||||
}
|
||||
|
||||
focusPreviousTag(tag: SNTag): void {
|
||||
const previousTagIndex = this.getTagIndex(tag, this.tags) - 1;
|
||||
if (previousTagIndex > -1 && this.tags.length > previousTagIndex) {
|
||||
const previousTag = this.tags[previousTagIndex];
|
||||
this.setFocusedTagUuid(previousTag.uuid);
|
||||
}
|
||||
}
|
||||
|
||||
focusPreviousTagResult(tagResult: SNTag): void {
|
||||
const previousTagResultIndex =
|
||||
this.getTagIndex(tagResult, this.autocompleteTagResults) - 1;
|
||||
if (
|
||||
previousTagResultIndex > -1 &&
|
||||
this.autocompleteTagResults.length > previousTagResultIndex
|
||||
) {
|
||||
const previousTagResult =
|
||||
this.autocompleteTagResults[previousTagResultIndex];
|
||||
this.setFocusedTagResultUuid(previousTagResult.uuid);
|
||||
}
|
||||
}
|
||||
|
||||
searchActiveNoteAutocompleteTags(): void {
|
||||
const newResults = this.application.searchTags(
|
||||
this.autocompleteSearchQuery,
|
||||
this.activeNote
|
||||
);
|
||||
this.setAutocompleteTagResults(newResults);
|
||||
}
|
||||
|
||||
getTagIndex(tag: SNTag, tagsArr: SNTag[]): number {
|
||||
return tagsArr.findIndex((t) => t.uuid === tag.uuid);
|
||||
}
|
||||
|
||||
reloadTags(): void {
|
||||
const { activeNote } = this;
|
||||
if (activeNote) {
|
||||
const tags = this.application.getSortedTagsForNote(activeNote);
|
||||
this.setTags(tags);
|
||||
}
|
||||
}
|
||||
|
||||
reloadTagsContainerMaxWidth(): void {
|
||||
const EDITOR_ELEMENT_ID = 'editor-column';
|
||||
const editorWidth = document.getElementById(EDITOR_ELEMENT_ID)?.clientWidth;
|
||||
if (editorWidth) {
|
||||
this.setTagsContainerMaxWidth(editorWidth);
|
||||
}
|
||||
}
|
||||
|
||||
async addTagToActiveNote(tag: SNTag): Promise<void> {
|
||||
const { activeNote } = this;
|
||||
if (activeNote) {
|
||||
const parentChainTags = this.application.getTagParentChain(tag);
|
||||
const tagsToAdd = [...parentChainTags, tag];
|
||||
await Promise.all(
|
||||
tagsToAdd.map(async (tag) => {
|
||||
await this.application.changeItem(tag.uuid, (mutator) => {
|
||||
mutator.addItemAsRelationship(activeNote);
|
||||
});
|
||||
})
|
||||
);
|
||||
this.application.sync();
|
||||
this.reloadTags();
|
||||
}
|
||||
}
|
||||
|
||||
async removeTagFromActiveNote(tag: SNTag): Promise<void> {
|
||||
const { activeNote } = this;
|
||||
if (activeNote) {
|
||||
await this.application.changeItem(tag.uuid, (mutator) => {
|
||||
mutator.removeItemAsRelationship(activeNote);
|
||||
});
|
||||
this.application.sync();
|
||||
this.reloadTags();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from 'mobx';
|
||||
import { WebApplication } from '../application';
|
||||
import { Editor } from '../editor';
|
||||
import { AppState } from './app_state';
|
||||
|
||||
export class NotesState {
|
||||
lastSelectedNote: SNNote | undefined;
|
||||
@@ -32,6 +33,7 @@ export class NotesState {
|
||||
|
||||
constructor(
|
||||
private application: WebApplication,
|
||||
private appState: AppState,
|
||||
private onActiveEditorChanged: () => Promise<void>,
|
||||
appEventListeners: (() => void)[]
|
||||
) {
|
||||
@@ -168,6 +170,8 @@ export class NotesState {
|
||||
} else {
|
||||
this.activeEditor.setNote(note);
|
||||
}
|
||||
|
||||
this.appState.noteTags.reloadTags();
|
||||
await this.onActiveEditorChanged();
|
||||
|
||||
if (note.waitingForKey) {
|
||||
@@ -326,11 +330,17 @@ export class NotesState {
|
||||
|
||||
async addTagToSelectedNotes(tag: SNTag): Promise<void> {
|
||||
const selectedNotes = Object.values(this.selectedNotes);
|
||||
await this.application.changeItem(tag.uuid, (mutator) => {
|
||||
for (const note of selectedNotes) {
|
||||
mutator.addItemAsRelationship(note);
|
||||
}
|
||||
});
|
||||
const parentChainTags = this.application.getTagParentChain(tag);
|
||||
const tagsToAdd = [...parentChainTags, tag];
|
||||
await Promise.all(
|
||||
tagsToAdd.map(async (tag) => {
|
||||
await this.application.changeItem(tag.uuid, (mutator) => {
|
||||
for (const note of selectedNotes) {
|
||||
mutator.addItemAsRelationship(note);
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
this.application.sync();
|
||||
}
|
||||
|
||||
@@ -342,13 +352,13 @@ export class NotesState {
|
||||
}
|
||||
});
|
||||
this.application.sync();
|
||||
|
||||
}
|
||||
|
||||
isTagInSelectedNotes(tag: SNTag): boolean {
|
||||
const selectedNotes = Object.values(this.selectedNotes);
|
||||
return selectedNotes.every((note) =>
|
||||
this.application
|
||||
.getAppState()
|
||||
this.appState
|
||||
.getNoteTags(note)
|
||||
.find((noteTag) => noteTag.uuid === tag.uuid)
|
||||
);
|
||||
|
||||
@@ -24,50 +24,37 @@
|
||||
ng-if="self.showLockedIcon"
|
||||
)
|
||||
| {{self.lockText}}
|
||||
#editor-title-bar.section-title-bar.flex.items-center.justify-between.w-full(
|
||||
#editor-title-bar.section-title-bar.w-full(
|
||||
ng-show='self.note && !self.note.errorDecrypting'
|
||||
)
|
||||
div.flex-grow(
|
||||
ng-class="{'locked' : self.noteLocked}"
|
||||
)
|
||||
.title
|
||||
input#note-title-editor.input(
|
||||
ng-blur='self.onTitleBlur()',
|
||||
ng-change='self.onTitleChange()',
|
||||
ng-disabled='self.noteLocked',
|
||||
ng-focus='self.onTitleFocus()',
|
||||
ng-keyup='$event.keyCode == 13 && self.onTitleEnter($event)',
|
||||
ng-model='self.editorValues.title',
|
||||
select-on-focus='true',
|
||||
spellcheck='false'
|
||||
)
|
||||
.editor-tags
|
||||
#note-tags-component-container(ng-if='self.state.tagsComponent && !self.note.errorDecrypting')
|
||||
component-view.component-view(
|
||||
component-uuid='self.state.tagsComponent.uuid',
|
||||
ng-style="self.notesLocked && {'pointer-events' : 'none'}",
|
||||
application='self.application'
|
||||
)
|
||||
input.tags-input(
|
||||
ng-blur='self.onTagsInputBlur()',
|
||||
ng-disabled='self.noteLocked',
|
||||
ng-if='!self.state.tagsComponent',
|
||||
ng-keyup='$event.keyCode == 13 && $event.target.blur();',
|
||||
ng-model='self.editorValues.tagsInputValue',
|
||||
placeholder='#tags',
|
||||
spellcheck='false',
|
||||
type='text'
|
||||
)
|
||||
div.flex.items-center
|
||||
#save-status
|
||||
.message(
|
||||
ng-class="{'warning sk-bold': self.state.syncTakingTooLong, 'danger sk-bold': self.state.saveError}"
|
||||
) {{self.state.noteStatus.message}}
|
||||
.desc(ng-show='self.state.noteStatus.desc') {{self.state.noteStatus.desc}}
|
||||
notes-options-panel(
|
||||
app-state='self.appState',
|
||||
ng-if='self.appState.notes.selectedNotesCount > 0'
|
||||
div.flex.items-center.justify-between.h-8
|
||||
div.flex-grow(
|
||||
ng-class="{'locked' : self.noteLocked}"
|
||||
)
|
||||
.title.overflow-auto
|
||||
input#note-title-editor.input(
|
||||
ng-blur='self.onTitleBlur()',
|
||||
ng-change='self.onTitleChange()',
|
||||
ng-disabled='self.noteLocked',
|
||||
ng-focus='self.onTitleFocus()',
|
||||
ng-keyup='$event.keyCode == 13 && self.onTitleEnter($event)',
|
||||
ng-model='self.editorValues.title',
|
||||
select-on-focus='true',
|
||||
spellcheck='false'
|
||||
)
|
||||
div.flex.items-center
|
||||
#save-status
|
||||
.message(
|
||||
ng-class="{'warning sk-bold': self.state.syncTakingTooLong, 'danger sk-bold': self.state.saveError}"
|
||||
) {{self.state.noteStatus.message}}
|
||||
.desc(ng-show='self.state.noteStatus.desc') {{self.state.noteStatus.desc}}
|
||||
notes-options-panel(
|
||||
app-state='self.appState',
|
||||
ng-if='self.appState.notes.selectedNotesCount > 0'
|
||||
)
|
||||
note-tags-container(
|
||||
app-state='self.appState'
|
||||
)
|
||||
.sn-component(ng-if='self.note')
|
||||
#editor-menu-bar.sk-app-bar.no-edges
|
||||
.left
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import {
|
||||
STRING_ARCHIVE_LOCKED_ATTEMPT,
|
||||
STRING_SAVING_WHILE_DOCUMENT_HIDDEN,
|
||||
STRING_UNARCHIVE_LOCKED_ATTEMPT,
|
||||
} from './../../strings';
|
||||
import { Editor } from '@/ui_models/editor';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
@@ -14,15 +12,12 @@ import {
|
||||
ContentType,
|
||||
SNComponent,
|
||||
SNNote,
|
||||
SNTag,
|
||||
NoteMutator,
|
||||
Uuids,
|
||||
ComponentArea,
|
||||
ComponentAction,
|
||||
PrefKey,
|
||||
ComponentMutator,
|
||||
} from '@standardnotes/snjs';
|
||||
import find from 'lodash/find';
|
||||
import { isDesktopApplication } from '@/utils';
|
||||
import { KeyboardModifier, KeyboardKey } from '@/services/ioService';
|
||||
import template from './editor-view.pug';
|
||||
@@ -36,9 +31,8 @@ import {
|
||||
STRING_DELETE_LOCKED_ATTEMPT,
|
||||
STRING_EDIT_LOCKED_ATTEMPT,
|
||||
StringDeleteNote,
|
||||
StringEmptyTrash,
|
||||
} from '@/strings';
|
||||
import { alertDialog, confirmDialog } from '@/services/alertService';
|
||||
import { confirmDialog } from '@/services/alertService';
|
||||
|
||||
const NOTE_PREVIEW_CHAR_LIMIT = 80;
|
||||
const MINIMUM_STATUS_DURATION = 400;
|
||||
@@ -50,7 +44,6 @@ const ElementIds = {
|
||||
NoteTextEditor: 'note-text-editor',
|
||||
NoteTitleEditor: 'note-title-editor',
|
||||
EditorContent: 'editor-content',
|
||||
NoteTagsComponentContainer: 'note-tags-component-container',
|
||||
};
|
||||
|
||||
type NoteStatus = {
|
||||
@@ -61,10 +54,8 @@ type NoteStatus = {
|
||||
type EditorState = {
|
||||
stackComponents: SNComponent[];
|
||||
editorComponent?: SNComponent;
|
||||
tagsComponent?: SNComponent;
|
||||
saveError?: any;
|
||||
noteStatus?: NoteStatus;
|
||||
tagsAsStrings?: string;
|
||||
marginResizersEnabled?: boolean;
|
||||
monospaceFont?: boolean;
|
||||
isDesktop?: boolean;
|
||||
@@ -83,14 +74,11 @@ type EditorState = {
|
||||
/** Setting to true then false will allow the main content textarea to be destroyed
|
||||
* then re-initialized. Used when reloading spellcheck status. */
|
||||
textareaUnloading: boolean;
|
||||
/** Fields that can be directly mutated by the template */
|
||||
mutable: any;
|
||||
};
|
||||
|
||||
type EditorValues = {
|
||||
title: string;
|
||||
text: string;
|
||||
tagsInputValue?: string;
|
||||
};
|
||||
|
||||
function copyEditorValues(values: EditorValues) {
|
||||
@@ -117,13 +105,10 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
public editorValues: EditorValues = { title: '', text: '' };
|
||||
onEditorLoad?: () => void;
|
||||
|
||||
private tags: SNTag[] = [];
|
||||
|
||||
private removeAltKeyObserver?: any;
|
||||
private removeTrashKeyObserver?: any;
|
||||
private removeTabObserver?: any;
|
||||
|
||||
private removeTagsObserver!: () => void;
|
||||
private removeComponentsObserver!: () => void;
|
||||
|
||||
prefKeyMonospace: string;
|
||||
@@ -153,9 +138,7 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
|
||||
deinit() {
|
||||
this.editor.clearNoteChangeListener();
|
||||
this.removeTagsObserver();
|
||||
this.removeComponentsObserver();
|
||||
(this.removeTagsObserver as any) = undefined;
|
||||
(this.removeComponentsObserver as any) = undefined;
|
||||
this.removeAltKeyObserver();
|
||||
this.removeAltKeyObserver = undefined;
|
||||
@@ -172,7 +155,6 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
this.statusTimeout = undefined;
|
||||
(this.onPanelResizeFinish as any) = undefined;
|
||||
(this.editorMenuOnSelect as any) = undefined;
|
||||
this.tags = [];
|
||||
super.deinit();
|
||||
}
|
||||
|
||||
@@ -194,7 +176,6 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
if (isPayloadSourceRetrieved(source!)) {
|
||||
this.editorValues.title = note.title;
|
||||
this.editorValues.text = note.text;
|
||||
this.reloadTags();
|
||||
}
|
||||
if (!this.editorValues.title) {
|
||||
this.editorValues.title = note.title;
|
||||
@@ -237,9 +218,6 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
noteStatus: undefined,
|
||||
editorUnloading: false,
|
||||
textareaUnloading: false,
|
||||
mutable: {
|
||||
tagsString: '',
|
||||
},
|
||||
} as EditorState;
|
||||
}
|
||||
|
||||
@@ -302,10 +280,8 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
this.editorValues.title = note.title;
|
||||
this.editorValues.text = note.text;
|
||||
this.reloadEditor();
|
||||
this.reloadTags();
|
||||
this.reloadPreferences();
|
||||
this.reloadStackComponents();
|
||||
this.reloadNoteTagsComponent();
|
||||
if (note.dirty) {
|
||||
this.showSavingStatus();
|
||||
}
|
||||
@@ -335,13 +311,6 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
}
|
||||
|
||||
streamItems() {
|
||||
this.removeTagsObserver = this.application.streamItems(
|
||||
ContentType.Tag,
|
||||
() => {
|
||||
this.reloadTags();
|
||||
}
|
||||
);
|
||||
|
||||
this.removeComponentsObserver = this.application.streamItems(
|
||||
ContentType.Component,
|
||||
async (_items, source) => {
|
||||
@@ -350,7 +319,6 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
}
|
||||
if (!this.note) return;
|
||||
this.reloadStackComponents();
|
||||
this.reloadNoteTagsComponent();
|
||||
this.reloadEditor();
|
||||
}
|
||||
);
|
||||
@@ -487,7 +455,6 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
const title = editorValues.title;
|
||||
const text = editorValues.text;
|
||||
const isTemplate = this.editor.isTemplateNote;
|
||||
const selectedTag = this.appState.selectedTag;
|
||||
if (document.hidden) {
|
||||
this.application.alertService.alert(STRING_SAVING_WHILE_DOCUMENT_HIDDEN);
|
||||
return;
|
||||
@@ -499,14 +466,6 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
if (isTemplate) {
|
||||
await this.editor.insertTemplatedNote();
|
||||
}
|
||||
if (
|
||||
!selectedTag?.isSmartTag &&
|
||||
!selectedTag?.hasRelationshipWithItem(note)
|
||||
) {
|
||||
await this.application.changeItem(selectedTag!.uuid, (mutator) => {
|
||||
mutator.addItemAsRelationship(note);
|
||||
});
|
||||
}
|
||||
if (!this.application.findItem(note.uuid)) {
|
||||
this.application.alertService.alert(STRING_INVALID_NOTE);
|
||||
return;
|
||||
@@ -697,105 +656,6 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
this.application.deleteItem(note);
|
||||
}
|
||||
|
||||
async reloadTags() {
|
||||
if (!this.note) {
|
||||
return;
|
||||
}
|
||||
const tags = this.appState.getNoteTags(this.note);
|
||||
if (tags.length !== this.tags.length) {
|
||||
this.reloadTagsString(tags);
|
||||
} else {
|
||||
/** Check that all tags are the same */
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
const localTag = this.tags[i];
|
||||
const tag = tags[i];
|
||||
if (tag.title !== localTag.title || tag.uuid !== localTag.uuid) {
|
||||
this.reloadTagsString(tags);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.tags = tags;
|
||||
}
|
||||
|
||||
private async reloadTagsString(tags: SNTag[]) {
|
||||
const string = SNTag.arrayToDisplayString(tags);
|
||||
await this.flushUI();
|
||||
this.editorValues.tagsInputValue = string;
|
||||
}
|
||||
|
||||
private addTag(tag: SNTag) {
|
||||
const tags = this.appState.getNoteTags(this.note);
|
||||
const strings = tags.map((currentTag) => {
|
||||
return currentTag.title;
|
||||
});
|
||||
strings.push(tag.title);
|
||||
this.saveTagsFromStrings(strings);
|
||||
}
|
||||
|
||||
removeTag(tag: SNTag) {
|
||||
const tags = this.appState.getNoteTags(this.note);
|
||||
const strings = tags
|
||||
.map((currentTag) => {
|
||||
return currentTag.title;
|
||||
})
|
||||
.filter((title) => {
|
||||
return title !== tag.title;
|
||||
});
|
||||
this.saveTagsFromStrings(strings);
|
||||
}
|
||||
|
||||
onTagsInputBlur() {
|
||||
this.saveTagsFromStrings();
|
||||
this.focusEditor();
|
||||
}
|
||||
|
||||
public async saveTagsFromStrings(strings?: string[]) {
|
||||
if (
|
||||
!strings &&
|
||||
this.editorValues.tagsInputValue === this.state.tagsAsStrings
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (!strings) {
|
||||
strings = this.editorValues
|
||||
.tagsInputValue!.split('#')
|
||||
.filter((string) => {
|
||||
return string.length > 0;
|
||||
})
|
||||
.map((string) => {
|
||||
return string.trim();
|
||||
});
|
||||
}
|
||||
const note = this.note;
|
||||
const currentTags = this.appState.getNoteTags(note);
|
||||
const removeTags = [];
|
||||
for (const tag of currentTags) {
|
||||
if (strings.indexOf(tag.title) === -1) {
|
||||
removeTags.push(tag);
|
||||
}
|
||||
}
|
||||
for (const tag of removeTags) {
|
||||
await this.application.changeItem(tag.uuid, (mutator) => {
|
||||
mutator.removeItemAsRelationship(note);
|
||||
});
|
||||
}
|
||||
const newRelationships: SNTag[] = [];
|
||||
for (const title of strings) {
|
||||
const existingRelationship = find(currentTags, { title: title });
|
||||
if (!existingRelationship) {
|
||||
newRelationships.push(await this.application.findOrCreateTag(title));
|
||||
}
|
||||
}
|
||||
if (newRelationships.length > 0) {
|
||||
await this.application.changeItems(Uuids(newRelationships), (mutator) => {
|
||||
mutator.addItemAsRelationship(note);
|
||||
});
|
||||
}
|
||||
this.application.sync();
|
||||
this.reloadTags();
|
||||
}
|
||||
|
||||
async onPanelResizeFinish(width: number, left: number, isMaxWidth: boolean) {
|
||||
if (isMaxWidth) {
|
||||
await this.application.setPreference(PrefKey.EditorWidth, null);
|
||||
@@ -900,7 +760,6 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
{
|
||||
identifier: 'editor',
|
||||
areas: [
|
||||
ComponentArea.NoteTags,
|
||||
ComponentArea.EditorStack,
|
||||
ComponentArea.Editor,
|
||||
],
|
||||
@@ -908,7 +767,6 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
const currentEditor = this.state.editorComponent;
|
||||
if (
|
||||
componentUuid === currentEditor?.uuid ||
|
||||
componentUuid === this.state.tagsComponent?.uuid ||
|
||||
Uuids(this.state.stackComponents).includes(componentUuid)
|
||||
) {
|
||||
return this.note;
|
||||
@@ -919,78 +777,10 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
this.closeAllMenus();
|
||||
}
|
||||
},
|
||||
actionHandler: (component, action, data) => {
|
||||
if (action === ComponentAction.SetSize) {
|
||||
const setSize = (
|
||||
element: HTMLElement,
|
||||
size: { width: string | number; height: string | number }
|
||||
) => {
|
||||
const widthString =
|
||||
typeof size.width === 'string' ? size.width : `${data.width}px`;
|
||||
const heightString =
|
||||
typeof size.height === 'string'
|
||||
? size.height
|
||||
: `${data.height}px`;
|
||||
element.setAttribute(
|
||||
'style',
|
||||
`width: ${widthString}; height: ${heightString};`
|
||||
);
|
||||
};
|
||||
if (data.type === 'container') {
|
||||
if (component.area === ComponentArea.NoteTags) {
|
||||
const container = document.getElementById(
|
||||
ElementIds.NoteTagsComponentContainer
|
||||
);
|
||||
setSize(container!, {
|
||||
width: data.width!,
|
||||
height: data.height!,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (action === ComponentAction.AssociateItem) {
|
||||
if (data.item!.content_type === ContentType.Tag) {
|
||||
const tag = this.application.findItem(data.item!.uuid) as SNTag;
|
||||
this.addTag(tag);
|
||||
}
|
||||
} else if (action === ComponentAction.DeassociateItem) {
|
||||
const tag = this.application.findItem(data.item!.uuid) as SNTag;
|
||||
this.removeTag(tag);
|
||||
} else if (action === ComponentAction.SaveSuccess) {
|
||||
const savedUuid = data.item ? data.item.uuid : data.items![0].uuid;
|
||||
if (savedUuid === this.note.uuid) {
|
||||
const selectedTag = this.appState.selectedTag;
|
||||
if (
|
||||
!selectedTag?.isSmartTag &&
|
||||
!selectedTag?.hasRelationshipWithItem(this.note)
|
||||
) {
|
||||
this.application.changeAndSaveItem(
|
||||
selectedTag!.uuid,
|
||||
(mutator) => {
|
||||
mutator.addItemAsRelationship(this.note);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async reloadNoteTagsComponent() {
|
||||
const [
|
||||
tagsComponent,
|
||||
] = this.application.componentManager!.componentsForArea(
|
||||
ComponentArea.NoteTags
|
||||
);
|
||||
await this.setState({
|
||||
tagsComponent: tagsComponent?.active ? tagsComponent : undefined,
|
||||
});
|
||||
this.application.componentManager!.contextItemDidChangeInArea(
|
||||
ComponentArea.NoteTags
|
||||
);
|
||||
}
|
||||
|
||||
async reloadStackComponents() {
|
||||
const stackComponents = sortAlphabetically(
|
||||
this.application
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { WebDirective } from './../../types';
|
||||
import template from './editor-group-view.pug';
|
||||
import { Editor } from '@/ui_models/editor';
|
||||
@@ -15,7 +14,7 @@ class EditorGroupViewCtrl extends PureViewCtrl<unknown, {
|
||||
super($timeout);
|
||||
this.state = {
|
||||
showMultipleSelectedNotes: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
|
||||
@@ -169,5 +169,6 @@
|
||||
default-width="300"
|
||||
hoverable="true"
|
||||
on-resize-finish="self.onPanelResize"
|
||||
on-width-event="self.onPanelWidthEvent"
|
||||
panel-id="'notes-column'"
|
||||
)
|
||||
|
||||
@@ -93,6 +93,7 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesCtrlState> {
|
||||
};
|
||||
this.onWindowResize = this.onWindowResize.bind(this);
|
||||
this.onPanelResize = this.onPanelResize.bind(this);
|
||||
this.onPanelWidthEvent = this.onPanelWidthEvent.bind(this);
|
||||
window.addEventListener('resize', this.onWindowResize, true);
|
||||
this.registerKeyboardShortcuts();
|
||||
this.autorun(async () => {
|
||||
@@ -133,6 +134,7 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesCtrlState> {
|
||||
window.removeEventListener('resize', this.onWindowResize, true);
|
||||
(this.onWindowResize as any) = undefined;
|
||||
(this.onPanelResize as any) = undefined;
|
||||
(this.onPanelWidthEvent as any) = undefined;
|
||||
this.newNoteKeyObserver();
|
||||
this.nextNoteKeyObserver();
|
||||
this.previousNoteKeyObserver();
|
||||
@@ -408,6 +410,7 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesCtrlState> {
|
||||
await this.appState.createEditor(title);
|
||||
await this.flushUI();
|
||||
await this.reloadNotes();
|
||||
await this.appState.noteTags.reloadTags();
|
||||
}
|
||||
|
||||
async handleTagChange(tag: SNTag) {
|
||||
@@ -642,10 +645,11 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesCtrlState> {
|
||||
|
||||
onPanelResize(
|
||||
newWidth: number,
|
||||
_: number,
|
||||
newLeft: number,
|
||||
__: boolean,
|
||||
isCollapsed: boolean
|
||||
) {
|
||||
this.appState.noteTags.reloadTagsContainerMaxWidth();
|
||||
this.application.setPreference(
|
||||
PrefKey.NotesPanelWidth,
|
||||
newWidth
|
||||
@@ -656,6 +660,10 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesCtrlState> {
|
||||
);
|
||||
}
|
||||
|
||||
onPanelWidthEvent(): void {
|
||||
this.appState.noteTags.reloadTagsContainerMaxWidth();
|
||||
}
|
||||
|
||||
paginate() {
|
||||
this.notesToDisplay += this.pageSize;
|
||||
this.reloadNotes();
|
||||
|
||||
@@ -86,34 +86,6 @@ $heading-height: 75px;
|
||||
width: 100%;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
|
||||
#note-tags-component-container {
|
||||
height: 50px;
|
||||
overflow: auto; // Required for expired sub to not overflow
|
||||
|
||||
.component-view {
|
||||
// see comment under main .component-view css defintion
|
||||
position: inherit;
|
||||
}
|
||||
|
||||
iframe {
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
position: absolute; // Required for autocomplete window to show
|
||||
}
|
||||
}
|
||||
|
||||
.tags-input {
|
||||
background-color: transparent;
|
||||
color: var(--sn-stylekit-foreground-color);
|
||||
width: 100%;
|
||||
border: none;
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,10 @@
|
||||
border-color: var(--sn-stylekit-background-color);
|
||||
}
|
||||
|
||||
.focus\:border-bottom:focus {
|
||||
border-bottom: 2px solid var(--sn-stylekit-info-color);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
@@ -53,7 +57,54 @@
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.py-1\.5 {
|
||||
.ml-1 {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.mr-1 {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.mr-10 {
|
||||
margin-right: 2.5rem;
|
||||
}
|
||||
|
||||
.-mt-1 {
|
||||
margin-top: -0.25rem;
|
||||
}
|
||||
|
||||
.-mr-1 {
|
||||
margin-right: -0.25rem;
|
||||
}
|
||||
|
||||
.-mr-2 {
|
||||
margin-right: -0.5rem;
|
||||
}
|
||||
|
||||
.py-1 {
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.pl-1 {
|
||||
padding-left: 0.25rem;
|
||||
}
|
||||
|
||||
.pr-2 {
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.px-1 {
|
||||
padding-left: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
|
||||
.px-2 {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.py-1\.5 {
|
||||
padding-top: 0.375rem;
|
||||
padding-bottom: 0.375rem;
|
||||
}
|
||||
@@ -70,6 +121,10 @@
|
||||
color: var(--sn-stylekit-danger-color);
|
||||
}
|
||||
|
||||
.color-info {
|
||||
color: var(--sn-stylekit-info-color);
|
||||
}
|
||||
|
||||
.ring-info {
|
||||
box-shadow: 0 0 0 2px var(--sn-stylekit-info-color);
|
||||
}
|
||||
@@ -90,6 +145,14 @@
|
||||
@extend .color-text;
|
||||
}
|
||||
|
||||
.hover\:bg-secondary-contrast:hover {
|
||||
@extend .bg-secondary-contrast;
|
||||
}
|
||||
|
||||
.focus\:bg-secondary-contrast:focus {
|
||||
@extend .bg-secondary-contrast;
|
||||
}
|
||||
|
||||
.focus\:inner-ring-info:focus {
|
||||
@extend .inner-ring-info;
|
||||
}
|
||||
@@ -111,6 +174,10 @@
|
||||
line-height: 2.25rem;
|
||||
}
|
||||
|
||||
.w-0 {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.w-3\.5 {
|
||||
width: 0.875rem;
|
||||
}
|
||||
@@ -123,18 +190,34 @@
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
.max-w-60 {
|
||||
max-width: 15rem;
|
||||
.max-w-290px {
|
||||
max-width: 290px;
|
||||
}
|
||||
|
||||
.max-w-80 {
|
||||
.max-w-xs {
|
||||
max-width: 20rem;
|
||||
}
|
||||
|
||||
.max-w-40 {
|
||||
max-width: 10rem;
|
||||
}
|
||||
|
||||
.min-w-5 {
|
||||
min-width: 1.25rem;
|
||||
}
|
||||
|
||||
.min-w-40 {
|
||||
min-width: 10rem;
|
||||
}
|
||||
|
||||
.h-1px {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.h-0 {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.h-3\.5 {
|
||||
height: 0.875rem;
|
||||
}
|
||||
@@ -147,18 +230,38 @@
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
.h-6 {
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.h-7 {
|
||||
height: 1.75rem;
|
||||
}
|
||||
|
||||
.h-8 {
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.h-9 {
|
||||
height: 2.25rem;
|
||||
}
|
||||
|
||||
.h-10 {
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.h-18 {
|
||||
height: 4.5rem;
|
||||
}
|
||||
|
||||
.max-h-120 {
|
||||
max-height: 30rem;
|
||||
}
|
||||
|
||||
.min-h-5 {
|
||||
min-height: 1.25rem;
|
||||
}
|
||||
|
||||
.fixed {
|
||||
position: fixed;
|
||||
}
|
||||
@@ -167,12 +270,8 @@
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.items-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.whitespace-nowrap {
|
||||
white-space: nowrap;
|
||||
.overflow-auto {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.overflow-hidden {
|
||||
@@ -183,6 +282,34 @@
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.items-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.p-2 {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.whitespace-pre-wrap {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.whitespace-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.w-80 {
|
||||
width: 20rem;
|
||||
}
|
||||
|
||||
.w-70 {
|
||||
width: 17.5rem;
|
||||
}
|
||||
|
||||
/**
|
||||
* A button that is just an icon. Separated from .sn-button because there
|
||||
* is almost no style overlap.
|
||||
@@ -215,11 +342,16 @@
|
||||
@extend .h-5;
|
||||
@extend .w-5;
|
||||
@extend .fill-current;
|
||||
|
||||
&.sn-icon--small {
|
||||
@extend .h-3\.5 ;
|
||||
@extend .w-3\.5 ;
|
||||
}
|
||||
}
|
||||
|
||||
.sn-dropdown {
|
||||
@extend .bg-default;
|
||||
@extend .min-w-80;
|
||||
// @extend .min-w-80;
|
||||
@extend .rounded;
|
||||
@extend .box-shadow;
|
||||
|
||||
@@ -238,6 +370,10 @@
|
||||
@extend .duration-150;
|
||||
@extend .slide-down-animation;
|
||||
}
|
||||
|
||||
&.sn-dropdown--small {
|
||||
@extend .min-w-40;
|
||||
}
|
||||
}
|
||||
|
||||
/** Lesser specificity will give priority to reach's styles */
|
||||
@@ -275,7 +411,7 @@
|
||||
top: 50%;
|
||||
transform: translate(0px, -50%);
|
||||
|
||||
&.sn-switch-handle-right {
|
||||
&.sn-switch-handle--right {
|
||||
transform: translate(
|
||||
calc(2rem - 1.125rem),
|
||||
-50%
|
||||
@@ -286,3 +422,40 @@
|
||||
.sn-component .sk-app-bar .sk-app-bar-item {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.sn-dropdown-item {
|
||||
@extend .flex;
|
||||
@extend .items-center;
|
||||
@extend .border-0;
|
||||
@extend .focus\:inner-ring-info;
|
||||
@extend .cursor-pointer;
|
||||
@extend .hover\:bg-contrast;
|
||||
@extend .color-text;
|
||||
@extend .bg-transparent;
|
||||
@extend .px-3;
|
||||
@extend .py-1\.5;
|
||||
@extend .text-left;
|
||||
@extend .w-full;
|
||||
|
||||
&.sn-dropdown-item--no-icon {
|
||||
@extend .py-2;
|
||||
}
|
||||
}
|
||||
|
||||
.sn-tag {
|
||||
@extend .h-6;
|
||||
@extend .bg-contrast;
|
||||
@extend .border-0;
|
||||
@extend .rounded;
|
||||
@extend .text-xs;
|
||||
@extend .color-text;
|
||||
@extend .py-1;
|
||||
@extend .py-2;
|
||||
@extend .pr-2;
|
||||
@extend .flex;
|
||||
@extend .items-center;
|
||||
@extend .mt-2;
|
||||
@extend .cursor-pointer;
|
||||
@extend .hover\:bg-secondary-contrast;
|
||||
@extend .focus\:bg-secondary-contrast;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "standard-notes-web",
|
||||
"version": "3.7.1",
|
||||
"version": "3.9.0-beta01",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -57,7 +57,7 @@
|
||||
"serve-static": "^1.14.1",
|
||||
"sn-stylekit": "5.1.0",
|
||||
"ts-loader": "^8.0.17",
|
||||
"typescript": "^4.1.5",
|
||||
"typescript": "4.2.3",
|
||||
"typescript-eslint": "0.0.1-alpha.0",
|
||||
"webpack": "^4.44.1",
|
||||
"webpack-cli": "^3.3.12",
|
||||
@@ -71,7 +71,7 @@
|
||||
"@reach/checkbox": "^0.13.2",
|
||||
"@reach/dialog": "^0.13.0",
|
||||
"@standardnotes/sncrypto-web": "1.2.10",
|
||||
"@standardnotes/snjs": "2.5.0",
|
||||
"@standardnotes/snjs": "2.6.0",
|
||||
"mobx": "^6.1.6",
|
||||
"mobx-react-lite": "^3.2.0",
|
||||
"preact": "^10.5.12"
|
||||
|
||||
16
yarn.lock
16
yarn.lock
@@ -1936,10 +1936,10 @@
|
||||
"@standardnotes/sncrypto-common" "^1.2.7"
|
||||
libsodium-wrappers "^0.7.8"
|
||||
|
||||
"@standardnotes/snjs@2.5.0":
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.5.0.tgz#fcd45f8c6884fcc204633be33366b59ede71c5b1"
|
||||
integrity sha512-VWThoZhymoCOqRkZjXj3vDhQGAPMt+KUrB/FyYZkl+9jVCMX6NIGziLb8fThFaZzoyC/qp5BuoceZlbyrggOnw==
|
||||
"@standardnotes/snjs@2.6.0":
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.6.0.tgz#8ebdfcb0918c308198b38a63d7aa946387b83ac4"
|
||||
integrity sha512-Gb/kAdMtjVlSiQH7pkDzFxKtIrrY43i2hSejO2c+zCviZspiDZPpXLpEhMJ295ow2tluhOf8zfBUda3LMC6oDw==
|
||||
dependencies:
|
||||
"@standardnotes/auth" "^2.0.0"
|
||||
"@standardnotes/sncrypto-common" "^1.2.9"
|
||||
@@ -8491,10 +8491,10 @@ typescript-eslint@0.0.1-alpha.0:
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-0.0.1-alpha.0.tgz#285d68a4e96588295cd436278801bcb6a6b916c1"
|
||||
integrity sha512-1hNKM37dAWML/2ltRXupOq2uqcdRQyDFphl+341NTPXFLLLiDhErXx8VtaSLh3xP7SyHZdcCgpt9boYYVb3fQg==
|
||||
|
||||
typescript@^4.1.5:
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.5.tgz#123a3b214aaff3be32926f0d8f1f6e704eb89a72"
|
||||
integrity sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA==
|
||||
typescript@4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3"
|
||||
integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==
|
||||
|
||||
uglify-js@3.4.x:
|
||||
version "3.4.10"
|
||||
|
||||
Reference in New Issue
Block a user