Editor TypeScript

This commit is contained in:
Mo Bitar
2020-04-11 19:42:50 -05:00
parent 16bde8d1d4
commit 2bc3658f1a
15 changed files with 794 additions and 614 deletions

View File

@@ -43,6 +43,10 @@ export class PureCtrl {
this.deinit(); this.deinit();
} }
public get appState() {
return this.application!.getAppState();
}
/** @private */ /** @private */
async resetState() { async resetState() {
this.state = this.getInitialState(); this.state = this.getInitialState();
@@ -66,6 +70,10 @@ export class PureCtrl {
}); });
} }
async updateUI(func: () => void) {
this.$timeout(func);
}
initProps(props: CtrlProps) { initProps(props: CtrlProps) {
if (Object.keys(this.props).length > 0) { if (Object.keys(this.props).length > 0) {
throw 'Already init-ed props.'; throw 'Already init-ed props.';

View File

@@ -157,10 +157,10 @@ class FooterCtrl extends PureCtrl {
streamItems() { streamItems() {
this.application.streamItems({ this.application.streamItems({
contentType: ContentTypes.Component, contentType: ContentType.Component,
stream: async () => { stream: async () => {
this.rooms = this.application.getItems({ this.rooms = this.application.getItems({
contentType: ContentTypes.Component contentType: ContentType.Component
}).filter((candidate) => { }).filter((candidate) => {
return candidate.area === 'rooms' && !candidate.deleted; return candidate.area === 'rooms' && !candidate.deleted;
}); });
@@ -175,7 +175,7 @@ class FooterCtrl extends PureCtrl {
contentType: 'SN|Theme', contentType: 'SN|Theme',
stream: async () => { stream: async () => {
const themes = this.application.getDisplayableItems({ const themes = this.application.getDisplayableItems({
contentType: ContentTypes.Theme contentType: ContentType.Theme
}).filter((candidate) => { }).filter((candidate) => {
return ( return (
!candidate.deleted && !candidate.deleted &&

View File

@@ -3,7 +3,7 @@ import template from '%/notes.pug';
import { ApplicationEvent, ContentTypes, removeFromArray } from 'snjs'; import { ApplicationEvent, ContentTypes, removeFromArray } from 'snjs';
import { PureCtrl } from '@Controllers'; import { PureCtrl } from '@Controllers';
import { AppStateEvent } from '@/services/state'; import { AppStateEvent } from '@/services/state';
import { KeyboardModifiers, KeyboardKeys } from '@/services/keyboardManager'; import { KeyboardModifier, KeyboardKey } from '@/services/keyboardManager';
import { import {
PrefKeys PrefKeys
} from '@/services/preferencesManager'; } from '@/services/preferencesManager';
@@ -155,7 +155,7 @@ class NotesCtrl extends PureCtrl {
streamNotesAndTags() { streamNotesAndTags() {
this.application.streamItems({ this.application.streamItems({
contentType: [ContentTypes.Note, ContentTypes.Tag], contentType: [ContentType.Note, ContentType.Tag],
stream: async ({ items }) => { stream: async ({ items }) => {
await this.reloadNotes(); await this.reloadNotes();
const selectedNote = this.state.selectedNote; const selectedNote = this.state.selectedNote;
@@ -169,7 +169,7 @@ class NotesCtrl extends PureCtrl {
} }
/** Note has changed values, reset its flags */ /** 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) { for (const note of notes) {
if(note.deleted) { if(note.deleted) {
continue; continue;
@@ -202,7 +202,7 @@ class NotesCtrl extends PureCtrl {
title = `Note ${this.state.notes.length + 1}`; title = `Note ${this.state.notes.length + 1}`;
} }
const newNote = await this.application.createManagedItem({ const newNote = await this.application.createManagedItem({
contentType: ContentTypes.Note, contentType: ContentType.Note,
content: { content: {
text: '', text: '',
title: title title: title
@@ -680,8 +680,8 @@ class NotesCtrl extends PureCtrl {
this.newNoteKeyObserver = this.application.getKeyboardService().addKeyObserver({ this.newNoteKeyObserver = this.application.getKeyboardService().addKeyObserver({
key: 'n', key: 'n',
modifiers: [ modifiers: [
KeyboardModifiers.Meta, KeyboardModifier.Meta,
KeyboardModifiers.Ctrl KeyboardModifier.Ctrl
], ],
onKeyDown: (event) => { onKeyDown: (event) => {
event.preventDefault(); event.preventDefault();
@@ -690,7 +690,7 @@ class NotesCtrl extends PureCtrl {
}); });
this.nextNoteKeyObserver = this.application.getKeyboardService().addKeyObserver({ this.nextNoteKeyObserver = this.application.getKeyboardService().addKeyObserver({
key: KeyboardKeys.Down, key: KeyboardKey.Down,
elements: [ elements: [
document.body, document.body,
this.getSearchBar() this.getSearchBar()
@@ -705,7 +705,7 @@ class NotesCtrl extends PureCtrl {
}); });
this.nextNoteKeyObserver = this.application.getKeyboardService().addKeyObserver({ this.nextNoteKeyObserver = this.application.getKeyboardService().addKeyObserver({
key: KeyboardKeys.Up, key: KeyboardKey.Up,
element: document.body, element: document.body,
onKeyDown: (event) => { onKeyDown: (event) => {
this.selectPreviousNote(); this.selectPreviousNote();
@@ -715,8 +715,8 @@ class NotesCtrl extends PureCtrl {
this.searchKeyObserver = this.application.getKeyboardService().addKeyObserver({ this.searchKeyObserver = this.application.getKeyboardService().addKeyObserver({
key: "f", key: "f",
modifiers: [ modifiers: [
KeyboardModifiers.Meta, KeyboardModifier.Meta,
KeyboardModifiers.Shift KeyboardModifier.Shift
], ],
onKeyDown: (event) => { onKeyDown: (event) => {
const searchBar = this.getSearchBar(); const searchBar = this.getSearchBar();

View File

@@ -1,21 +1,40 @@
import { WebDirective, PanelPuppet } from './../types';
import { WebApplication } from './../application';
import { import {
SNNote, SNNote,
SNSmartTag, SNTag,
ContentTypes, ContentType,
ApplicationEvent, ApplicationEvent,
ComponentActions ComponentAction,
SNSmartTag,
ComponentArea,
SNComponent
} from 'snjs'; } from 'snjs';
import template from '%/tags.pug'; import template from '%/tags.pug';
import { AppStateEvent } from '@/services/state'; import { AppStateEvent } from '@/services/state';
import { PANEL_NAME_TAGS } from '@/controllers/constants'; import { PANEL_NAME_TAGS } from '@/controllers/constants';
import { PrefKeys } from '@/services/preferencesManager'; import { PrefKeys } from '@/services/preferencesManager';
import { STRING_DELETE_TAG } from '@/strings'; 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 { 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 */ /* @ngInject */
constructor( constructor(
$timeout, $timeout: ng.ITimeoutService,
) { ) {
super($timeout); super($timeout);
this.panelPuppet = { this.panelPuppet = {
@@ -25,7 +44,7 @@ class TagsPanelCtrl extends PureCtrl {
deinit() { deinit() {
this.unregisterComponent(); this.unregisterComponent();
this.unregisterComponent = null; this.unregisterComponent = undefined;
super.deinit(); super.deinit();
} }
@@ -37,12 +56,12 @@ class TagsPanelCtrl extends PureCtrl {
}; };
} }
onAppStart() { async onAppStart() {
super.onAppStart(); super.onAppStart();
this.registerComponentHandler(); this.registerComponentHandler();
} }
onAppLaunch() { async onAppLaunch() {
super.onAppLaunch(); super.onAppLaunch();
this.loadPreferences(); this.loadPreferences();
this.beginStreamingItems(); this.beginStreamingItems();
@@ -64,20 +83,21 @@ class TagsPanelCtrl extends PureCtrl {
* @access private * @access private
*/ */
getMappedTags() { getMappedTags() {
const tags = this.application.getItems({ contentType: ContentTypes.Tag }); const tags = this.application.getItems(ContentType.Tag) as SNTag[];
return tags.sort((a, b) => { return tags.sort((a, b) => {
return a.content.title < b.content.title ? -1 : 1; return a.title < b.title ? -1 : 1;
}); });
} }
beginStreamingItems() { beginStreamingItems() {
this.application.streamItems({ this.application.streamItems(
contentType: ContentTypes.Tag, ContentType.Tag,
stream: async ({ items }) => { async (items) => {
await this.setState({ await this.setState({
tags: this.getMappedTags(), tags: this.getMappedTags(),
smartTags: this.application.getSmartTags(), smartTags: this.application.getSmartTags(),
}); });
this.reloadTitles(items as SNTag[]);
this.reloadNoteCounts(); this.reloadNoteCounts();
if (this.state.selectedTag) { if (this.state.selectedTag) {
/** If the selected tag has been deleted, revert to All view. */ /** 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 */ /** @override */
onAppStateEvent(eventName, data) { onAppStateEvent(eventName: AppStateEvent, data?: any) {
if (eventName === AppStateEvent.PreferencesChanged) { if (eventName === AppStateEvent.PreferencesChanged) {
this.loadPreferences(); this.loadPreferences();
} else if (eventName === AppStateEvent.TagChanged) { } else if (eventName === AppStateEvent.TagChanged) {
@@ -105,7 +131,7 @@ class TagsPanelCtrl extends PureCtrl {
/** @override */ /** @override */
async onAppEvent(eventName) { async onAppEvent(eventName: ApplicationEvent) {
super.onAppEvent(eventName); super.onAppEvent(eventName);
if (eventName === ApplicationEvent.LocalDataIncrementalLoad) { if (eventName === ApplicationEvent.LocalDataIncrementalLoad) {
this.reloadNoteCounts(); this.reloadNoteCounts();
@@ -119,19 +145,25 @@ class TagsPanelCtrl extends PureCtrl {
} }
reloadNoteCounts() { reloadNoteCounts() {
let allTags = []; let allTags: Array<SNTag | SNSmartTag> = [];
if (this.state.tags) { if (this.state.tags) {
allTags = allTags.concat(this.state.tags); allTags = allTags.concat(this.state.tags);
} }
if (this.state.smartTags) { if (this.state.smartTags) {
allTags = allTags.concat(this.state.smartTags); allTags = allTags.concat(this.state.smartTags);
} }
const noteCounts = {}; const noteCounts: NoteCounts = {};
for (const tag of allTags) { for (const tag of allTags) {
const validNotes = SNNote.filterDummyNotes(tag.notes).filter((note) => { if (tag.isSmartTag()) {
return !note.archived && !note.content.trashed; const notes = this.application.notesMatchingSmartTag(tag as SNSmartTag);
}); noteCounts[tag.uuid] = notes.length;
noteCounts[tag.uuid] = validNotes.length; } else {
const notes = this.application.referencesForItem(tag, ContentType.Note)
.filter((note) => {
return !note.archived && !note.trashed;
})
noteCounts[tag.uuid] = notes.length;
}
} }
this.setState({ this.setState({
noteCounts: noteCounts noteCounts: noteCounts
@@ -144,17 +176,22 @@ class TagsPanelCtrl extends PureCtrl {
} }
const width = this.application.getPrefsService().getValue(PrefKeys.TagsPanelWidth); const width = this.application.getPrefsService().getValue(PrefKeys.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.getAppState().panelDidResize(
PANEL_NAME_TAGS, 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( this.application.getPrefsService().setUserPrefValue(
PrefKeys.TagsPanelWidth, PrefKeys.TagsPanelWidth,
newWidth, newWidth,
@@ -167,50 +204,49 @@ class TagsPanelCtrl extends PureCtrl {
} }
registerComponentHandler() { registerComponentHandler() {
this.unregisterComponent = this.application.componentManager.registerHandler({ this.unregisterComponent = this.application.componentManager!.registerHandler({
identifier: 'tags', identifier: 'tags',
areas: ['tags-list'], areas: [ComponentArea.TagsList],
activationHandler: (component) => { activationHandler: (component) => {
this.component = component; this.component = component;
}, },
contextRequestHandler: (component) => { contextRequestHandler: () => {
return null; return undefined;
}, },
actionHandler: (_, action, data) => { actionHandler: (_, action, data) => {
if (action === ComponentActions.SelectItem) { if (action === ComponentAction.SelectItem) {
if (data.item.content_type === ContentTypes.Tag) { if (data.item.content_type === ContentType.Tag) {
const tag = this.application.findItem({ uuid: data.item.uuid }); const tag = this.application.findItem(data.item.uuid);
if (tag) { if (tag) {
this.selectTag(tag); this.selectTag(tag as SNTag);
} }
} else if (data.item.content_type === ContentTypes.SmartTag) { } else if (data.item.content_type === ContentType.SmartTag) {
this.application.createTemplateItem({ this.application.createTemplateItem(
contentType: ContentTypes.SmartTag, ContentType.SmartTag,
content: data.item.content data.item.content
}).then(smartTag => { ).then(smartTag => {
this.selectTag(smartTag); this.selectTag(smartTag as SNSmartTag);
}); });
} }
} else if (action === ComponentActions.ClearSelection) { } else if (action === ComponentAction.ClearSelection) {
this.selectTag(this.state.smartTags[0]); this.selectTag(this.state.smartTags[0]);
} }
} }
}); });
} }
async selectTag(tag) { async selectTag(tag: SNTag) {
if (tag.isSmartTag()) { if (tag.isSmartTag()) {
Object.defineProperty(tag, 'notes', { Object.defineProperty(tag, 'notes', {
get: () => { get: () => {
return this.application.getNotesMatchingSmartTag({ return this.application.notesMatchingSmartTag(tag as SNSmartTag);
smartTag: tag
});
} }
}); });
} }
if (tag.content.conflict_of) { if (tag.conflictOf) {
tag.content.conflict_of = null; this.application.changeAndSaveItem(tag.uuid, (mutator) => {
this.application.saveItem({ item: tag }); mutator.conflictOf = undefined;
})
} }
this.application.getAppState().setSelectedTag(tag); this.application.getAppState().setSelectedTag(tag);
} }
@@ -219,9 +255,9 @@ class TagsPanelCtrl extends PureCtrl {
if (this.state.editingTag) { if (this.state.editingTag) {
return; return;
} }
const newTag = await this.application.createTemplateItem({ const newTag = await this.application.createTemplateItem(
contentType: ContentTypes.Tag ContentType.Tag
}); );
this.setState({ this.setState({
tags: [newTag].concat(this.state.tags), tags: [newTag].concat(this.state.tags),
previousTag: this.state.selectedTag, previousTag: this.state.selectedTag,
@@ -231,14 +267,14 @@ class TagsPanelCtrl extends PureCtrl {
}); });
} }
tagTitleDidChange(tag) { onTagTitleChange(tag: SNTag | SNSmartTag) {
this.setState({ this.setState({
editingTag: tag editingTag: tag
}); });
} }
async saveTag($event, tag) { async saveTag($event: Event, tag: SNTag) {
$event.target.blur(); ($event.target! as HTMLInputElement).blur();
await this.setState({ await this.setState({
editingTag: null, editingTag: null,
}); });
@@ -246,8 +282,8 @@ class TagsPanelCtrl extends PureCtrl {
if (!tag.title || tag.title.length === 0) { if (!tag.title || tag.title.length === 0) {
let newSelectedTag = this.state.selectedTag; let newSelectedTag = this.state.selectedTag;
if (this.state.editingTag) { if (this.state.editingTag) {
tag.title = this.editingOriginalName; this.titles[tag.uuid] = this.editingOriginalName;
this.editingOriginalName = null; this.editingOriginalName = undefined;
} else if (this.state.newTag) { } else if (this.state.newTag) {
newSelectedTag = this.state.previousTag; newSelectedTag = this.state.previousTag;
} }
@@ -259,14 +295,14 @@ class TagsPanelCtrl extends PureCtrl {
return; 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; const alreadyExists = matchingTag && matchingTag !== tag;
if (this.state.newTag === tag && alreadyExists) { if (this.state.newTag === tag && alreadyExists) {
this.application.alertService.alert({ this.application.alertService!.alert(
text: "A tag with this name already exists." "A tag with this name already exists."
}); );
this.setState({ this.setState({
newTag: null, newTag: null,
tags: this.getMappedTags(), tags: this.getMappedTags(),
@@ -274,40 +310,48 @@ class TagsPanelCtrl extends PureCtrl {
}); });
return; return;
} }
this.application.changeAndSaveItem(tag.uuid, (mutator) => {
this.application.saveItem({ item: tag }); const tagMutator = mutator as TagMutator;
tagMutator.title = this.titles[tag.uuid]!;
});
this.selectTag(tag); this.selectTag(tag);
this.setState({ this.setState({
newTag: null newTag: null
}); });
} }
async selectedRenameTag($event, tag) { async selectedRenameTag(tag: SNTag) {
this.editingOriginalName = tag.title; this.editingOriginalName = tag.title;
await this.setState({ await this.setState({
editingTag: tag editingTag: tag
}); });
document.getElementById('tag-' + tag.uuid).focus(); document.getElementById('tag-' + tag.uuid)!.focus();
} }
selectedDeleteTag(tag) { selectedDeleteTag(tag: SNTag) {
this.removeTag(tag); this.removeTag(tag);
} }
removeTag(tag) { removeTag(tag: SNTag) {
this.application.alertService.confirm({ this.application.alertService!.confirm(
text: STRING_DELETE_TAG, STRING_DELETE_TAG,
destructive: true, undefined,
onConfirm: () => { undefined,
this.application.deleteItem({ item: tag }); undefined,
() => {
/* On confirm */
this.application.deleteItem(tag);
this.selectTag(this.state.smartTags[0]); this.selectTag(this.state.smartTags[0]);
} },
}); undefined,
true,
);
} }
} }
export class TagsPanel { export class TagsPanel extends WebDirective {
constructor() { constructor() {
super();
this.restrict = 'E'; this.restrict = 'E';
this.scope = { this.scope = {
application: '=' application: '='

View File

@@ -27,7 +27,7 @@ class RevisionPreviewModalCtrl {
async configure() { async configure() {
this.note = await this.application.createTemplateItem({ this.note = await this.application.createTemplateItem({
contentType: ContentTypes.Note, contentType: ContentType.Note,
content: this.content content: this.content
}); });
@@ -45,7 +45,7 @@ class RevisionPreviewModalCtrl {
* editor object has non-copyable properties like .window, which cannot be transfered * editor object has non-copyable properties like .window, which cannot be transfered
*/ */
const editorCopy = await this.application.createTemplateItem({ const editorCopy = await this.application.createTemplateItem({
contentType: ContentTypes.Component, contentType: ContentType.Component,
content: editorForNote.content content: editorForNote.content
}); });
this.application.component.setReadonlyStateForComponent(editorCopy, true, true); this.application.component.setReadonlyStateForComponent(editorCopy, true, true);

View File

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

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

View File

@@ -26,7 +26,7 @@ export class NativeExtManager extends ApplicationService {
get extManagerPred() { get extManagerPred() {
const extManagerId = 'org.standardnotes.extensions-manager'; const extManagerId = 'org.standardnotes.extensions-manager';
return SNPredicate.CompoundPredicate([ return SNPredicate.CompoundPredicate([
new SNPredicate('content_type', '=', ContentTypes.Component), new SNPredicate('content_type', '=', ContentType.Component),
new SNPredicate('package_info.identifier', '=', extManagerId) new SNPredicate('package_info.identifier', '=', extManagerId)
]); ]);
} }
@@ -34,7 +34,7 @@ export class NativeExtManager extends ApplicationService {
get batchManagerPred() { get batchManagerPred() {
const batchMgrId = 'org.standardnotes.batch-manager'; const batchMgrId = 'org.standardnotes.batch-manager';
return SNPredicate.CompoundPredicate([ return SNPredicate.CompoundPredicate([
new SNPredicate('content_type', '=', ContentTypes.Component), new SNPredicate('content_type', '=', ContentType.Component),
new SNPredicate('package_info.identifier', '=', batchMgrId) new SNPredicate('package_info.identifier', '=', batchMgrId)
]); ]);
} }
@@ -65,8 +65,8 @@ export class NativeExtManager extends ApplicationService {
} }
// Handle addition of SN|ExtensionRepo permission // Handle addition of SN|ExtensionRepo permission
const permission = extensionsManager.content.permissions.find((p) => p.name === STREAM_ITEMS_PERMISSION); const permission = extensionsManager.content.permissions.find((p) => p.name === STREAM_ITEMS_PERMISSION);
if (!permission.content_types.includes(ContentTypes.ExtensionRepo)) { if (!permission.content_types.includes(ContentType.ExtensionRepo)) {
permission.content_types.push(ContentTypes.ExtensionRepo); permission.content_types.push(ContentType.ExtensionRepo);
needsSync = true; needsSync = true;
} }
if (needsSync) { if (needsSync) {
@@ -92,13 +92,13 @@ export class NativeExtManager extends ApplicationService {
{ {
name: STREAM_ITEMS_PERMISSION, name: STREAM_ITEMS_PERMISSION,
content_types: [ content_types: [
ContentTypes.Component, ContentType.Component,
ContentTypes.Theme, ContentType.Theme,
ContentTypes.ServerExtension, ContentType.ServerExtension,
ContentTypes.ActionsExtension, ContentType.ActionsExtension,
ContentTypes.Mfa, ContentType.Mfa,
ContentTypes.Editor, ContentType.Editor,
ContentTypes.ExtensionRepo ContentType.ExtensionRepo
] ]
} }
] ]
@@ -110,7 +110,7 @@ export class NativeExtManager extends ApplicationService {
} }
const payload = CreateMaxPayloadFromAnyObject({ const payload = CreateMaxPayloadFromAnyObject({
object: { object: {
content_type: ContentTypes.Component, content_type: ContentType.Component,
content: content content: content
} }
}); });
@@ -146,7 +146,7 @@ export class NativeExtManager extends ApplicationService {
} }
const payload = CreateMaxPayloadFromAnyObject({ const payload = CreateMaxPayloadFromAnyObject({
object: { object: {
content_type: ContentTypes.Component, content_type: ContentType.Component,
content: content content: content
} }
}); });
@@ -172,8 +172,8 @@ export class NativeExtManager extends ApplicationService {
} }
// Handle addition of SN|ExtensionRepo permission // Handle addition of SN|ExtensionRepo permission
const permission = batchManager.content.permissions.find((p) => p.name === STREAM_ITEMS_PERMISSION); const permission = batchManager.content.permissions.find((p) => p.name === STREAM_ITEMS_PERMISSION);
if (!permission.content_types.includes(ContentTypes.ExtensionRepo)) { if (!permission.content_types.includes(ContentType.ExtensionRepo)) {
permission.content_types.push(ContentTypes.ExtensionRepo); permission.content_types.push(ContentType.ExtensionRepo);
needsSync = true; needsSync = true;
} }
if (needsSync) { if (needsSync) {

View File

@@ -33,7 +33,7 @@ export class PreferencesManager extends ApplicationService {
streamPreferences() { streamPreferences() {
this.application.streamItems({ this.application.streamItems({
contentType: ContentTypes.UserPrefs, contentType: ContentType.UserPrefs,
stream: () => { stream: () => {
this.loadSingleton(); this.loadSingleton();
} }
@@ -41,7 +41,7 @@ export class PreferencesManager extends ApplicationService {
} }
async loadSingleton() { async loadSingleton() {
const contentType = ContentTypes.UserPrefs; const contentType = ContentType.UserPrefs;
const predicate = new SNPredicate('content_type', '=', contentType); const predicate = new SNPredicate('content_type', '=', contentType);
this.userPreferences = await this.application.singletonManager.findOrCreateSingleton({ this.userPreferences = await this.application.singletonManager.findOrCreateSingleton({
predicate: predicate, predicate: predicate,

View File

@@ -140,7 +140,7 @@ export class AppState {
); );
} }
async setSelectedNote(note: SNNote) { async setSelectedNote(note?: SNNote) {
const run = async () => { const run = async () => {
const previousNote = this.selectedNote; const previousNote = this.selectedNote;
this.selectedNote = note; 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() { getSelectedTag() {
return this.selectedTag; return this.selectedTag;
} }

View File

@@ -3,6 +3,7 @@ export class WebDirective implements ng.IDirective {
controllerAs?: string; controllerAs?: string;
bindToController?: boolean | { [boundProperty: string]: string }; bindToController?: boolean | { [boundProperty: string]: string };
restrict?: string; restrict?: string;
replace?: boolean
scope?: boolean | { [boundProperty: string]: string }; scope?: boolean | { [boundProperty: string]: string };
template?: string | ((tElement: any, tAttrs: any) => string); template?: string | ((tElement: any, tAttrs: any) => string);
} }
@@ -15,4 +16,13 @@ export enum PasswordWizardType {
export interface PasswordWizardScope extends Partial<ng.IScope> { export interface PasswordWizardScope extends Partial<ng.IScope> {
type: PasswordWizardType, type: PasswordWizardType,
application: any application: any
}
export type PanelPuppet = {
onReady: () => void
ready?: boolean
setWidth?: (width: number) => void
setLeft?: (left: number) => void
isCollapsed?: () => boolean
flash?: () => void
} }

View File

@@ -17,12 +17,12 @@
) )
.title .title
input#note-title-editor.input( input#note-title-editor.input(
ng-blur='self.onNameBlur()', ng-blur='self.onTitleBlur()',
ng-change='self.onTitleChange()', ng-change='self.onTitleChange()',
ng-disabled='self.noteLocked', ng-disabled='self.noteLocked',
ng-focus='self.onNameFocus()', ng-focus='self.onTitleFocus()',
ng-keyup='$event.keyCode == 13 && self.onTitleEnter($event)', ng-keyup='$event.keyCode == 13 && self.onTitleEnter($event)',
ng-model='self.state.note.title', ng-model='self.editorValues.title',
select-on-click='true', select-on-click='true',
spellcheck='false') spellcheck='false')
#save-status #save-status
@@ -39,11 +39,11 @@
application='self.application' application='self.application'
) )
input.tags-input( input.tags-input(
ng-blur='self.saveTags()', ng-blur='self.saveTagsFromStrings()',
ng-disabled='self.noteLocked', ng-disabled='self.noteLocked',
ng-if='!(self.state.tagsComponent && self.state.tagsComponent.active)', ng-if='!(self.state.tagsComponent && self.state.tagsComponent.active)',
ng-keyup='$event.keyCode == 13 && $event.target.blur();', ng-keyup='$event.keyCode == 13 && $event.target.blur();',
ng-model='self.state.mutable.tagsString', ng-model='self.editorValues.tagsInputValue',
placeholder='#tags', placeholder='#tags',
spellcheck='false', spellcheck='false',
type='text' type='text'
@@ -85,7 +85,7 @@
) )
menu-row( menu-row(
action='self.selectedMenuItem(true); self.toggleNotePreview()', action='self.selectedMenuItem(true); self.toggleNotePreview()',
circle="self.state.note.content.hidePreview ? 'danger' : 'success'", circle="self.state.note.hidePreview ? 'danger' : 'success'",
circle-align="'right'", circle-align="'right'",
desc="'Hide or unhide the note preview from the list of notes'", desc="'Hide or unhide the note preview from the list of notes'",
label="'Preview'" label="'Preview'"
@@ -130,7 +130,7 @@
.sk-menu-panel-header .sk-menu-panel-header
.sk-menu-panel-header-title Global Display .sk-menu-panel-header-title Global Display
menu-row( menu-row(
action="self.selectedMenuItem(true); self.toggleKey(self.prefKeyMonospace)", action="self.selectedMenuItem(true); self.togglePrefKey(self.prefKeyMonospace)",
circle="self.state.monospaceEnabled ? 'success' : 'neutral'", circle="self.state.monospaceEnabled ? 'success' : 'neutral'",
desc="'Toggles the font style for the default editor'", desc="'Toggles the font style for the default editor'",
disabled='self.state.selectedEditor', disabled='self.state.selectedEditor',
@@ -138,7 +138,7 @@
subtitle="self.state.selectedEditor ? 'Not available with editor extensions' : null" subtitle="self.state.selectedEditor ? 'Not available with editor extensions' : null"
) )
menu-row( menu-row(
action="self.selectedMenuItem(true); self.toggleKey(self.prefKeySpellcheck)", action="self.selectedMenuItem(true); self.togglePrefKey(self.prefKeySpellcheck)",
circle="self.state.spellcheck ? 'success' : 'neutral'", circle="self.state.spellcheck ? 'success' : 'neutral'",
desc="'Toggles spellcheck for the default editor'", desc="'Toggles spellcheck for the default editor'",
disabled='self.state.selectedEditor', disabled='self.state.selectedEditor',
@@ -149,7 +149,7 @@
: (self.state.isDesktop ? 'May degrade editor performance' : null) : (self.state.isDesktop ? 'May degrade editor performance' : null)
`) `)
menu-row( menu-row(
action="self.selectedMenuItem(true); self.toggleKey(self.prefKeyMarginResizers)", action="self.selectedMenuItem(true); self.togglePrefKey(self.prefKeyMarginResizers)",
circle="self.state.marginResizersEnabled ? 'success' : 'neutral'", circle="self.state.marginResizersEnabled ? 'success' : 'neutral'",
desc="'Allows for editor left and right margins to be resized'", desc="'Allows for editor left and right margins to be resized'",
faded='!self.state.marginResizersEnabled', faded='!self.state.marginResizersEnabled',
@@ -217,7 +217,7 @@
ng-click='self.clickedTextArea()', ng-click='self.clickedTextArea()',
ng-focus='self.onContentFocus()', ng-focus='self.onContentFocus()',
ng-if='!self.state.selectedEditor', ng-if='!self.state.selectedEditor',
ng-model='self.state.note.text', ng-model='self.editorValues.text',
ng-model-options='{ debounce: self.state.editorDebounce}', ng-model-options='{ debounce: self.state.editorDebounce}',
ng-readonly='self.noteLocked', ng-readonly='self.noteLocked',
ng-trim='false' ng-trim='false'

View File

@@ -120,8 +120,8 @@
.note-preview( .note-preview(
ng-if=` ng-if=`
!self.state.hideNotePreview && !self.state.hideNotePreview &&
!note.content.hidePreview && !note.hidePreview &&
!note.content.protected` !note.protected`
) )
.html-preview( .html-preview(
ng-bind-html='note.content.preview_html', ng-bind-html='note.content.preview_html',

View File

@@ -23,7 +23,11 @@
ng-repeat='tag in self.state.smartTags' ng-repeat='tag in self.state.smartTags'
) )
.tag-info .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]}} .count(ng-show='tag.content.isAllTag') {{self.state.noteCounts[tag.uuid]}}
.tags-title-section.section-title-bar .tags-title-section.section-title-bar
.section-title-bar-header .section-title-bar-header
@@ -38,12 +42,12 @@
.tag-icon # .tag-icon #
input.title( input.title(
ng-attr-id='tag-{{tag.uuid}}', ng-attr-id='tag-{{tag.uuid}}',
ng-blur='self.saveTag($event, tag)', ng-blur='self.saveTag($event, tag)'
ng-change='self.tagTitleDidChange(tag)', ng-change='self.onTagTitleChange(tag)',
ng-model='self.titles[tag.uuid]',
ng-class="{'editing' : self.state.editingTag == tag}", ng-class="{'editing' : self.state.editingTag == tag}",
ng-click='self.selectTag(tag)', ng-click='self.selectTag(tag)',
ng-keyup='$event.keyCode == 13 && $event.target.blur()', ng-keyup='$event.keyCode == 13 && $event.target.blur()',
ng-model='tag.title',
should-focus='self.state.newTag || self.state.editingTag == tag', should-focus='self.state.newTag || self.state.editingTag == tag',
sn-autofocus='true', sn-autofocus='true',
spellcheck='false' spellcheck='false'
@@ -53,8 +57,8 @@
.danger.small-text.bold(ng-show='tag.errorDecrypting && !tag.waitingForKey') Missing Keys .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 .info.small-text.bold(ng-show='tag.errorDecrypting && tag.waitingForKey') Waiting For Keys
.menu(ng-show='self.state.selectedTag == tag') .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.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.saveTag($event, tag)' ng-show='self.state.editingTag') Save
a.item(ng-click='self.selectedDeleteTag(tag)') Delete a.item(ng-click='self.selectedDeleteTag(tag)') Delete
.no-tags-placeholder(ng-show='self.state.tags.length == 0') .no-tags-placeholder(ng-show='self.state.tags.length == 0')
| No tags. Create one using the add button above. | No tags. Create one using the add button above.