refactor: move tags to react (#753)
* refactor: move Tags list to react * refactor: extract TagsListItem and simplify hooks * refactor: remove comment & dead code * fix: mobx warnings & safari bug * fix: text select on non-safari * fix: remove unecessary comments * style: apply prettier format * style: apply formatting on tags_view * refactor: remove the angular tags rendering * feat: add back the "select previous tag" behavior * style: simplify code and avoid important * style: remove note on state
This commit is contained in:
@@ -80,6 +80,7 @@ import { NotesListOptionsDirective } from './components/NotesListOptionsMenu';
|
|||||||
import { PurchaseFlowDirective } from './purchaseFlow';
|
import { PurchaseFlowDirective } from './purchaseFlow';
|
||||||
import { QuickSettingsMenuDirective } from './components/QuickSettingsMenu';
|
import { QuickSettingsMenuDirective } from './components/QuickSettingsMenu';
|
||||||
import { ComponentViewDirective } from '@/components/ComponentView';
|
import { ComponentViewDirective } from '@/components/ComponentView';
|
||||||
|
import { TagsListDirective } from '@/components/TagsList';
|
||||||
|
|
||||||
function reloadHiddenFirefoxTab(): boolean {
|
function reloadHiddenFirefoxTab(): boolean {
|
||||||
/**
|
/**
|
||||||
@@ -181,6 +182,7 @@ const startApplication: StartApplication = async function startApplication(
|
|||||||
.directive('notesListOptionsMenu', NotesListOptionsDirective)
|
.directive('notesListOptionsMenu', NotesListOptionsDirective)
|
||||||
.directive('icon', IconDirective)
|
.directive('icon', IconDirective)
|
||||||
.directive('noteTagsContainer', NoteTagsContainerDirective)
|
.directive('noteTagsContainer', NoteTagsContainerDirective)
|
||||||
|
.directive('tags', TagsListDirective)
|
||||||
.directive('preferences', PreferencesDirective)
|
.directive('preferences', PreferencesDirective)
|
||||||
.directive('purchaseFlow', PurchaseFlowDirective);
|
.directive('purchaseFlow', PurchaseFlowDirective);
|
||||||
|
|
||||||
|
|||||||
136
app/assets/javascripts/components/TagsList.tsx
Normal file
136
app/assets/javascripts/components/TagsList.tsx
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import { confirmDialog } from '@/services/alertService';
|
||||||
|
import { STRING_DELETE_TAG } from '@/strings';
|
||||||
|
import { WebApplication } from '@/ui_models/application';
|
||||||
|
import { AppState } from '@/ui_models/app_state';
|
||||||
|
import { SNTag, TagMutator } from '@standardnotes/snjs';
|
||||||
|
import { runInAction } from 'mobx';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { FunctionComponent } from 'preact';
|
||||||
|
import { useCallback } from 'preact/hooks';
|
||||||
|
import { TagsListItem } from './TagsListItem';
|
||||||
|
import { toDirective } from './utils';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
application: WebApplication;
|
||||||
|
appState: AppState;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tagsWithOptionalTemplate = (
|
||||||
|
template: SNTag | undefined,
|
||||||
|
tags: SNTag[]
|
||||||
|
): SNTag[] => {
|
||||||
|
if (!template) {
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
return [template, ...tags];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TagsList: FunctionComponent<Props> = observer(
|
||||||
|
({ application, appState }) => {
|
||||||
|
const templateTag = appState.templateTag;
|
||||||
|
const tags = appState.tags.tags;
|
||||||
|
const allTags = tagsWithOptionalTemplate(templateTag, tags);
|
||||||
|
|
||||||
|
const selectTag = useCallback(
|
||||||
|
(tag: SNTag) => {
|
||||||
|
appState.setSelectedTag(tag);
|
||||||
|
},
|
||||||
|
[appState]
|
||||||
|
);
|
||||||
|
|
||||||
|
const saveTag = useCallback(
|
||||||
|
async (tag: SNTag, newTitle: string) => {
|
||||||
|
const templateTag = appState.templateTag;
|
||||||
|
|
||||||
|
const hasEmptyTitle = newTitle.length === 0;
|
||||||
|
const hasNotChangedTitle = newTitle === tag.title;
|
||||||
|
const isTemplateChange = templateTag && tag.uuid === templateTag.uuid;
|
||||||
|
const hasDuplicatedTitle = !!application.findTagByTitle(newTitle);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
appState.templateTag = undefined;
|
||||||
|
appState.editingTag = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasEmptyTitle || hasNotChangedTitle) {
|
||||||
|
if (isTemplateChange) {
|
||||||
|
appState.undoCreateNewTag();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasDuplicatedTitle) {
|
||||||
|
if (isTemplateChange) {
|
||||||
|
appState.undoCreateNewTag();
|
||||||
|
}
|
||||||
|
application.alertService?.alert(
|
||||||
|
'A tag with this name already exists.'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTemplateChange) {
|
||||||
|
const insertedTag = await application.insertItem(templateTag);
|
||||||
|
const changedTag = await application.changeItem<TagMutator>(
|
||||||
|
insertedTag.uuid,
|
||||||
|
(m) => {
|
||||||
|
m.title = newTitle;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
selectTag(changedTag as SNTag);
|
||||||
|
await application.saveItem(insertedTag.uuid);
|
||||||
|
} else {
|
||||||
|
await application.changeAndSaveItem<TagMutator>(
|
||||||
|
tag.uuid,
|
||||||
|
(mutator) => {
|
||||||
|
mutator.title = newTitle;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[appState, application, selectTag]
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeTag = useCallback(
|
||||||
|
async (tag: SNTag) => {
|
||||||
|
if (
|
||||||
|
await confirmDialog({
|
||||||
|
text: STRING_DELETE_TAG,
|
||||||
|
confirmButtonStyle: 'danger',
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
appState.removeTag(tag);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[appState]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{allTags.length === 0 ? (
|
||||||
|
<div className="no-tags-placeholder">
|
||||||
|
No tags. Create one using the add button above.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{allTags.map((tag) => {
|
||||||
|
return (
|
||||||
|
<TagsListItem
|
||||||
|
key={tag.uuid}
|
||||||
|
tag={tag}
|
||||||
|
selectTag={selectTag}
|
||||||
|
saveTag={saveTag}
|
||||||
|
removeTag={removeTag}
|
||||||
|
appState={appState}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const TagsListDirective = toDirective<Props>(TagsList);
|
||||||
134
app/assets/javascripts/components/TagsListItem.tsx
Normal file
134
app/assets/javascripts/components/TagsListItem.tsx
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import { SNTag } from '@standardnotes/snjs';
|
||||||
|
import { runInAction } from 'mobx';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { FunctionComponent, JSX } from 'preact';
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
tag: SNTag;
|
||||||
|
selectTag: (tag: SNTag) => void;
|
||||||
|
removeTag: (tag: SNTag) => void;
|
||||||
|
saveTag: (tag: SNTag, newTitle: string) => void;
|
||||||
|
appState: TagsListState;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TagsListState = {
|
||||||
|
readonly selectedTag: SNTag | undefined;
|
||||||
|
editingTag: SNTag | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TagsListItem: FunctionComponent<Props> = observer(
|
||||||
|
({ tag, selectTag, saveTag, removeTag, appState }) => {
|
||||||
|
const [title, setTitle] = useState(tag.title || '');
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const isSelected = appState.selectedTag === tag;
|
||||||
|
const isEditing = appState.editingTag === tag;
|
||||||
|
const noteCounts = tag.noteCount;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTitle(tag.title || '');
|
||||||
|
}, [setTitle, tag]);
|
||||||
|
|
||||||
|
const selectCurrentTag = useCallback(() => {
|
||||||
|
if (isEditing || isSelected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectTag(tag);
|
||||||
|
}, [isSelected, isEditing, selectTag, tag]);
|
||||||
|
|
||||||
|
const onBlur = useCallback(() => {
|
||||||
|
saveTag(tag, title);
|
||||||
|
}, [tag, saveTag, title]);
|
||||||
|
|
||||||
|
const onInput = useCallback(
|
||||||
|
(e: JSX.TargetedEvent<HTMLInputElement>) => {
|
||||||
|
const value = (e.target as HTMLInputElement).value;
|
||||||
|
setTitle(value);
|
||||||
|
},
|
||||||
|
[setTitle]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onKeyUp = useCallback(
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
if (e.code === 'Enter') {
|
||||||
|
inputRef.current?.blur();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[inputRef]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEditing) {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, [inputRef, isEditing]);
|
||||||
|
|
||||||
|
const onClickRename = useCallback(() => {
|
||||||
|
runInAction(() => {
|
||||||
|
appState.editingTag = tag;
|
||||||
|
});
|
||||||
|
}, [appState, tag]);
|
||||||
|
|
||||||
|
const onClickSave = useCallback(() => {
|
||||||
|
inputRef.current?.blur();
|
||||||
|
}, [inputRef]);
|
||||||
|
|
||||||
|
const onClickDelete = useCallback(() => {
|
||||||
|
removeTag(tag);
|
||||||
|
}, [removeTag, tag]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`tag ${isSelected ? 'selected' : ''}`}
|
||||||
|
onClick={selectCurrentTag}
|
||||||
|
>
|
||||||
|
{!tag.errorDecrypting ? (
|
||||||
|
<div className="tag-info">
|
||||||
|
<div className="tag-icon">#</div>
|
||||||
|
<input
|
||||||
|
className={`title ${isEditing ? 'editing' : ''}`}
|
||||||
|
id={`react-tag-${tag.uuid}`}
|
||||||
|
onBlur={onBlur}
|
||||||
|
onInput={onInput}
|
||||||
|
value={title}
|
||||||
|
onKeyUp={onKeyUp}
|
||||||
|
spellCheck={false}
|
||||||
|
ref={inputRef}
|
||||||
|
/>
|
||||||
|
<div className="count">{noteCounts}</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{tag.conflictOf && (
|
||||||
|
<div className="danger small-text font-bold">
|
||||||
|
Conflicted Copy {tag.conflictOf}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{tag.errorDecrypting && !tag.waitingForKey && (
|
||||||
|
<div className="danger small-text font-bold">Missing Keys</div>
|
||||||
|
)}
|
||||||
|
{tag.errorDecrypting && tag.waitingForKey && (
|
||||||
|
<div className="info small-text font-bold">Waiting For Keys</div>
|
||||||
|
)}
|
||||||
|
{isSelected && (
|
||||||
|
<div className="menu">
|
||||||
|
{!isEditing && (
|
||||||
|
<a className="item" onClick={onClickRename}>
|
||||||
|
Rename
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{isEditing && (
|
||||||
|
<a className="item" onClick={onClickSave}>
|
||||||
|
Save
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
<a className="item" onClick={onClickDelete}>
|
||||||
|
Delete
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -1,30 +1,36 @@
|
|||||||
import { isDesktopApplication } from '@/utils';
|
|
||||||
import pull from 'lodash/pull';
|
|
||||||
import {
|
|
||||||
ApplicationEvent,
|
|
||||||
SNTag,
|
|
||||||
SNNote,
|
|
||||||
ContentType,
|
|
||||||
PayloadSource,
|
|
||||||
DeinitSource,
|
|
||||||
PrefKey,
|
|
||||||
} from '@standardnotes/snjs';
|
|
||||||
import { WebApplication } from '@/ui_models/application';
|
|
||||||
import { Editor } from '@/ui_models/editor';
|
|
||||||
import { action, makeObservable, observable } from 'mobx';
|
|
||||||
import { Bridge } from '@/services/bridge';
|
import { Bridge } from '@/services/bridge';
|
||||||
import { storage, StorageKey } from '@/services/localStorage';
|
import { storage, StorageKey } from '@/services/localStorage';
|
||||||
|
import { WebApplication } from '@/ui_models/application';
|
||||||
|
import { AccountMenuState } from '@/ui_models/app_state/account_menu_state';
|
||||||
|
import { Editor } from '@/ui_models/editor';
|
||||||
|
import { isDesktopApplication } from '@/utils';
|
||||||
|
import {
|
||||||
|
ApplicationEvent,
|
||||||
|
ContentType,
|
||||||
|
DeinitSource,
|
||||||
|
PayloadSource,
|
||||||
|
PrefKey,
|
||||||
|
SNNote,
|
||||||
|
SNTag,
|
||||||
|
} from '@standardnotes/snjs';
|
||||||
|
import pull from 'lodash/pull';
|
||||||
|
import {
|
||||||
|
action,
|
||||||
|
computed,
|
||||||
|
makeObservable,
|
||||||
|
observable,
|
||||||
|
runInAction,
|
||||||
|
} from 'mobx';
|
||||||
import { ActionsMenuState } from './actions_menu_state';
|
import { ActionsMenuState } from './actions_menu_state';
|
||||||
|
import { NotesState } from './notes_state';
|
||||||
import { NoteTagsState } from './note_tags_state';
|
import { NoteTagsState } from './note_tags_state';
|
||||||
import { NoAccountWarningState } from './no_account_warning_state';
|
import { NoAccountWarningState } from './no_account_warning_state';
|
||||||
import { SyncState } from './sync_state';
|
|
||||||
import { SearchOptionsState } from './search_options_state';
|
|
||||||
import { NotesState } from './notes_state';
|
|
||||||
import { TagsState } from './tags_state';
|
|
||||||
import { AccountMenuState } from '@/ui_models/app_state/account_menu_state';
|
|
||||||
import { PreferencesState } from './preferences_state';
|
import { PreferencesState } from './preferences_state';
|
||||||
import { PurchaseFlowState } from './purchase_flow_state';
|
import { PurchaseFlowState } from './purchase_flow_state';
|
||||||
import { QuickSettingsState } from './quick_settings_state';
|
import { QuickSettingsState } from './quick_settings_state';
|
||||||
|
import { SearchOptionsState } from './search_options_state';
|
||||||
|
import { SyncState } from './sync_state';
|
||||||
|
import { TagsState } from './tags_state';
|
||||||
|
|
||||||
export enum AppStateEvent {
|
export enum AppStateEvent {
|
||||||
TagChanged,
|
TagChanged,
|
||||||
@@ -34,7 +40,7 @@ export enum AppStateEvent {
|
|||||||
BeganBackupDownload,
|
BeganBackupDownload,
|
||||||
EndedBackupDownload,
|
EndedBackupDownload,
|
||||||
WindowDidFocus,
|
WindowDidFocus,
|
||||||
WindowDidBlur
|
WindowDidBlur,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PanelResizedData = {
|
export type PanelResizedData = {
|
||||||
@@ -62,8 +68,13 @@ export class AppState {
|
|||||||
rootScopeCleanup1: any;
|
rootScopeCleanup1: any;
|
||||||
rootScopeCleanup2: any;
|
rootScopeCleanup2: any;
|
||||||
onVisibilityChange: any;
|
onVisibilityChange: any;
|
||||||
selectedTag?: SNTag;
|
|
||||||
showBetaWarning: boolean;
|
showBetaWarning: boolean;
|
||||||
|
|
||||||
|
selectedTag: SNTag | undefined;
|
||||||
|
previouslySelectedTag: SNTag | undefined;
|
||||||
|
editingTag: SNTag | undefined;
|
||||||
|
_templateTag: SNTag | undefined;
|
||||||
|
|
||||||
readonly quickSettingsMenu = new QuickSettingsState();
|
readonly quickSettingsMenu = new QuickSettingsState();
|
||||||
readonly accountMenu: AccountMenuState;
|
readonly accountMenu: AccountMenuState;
|
||||||
readonly actionsMenu = new ActionsMenuState();
|
readonly actionsMenu = new ActionsMenuState();
|
||||||
@@ -133,11 +144,25 @@ export class AppState {
|
|||||||
this.showBetaWarning = false;
|
this.showBetaWarning = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.selectedTag = undefined;
|
||||||
|
this.previouslySelectedTag = undefined;
|
||||||
|
this.editingTag = undefined;
|
||||||
|
this._templateTag = undefined;
|
||||||
|
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
showBetaWarning: observable,
|
showBetaWarning: observable,
|
||||||
isSessionsModalVisible: observable,
|
isSessionsModalVisible: observable,
|
||||||
preferences: observable,
|
preferences: observable,
|
||||||
|
|
||||||
|
selectedTag: observable,
|
||||||
|
previouslySelectedTag: observable,
|
||||||
|
_templateTag: observable,
|
||||||
|
templateTag: computed,
|
||||||
|
createNewTag: action,
|
||||||
|
editingTag: observable,
|
||||||
|
setSelectedTag: action,
|
||||||
|
removeTag: action,
|
||||||
|
|
||||||
enableBetaWarning: action,
|
enableBetaWarning: action,
|
||||||
disableBetaWarning: action,
|
disableBetaWarning: action,
|
||||||
openSessionsModal: action,
|
openSessionsModal: action,
|
||||||
@@ -269,10 +294,13 @@ export class AppState {
|
|||||||
}
|
}
|
||||||
if (this.selectedTag) {
|
if (this.selectedTag) {
|
||||||
const matchingTag = items.find(
|
const matchingTag = items.find(
|
||||||
(candidate) => candidate.uuid === this.selectedTag!.uuid
|
(candidate) =>
|
||||||
|
this.selectedTag && candidate.uuid === this.selectedTag.uuid
|
||||||
);
|
);
|
||||||
if (matchingTag) {
|
if (matchingTag) {
|
||||||
this.selectedTag = matchingTag as SNTag;
|
runInAction(() => {
|
||||||
|
this.selectedTag = matchingTag as SNTag;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -343,17 +371,69 @@ export class AppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setSelectedTag(tag: SNTag) {
|
setSelectedTag(tag: SNTag) {
|
||||||
|
if (tag.conflictOf) {
|
||||||
|
this.application.changeAndSaveItem(tag.uuid, (mutator) => {
|
||||||
|
mutator.conflictOf = undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (this.selectedTag === tag) {
|
if (this.selectedTag === tag) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const previousTag = this.selectedTag;
|
|
||||||
|
this.previouslySelectedTag = this.selectedTag;
|
||||||
this.selectedTag = tag;
|
this.selectedTag = tag;
|
||||||
|
|
||||||
|
if (this.templateTag?.uuid === tag.uuid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.notifyEvent(AppStateEvent.TagChanged, {
|
this.notifyEvent(AppStateEvent.TagChanged, {
|
||||||
tag: tag,
|
tag: tag,
|
||||||
previousTag: previousTag,
|
previousTag: this.previouslySelectedTag,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getSelectedTag() {
|
||||||
|
return this.selectedTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get templateTag(): SNTag | undefined {
|
||||||
|
return this._templateTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set templateTag(tag: SNTag | undefined) {
|
||||||
|
const previous = this._templateTag;
|
||||||
|
this._templateTag = tag;
|
||||||
|
|
||||||
|
if (tag) {
|
||||||
|
this.setSelectedTag(tag);
|
||||||
|
this.editingTag = tag;
|
||||||
|
} else if (previous) {
|
||||||
|
this.selectedTag =
|
||||||
|
previous === this.selectedTag ? undefined : this.selectedTag;
|
||||||
|
this.editingTag =
|
||||||
|
previous === this.editingTag ? undefined : this.editingTag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeTag(tag: SNTag) {
|
||||||
|
this.application.deleteItem(tag);
|
||||||
|
this.setSelectedTag(this.tags.smartTags[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createNewTag() {
|
||||||
|
const newTag = (await this.application.createTemplateItem(
|
||||||
|
ContentType.Tag
|
||||||
|
)) as SNTag;
|
||||||
|
this.templateTag = newTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async undoCreateNewTag() {
|
||||||
|
const previousTag = this.previouslySelectedTag || this.tags.smartTags[0];
|
||||||
|
this.setSelectedTag(previousTag);
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns the tags that are referncing this note */
|
/** Returns the tags that are referncing this note */
|
||||||
public getNoteTags(note: SNNote) {
|
public getNoteTags(note: SNNote) {
|
||||||
return this.application.referencingForItem(note).filter((ref) => {
|
return this.application.referencingForItem(note).filter((ref) => {
|
||||||
@@ -361,10 +441,6 @@ export class AppState {
|
|||||||
}) as SNTag[];
|
}) as SNTag[];
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSelectedTag() {
|
|
||||||
return this.selectedTag;
|
|
||||||
}
|
|
||||||
|
|
||||||
panelDidResize(name: string, collapsed: boolean) {
|
panelDidResize(name: string, collapsed: boolean) {
|
||||||
const data: PanelResizedData = {
|
const data: PanelResizedData = {
|
||||||
panel: name,
|
panel: name,
|
||||||
|
|||||||
@@ -33,35 +33,10 @@
|
|||||||
.section-title-bar-header
|
.section-title-bar-header
|
||||||
.sk-h3.title
|
.sk-h3.title
|
||||||
span.sk-bold Tags
|
span.sk-bold Tags
|
||||||
.tag(
|
tags(
|
||||||
ng-class="{'selected' : self.state.selectedTag == tag}",
|
application='self.application',
|
||||||
ng-click='self.selectTag(tag)',
|
app-state='self.appState'
|
||||||
ng-repeat='tag in self.state.tags track by tag.uuid'
|
)
|
||||||
)
|
|
||||||
.tag-info(ng-if="!tag.errorDecrypting")
|
|
||||||
.tag-icon #
|
|
||||||
input.title(
|
|
||||||
ng-attr-id='tag-{{tag.uuid}}',
|
|
||||||
ng-blur='self.saveTag($event, tag)'
|
|
||||||
ng-change='self.onTagTitleChange(tag)',
|
|
||||||
ng-model='self.titles[tag.uuid]',
|
|
||||||
ng-class="{'editing' : self.state.editingTag == tag}",
|
|
||||||
ng-click='self.selectTag(tag)',
|
|
||||||
ng-keyup='$event.keyCode == 13 && $event.target.blur()',
|
|
||||||
should-focus='self.state.templateTag || self.state.editingTag == tag',
|
|
||||||
sn-autofocus='true',
|
|
||||||
spellcheck='false'
|
|
||||||
)
|
|
||||||
.count {{self.state.noteCounts[tag.uuid]}}
|
|
||||||
.danger.small-text.font-bold(ng-show='tag.conflictOf') Conflicted Copy
|
|
||||||
.danger.small-text.font-bold(ng-show='tag.errorDecrypting && !tag.waitingForKey') Missing Keys
|
|
||||||
.info.small-text.font-bold(ng-show='tag.errorDecrypting && tag.waitingForKey') Waiting For Keys
|
|
||||||
.menu(ng-show='self.state.selectedTag == tag')
|
|
||||||
a.item(ng-click='self.selectedRenameTag(tag)' ng-show='!self.state.editingTag') Rename
|
|
||||||
a.item(ng-click='self.saveTag($event, tag)' ng-show='self.state.editingTag') Save
|
|
||||||
a.item(ng-click='self.selectedDeleteTag(tag)') Delete
|
|
||||||
.no-tags-placeholder(ng-show='self.state.tags.length == 0')
|
|
||||||
| No tags. Create one using the add button above.
|
|
||||||
panel-resizer(
|
panel-resizer(
|
||||||
collapsable='true',
|
collapsable='true',
|
||||||
control='self.panelPuppet',
|
control='self.panelPuppet',
|
||||||
|
|||||||
@@ -1,62 +1,46 @@
|
|||||||
import { PayloadContent } from '@standardnotes/snjs';
|
import { PanelPuppet, WebDirective } from '@/types';
|
||||||
import { WebDirective, PanelPuppet } from '@/types';
|
|
||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
import {
|
|
||||||
SNTag,
|
|
||||||
ContentType,
|
|
||||||
ApplicationEvent,
|
|
||||||
ComponentAction,
|
|
||||||
SNSmartTag,
|
|
||||||
ComponentArea,
|
|
||||||
SNComponent,
|
|
||||||
PrefKey,
|
|
||||||
UuidString,
|
|
||||||
TagMutator
|
|
||||||
} from '@standardnotes/snjs';
|
|
||||||
import template from './tags-view.pug';
|
|
||||||
import { AppStateEvent } from '@/ui_models/app_state';
|
import { AppStateEvent } from '@/ui_models/app_state';
|
||||||
import { PANEL_NAME_TAGS } from '@/views/constants';
|
import { PANEL_NAME_TAGS } from '@/views/constants';
|
||||||
import { STRING_DELETE_TAG } from '@/strings';
|
import {
|
||||||
|
ApplicationEvent,
|
||||||
|
ComponentAction,
|
||||||
|
ComponentArea,
|
||||||
|
ContentType,
|
||||||
|
PrefKey,
|
||||||
|
SNComponent,
|
||||||
|
SNSmartTag,
|
||||||
|
SNTag,
|
||||||
|
UuidString,
|
||||||
|
} from '@standardnotes/snjs';
|
||||||
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
||||||
import { confirmDialog } from '@/services/alertService';
|
import template from './tags-view.pug';
|
||||||
|
|
||||||
type NoteCounts = Partial<Record<string, number>>
|
type NoteCounts = Partial<Record<string, number>>;
|
||||||
|
|
||||||
type TagState = {
|
type TagState = {
|
||||||
tags: SNTag[]
|
smartTags: SNSmartTag[];
|
||||||
smartTags: SNSmartTag[]
|
noteCounts: NoteCounts;
|
||||||
noteCounts: NoteCounts
|
selectedTag?: SNTag;
|
||||||
selectedTag?: SNTag
|
};
|
||||||
/** If creating a new tag, the previously selected tag will be set here, so that if new
|
|
||||||
* tag creation is canceled, the previous tag is re-selected */
|
|
||||||
previousTag?: SNTag
|
|
||||||
/** If a tag is in edit state, it will be set as the editingTag */
|
|
||||||
editingTag?: SNTag
|
|
||||||
/** If a tag is new and not yet saved, it will be set as the template tag */
|
|
||||||
templateTag?: SNTag
|
|
||||||
}
|
|
||||||
|
|
||||||
class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
|
class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
|
||||||
|
|
||||||
/** Passed through template */
|
/** Passed through template */
|
||||||
readonly application!: WebApplication
|
readonly application!: WebApplication;
|
||||||
private readonly panelPuppet: PanelPuppet
|
private readonly panelPuppet: PanelPuppet;
|
||||||
private unregisterComponent?: any
|
private unregisterComponent?: any;
|
||||||
component?: SNComponent
|
component?: SNComponent;
|
||||||
/** The original name of the edtingTag before it began editing */
|
/** The original name of the edtingTag before it began editing */
|
||||||
private editingOriginalName?: string
|
formData: { tagTitle?: string } = {};
|
||||||
formData: { tagTitle?: string } = {}
|
titles: Partial<Record<UuidString, string>> = {};
|
||||||
titles: Partial<Record<UuidString, string>> = {}
|
private removeTagsObserver!: () => void;
|
||||||
private removeTagsObserver!: () => void
|
private removeFoldersObserver!: () => void;
|
||||||
private removeFoldersObserver!: () => void
|
|
||||||
|
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor(
|
constructor($timeout: ng.ITimeoutService) {
|
||||||
$timeout: ng.ITimeoutService,
|
|
||||||
) {
|
|
||||||
super($timeout);
|
super($timeout);
|
||||||
this.panelPuppet = {
|
this.panelPuppet = {
|
||||||
onReady: () => this.loadPreferences()
|
onReady: () => this.loadPreferences(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,16 +53,15 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
|
|||||||
super.deinit();
|
super.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
getInitialState() {
|
getInitialState(): TagState {
|
||||||
return {
|
return {
|
||||||
tags: [],
|
|
||||||
smartTags: [],
|
smartTags: [],
|
||||||
noteCounts: {},
|
noteCounts: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getState() {
|
getState(): TagState {
|
||||||
return this.state as TagState;
|
return this.state;
|
||||||
}
|
}
|
||||||
|
|
||||||
async onAppStart() {
|
async onAppStart() {
|
||||||
@@ -90,10 +73,9 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
|
|||||||
super.onAppLaunch();
|
super.onAppLaunch();
|
||||||
this.loadPreferences();
|
this.loadPreferences();
|
||||||
this.beginStreamingItems();
|
this.beginStreamingItems();
|
||||||
|
|
||||||
const smartTags = this.application.getSmartTags();
|
const smartTags = this.application.getSmartTags();
|
||||||
this.setState({
|
this.setState({ smartTags });
|
||||||
smartTags: smartTags,
|
|
||||||
});
|
|
||||||
this.selectTag(smartTags[0]);
|
this.selectTag(smartTags[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,28 +96,31 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
|
|||||||
this.removeTagsObserver = this.application.streamItems(
|
this.removeTagsObserver = this.application.streamItems(
|
||||||
[ContentType.Tag, ContentType.SmartTag],
|
[ContentType.Tag, ContentType.SmartTag],
|
||||||
async (items) => {
|
async (items) => {
|
||||||
|
const tags = items as Array<SNTag | SNSmartTag>;
|
||||||
|
|
||||||
await this.setState({
|
await this.setState({
|
||||||
tags: this.application.getDisplayableItems(ContentType.Tag) as SNTag[],
|
|
||||||
smartTags: this.application.getSmartTags(),
|
smartTags: this.application.getSmartTags(),
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const tag of items as Array<SNTag | SNSmartTag>) {
|
for (const tag of tags) {
|
||||||
this.titles[tag.uuid] = tag.title;
|
this.titles[tag.uuid] = tag.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.reloadNoteCounts();
|
this.reloadNoteCounts();
|
||||||
const selectedTag = this.state.selectedTag;
|
const selectedTag = this.state.selectedTag;
|
||||||
|
|
||||||
if (selectedTag) {
|
if (selectedTag) {
|
||||||
/** If the selected tag has been deleted, revert to All view. */
|
/** If the selected tag has been deleted, revert to All view. */
|
||||||
const matchingTag = items.find((tag) => {
|
const matchingTag = tags.find((tag) => {
|
||||||
return tag.uuid === selectedTag.uuid;
|
return tag.uuid === selectedTag.uuid;
|
||||||
}) as SNTag;
|
});
|
||||||
|
|
||||||
if (matchingTag) {
|
if (matchingTag) {
|
||||||
if (matchingTag.deleted) {
|
if (matchingTag.deleted) {
|
||||||
this.selectTag(this.getState().smartTags[0]);
|
this.selectTag(this.getState().smartTags[0]);
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedTag: matchingTag
|
selectedTag: matchingTag,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,7 +133,7 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
|
|||||||
onAppStateEvent(eventName: AppStateEvent) {
|
onAppStateEvent(eventName: AppStateEvent) {
|
||||||
if (eventName === AppStateEvent.TagChanged) {
|
if (eventName === AppStateEvent.TagChanged) {
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedTag: this.application.getAppState().getSelectedTag()
|
selectedTag: this.application.getAppState().getSelectedTag(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,37 +152,23 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reloadNoteCounts() {
|
reloadNoteCounts() {
|
||||||
let allTags: Array<SNTag | SNSmartTag> = [];
|
const smartTags = this.state.smartTags;
|
||||||
if (this.getState().tags) {
|
|
||||||
allTags = allTags.concat(this.getState().tags);
|
|
||||||
}
|
|
||||||
if (this.getState().smartTags) {
|
|
||||||
allTags = allTags.concat(this.getState().smartTags);
|
|
||||||
}
|
|
||||||
const noteCounts: NoteCounts = {};
|
const noteCounts: NoteCounts = {};
|
||||||
for (const tag of allTags) {
|
|
||||||
if (tag === this.state.templateTag) {
|
for (const tag of smartTags) {
|
||||||
continue;
|
/** Other smart tags do not contain counts */
|
||||||
}
|
if (tag.isAllTag) {
|
||||||
if (tag.isSmartTag) {
|
const notes = this.application
|
||||||
/** Other smart tags do not contain counts */
|
.notesMatchingSmartTag(tag as SNSmartTag)
|
||||||
if (tag.isAllTag) {
|
|
||||||
const notes = this.application.notesMatchingSmartTag(tag as SNSmartTag)
|
|
||||||
.filter((note) => {
|
|
||||||
return !note.archived && !note.trashed;
|
|
||||||
});
|
|
||||||
noteCounts[tag.uuid] = notes.length;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const notes = this.application.referencesForItem(tag, ContentType.Note)
|
|
||||||
.filter((note) => {
|
.filter((note) => {
|
||||||
return !note.archived && !note.trashed;
|
return !note.archived && !note.trashed;
|
||||||
});
|
});
|
||||||
noteCounts[tag.uuid] = notes.length;
|
noteCounts[tag.uuid] = notes.length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
noteCounts: noteCounts
|
noteCounts: noteCounts,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,14 +176,14 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
|
|||||||
if (!this.panelPuppet.ready) {
|
if (!this.panelPuppet.ready) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const width = this.application.getPreference(PrefKey.TagsPanelWidth);
|
const width = this.application.getPreference(PrefKey.TagsPanelWidth);
|
||||||
if (width) {
|
if (width) {
|
||||||
this.panelPuppet.setWidth!(width);
|
this.panelPuppet.setWidth!(width);
|
||||||
if (this.panelPuppet.isCollapsed!()) {
|
if (this.panelPuppet.isCollapsed!()) {
|
||||||
this.application.getAppState().panelDidResize(
|
this.application
|
||||||
PANEL_NAME_TAGS,
|
.getAppState()
|
||||||
this.panelPuppet.isCollapsed!()
|
.panelDidResize(PANEL_NAME_TAGS, this.panelPuppet.isCollapsed!());
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,36 +194,39 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
|
|||||||
_isAtMaxWidth: boolean,
|
_isAtMaxWidth: boolean,
|
||||||
isCollapsed: boolean
|
isCollapsed: boolean
|
||||||
) => {
|
) => {
|
||||||
this.application.setPreference(
|
this.application
|
||||||
PrefKey.TagsPanelWidth,
|
.setPreference(PrefKey.TagsPanelWidth, newWidth)
|
||||||
newWidth
|
.then(() => this.application.sync());
|
||||||
).then(() => this.application.sync());
|
this.application.getAppState().panelDidResize(PANEL_NAME_TAGS, isCollapsed);
|
||||||
this.application.getAppState().panelDidResize(
|
};
|
||||||
PANEL_NAME_TAGS,
|
|
||||||
isCollapsed
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
registerComponentHandler() {
|
registerComponentHandler() {
|
||||||
this.unregisterComponent = this.application.componentManager!.registerHandler({
|
this.unregisterComponent =
|
||||||
identifier: 'tags',
|
this.application.componentManager!.registerHandler({
|
||||||
areas: [ComponentArea.TagsList],
|
identifier: 'tags',
|
||||||
actionHandler: (_, action, data) => {
|
areas: [ComponentArea.TagsList],
|
||||||
if (action === ComponentAction.SelectItem) {
|
actionHandler: (_, action, data) => {
|
||||||
if (data.item!.content_type === ContentType.Tag) {
|
if (action === ComponentAction.SelectItem) {
|
||||||
const tag = this.application.findItem(data.item!.uuid);
|
const item = data.item;
|
||||||
if (tag) {
|
|
||||||
this.selectTag(tag as SNTag);
|
if (!item) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} else if (data.item!.content_type === ContentType.SmartTag) {
|
|
||||||
const matchingTag = this.getState().smartTags.find(t => t.uuid === data.item!.uuid);
|
if (item.content_type === ContentType.SmartTag) {
|
||||||
this.selectTag(matchingTag as SNSmartTag);
|
const matchingTag = this.getState().smartTags.find(
|
||||||
|
(t) => t.uuid === item.uuid
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchingTag) {
|
||||||
|
this.selectTag(matchingTag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (action === ComponentAction.ClearSelection) {
|
||||||
|
this.selectTag(this.getState().smartTags[0]);
|
||||||
}
|
}
|
||||||
} else if (action === ComponentAction.ClearSelection) {
|
},
|
||||||
this.selectTag(this.getState().smartTags[0]);
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async selectTag(tag: SNTag) {
|
async selectTag(tag: SNTag) {
|
||||||
@@ -265,123 +239,11 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async clickedAddNewTag() {
|
async clickedAddNewTag() {
|
||||||
if (this.getState().editingTag) {
|
if (this.appState.templateTag) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newTag = await this.application.createTemplateItem(
|
|
||||||
ContentType.Tag
|
|
||||||
) as SNTag;
|
|
||||||
this.setState({
|
|
||||||
tags: [newTag].concat(this.getState().tags),
|
|
||||||
previousTag: this.getState().selectedTag,
|
|
||||||
selectedTag: newTag,
|
|
||||||
editingTag: newTag,
|
|
||||||
templateTag: newTag
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onTagTitleChange(tag: SNTag | SNSmartTag) {
|
this.appState.createNewTag();
|
||||||
this.setState({
|
|
||||||
editingTag: tag
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveTag($event: Event, tag: SNTag) {
|
|
||||||
($event.target! as HTMLInputElement).blur();
|
|
||||||
if (this.getState().templateTag) {
|
|
||||||
if (!this.titles[tag.uuid]?.length) {
|
|
||||||
return this.undoCreateTag(tag);
|
|
||||||
}
|
|
||||||
return this.saveNewTag();
|
|
||||||
} else {
|
|
||||||
return this.saveTagRename(tag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async undoCreateTag(tag: SNTag) {
|
|
||||||
await this.setState({
|
|
||||||
templateTag: undefined,
|
|
||||||
editingTag: undefined,
|
|
||||||
selectedTag: this.appState.selectedTag,
|
|
||||||
tags: this.state.tags.filter(existingTag => existingTag !== tag)
|
|
||||||
});
|
|
||||||
delete this.titles[tag.uuid];
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveTagRename(tag: SNTag) {
|
|
||||||
const newTitle = this.titles[tag.uuid] || '';
|
|
||||||
if (newTitle.length === 0) {
|
|
||||||
this.titles[tag.uuid] = this.editingOriginalName;
|
|
||||||
this.editingOriginalName = undefined;
|
|
||||||
await this.setState({
|
|
||||||
editingTag: undefined
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const existingTag = this.application.findTagByTitle(newTitle);
|
|
||||||
if (existingTag && existingTag.uuid !== tag.uuid) {
|
|
||||||
this.application.alertService!.alert(
|
|
||||||
"A tag with this name already exists."
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.application.changeAndSaveItem<TagMutator>(tag.uuid, (mutator) => {
|
|
||||||
mutator.title = newTitle;
|
|
||||||
});
|
|
||||||
await this.setState({
|
|
||||||
editingTag: undefined
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveNewTag() {
|
|
||||||
const newTag = this.getState().templateTag!;
|
|
||||||
const newTitle = this.titles[newTag.uuid] || '';
|
|
||||||
if (newTitle.length === 0) {
|
|
||||||
await this.setState({
|
|
||||||
templateTag: undefined
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const existingTag = this.application.findTagByTitle(newTitle);
|
|
||||||
if (existingTag) {
|
|
||||||
this.application.alertService!.alert(
|
|
||||||
"A tag with this name already exists."
|
|
||||||
);
|
|
||||||
this.undoCreateTag(newTag);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const insertedTag = await this.application.insertItem(newTag);
|
|
||||||
const changedTag = await this.application.changeItem<TagMutator>(insertedTag.uuid, (m) => {
|
|
||||||
m.title = newTitle;
|
|
||||||
});
|
|
||||||
await this.setState({
|
|
||||||
templateTag: undefined,
|
|
||||||
editingTag: undefined
|
|
||||||
});
|
|
||||||
this.selectTag(changedTag as SNTag);
|
|
||||||
await this.application.saveItem(changedTag!.uuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
async selectedRenameTag(tag: SNTag) {
|
|
||||||
this.editingOriginalName = tag.title;
|
|
||||||
await this.setState({
|
|
||||||
editingTag: tag
|
|
||||||
});
|
|
||||||
document.getElementById('tag-' + tag.uuid)!.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedDeleteTag(tag: SNTag) {
|
|
||||||
this.removeTag(tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeTag(tag: SNTag) {
|
|
||||||
if (await confirmDialog({
|
|
||||||
text: STRING_DELETE_TAG,
|
|
||||||
confirmButtonStyle: 'danger'
|
|
||||||
})) {
|
|
||||||
this.application.deleteItem(tag);
|
|
||||||
this.selectTag(this.getState().smartTags[0]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,7 +252,7 @@ export class TagsView extends WebDirective {
|
|||||||
super();
|
super();
|
||||||
this.restrict = 'E';
|
this.restrict = 'E';
|
||||||
this.scope = {
|
this.scope = {
|
||||||
application: '='
|
application: '=',
|
||||||
};
|
};
|
||||||
this.template = template;
|
this.template = template;
|
||||||
this.replace = true;
|
this.replace = true;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
.tags {
|
.tags {
|
||||||
width: 180px;
|
width: 180px;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-khtml-user-select: none;
|
-khtml-user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
@@ -85,18 +87,25 @@
|
|||||||
|
|
||||||
// Required for Safari to avoid highlighting when dragging panel resizers
|
// Required for Safari to avoid highlighting when dragging panel resizers
|
||||||
// Make sure to undo if it's selected (for editing)
|
// Make sure to undo if it's selected (for editing)
|
||||||
|
user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-khtml-user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
|
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
&.editing {
|
&.editing {
|
||||||
-webkit-user-select: text !important;
|
pointer-events: auto;
|
||||||
|
user-select: text;
|
||||||
|
-moz-user-select: text;
|
||||||
|
-khtml-user-select: text;
|
||||||
|
-webkit-user-select: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
box-shadow: 0;
|
box-shadow: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> .count {
|
> .count {
|
||||||
|
|||||||
Reference in New Issue
Block a user