Editor TypeScript
This commit is contained in:
@@ -43,6 +43,10 @@ export class PureCtrl {
|
||||
this.deinit();
|
||||
}
|
||||
|
||||
public get appState() {
|
||||
return this.application!.getAppState();
|
||||
}
|
||||
|
||||
/** @private */
|
||||
async resetState() {
|
||||
this.state = this.getInitialState();
|
||||
@@ -66,6 +70,10 @@ export class PureCtrl {
|
||||
});
|
||||
}
|
||||
|
||||
async updateUI(func: () => void) {
|
||||
this.$timeout(func);
|
||||
}
|
||||
|
||||
initProps(props: CtrlProps) {
|
||||
if (Object.keys(this.props).length > 0) {
|
||||
throw 'Already init-ed props.';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -157,10 +157,10 @@ class FooterCtrl extends PureCtrl {
|
||||
|
||||
streamItems() {
|
||||
this.application.streamItems({
|
||||
contentType: ContentTypes.Component,
|
||||
contentType: ContentType.Component,
|
||||
stream: async () => {
|
||||
this.rooms = this.application.getItems({
|
||||
contentType: ContentTypes.Component
|
||||
contentType: ContentType.Component
|
||||
}).filter((candidate) => {
|
||||
return candidate.area === 'rooms' && !candidate.deleted;
|
||||
});
|
||||
@@ -175,7 +175,7 @@ class FooterCtrl extends PureCtrl {
|
||||
contentType: 'SN|Theme',
|
||||
stream: async () => {
|
||||
const themes = this.application.getDisplayableItems({
|
||||
contentType: ContentTypes.Theme
|
||||
contentType: ContentType.Theme
|
||||
}).filter((candidate) => {
|
||||
return (
|
||||
!candidate.deleted &&
|
||||
|
||||
@@ -3,7 +3,7 @@ import template from '%/notes.pug';
|
||||
import { ApplicationEvent, ContentTypes, removeFromArray } from 'snjs';
|
||||
import { PureCtrl } from '@Controllers';
|
||||
import { AppStateEvent } from '@/services/state';
|
||||
import { KeyboardModifiers, KeyboardKeys } from '@/services/keyboardManager';
|
||||
import { KeyboardModifier, KeyboardKey } from '@/services/keyboardManager';
|
||||
import {
|
||||
PrefKeys
|
||||
} from '@/services/preferencesManager';
|
||||
@@ -155,7 +155,7 @@ class NotesCtrl extends PureCtrl {
|
||||
|
||||
streamNotesAndTags() {
|
||||
this.application.streamItems({
|
||||
contentType: [ContentTypes.Note, ContentTypes.Tag],
|
||||
contentType: [ContentType.Note, ContentType.Tag],
|
||||
stream: async ({ items }) => {
|
||||
await this.reloadNotes();
|
||||
const selectedNote = this.state.selectedNote;
|
||||
@@ -169,7 +169,7 @@ class NotesCtrl extends PureCtrl {
|
||||
}
|
||||
|
||||
/** Note has changed values, reset its flags */
|
||||
const notes = items.filter((item) => item.content_type === ContentTypes.Note);
|
||||
const notes = items.filter((item) => item.content_type === ContentType.Note);
|
||||
for (const note of notes) {
|
||||
if(note.deleted) {
|
||||
continue;
|
||||
@@ -202,7 +202,7 @@ class NotesCtrl extends PureCtrl {
|
||||
title = `Note ${this.state.notes.length + 1}`;
|
||||
}
|
||||
const newNote = await this.application.createManagedItem({
|
||||
contentType: ContentTypes.Note,
|
||||
contentType: ContentType.Note,
|
||||
content: {
|
||||
text: '',
|
||||
title: title
|
||||
@@ -680,8 +680,8 @@ class NotesCtrl extends PureCtrl {
|
||||
this.newNoteKeyObserver = this.application.getKeyboardService().addKeyObserver({
|
||||
key: 'n',
|
||||
modifiers: [
|
||||
KeyboardModifiers.Meta,
|
||||
KeyboardModifiers.Ctrl
|
||||
KeyboardModifier.Meta,
|
||||
KeyboardModifier.Ctrl
|
||||
],
|
||||
onKeyDown: (event) => {
|
||||
event.preventDefault();
|
||||
@@ -690,7 +690,7 @@ class NotesCtrl extends PureCtrl {
|
||||
});
|
||||
|
||||
this.nextNoteKeyObserver = this.application.getKeyboardService().addKeyObserver({
|
||||
key: KeyboardKeys.Down,
|
||||
key: KeyboardKey.Down,
|
||||
elements: [
|
||||
document.body,
|
||||
this.getSearchBar()
|
||||
@@ -705,7 +705,7 @@ class NotesCtrl extends PureCtrl {
|
||||
});
|
||||
|
||||
this.nextNoteKeyObserver = this.application.getKeyboardService().addKeyObserver({
|
||||
key: KeyboardKeys.Up,
|
||||
key: KeyboardKey.Up,
|
||||
element: document.body,
|
||||
onKeyDown: (event) => {
|
||||
this.selectPreviousNote();
|
||||
@@ -715,8 +715,8 @@ class NotesCtrl extends PureCtrl {
|
||||
this.searchKeyObserver = this.application.getKeyboardService().addKeyObserver({
|
||||
key: "f",
|
||||
modifiers: [
|
||||
KeyboardModifiers.Meta,
|
||||
KeyboardModifiers.Shift
|
||||
KeyboardModifier.Meta,
|
||||
KeyboardModifier.Shift
|
||||
],
|
||||
onKeyDown: (event) => {
|
||||
const searchBar = this.getSearchBar();
|
||||
|
||||
@@ -1,21 +1,40 @@
|
||||
import { WebDirective, PanelPuppet } from './../types';
|
||||
import { WebApplication } from './../application';
|
||||
import {
|
||||
SNNote,
|
||||
SNSmartTag,
|
||||
ContentTypes,
|
||||
SNTag,
|
||||
ContentType,
|
||||
ApplicationEvent,
|
||||
ComponentActions
|
||||
ComponentAction,
|
||||
SNSmartTag,
|
||||
ComponentArea,
|
||||
SNComponent
|
||||
} from 'snjs';
|
||||
import template from '%/tags.pug';
|
||||
import { AppStateEvent } from '@/services/state';
|
||||
import { PANEL_NAME_TAGS } from '@/controllers/constants';
|
||||
import { PrefKeys } from '@/services/preferencesManager';
|
||||
import { STRING_DELETE_TAG } from '@/strings';
|
||||
import { PureCtrl } from '@Controllers';
|
||||
import { PureCtrl } from '@Controllers/abstract/pure_ctrl';
|
||||
import { UuidString } from '@/../../../../snjs/dist/@types/types';
|
||||
import { TagMutator } from '@/../../../../snjs/dist/@types/models/app/tag';
|
||||
|
||||
type NoteCounts = Partial<Record<string, number>>
|
||||
|
||||
class TagsPanelCtrl extends PureCtrl {
|
||||
|
||||
/** Passed through template */
|
||||
readonly application!: WebApplication
|
||||
private readonly panelPuppet: PanelPuppet
|
||||
private unregisterComponent?: any
|
||||
component?: SNComponent
|
||||
private editingOriginalName?: string
|
||||
formData: { tagTitle?: string } = {}
|
||||
titles: Partial<Record<UuidString, string>> = {}
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$timeout,
|
||||
$timeout: ng.ITimeoutService,
|
||||
) {
|
||||
super($timeout);
|
||||
this.panelPuppet = {
|
||||
@@ -25,7 +44,7 @@ class TagsPanelCtrl extends PureCtrl {
|
||||
|
||||
deinit() {
|
||||
this.unregisterComponent();
|
||||
this.unregisterComponent = null;
|
||||
this.unregisterComponent = undefined;
|
||||
super.deinit();
|
||||
}
|
||||
|
||||
@@ -37,12 +56,12 @@ class TagsPanelCtrl extends PureCtrl {
|
||||
};
|
||||
}
|
||||
|
||||
onAppStart() {
|
||||
async onAppStart() {
|
||||
super.onAppStart();
|
||||
this.registerComponentHandler();
|
||||
}
|
||||
|
||||
onAppLaunch() {
|
||||
async onAppLaunch() {
|
||||
super.onAppLaunch();
|
||||
this.loadPreferences();
|
||||
this.beginStreamingItems();
|
||||
@@ -64,20 +83,21 @@ class TagsPanelCtrl extends PureCtrl {
|
||||
* @access private
|
||||
*/
|
||||
getMappedTags() {
|
||||
const tags = this.application.getItems({ contentType: ContentTypes.Tag });
|
||||
const tags = this.application.getItems(ContentType.Tag) as SNTag[];
|
||||
return tags.sort((a, b) => {
|
||||
return a.content.title < b.content.title ? -1 : 1;
|
||||
return a.title < b.title ? -1 : 1;
|
||||
});
|
||||
}
|
||||
|
||||
beginStreamingItems() {
|
||||
this.application.streamItems({
|
||||
contentType: ContentTypes.Tag,
|
||||
stream: async ({ items }) => {
|
||||
this.application.streamItems(
|
||||
ContentType.Tag,
|
||||
async (items) => {
|
||||
await this.setState({
|
||||
tags: this.getMappedTags(),
|
||||
smartTags: this.application.getSmartTags(),
|
||||
});
|
||||
this.reloadTitles(items as SNTag[]);
|
||||
this.reloadNoteCounts();
|
||||
if (this.state.selectedTag) {
|
||||
/** If the selected tag has been deleted, revert to All view. */
|
||||
@@ -89,11 +109,17 @@ class TagsPanelCtrl extends PureCtrl {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
reloadTitles(tags: Array<SNTag | SNSmartTag>) {
|
||||
for(const tag of tags) {
|
||||
this.titles[tag.uuid] = tag.title;
|
||||
}
|
||||
}
|
||||
|
||||
/** @override */
|
||||
onAppStateEvent(eventName, data) {
|
||||
onAppStateEvent(eventName: AppStateEvent, data?: any) {
|
||||
if (eventName === AppStateEvent.PreferencesChanged) {
|
||||
this.loadPreferences();
|
||||
} else if (eventName === AppStateEvent.TagChanged) {
|
||||
@@ -105,7 +131,7 @@ class TagsPanelCtrl extends PureCtrl {
|
||||
|
||||
|
||||
/** @override */
|
||||
async onAppEvent(eventName) {
|
||||
async onAppEvent(eventName: ApplicationEvent) {
|
||||
super.onAppEvent(eventName);
|
||||
if (eventName === ApplicationEvent.LocalDataIncrementalLoad) {
|
||||
this.reloadNoteCounts();
|
||||
@@ -119,19 +145,25 @@ class TagsPanelCtrl extends PureCtrl {
|
||||
}
|
||||
|
||||
reloadNoteCounts() {
|
||||
let allTags = [];
|
||||
let allTags: Array<SNTag | SNSmartTag> = [];
|
||||
if (this.state.tags) {
|
||||
allTags = allTags.concat(this.state.tags);
|
||||
}
|
||||
if (this.state.smartTags) {
|
||||
allTags = allTags.concat(this.state.smartTags);
|
||||
}
|
||||
const noteCounts = {};
|
||||
const noteCounts: NoteCounts = {};
|
||||
for (const tag of allTags) {
|
||||
const validNotes = SNNote.filterDummyNotes(tag.notes).filter((note) => {
|
||||
return !note.archived && !note.content.trashed;
|
||||
});
|
||||
noteCounts[tag.uuid] = validNotes.length;
|
||||
if (tag.isSmartTag()) {
|
||||
const notes = this.application.notesMatchingSmartTag(tag as SNSmartTag);
|
||||
noteCounts[tag.uuid] = notes.length;
|
||||
} else {
|
||||
const notes = this.application.referencesForItem(tag, ContentType.Note)
|
||||
.filter((note) => {
|
||||
return !note.archived && !note.trashed;
|
||||
})
|
||||
noteCounts[tag.uuid] = notes.length;
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
noteCounts: noteCounts
|
||||
@@ -144,17 +176,22 @@ class TagsPanelCtrl extends PureCtrl {
|
||||
}
|
||||
const width = this.application.getPrefsService().getValue(PrefKeys.TagsPanelWidth);
|
||||
if (width) {
|
||||
this.panelPuppet.setWidth(width);
|
||||
if (this.panelPuppet.isCollapsed()) {
|
||||
this.panelPuppet.setWidth!(width);
|
||||
if (this.panelPuppet.isCollapsed!()) {
|
||||
this.application.getAppState().panelDidResize(
|
||||
PANEL_NAME_TAGS,
|
||||
this.panelPuppet.isCollapsed()
|
||||
this.panelPuppet.isCollapsed!()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPanelResize = (newWidth, lastLeft, isAtMaxWidth, isCollapsed) => {
|
||||
onPanelResize = (
|
||||
newWidth: number,
|
||||
lastLeft: number,
|
||||
isAtMaxWidth: boolean,
|
||||
isCollapsed: boolean
|
||||
) => {
|
||||
this.application.getPrefsService().setUserPrefValue(
|
||||
PrefKeys.TagsPanelWidth,
|
||||
newWidth,
|
||||
@@ -167,50 +204,49 @@ class TagsPanelCtrl extends PureCtrl {
|
||||
}
|
||||
|
||||
registerComponentHandler() {
|
||||
this.unregisterComponent = this.application.componentManager.registerHandler({
|
||||
this.unregisterComponent = this.application.componentManager!.registerHandler({
|
||||
identifier: 'tags',
|
||||
areas: ['tags-list'],
|
||||
areas: [ComponentArea.TagsList],
|
||||
activationHandler: (component) => {
|
||||
this.component = component;
|
||||
},
|
||||
contextRequestHandler: (component) => {
|
||||
return null;
|
||||
contextRequestHandler: () => {
|
||||
return undefined;
|
||||
},
|
||||
actionHandler: (_, action, data) => {
|
||||
if (action === ComponentActions.SelectItem) {
|
||||
if (data.item.content_type === ContentTypes.Tag) {
|
||||
const tag = this.application.findItem({ uuid: data.item.uuid });
|
||||
if (action === ComponentAction.SelectItem) {
|
||||
if (data.item.content_type === ContentType.Tag) {
|
||||
const tag = this.application.findItem(data.item.uuid);
|
||||
if (tag) {
|
||||
this.selectTag(tag);
|
||||
this.selectTag(tag as SNTag);
|
||||
}
|
||||
} else if (data.item.content_type === ContentTypes.SmartTag) {
|
||||
this.application.createTemplateItem({
|
||||
contentType: ContentTypes.SmartTag,
|
||||
content: data.item.content
|
||||
}).then(smartTag => {
|
||||
this.selectTag(smartTag);
|
||||
} else if (data.item.content_type === ContentType.SmartTag) {
|
||||
this.application.createTemplateItem(
|
||||
ContentType.SmartTag,
|
||||
data.item.content
|
||||
).then(smartTag => {
|
||||
this.selectTag(smartTag as SNSmartTag);
|
||||
});
|
||||
}
|
||||
} else if (action === ComponentActions.ClearSelection) {
|
||||
} else if (action === ComponentAction.ClearSelection) {
|
||||
this.selectTag(this.state.smartTags[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async selectTag(tag) {
|
||||
async selectTag(tag: SNTag) {
|
||||
if (tag.isSmartTag()) {
|
||||
Object.defineProperty(tag, 'notes', {
|
||||
get: () => {
|
||||
return this.application.getNotesMatchingSmartTag({
|
||||
smartTag: tag
|
||||
});
|
||||
return this.application.notesMatchingSmartTag(tag as SNSmartTag);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (tag.content.conflict_of) {
|
||||
tag.content.conflict_of = null;
|
||||
this.application.saveItem({ item: tag });
|
||||
if (tag.conflictOf) {
|
||||
this.application.changeAndSaveItem(tag.uuid, (mutator) => {
|
||||
mutator.conflictOf = undefined;
|
||||
})
|
||||
}
|
||||
this.application.getAppState().setSelectedTag(tag);
|
||||
}
|
||||
@@ -219,9 +255,9 @@ class TagsPanelCtrl extends PureCtrl {
|
||||
if (this.state.editingTag) {
|
||||
return;
|
||||
}
|
||||
const newTag = await this.application.createTemplateItem({
|
||||
contentType: ContentTypes.Tag
|
||||
});
|
||||
const newTag = await this.application.createTemplateItem(
|
||||
ContentType.Tag
|
||||
);
|
||||
this.setState({
|
||||
tags: [newTag].concat(this.state.tags),
|
||||
previousTag: this.state.selectedTag,
|
||||
@@ -231,14 +267,14 @@ class TagsPanelCtrl extends PureCtrl {
|
||||
});
|
||||
}
|
||||
|
||||
tagTitleDidChange(tag) {
|
||||
onTagTitleChange(tag: SNTag | SNSmartTag) {
|
||||
this.setState({
|
||||
editingTag: tag
|
||||
});
|
||||
}
|
||||
|
||||
async saveTag($event, tag) {
|
||||
$event.target.blur();
|
||||
async saveTag($event: Event, tag: SNTag) {
|
||||
($event.target! as HTMLInputElement).blur();
|
||||
await this.setState({
|
||||
editingTag: null,
|
||||
});
|
||||
@@ -246,8 +282,8 @@ class TagsPanelCtrl extends PureCtrl {
|
||||
if (!tag.title || tag.title.length === 0) {
|
||||
let newSelectedTag = this.state.selectedTag;
|
||||
if (this.state.editingTag) {
|
||||
tag.title = this.editingOriginalName;
|
||||
this.editingOriginalName = null;
|
||||
this.titles[tag.uuid] = this.editingOriginalName;
|
||||
this.editingOriginalName = undefined;
|
||||
} else if (this.state.newTag) {
|
||||
newSelectedTag = this.state.previousTag;
|
||||
}
|
||||
@@ -259,14 +295,14 @@ class TagsPanelCtrl extends PureCtrl {
|
||||
return;
|
||||
}
|
||||
|
||||
this.editingOriginalName = null;
|
||||
this.editingOriginalName = undefined;
|
||||
|
||||
const matchingTag = this.application.findTag({ title: tag.title });
|
||||
const matchingTag = this.application.findTag(tag.title);
|
||||
const alreadyExists = matchingTag && matchingTag !== tag;
|
||||
if (this.state.newTag === tag && alreadyExists) {
|
||||
this.application.alertService.alert({
|
||||
text: "A tag with this name already exists."
|
||||
});
|
||||
this.application.alertService!.alert(
|
||||
"A tag with this name already exists."
|
||||
);
|
||||
this.setState({
|
||||
newTag: null,
|
||||
tags: this.getMappedTags(),
|
||||
@@ -274,40 +310,48 @@ class TagsPanelCtrl extends PureCtrl {
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.application.saveItem({ item: tag });
|
||||
this.application.changeAndSaveItem(tag.uuid, (mutator) => {
|
||||
const tagMutator = mutator as TagMutator;
|
||||
tagMutator.title = this.titles[tag.uuid]!;
|
||||
});
|
||||
this.selectTag(tag);
|
||||
this.setState({
|
||||
newTag: null
|
||||
});
|
||||
}
|
||||
|
||||
async selectedRenameTag($event, tag) {
|
||||
async selectedRenameTag(tag: SNTag) {
|
||||
this.editingOriginalName = tag.title;
|
||||
await this.setState({
|
||||
editingTag: tag
|
||||
});
|
||||
document.getElementById('tag-' + tag.uuid).focus();
|
||||
document.getElementById('tag-' + tag.uuid)!.focus();
|
||||
}
|
||||
|
||||
selectedDeleteTag(tag) {
|
||||
selectedDeleteTag(tag: SNTag) {
|
||||
this.removeTag(tag);
|
||||
}
|
||||
|
||||
removeTag(tag) {
|
||||
this.application.alertService.confirm({
|
||||
text: STRING_DELETE_TAG,
|
||||
destructive: true,
|
||||
onConfirm: () => {
|
||||
this.application.deleteItem({ item: tag });
|
||||
removeTag(tag: SNTag) {
|
||||
this.application.alertService!.confirm(
|
||||
STRING_DELETE_TAG,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
() => {
|
||||
/* On confirm */
|
||||
this.application.deleteItem(tag);
|
||||
this.selectTag(this.state.smartTags[0]);
|
||||
}
|
||||
});
|
||||
},
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class TagsPanel {
|
||||
export class TagsPanel extends WebDirective {
|
||||
constructor() {
|
||||
super();
|
||||
this.restrict = 'E';
|
||||
this.scope = {
|
||||
application: '='
|
||||
@@ -27,7 +27,7 @@ class RevisionPreviewModalCtrl {
|
||||
|
||||
async configure() {
|
||||
this.note = await this.application.createTemplateItem({
|
||||
contentType: ContentTypes.Note,
|
||||
contentType: ContentType.Note,
|
||||
content: this.content
|
||||
});
|
||||
|
||||
@@ -45,7 +45,7 @@ class RevisionPreviewModalCtrl {
|
||||
* editor object has non-copyable properties like .window, which cannot be transfered
|
||||
*/
|
||||
const editorCopy = await this.application.createTemplateItem({
|
||||
contentType: ContentTypes.Component,
|
||||
contentType: ContentType.Component,
|
||||
content: editorForNote.content
|
||||
});
|
||||
this.application.component.setReadonlyStateForComponent(editorCopy, true, true);
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
/** @public */
|
||||
export const KeyboardKeys = {
|
||||
Tab: "Tab",
|
||||
Backspace: "Backspace",
|
||||
Up: "ArrowUp",
|
||||
Down: "ArrowDown",
|
||||
};
|
||||
/** @public */
|
||||
export const KeyboardModifiers = {
|
||||
Shift: "Shift",
|
||||
Ctrl: "Control",
|
||||
/** ⌘ key on Mac, ⊞ key on Windows */
|
||||
Meta: "Meta",
|
||||
Alt: "Alt",
|
||||
};
|
||||
/** @private */
|
||||
const KeyboardKeyEvents = {
|
||||
Down: "KeyEventDown",
|
||||
Up: "KeyEventUp"
|
||||
};
|
||||
|
||||
export class KeyboardManager {
|
||||
constructor() {
|
||||
this.observers = [];
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
this.handleKeyUp = this.handleKeyUp.bind(this);
|
||||
window.addEventListener('keydown', this.handleKeyDown);
|
||||
window.addEventListener('keyup', this.handleKeyUp);
|
||||
}
|
||||
|
||||
/** @access public */
|
||||
deinit() {
|
||||
this.observers.length = 0;
|
||||
window.removeEventListener('keydown', this.handleKeyDown);
|
||||
window.removeEventListener('keyup', this.handleKeyUp);
|
||||
this.handleKeyDown = null;
|
||||
this.handleKeyUp = null;
|
||||
}
|
||||
|
||||
modifiersForEvent(event) {
|
||||
const allModifiers = Object.keys(KeyboardModifiers).map((key) => KeyboardModifiers[key]);
|
||||
const eventModifiers = allModifiers.filter((modifier) => {
|
||||
// For a modifier like ctrlKey, must check both event.ctrlKey and event.key.
|
||||
// That's because on keyup, event.ctrlKey would be false, but event.key == Control would be true.
|
||||
const matches = (
|
||||
((event.ctrlKey || event.key === KeyboardModifiers.Ctrl) && modifier === KeyboardModifiers.Ctrl) ||
|
||||
((event.metaKey || event.key === KeyboardModifiers.Meta) && modifier === KeyboardModifiers.Meta) ||
|
||||
((event.altKey || event.key === KeyboardModifiers.Alt) && modifier === KeyboardModifiers.Alt) ||
|
||||
((event.shiftKey || event.key === KeyboardModifiers.Shift) && modifier === KeyboardModifiers.Shift)
|
||||
);
|
||||
|
||||
return matches;
|
||||
});
|
||||
|
||||
return eventModifiers;
|
||||
}
|
||||
|
||||
eventMatchesKeyAndModifiers(event, key, modifiers = []) {
|
||||
const eventModifiers = this.modifiersForEvent(event);
|
||||
|
||||
if (eventModifiers.length !== modifiers.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const modifier of modifiers) {
|
||||
if (!eventModifiers.includes(modifier)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Modifers match, check key
|
||||
if (!key) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// In the browser, shift + f results in key 'f', but in Electron, shift + f results in 'F'
|
||||
// In our case we don't differentiate between the two.
|
||||
return key.toLowerCase() === event.key.toLowerCase();
|
||||
}
|
||||
|
||||
notifyObserver(event, keyEventType) {
|
||||
for (const observer of this.observers) {
|
||||
if (observer.element && event.target !== observer.element) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (observer.elements && !observer.elements.includes(event.target)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (observer.notElement && observer.notElement === event.target) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (observer.notElementIds && observer.notElementIds.includes(event.target.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.eventMatchesKeyAndModifiers(event, observer.key, observer.modifiers)) {
|
||||
const callback = keyEventType === KeyboardKeyEvents.Down ? observer.onKeyDown : observer.onKeyUp;
|
||||
if (callback) {
|
||||
callback(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDown(event) {
|
||||
this.notifyObserver(event, KeyboardKeyEvents.Down);
|
||||
}
|
||||
|
||||
handleKeyUp(event) {
|
||||
this.notifyObserver(event, KeyboardKeyEvents.Up);
|
||||
}
|
||||
|
||||
addKeyObserver({ key, modifiers, onKeyDown, onKeyUp, element, elements, notElement, notElementIds }) {
|
||||
const observer = { key, modifiers, onKeyDown, onKeyUp, element, elements, notElement, notElementIds };
|
||||
this.observers.push(observer);
|
||||
return observer;
|
||||
}
|
||||
|
||||
removeKeyObserver(observer) {
|
||||
this.observers.splice(this.observers.indexOf(observer), 1);
|
||||
}
|
||||
}
|
||||
147
app/assets/javascripts/services/keyboardManager.ts
Normal file
147
app/assets/javascripts/services/keyboardManager.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { removeFromArray } from 'snjs';
|
||||
export enum KeyboardKey {
|
||||
Tab = "Tab",
|
||||
Backspace = "Backspace",
|
||||
Up = "ArrowUp",
|
||||
Down = "ArrowDown",
|
||||
};
|
||||
|
||||
export enum KeyboardModifier {
|
||||
Shift = "Shift",
|
||||
Ctrl = "Control",
|
||||
/** ⌘ key on Mac, ⊞ key on Windows */
|
||||
Meta = "Meta",
|
||||
Alt = "Alt",
|
||||
};
|
||||
|
||||
enum KeyboardKeyEvent {
|
||||
Down = "KeyEventDown",
|
||||
Up = "KeyEventUp"
|
||||
};
|
||||
|
||||
type KeyboardObserver = {
|
||||
key?: KeyboardKey
|
||||
modifiers?: KeyboardModifier[]
|
||||
onKeyDown?: (event: KeyboardEvent) => void
|
||||
onKeyUp?: (event: KeyboardEvent) => void
|
||||
element?: HTMLElement
|
||||
elements?: HTMLElement[]
|
||||
notElement?: HTMLElement
|
||||
notElementIds?: string[]
|
||||
}
|
||||
|
||||
export class KeyboardManager {
|
||||
|
||||
private observers: KeyboardObserver[] = []
|
||||
private handleKeyDown: any
|
||||
private handleKeyUp: any
|
||||
|
||||
constructor() {
|
||||
this.handleKeyDown = (event: KeyboardEvent) => {
|
||||
this.notifyObserver(event, KeyboardKeyEvent.Down);
|
||||
}
|
||||
this.handleKeyUp = (event: KeyboardEvent) => {
|
||||
this.notifyObserver(event, KeyboardKeyEvent.Up);
|
||||
}
|
||||
window.addEventListener('keydown', this.handleKeyDown);
|
||||
window.addEventListener('keyup', this.handleKeyUp);
|
||||
}
|
||||
|
||||
public deinit() {
|
||||
this.observers.length = 0;
|
||||
window.removeEventListener('keydown', this.handleKeyDown);
|
||||
window.removeEventListener('keyup', this.handleKeyUp);
|
||||
this.handleKeyDown = undefined;
|
||||
this.handleKeyUp = undefined;
|
||||
}
|
||||
|
||||
modifiersForEvent(event: KeyboardEvent) {
|
||||
const allModifiers = Object.values(KeyboardModifier);
|
||||
const eventModifiers = allModifiers.filter((modifier) => {
|
||||
// For a modifier like ctrlKey, must check both event.ctrlKey and event.key.
|
||||
// That's because on keyup, event.ctrlKey would be false, but event.key == Control would be true.
|
||||
const matches = (
|
||||
(
|
||||
(event.ctrlKey || event.key === KeyboardModifier.Ctrl)
|
||||
&& modifier === KeyboardModifier.Ctrl
|
||||
) ||
|
||||
(
|
||||
(event.metaKey || event.key === KeyboardModifier.Meta)
|
||||
&& modifier === KeyboardModifier.Meta
|
||||
) ||
|
||||
(
|
||||
(event.altKey || event.key === KeyboardModifier.Alt)
|
||||
&& modifier === KeyboardModifier.Alt
|
||||
) ||
|
||||
(
|
||||
(event.shiftKey || event.key === KeyboardModifier.Shift)
|
||||
&& modifier === KeyboardModifier.Shift
|
||||
)
|
||||
);
|
||||
|
||||
return matches;
|
||||
});
|
||||
|
||||
return eventModifiers;
|
||||
}
|
||||
|
||||
eventMatchesKeyAndModifiers(
|
||||
event: KeyboardEvent,
|
||||
key: KeyboardKey,
|
||||
modifiers: KeyboardModifier[] = []
|
||||
) {
|
||||
const eventModifiers = this.modifiersForEvent(event);
|
||||
if (eventModifiers.length !== modifiers.length) {
|
||||
return false;
|
||||
}
|
||||
for (const modifier of modifiers) {
|
||||
if (!eventModifiers.includes(modifier)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Modifers match, check key
|
||||
if (!key) {
|
||||
return true;
|
||||
}
|
||||
// In the browser, shift + f results in key 'f', but in Electron, shift + f results in 'F'
|
||||
// In our case we don't differentiate between the two.
|
||||
return key.toLowerCase() === event.key.toLowerCase();
|
||||
}
|
||||
|
||||
notifyObserver(event: KeyboardEvent, keyEvent: KeyboardKeyEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
for (const observer of this.observers) {
|
||||
if (observer.element && event.target !== observer.element) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (observer.elements && !observer.elements.includes(target)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (observer.notElement && observer.notElement === event.target) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (observer.notElementIds && observer.notElementIds.includes(target.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.eventMatchesKeyAndModifiers(event, observer.key!, observer.modifiers)) {
|
||||
const callback = keyEvent === KeyboardKeyEvent.Down
|
||||
? observer.onKeyDown
|
||||
: observer.onKeyUp;
|
||||
if (callback) {
|
||||
callback(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addKeyObserver(observer: KeyboardObserver) {
|
||||
this.observers.push(observer);
|
||||
return () => {
|
||||
removeFromArray(this.observers, observer);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ export class NativeExtManager extends ApplicationService {
|
||||
get extManagerPred() {
|
||||
const extManagerId = 'org.standardnotes.extensions-manager';
|
||||
return SNPredicate.CompoundPredicate([
|
||||
new SNPredicate('content_type', '=', ContentTypes.Component),
|
||||
new SNPredicate('content_type', '=', ContentType.Component),
|
||||
new SNPredicate('package_info.identifier', '=', extManagerId)
|
||||
]);
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export class NativeExtManager extends ApplicationService {
|
||||
get batchManagerPred() {
|
||||
const batchMgrId = 'org.standardnotes.batch-manager';
|
||||
return SNPredicate.CompoundPredicate([
|
||||
new SNPredicate('content_type', '=', ContentTypes.Component),
|
||||
new SNPredicate('content_type', '=', ContentType.Component),
|
||||
new SNPredicate('package_info.identifier', '=', batchMgrId)
|
||||
]);
|
||||
}
|
||||
@@ -65,8 +65,8 @@ export class NativeExtManager extends ApplicationService {
|
||||
}
|
||||
// Handle addition of SN|ExtensionRepo permission
|
||||
const permission = extensionsManager.content.permissions.find((p) => p.name === STREAM_ITEMS_PERMISSION);
|
||||
if (!permission.content_types.includes(ContentTypes.ExtensionRepo)) {
|
||||
permission.content_types.push(ContentTypes.ExtensionRepo);
|
||||
if (!permission.content_types.includes(ContentType.ExtensionRepo)) {
|
||||
permission.content_types.push(ContentType.ExtensionRepo);
|
||||
needsSync = true;
|
||||
}
|
||||
if (needsSync) {
|
||||
@@ -92,13 +92,13 @@ export class NativeExtManager extends ApplicationService {
|
||||
{
|
||||
name: STREAM_ITEMS_PERMISSION,
|
||||
content_types: [
|
||||
ContentTypes.Component,
|
||||
ContentTypes.Theme,
|
||||
ContentTypes.ServerExtension,
|
||||
ContentTypes.ActionsExtension,
|
||||
ContentTypes.Mfa,
|
||||
ContentTypes.Editor,
|
||||
ContentTypes.ExtensionRepo
|
||||
ContentType.Component,
|
||||
ContentType.Theme,
|
||||
ContentType.ServerExtension,
|
||||
ContentType.ActionsExtension,
|
||||
ContentType.Mfa,
|
||||
ContentType.Editor,
|
||||
ContentType.ExtensionRepo
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -110,7 +110,7 @@ export class NativeExtManager extends ApplicationService {
|
||||
}
|
||||
const payload = CreateMaxPayloadFromAnyObject({
|
||||
object: {
|
||||
content_type: ContentTypes.Component,
|
||||
content_type: ContentType.Component,
|
||||
content: content
|
||||
}
|
||||
});
|
||||
@@ -146,7 +146,7 @@ export class NativeExtManager extends ApplicationService {
|
||||
}
|
||||
const payload = CreateMaxPayloadFromAnyObject({
|
||||
object: {
|
||||
content_type: ContentTypes.Component,
|
||||
content_type: ContentType.Component,
|
||||
content: content
|
||||
}
|
||||
});
|
||||
@@ -172,8 +172,8 @@ export class NativeExtManager extends ApplicationService {
|
||||
}
|
||||
// Handle addition of SN|ExtensionRepo permission
|
||||
const permission = batchManager.content.permissions.find((p) => p.name === STREAM_ITEMS_PERMISSION);
|
||||
if (!permission.content_types.includes(ContentTypes.ExtensionRepo)) {
|
||||
permission.content_types.push(ContentTypes.ExtensionRepo);
|
||||
if (!permission.content_types.includes(ContentType.ExtensionRepo)) {
|
||||
permission.content_types.push(ContentType.ExtensionRepo);
|
||||
needsSync = true;
|
||||
}
|
||||
if (needsSync) {
|
||||
|
||||
@@ -33,7 +33,7 @@ export class PreferencesManager extends ApplicationService {
|
||||
|
||||
streamPreferences() {
|
||||
this.application.streamItems({
|
||||
contentType: ContentTypes.UserPrefs,
|
||||
contentType: ContentType.UserPrefs,
|
||||
stream: () => {
|
||||
this.loadSingleton();
|
||||
}
|
||||
@@ -41,7 +41,7 @@ export class PreferencesManager extends ApplicationService {
|
||||
}
|
||||
|
||||
async loadSingleton() {
|
||||
const contentType = ContentTypes.UserPrefs;
|
||||
const contentType = ContentType.UserPrefs;
|
||||
const predicate = new SNPredicate('content_type', '=', contentType);
|
||||
this.userPreferences = await this.application.singletonManager.findOrCreateSingleton({
|
||||
predicate: predicate,
|
||||
|
||||
@@ -140,7 +140,7 @@ export class AppState {
|
||||
);
|
||||
}
|
||||
|
||||
async setSelectedNote(note: SNNote) {
|
||||
async setSelectedNote(note?: SNNote) {
|
||||
const run = async () => {
|
||||
const previousNote = this.selectedNote;
|
||||
this.selectedNote = note;
|
||||
@@ -166,6 +166,12 @@ export class AppState {
|
||||
}
|
||||
}
|
||||
|
||||
getNoteTags(note: SNNote) {
|
||||
return this.application.referencesForItem(note).filter((ref) => {
|
||||
return ref.content_type === note.content_type;
|
||||
}) as SNTag[]
|
||||
}
|
||||
|
||||
getSelectedTag() {
|
||||
return this.selectedTag;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ export class WebDirective implements ng.IDirective {
|
||||
controllerAs?: string;
|
||||
bindToController?: boolean | { [boundProperty: string]: string };
|
||||
restrict?: string;
|
||||
replace?: boolean
|
||||
scope?: boolean | { [boundProperty: string]: string };
|
||||
template?: string | ((tElement: any, tAttrs: any) => string);
|
||||
}
|
||||
@@ -15,4 +16,13 @@ export enum PasswordWizardType {
|
||||
export interface PasswordWizardScope extends Partial<ng.IScope> {
|
||||
type: PasswordWizardType,
|
||||
application: any
|
||||
}
|
||||
|
||||
export type PanelPuppet = {
|
||||
onReady: () => void
|
||||
ready?: boolean
|
||||
setWidth?: (width: number) => void
|
||||
setLeft?: (left: number) => void
|
||||
isCollapsed?: () => boolean
|
||||
flash?: () => void
|
||||
}
|
||||
@@ -17,12 +17,12 @@
|
||||
)
|
||||
.title
|
||||
input#note-title-editor.input(
|
||||
ng-blur='self.onNameBlur()',
|
||||
ng-blur='self.onTitleBlur()',
|
||||
ng-change='self.onTitleChange()',
|
||||
ng-disabled='self.noteLocked',
|
||||
ng-focus='self.onNameFocus()',
|
||||
ng-focus='self.onTitleFocus()',
|
||||
ng-keyup='$event.keyCode == 13 && self.onTitleEnter($event)',
|
||||
ng-model='self.state.note.title',
|
||||
ng-model='self.editorValues.title',
|
||||
select-on-click='true',
|
||||
spellcheck='false')
|
||||
#save-status
|
||||
@@ -39,11 +39,11 @@
|
||||
application='self.application'
|
||||
)
|
||||
input.tags-input(
|
||||
ng-blur='self.saveTags()',
|
||||
ng-blur='self.saveTagsFromStrings()',
|
||||
ng-disabled='self.noteLocked',
|
||||
ng-if='!(self.state.tagsComponent && self.state.tagsComponent.active)',
|
||||
ng-keyup='$event.keyCode == 13 && $event.target.blur();',
|
||||
ng-model='self.state.mutable.tagsString',
|
||||
ng-model='self.editorValues.tagsInputValue',
|
||||
placeholder='#tags',
|
||||
spellcheck='false',
|
||||
type='text'
|
||||
@@ -85,7 +85,7 @@
|
||||
)
|
||||
menu-row(
|
||||
action='self.selectedMenuItem(true); self.toggleNotePreview()',
|
||||
circle="self.state.note.content.hidePreview ? 'danger' : 'success'",
|
||||
circle="self.state.note.hidePreview ? 'danger' : 'success'",
|
||||
circle-align="'right'",
|
||||
desc="'Hide or unhide the note preview from the list of notes'",
|
||||
label="'Preview'"
|
||||
@@ -130,7 +130,7 @@
|
||||
.sk-menu-panel-header
|
||||
.sk-menu-panel-header-title Global Display
|
||||
menu-row(
|
||||
action="self.selectedMenuItem(true); self.toggleKey(self.prefKeyMonospace)",
|
||||
action="self.selectedMenuItem(true); self.togglePrefKey(self.prefKeyMonospace)",
|
||||
circle="self.state.monospaceEnabled ? 'success' : 'neutral'",
|
||||
desc="'Toggles the font style for the default editor'",
|
||||
disabled='self.state.selectedEditor',
|
||||
@@ -138,7 +138,7 @@
|
||||
subtitle="self.state.selectedEditor ? 'Not available with editor extensions' : null"
|
||||
)
|
||||
menu-row(
|
||||
action="self.selectedMenuItem(true); self.toggleKey(self.prefKeySpellcheck)",
|
||||
action="self.selectedMenuItem(true); self.togglePrefKey(self.prefKeySpellcheck)",
|
||||
circle="self.state.spellcheck ? 'success' : 'neutral'",
|
||||
desc="'Toggles spellcheck for the default editor'",
|
||||
disabled='self.state.selectedEditor',
|
||||
@@ -149,7 +149,7 @@
|
||||
: (self.state.isDesktop ? 'May degrade editor performance' : null)
|
||||
`)
|
||||
menu-row(
|
||||
action="self.selectedMenuItem(true); self.toggleKey(self.prefKeyMarginResizers)",
|
||||
action="self.selectedMenuItem(true); self.togglePrefKey(self.prefKeyMarginResizers)",
|
||||
circle="self.state.marginResizersEnabled ? 'success' : 'neutral'",
|
||||
desc="'Allows for editor left and right margins to be resized'",
|
||||
faded='!self.state.marginResizersEnabled',
|
||||
@@ -217,7 +217,7 @@
|
||||
ng-click='self.clickedTextArea()',
|
||||
ng-focus='self.onContentFocus()',
|
||||
ng-if='!self.state.selectedEditor',
|
||||
ng-model='self.state.note.text',
|
||||
ng-model='self.editorValues.text',
|
||||
ng-model-options='{ debounce: self.state.editorDebounce}',
|
||||
ng-readonly='self.noteLocked',
|
||||
ng-trim='false'
|
||||
|
||||
@@ -120,8 +120,8 @@
|
||||
.note-preview(
|
||||
ng-if=`
|
||||
!self.state.hideNotePreview &&
|
||||
!note.content.hidePreview &&
|
||||
!note.content.protected`
|
||||
!note.hidePreview &&
|
||||
!note.protected`
|
||||
)
|
||||
.html-preview(
|
||||
ng-bind-html='note.content.preview_html',
|
||||
|
||||
@@ -23,7 +23,11 @@
|
||||
ng-repeat='tag in self.state.smartTags'
|
||||
)
|
||||
.tag-info
|
||||
input.title(ng-disabled='true', ng-model='tag.title')
|
||||
input.title(
|
||||
ng-disabled='true',
|
||||
ng-change='self.onTagTitleChange(tag)'
|
||||
ng-model='self.titles[tag.uuid]'
|
||||
)
|
||||
.count(ng-show='tag.content.isAllTag') {{self.state.noteCounts[tag.uuid]}}
|
||||
.tags-title-section.section-title-bar
|
||||
.section-title-bar-header
|
||||
@@ -38,12 +42,12 @@
|
||||
.tag-icon #
|
||||
input.title(
|
||||
ng-attr-id='tag-{{tag.uuid}}',
|
||||
ng-blur='self.saveTag($event, tag)',
|
||||
ng-change='self.tagTitleDidChange(tag)',
|
||||
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()',
|
||||
ng-model='tag.title',
|
||||
should-focus='self.state.newTag || self.state.editingTag == tag',
|
||||
sn-autofocus='true',
|
||||
spellcheck='false'
|
||||
@@ -53,8 +57,8 @@
|
||||
.danger.small-text.bold(ng-show='tag.errorDecrypting && !tag.waitingForKey') Missing Keys
|
||||
.info.small-text.bold(ng-show='tag.errorDecrypting && tag.waitingForKey') Waiting For Keys
|
||||
.menu(ng-show='self.state.selectedTag == tag')
|
||||
a.item(ng-click='self.selectedRenameTag($event, 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.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.
|
||||
|
||||
Reference in New Issue
Block a user