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:
Laurent Senta
2021-11-29 17:33:00 +01:00
committed by GitHub
parent 6f3a749e52
commit 4d8ba3320a
7 changed files with 480 additions and 286 deletions

View File

@@ -80,6 +80,7 @@ import { NotesListOptionsDirective } from './components/NotesListOptionsMenu';
import { PurchaseFlowDirective } from './purchaseFlow';
import { QuickSettingsMenuDirective } from './components/QuickSettingsMenu';
import { ComponentViewDirective } from '@/components/ComponentView';
import { TagsListDirective } from '@/components/TagsList';
function reloadHiddenFirefoxTab(): boolean {
/**
@@ -181,6 +182,7 @@ const startApplication: StartApplication = async function startApplication(
.directive('notesListOptionsMenu', NotesListOptionsDirective)
.directive('icon', IconDirective)
.directive('noteTagsContainer', NoteTagsContainerDirective)
.directive('tags', TagsListDirective)
.directive('preferences', PreferencesDirective)
.directive('purchaseFlow', PurchaseFlowDirective);

View 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);

View 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>
);
}
);

View File

@@ -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 { 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 { NotesState } from './notes_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';
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 { PurchaseFlowState } from './purchase_flow_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 {
TagChanged,
@@ -34,7 +40,7 @@ export enum AppStateEvent {
BeganBackupDownload,
EndedBackupDownload,
WindowDidFocus,
WindowDidBlur
WindowDidBlur,
}
export type PanelResizedData = {
@@ -62,8 +68,13 @@ export class AppState {
rootScopeCleanup1: any;
rootScopeCleanup2: any;
onVisibilityChange: any;
selectedTag?: SNTag;
showBetaWarning: boolean;
selectedTag: SNTag | undefined;
previouslySelectedTag: SNTag | undefined;
editingTag: SNTag | undefined;
_templateTag: SNTag | undefined;
readonly quickSettingsMenu = new QuickSettingsState();
readonly accountMenu: AccountMenuState;
readonly actionsMenu = new ActionsMenuState();
@@ -133,11 +144,25 @@ export class AppState {
this.showBetaWarning = false;
}
this.selectedTag = undefined;
this.previouslySelectedTag = undefined;
this.editingTag = undefined;
this._templateTag = undefined;
makeObservable(this, {
showBetaWarning: observable,
isSessionsModalVisible: observable,
preferences: observable,
selectedTag: observable,
previouslySelectedTag: observable,
_templateTag: observable,
templateTag: computed,
createNewTag: action,
editingTag: observable,
setSelectedTag: action,
removeTag: action,
enableBetaWarning: action,
disableBetaWarning: action,
openSessionsModal: action,
@@ -269,10 +294,13 @@ export class AppState {
}
if (this.selectedTag) {
const matchingTag = items.find(
(candidate) => candidate.uuid === this.selectedTag!.uuid
(candidate) =>
this.selectedTag && candidate.uuid === this.selectedTag.uuid
);
if (matchingTag) {
this.selectedTag = matchingTag as SNTag;
runInAction(() => {
this.selectedTag = matchingTag as SNTag;
});
}
}
}
@@ -343,17 +371,69 @@ export class AppState {
}
setSelectedTag(tag: SNTag) {
if (tag.conflictOf) {
this.application.changeAndSaveItem(tag.uuid, (mutator) => {
mutator.conflictOf = undefined;
});
}
if (this.selectedTag === tag) {
return;
}
const previousTag = this.selectedTag;
this.previouslySelectedTag = this.selectedTag;
this.selectedTag = tag;
if (this.templateTag?.uuid === tag.uuid) {
return;
}
this.notifyEvent(AppStateEvent.TagChanged, {
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 */
public getNoteTags(note: SNNote) {
return this.application.referencingForItem(note).filter((ref) => {
@@ -361,10 +441,6 @@ export class AppState {
}) as SNTag[];
}
public getSelectedTag() {
return this.selectedTag;
}
panelDidResize(name: string, collapsed: boolean) {
const data: PanelResizedData = {
panel: name,

View File

@@ -33,35 +33,10 @@
.section-title-bar-header
.sk-h3.title
span.sk-bold Tags
.tag(
ng-class="{'selected' : self.state.selectedTag == tag}",
ng-click='self.selectTag(tag)',
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.
tags(
application='self.application',
app-state='self.appState'
)
panel-resizer(
collapsable='true',
control='self.panelPuppet',

View File

@@ -1,62 +1,46 @@
import { PayloadContent } from '@standardnotes/snjs';
import { WebDirective, PanelPuppet } from '@/types';
import { PanelPuppet, WebDirective } from '@/types';
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 { 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 { confirmDialog } from '@/services/alertService';
import template from './tags-view.pug';
type NoteCounts = Partial<Record<string, number>>
type NoteCounts = Partial<Record<string, number>>;
type TagState = {
tags: SNTag[]
smartTags: SNSmartTag[]
noteCounts: NoteCounts
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
}
smartTags: SNSmartTag[];
noteCounts: NoteCounts;
selectedTag?: SNTag;
};
class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
/** Passed through template */
readonly application!: WebApplication
private readonly panelPuppet: PanelPuppet
private unregisterComponent?: any
component?: SNComponent
readonly application!: WebApplication;
private readonly panelPuppet: PanelPuppet;
private unregisterComponent?: any;
component?: SNComponent;
/** The original name of the edtingTag before it began editing */
private editingOriginalName?: string
formData: { tagTitle?: string } = {}
titles: Partial<Record<UuidString, string>> = {}
private removeTagsObserver!: () => void
private removeFoldersObserver!: () => void
formData: { tagTitle?: string } = {};
titles: Partial<Record<UuidString, string>> = {};
private removeTagsObserver!: () => void;
private removeFoldersObserver!: () => void;
/* @ngInject */
constructor(
$timeout: ng.ITimeoutService,
) {
constructor($timeout: ng.ITimeoutService) {
super($timeout);
this.panelPuppet = {
onReady: () => this.loadPreferences()
onReady: () => this.loadPreferences(),
};
}
@@ -69,16 +53,15 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
super.deinit();
}
getInitialState() {
getInitialState(): TagState {
return {
tags: [],
smartTags: [],
noteCounts: {},
};
}
getState() {
return this.state as TagState;
getState(): TagState {
return this.state;
}
async onAppStart() {
@@ -90,10 +73,9 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
super.onAppLaunch();
this.loadPreferences();
this.beginStreamingItems();
const smartTags = this.application.getSmartTags();
this.setState({
smartTags: smartTags,
});
this.setState({ smartTags });
this.selectTag(smartTags[0]);
}
@@ -114,28 +96,31 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
this.removeTagsObserver = this.application.streamItems(
[ContentType.Tag, ContentType.SmartTag],
async (items) => {
const tags = items as Array<SNTag | SNSmartTag>;
await this.setState({
tags: this.application.getDisplayableItems(ContentType.Tag) as SNTag[],
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.reloadNoteCounts();
const selectedTag = this.state.selectedTag;
if (selectedTag) {
/** 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;
}) as SNTag;
});
if (matchingTag) {
if (matchingTag.deleted) {
this.selectTag(this.getState().smartTags[0]);
} else {
this.setState({
selectedTag: matchingTag
selectedTag: matchingTag,
});
}
}
@@ -148,7 +133,7 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
onAppStateEvent(eventName: AppStateEvent) {
if (eventName === AppStateEvent.TagChanged) {
this.setState({
selectedTag: this.application.getAppState().getSelectedTag()
selectedTag: this.application.getAppState().getSelectedTag(),
});
}
}
@@ -167,37 +152,23 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
}
reloadNoteCounts() {
let allTags: Array<SNTag | SNSmartTag> = [];
if (this.getState().tags) {
allTags = allTags.concat(this.getState().tags);
}
if (this.getState().smartTags) {
allTags = allTags.concat(this.getState().smartTags);
}
const smartTags = this.state.smartTags;
const noteCounts: NoteCounts = {};
for (const tag of allTags) {
if (tag === this.state.templateTag) {
continue;
}
if (tag.isSmartTag) {
/** Other smart tags do not contain counts */
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)
for (const tag of smartTags) {
/** Other smart tags do not contain counts */
if (tag.isAllTag) {
const notes = this.application
.notesMatchingSmartTag(tag as SNSmartTag)
.filter((note) => {
return !note.archived && !note.trashed;
});
noteCounts[tag.uuid] = notes.length;
}
}
this.setState({
noteCounts: noteCounts
noteCounts: noteCounts,
});
}
@@ -205,14 +176,14 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
if (!this.panelPuppet.ready) {
return;
}
const width = this.application.getPreference(PrefKey.TagsPanelWidth);
if (width) {
this.panelPuppet.setWidth!(width);
if (this.panelPuppet.isCollapsed!()) {
this.application.getAppState().panelDidResize(
PANEL_NAME_TAGS,
this.panelPuppet.isCollapsed!()
);
this.application
.getAppState()
.panelDidResize(PANEL_NAME_TAGS, this.panelPuppet.isCollapsed!());
}
}
}
@@ -223,36 +194,39 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
_isAtMaxWidth: boolean,
isCollapsed: boolean
) => {
this.application.setPreference(
PrefKey.TagsPanelWidth,
newWidth
).then(() => this.application.sync());
this.application.getAppState().panelDidResize(
PANEL_NAME_TAGS,
isCollapsed
);
}
this.application
.setPreference(PrefKey.TagsPanelWidth, newWidth)
.then(() => this.application.sync());
this.application.getAppState().panelDidResize(PANEL_NAME_TAGS, isCollapsed);
};
registerComponentHandler() {
this.unregisterComponent = this.application.componentManager!.registerHandler({
identifier: 'tags',
areas: [ComponentArea.TagsList],
actionHandler: (_, action, data) => {
if (action === ComponentAction.SelectItem) {
if (data.item!.content_type === ContentType.Tag) {
const tag = this.application.findItem(data.item!.uuid);
if (tag) {
this.selectTag(tag as SNTag);
this.unregisterComponent =
this.application.componentManager!.registerHandler({
identifier: 'tags',
areas: [ComponentArea.TagsList],
actionHandler: (_, action, data) => {
if (action === ComponentAction.SelectItem) {
const item = data.item;
if (!item) {
return;
}
} else if (data.item!.content_type === ContentType.SmartTag) {
const matchingTag = this.getState().smartTags.find(t => t.uuid === data.item!.uuid);
this.selectTag(matchingTag as SNSmartTag);
if (item.content_type === ContentType.SmartTag) {
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) {
@@ -265,123 +239,11 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
}
async clickedAddNewTag() {
if (this.getState().editingTag) {
if (this.appState.templateTag) {
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.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]);
}
this.appState.createNewTag();
}
}
@@ -390,7 +252,7 @@ export class TagsView extends WebDirective {
super();
this.restrict = 'E';
this.scope = {
application: '='
application: '=',
};
this.template = template;
this.replace = true;

View File

@@ -1,6 +1,8 @@
.tags {
width: 180px;
flex-grow: 0;
user-select: none;
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
@@ -85,18 +87,25 @@
// Required for Safari to avoid highlighting when dragging panel resizers
// 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;
pointer-events: none;
&.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 {
outline: 0;
box-shadow: 0;
}
pointer-events: none;
}
> .count {