diff --git a/app/assets/javascripts/app.ts b/app/assets/javascripts/app.ts index 0550b3c55..a9b5dd44f 100644 --- a/app/assets/javascripts/app.ts +++ b/app/assets/javascripts/app.ts @@ -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); diff --git a/app/assets/javascripts/components/TagsList.tsx b/app/assets/javascripts/components/TagsList.tsx new file mode 100644 index 000000000..92f19f797 --- /dev/null +++ b/app/assets/javascripts/components/TagsList.tsx @@ -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 = 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( + insertedTag.uuid, + (m) => { + m.title = newTitle; + } + ); + + selectTag(changedTag as SNTag); + await application.saveItem(insertedTag.uuid); + } else { + await application.changeAndSaveItem( + 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 ? ( +
+ No tags. Create one using the add button above. +
+ ) : ( + <> + {allTags.map((tag) => { + return ( + + ); + })} + + )} + + ); + } +); + +export const TagsListDirective = toDirective(TagsList); diff --git a/app/assets/javascripts/components/TagsListItem.tsx b/app/assets/javascripts/components/TagsListItem.tsx new file mode 100644 index 000000000..091c8e68f --- /dev/null +++ b/app/assets/javascripts/components/TagsListItem.tsx @@ -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 = observer( + ({ tag, selectTag, saveTag, removeTag, appState }) => { + const [title, setTitle] = useState(tag.title || ''); + const inputRef = useRef(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) => { + 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 ( +
+ {!tag.errorDecrypting ? ( +
+
#
+ +
{noteCounts}
+
+ ) : null} + {tag.conflictOf && ( +
+ Conflicted Copy {tag.conflictOf} +
+ )} + {tag.errorDecrypting && !tag.waitingForKey && ( +
Missing Keys
+ )} + {tag.errorDecrypting && tag.waitingForKey && ( +
Waiting For Keys
+ )} + {isSelected && ( +
+ {!isEditing && ( + + Rename + + )} + {isEditing && ( + + Save + + )} + + Delete + +
+ )} +
+ ); + } +); diff --git a/app/assets/javascripts/ui_models/app_state/app_state.ts b/app/assets/javascripts/ui_models/app_state/app_state.ts index d82f1df5d..1361419e8 100644 --- a/app/assets/javascripts/ui_models/app_state/app_state.ts +++ b/app/assets/javascripts/ui_models/app_state/app_state.ts @@ -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, diff --git a/app/assets/javascripts/views/tags/tags-view.pug b/app/assets/javascripts/views/tags/tags-view.pug index 344153a98..f9c122c5b 100644 --- a/app/assets/javascripts/views/tags/tags-view.pug +++ b/app/assets/javascripts/views/tags/tags-view.pug @@ -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', diff --git a/app/assets/javascripts/views/tags/tags_view.ts b/app/assets/javascripts/views/tags/tags_view.ts index a117ee546..4bd6f721d 100644 --- a/app/assets/javascripts/views/tags/tags_view.ts +++ b/app/assets/javascripts/views/tags/tags_view.ts @@ -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> +type NoteCounts = Partial>; 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 { - /** 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> = {} - private removeTagsObserver!: () => void - private removeFoldersObserver!: () => void + formData: { tagTitle?: string } = {}; + titles: Partial> = {}; + 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 { 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 { 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 { this.removeTagsObserver = this.application.streamItems( [ContentType.Tag, ContentType.SmartTag], async (items) => { + const tags = items as Array; + await this.setState({ - tags: this.application.getDisplayableItems(ContentType.Tag) as SNTag[], smartTags: this.application.getSmartTags(), }); - for (const tag of items as Array) { + 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 { 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 { } reloadNoteCounts() { - let allTags: Array = []; - 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 { 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 { _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 { } 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(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(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; diff --git a/app/assets/stylesheets/_tags.scss b/app/assets/stylesheets/_tags.scss index 300d5af52..f52f020cf 100644 --- a/app/assets/stylesheets/_tags.scss +++ b/app/assets/stylesheets/_tags.scss @@ -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 {