Remove dummy concept in favor of editor group and editors

This commit is contained in:
Mo Bitar
2020-04-14 15:01:32 -05:00
parent ef66170ba4
commit 9cf99896a5
81 changed files with 8136 additions and 7179 deletions

View File

@@ -5,18 +5,17 @@ declare const __VERSION__: string
import angular from 'angular';
import { configRoutes } from './routes';
import {
ApplicationManager
} from './applicationManager';
import { ApplicationGroup } from './ui_models/application_group';
import {
Root,
ApplicationGroupView,
ApplicationView,
TagsPanel,
NotesPanel,
EditorPanel,
Footer
} from './controllers';
EditorGroupView,
EditorView,
TagsView,
NotesView,
FooterView
} from '@/views';
import {
autofocus,
@@ -62,13 +61,13 @@ angular
// Controllers
angular
.module('app')
.directive('root', () => new Root())
.directive('applicationGroupView', () => new ApplicationGroupView())
.directive('applicationView', () => new ApplicationView())
.directive('tagsPanel', () => new TagsPanel())
.directive('notesPanel', () => new NotesPanel())
.directive('editorPanel', () => new EditorPanel())
.directive('footer', () => new Footer())
// .directive('lockScreen', () => new LockScreen());
.directive('editorGroupView', () => new EditorGroupView())
.directive('editorView', () => new EditorView())
.directive('tagsView', () => new TagsView())
.directive('notesView', () => new NotesView())
.directive('footerView', () => new FooterView())
// Directives - Functional
angular
@@ -78,9 +77,7 @@ angular
.directive('delayHide', delayHide)
.directive('elemReady', elemReady)
.directive('fileChange', fileChange)
.directive('infiniteScroll', [
infiniteScroll
])
.directive('infiniteScroll', [infiniteScroll])
.directive('lowercase', lowercase)
.directive('selectOnClick', ['$window', selectOnClick])
.directive('snEnter', snEnter);
@@ -93,11 +90,6 @@ angular
.directive('challengeModal', () => new ChallengeModal())
.directive('componentModal', () => new ComponentModal())
.directive('componentView', () => new ComponentView())
// .directive(
// 'componentView',
// ($rootScope, componentManager, desktopManager, $timeout) =>
// new ComponentView($rootScope, componentManager, desktopManager, $timeout)
// )
.directive('editorMenu', () => new EditorMenu())
.directive('inputModal', () => new InputModal())
.directive('menuRow', () => new MenuRow())
@@ -116,4 +108,4 @@ angular
.filter('trusted', ['$sce', trusted]);
// Services
angular.module('app').service('applicationManager', ApplicationManager);
angular.module('app').service('mainApplicationGroup', ApplicationGroup);

View File

@@ -1,7 +0,0 @@
export { PureCtrl } from './abstract/pure_ctrl';
export { EditorPanel } from './editor';
export { Footer } from './footer';
export { NotesPanel } from './notes/notes';
export { TagsPanel } from './tags';
export { Root } from './root';
export { ApplicationView } from './applicationView';

View File

@@ -1,37 +0,0 @@
import { ApplicationManager } from './../applicationManager';
import { WebDirective } from './../types';
import template from '%/root.pug';
import { WebApplication } from '@/application';
class RootCtrl {
private $timeout: ng.ITimeoutService
private applicationManager: ApplicationManager
public applications: WebApplication[] = []
/* @ngInject */
constructor($timeout: ng.ITimeoutService, applicationManager: ApplicationManager) {
this.$timeout = $timeout;
this.applicationManager = applicationManager;
this.applicationManager.addApplicationChangeObserver(() => {
this.reload();
});
}
reload() {
this.$timeout(() => {
this.applications = this.applicationManager.getApplications();
});
}
}
export class Root extends WebDirective {
constructor() {
super();
this.template = template;
this.controller = RootCtrl;
this.replace = true;
this.controllerAs = 'self';
this.bindToController = true;
}
}

View File

@@ -1,6 +1,4 @@
import { WebApplication } from './application';
import { SNAlertService } from "../../../../snjs/dist/@types";
import { RawPayload } from '../../../../snjs/dist/@types/protocol/payloads/generator';
const DB_NAME = 'standardnotes';
const STORE_NAME = 'items';

View File

@@ -1,16 +1,20 @@
import { debounce } from '@/utils';
/* @ngInject */
export function infiniteScroll() {
return {
link: function (scope: ng.IScope, elem: JQLite, attrs: any) {
const scopeAny = scope as any;
const offset = parseInt(attrs.threshold) || 0;
const e = elem[0];
const element = elem[0];
scopeAny.paginate = debounce(() => {
scope.$apply(attrs.infiniteScroll);
}, 100);
scopeAny.onScroll = () => {
if (
scope.$eval(attrs.canLoad) &&
e.scrollTop + e.offsetHeight >= e.scrollHeight - offset
element.scrollTop + element.offsetHeight >= element.scrollHeight - offset
) {
scope.$apply(attrs.infiniteScroll);
scopeAny.paginate();
}
};
elem.on('scroll', scopeAny.onScroll);

View File

@@ -2,7 +2,7 @@ import { WebDirective } from './../../types';
import { isDesktopApplication, isNullOrUndefined } from '@/utils';
import template from '%/directives/account-menu.pug';
import { ProtectedAction, ContentType } from 'snjs';
import { PureCtrl } from '@Controllers/abstract/pure_ctrl';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import {
STRING_ACCOUNT_MENU_UNCHECK_MERGE,
STRING_SIGN_OUT_CONFIRMATION,
@@ -59,7 +59,7 @@ type AccountMenuState = {
importData: any
}
class AccountMenuCtrl extends PureCtrl {
class AccountMenuCtrl extends PureViewCtrl {
public appVersion: string
private syncStatus?: SyncOpStatus

View File

@@ -1,7 +1,7 @@
import { WebApplication } from '@/application';
import { WebApplication } from '@/ui_models/application';
import { WebDirective } from './../../types';
import template from '%/directives/actions-menu.pug';
import { PureCtrl } from '@Controllers/abstract/pure_ctrl';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import { SNItem, Action, SNActionsExtension } from '@/../../../../snjs/dist/@types';
import { ActionResponse } from '@/../../../../snjs/dist/@types/services/actions_service';
@@ -10,7 +10,7 @@ type ActionsMenuScope = {
item: SNItem
}
class ActionsMenuCtrl extends PureCtrl implements ActionsMenuScope {
class ActionsMenuCtrl extends PureViewCtrl implements ActionsMenuScope {
application!: WebApplication
item!: SNItem

View File

@@ -1,5 +1,5 @@
import { WebDirective } from './../../types';
import { WebApplication } from './../../application';
import { WebApplication } from '@/ui_models/application';
import template from '%/directives/challenge-modal.pug';
import {
ChallengeType,
@@ -8,7 +8,7 @@ import {
Challenge,
ChallengeOrchestrator
} from 'snjs';
import { PureCtrl } from '@Controllers/abstract/pure_ctrl';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
type InputValue = {
value: string
@@ -28,7 +28,7 @@ type ChallengeModalState = {
processing: boolean
}
class ChallengeModalCtrl extends PureCtrl implements ChallengeModalScope {
class ChallengeModalCtrl extends PureViewCtrl implements ChallengeModalScope {
private $element: JQLite
private processingTypes: ChallengeType[] = []
application!: WebApplication
@@ -159,7 +159,8 @@ export class ChallengeModal extends WebDirective {
this.template = template;
this.controller = ChallengeModalCtrl;
this.controllerAs = 'ctrl';
this.bindToController = {
this.bindToController = true;
this.scope = {
challenge: '=',
orchestrator: '=',
application: '='

View File

@@ -1,4 +1,4 @@
import { WebApplication } from './../../application';
import { WebApplication } from '@/ui_models/application';
import { SNComponent } from 'snjs';
import { WebDirective } from './../../types';
import template from '%/directives/component-modal.pug';

View File

@@ -1,4 +1,4 @@
import { WebApplication } from '@/application';
import { WebApplication } from '@/ui_models/application';
import { SNComponent } from 'snjs';
import { WebDirective } from './../../types';
import template from '%/directives/component-view.pug';

View File

@@ -1,9 +1,9 @@
import { WebDirective } from './../../types';
import { WebApplication } from '@/application';
import { WebApplication } from '@/ui_models/application';
import { SNComponent, SNItem, ComponentArea } from 'snjs';
import { isDesktopApplication } from '@/utils';
import template from '%/directives/editor-menu.pug';
import { PureCtrl } from '@Controllers/abstract/pure_ctrl';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import { ComponentMutator } from '@/../../../../snjs/dist/@types/models';
interface EditorMenuScope {
@@ -13,7 +13,7 @@ interface EditorMenuScope {
application: WebApplication
}
class EditorMenuCtrl extends PureCtrl implements EditorMenuScope {
class EditorMenuCtrl extends PureViewCtrl implements EditorMenuScope {
callback!: (component: SNComponent) => void
selectedEditor!: SNComponent

View File

@@ -84,7 +84,7 @@ class PanelResizerCtrl implements PanelResizerScope {
this.$timeout = $timeout;
/** To allow for registering events */
this.handleResize = this.handleResize.bind(this);
this.handleResize = debounce(this.handleResize.bind(this), 250);
this.onMouseMove = this.onMouseMove.bind(this);
this.onMouseUp = this.onMouseUp.bind(this);
this.onMouseDown = this.onMouseDown.bind(this);
@@ -163,13 +163,11 @@ class PanelResizerCtrl implements PanelResizerScope {
}
handleResize() {
debounce(() => {
this.reloadDefaultValues();
this.handleWidthEvent();
this.$timeout(() => {
this.finishSettingWidth();
});
}, 250);
this.reloadDefaultValues();
this.handleWidthEvent();
this.$timeout(() => {
this.finishSettingWidth();
});
}
getParentRect() {

View File

@@ -1,7 +1,7 @@
import { WebApplication } from './../../application';
import { WebApplication } from '@/ui_models/application';
import { PasswordWizardScope, PasswordWizardType, WebDirective } from './../../types';
import template from '%/directives/password-wizard.pug';
import { PureCtrl } from '@Controllers/abstract/pure_ctrl';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
const DEFAULT_CONTINUE_TITLE = "Continue";
const Steps = {
@@ -9,7 +9,7 @@ const Steps = {
FinishStep: 2
};
class PasswordWizardCtrl extends PureCtrl implements PasswordWizardScope {
class PasswordWizardCtrl extends PureViewCtrl implements PasswordWizardScope {
$element: JQLite
application!: WebApplication
type!: PasswordWizardType

View File

@@ -1,5 +1,5 @@
import { WebDirective } from './../../types';
import { WebApplication } from '@/application';
import { WebApplication } from '@/ui_models/application';
import { ProtectedAction, PrivilegeCredential, PrivilegeSessionLength } from 'snjs';
import template from '%/directives/privileges-auth-modal.pug';

View File

@@ -1,8 +1,8 @@
import { WebDirective } from './../../types';
import { WebApplication } from '@/application';
import { WebApplication } from '@/ui_models/application';
import template from '%/directives/privileges-management-modal.pug';
import { PrivilegeCredential, ProtectedAction, SNPrivileges, PrivilegeSessionLength } from 'snjs';
import { PureCtrl } from '@Controllers/abstract/pure_ctrl';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import { PrivilegeMutator } from '@/../../../../snjs/dist/@types/models';
type DisplayInfo = {
@@ -10,7 +10,7 @@ type DisplayInfo = {
prompt: string
}
class PrivilegesManagementModalCtrl extends PureCtrl {
class PrivilegesManagementModalCtrl extends PureViewCtrl {
hasPasscode = false
hasAccount = false

View File

@@ -1,4 +1,4 @@
import { WebApplication } from '@/application';
import { WebApplication } from '@/ui_models/application';
import { WebDirective } from './../../types';
import {
ContentType,

View File

@@ -1,5 +1,5 @@
import { WebDirective } from './../../types';
import { WebApplication } from '@/application';
import { WebApplication } from '@/ui_models/application';
import template from '%/directives/session-history-menu.pug';
import { SNItem, ItemHistoryEntry, ItemHistory } from '@/../../../../snjs/dist/@types';

View File

@@ -1,4 +1,4 @@
import { WebApplication } from '@/application';
import { WebApplication } from '@/ui_models/application';
import { WebDirective } from './../../types';
import template from '%/directives/sync-resolution-menu.pug';

View File

@@ -4,8 +4,8 @@ import { SKAlert } from 'sn-stylekit';
export class AlertService extends SNAlertService {
async alert(
title: string,
text: string,
title: string,
closeButtonText = 'OK',
onClose: () => void
) {
@@ -28,8 +28,8 @@ export class AlertService extends SNAlertService {
}
async confirm(
title: string,
text: string,
title: string,
confirmButtonText = 'Confirm',
cancelButtonText = 'Cancel',
onConfirm: () => void,

View File

@@ -1,4 +1,4 @@
import { WebApplication } from '@/application';
import { WebApplication } from '@/ui_models/application';
import { EncryptionIntent, ProtectedAction, SNItem, ContentType, SNNote } from 'snjs';
export class ArchiveManager {

View File

@@ -1,6 +1,6 @@
import { SNComponent, PurePayload, ComponentMutator, AppDataField } from 'snjs';
/* eslint-disable camelcase */
import { WebApplication } from '@/application';
import { WebApplication } from '@/ui_models/application';
// An interface used by the Desktop app to interact with SN
import { isDesktopApplication } from '@/utils';
import { EncryptionIntent, ApplicationService, ApplicationEvent, removeFromArray } from 'snjs';

View File

@@ -1,4 +1,4 @@
import { WebApplication } from './../application';
import { WebApplication } from '@/ui_models/application';
import { isDesktopApplication } from '@/utils';
import { AppStateEvent } from '@/services/state';

View File

@@ -1,4 +1,4 @@
import { WebApplication } from '@/application';
import { WebApplication } from '@/ui_models/application';
import {
SNPredicate,
ContentType,

View File

@@ -1,11 +1,20 @@
import { WebApplication } from './../application';
import { isDesktopApplication } from '@/utils';
import pull from 'lodash/pull';
import { ProtectedAction, ApplicationEvent, SNTag, SNNote, SNUserPrefs, ContentType, SNSmartTag } from 'snjs';
import {
ProtectedAction,
ApplicationEvent,
SNTag,
SNNote,
SNUserPrefs,
ContentType,
SNSmartTag
} from 'snjs';
import { WebApplication } from '@/ui_models/application';
import { Editor } from '@/ui_models/editor';
export enum AppStateEvent {
TagChanged = 1,
NoteChanged = 2,
ActiveEditorChanged = 2,
PreferencesChanged = 3,
PanelResized = 4,
EditorFocused = 5,
@@ -34,8 +43,8 @@ export class AppState {
rootScopeCleanup2: any
onVisibilityChange: any
selectedTag?: SNTag
selectedNote?: SNNote
userPreferences?: SNUserPrefs
multiEditorEnabled = false
/* @ngInject */
constructor(
@@ -74,16 +83,90 @@ export class AppState {
this.onVisibilityChange = undefined;
}
/**
* Creates a new editor if one doesn't exist. If one does, we'll replace the
* editor's note with an empty one.
*/
createEditor(title?: string) {
const activeEditor = this.getActiveEditor();
if (!activeEditor || this.multiEditorEnabled) {
this.application.editorGroup.createEditor(title);
} else {
activeEditor.reset(title);
}
}
async openEditor(noteUuid: string) {
const note = this.application.findItem(noteUuid) as SNNote;
const run = async () => {
const activeEditor = this.getActiveEditor();
if (!activeEditor || this.multiEditorEnabled) {
this.application.editorGroup.createEditor(noteUuid);
} else {
activeEditor.setNote(note);
}
await this.notifyEvent(AppStateEvent.ActiveEditorChanged);
};
if (note && note.safeContent.protected &&
await this.application.privilegesService!.actionRequiresPrivilege(
ProtectedAction.ViewProtectedNotes
)) {
return new Promise((resolve) => {
this.application.presentPrivilegesModal(
ProtectedAction.ViewProtectedNotes,
() => {
run().then(resolve);
}
);
});
} else {
return run();
}
}
getActiveEditor() {
return this.application.editorGroup.editors[0];
}
getEditors() {
return this.application.editorGroup.editors;
}
closeEditor(editor: Editor) {
this.application.editorGroup.closeEditor(editor);
}
closeActiveEditor() {
this.application.editorGroup.closeActiveEditor();
}
closeAllEditors() {
this.application.editorGroup.closeAllEditors();
}
editorForNote(note: SNNote) {
for (const editor of this.getEditors()) {
if (editor.note.uuid === note.uuid) {
return editor;
}
}
}
streamNotesAndTags() {
this.application!.streamItems(
[ContentType.Note, ContentType.Tag],
async (items) => {
if(this.selectedNote) {
const matchingNote = items.find((candidate) => candidate.uuid === this.selectedNote!.uuid);
if(matchingNote) {
this.selectedNote = matchingNote as SNNote;
/** Close any editors for deleted notes */
const notes = items.filter((candidate) => candidate.content_type === ContentType.Note) as SNNote[];
for (const note of notes) {
if (note.deleted) {
const editor = this.editorForNote(note);
if (editor) {
this.closeEditor(editor);
}
}
}
if (this.selectedTag) {
const matchingTag = items.find((candidate) => candidate.uuid === this.selectedTag!.uuid);
if (matchingTag) {
@@ -161,32 +244,6 @@ export class AppState {
);
}
async setSelectedNote(note?: SNNote) {
const run = async () => {
const previousNote = this.selectedNote;
this.selectedNote = note;
await this.notifyEvent(
AppStateEvent.NoteChanged,
{ previousNote: previousNote }
);
};
if (note && note.safeContent.protected &&
await this.application.privilegesService!.actionRequiresPrivilege(
ProtectedAction.ViewProtectedNotes
)) {
return new Promise((resolve) => {
this.application.presentPrivilegesModal(
ProtectedAction.ViewProtectedNotes,
() => {
run().then(resolve);
}
);
});
} else {
return run();
}
}
/** Returns the tags that are referncing this note */
getNoteTags(note: SNNote) {
return this.application.referencingForItem(note).filter((ref) => {
@@ -196,11 +253,11 @@ export class AppState {
/** Returns the notes this tag references */
getTagNotes(tag: SNTag) {
if(tag.isSmartTag()) {
if (tag.isSmartTag()) {
return this.application.notesMatchingSmartTag(tag as SNSmartTag);
} else {
return this.application.referencesForItem(tag).filter((ref) => {
return ref.content_type === tag.content_type;
return ref.content_type === ContentType.Note;
}) as SNNote[]
}
}
@@ -209,10 +266,6 @@ export class AppState {
return this.selectedTag;
}
getSelectedNote() {
return this.selectedNote;
}
setUserPreferences(preferences: SNUserPrefs) {
this.userPreferences = preferences;
this.notifyEvent(

View File

@@ -1,5 +1,5 @@
import { removeFromArray } from 'snjs';
import { FooterStatus } from './../types';
import { FooterStatus } from '@/types';
type StatusCallback = (string: string) => void

View File

@@ -1,4 +1,4 @@
import { WebApplication } from '@/application';
import { WebApplication } from '@/ui_models/application';
import _ from 'lodash';
import {
StorageValueModes,

View File

@@ -14,7 +14,9 @@
"paths": {
"%/*": ["../templates/*"],
"@/*": ["./*"],
"@Controllers/*": ["./controllers/*"]
"@Controllers/*": ["./controllers/*"],
"@Views/*": ["./views/*"],
"@Services/*": ["./services/*"],
}
}
}

View File

@@ -1,5 +1,6 @@
import { InputModalScope } from './directives/views/inputModal';
import { PasswordWizardType, PasswordWizardScope } from './types';
import { EditorGroup } from '@/ui_models/editor_group';
import { InputModalScope } from '@/directives/views/inputModal';
import { PasswordWizardType, PasswordWizardScope } from '@/types';
import {
Environment,
SNApplication,
@@ -23,7 +24,7 @@ import {
ThemeManager,
PreferencesManager,
KeyboardManager
} from './services';
} from '@/services';
type WebServices = {
appState: AppState
@@ -44,6 +45,7 @@ export class WebApplication extends SNApplication {
private onDeinit?: (app: WebApplication) => void
private webServices!: WebServices
private currentAuthenticationElement?: JQLite
public editorGroup: EditorGroup
/* @ngInject */
constructor(
@@ -71,6 +73,7 @@ export class WebApplication extends SNApplication {
this.scope = scope;
this.onDeinit = onDeinit;
deviceInterface.setApplication(this);
this.editorGroup = new EditorGroup(this);
}
/** @override */
@@ -86,6 +89,7 @@ export class WebApplication extends SNApplication {
this.onDeinit!(this);
this.onDeinit = undefined;
this.$compile = undefined;
this.editorGroup.deinit();
(this.scope! as any).application = undefined;
this.scope!.$destroy();
this.scope = undefined;

View File

@@ -10,11 +10,11 @@ import {
StatusManager,
ThemeManager,
AppState
} from './services';
} from '@/services';
type AppManagerChangeCallback = () => void
export class ApplicationManager {
export class ApplicationGroup {
$compile: ng.ICompileService
$rootScope: ng.IRootScopeService

View File

@@ -0,0 +1,104 @@
import { SNNote, ContentType, PayloadSource } from 'snjs';
import { WebApplication } from './application';
export class Editor {
public note!: SNNote
private application: WebApplication
private _onNoteChange?: () => void
private _onNoteValueChange?: (note: SNNote, source?: PayloadSource) => void
private removeStreamObserver: () => void
public isTemplateNote = true
constructor(
application: WebApplication,
noteUuid?: string,
noteTitle?: string
) {
this.application = application;
if (noteUuid) {
this.note = application.findItem(noteUuid) as SNNote;
} else {
this.reset(noteTitle);
}
this.removeStreamObserver = this.application.streamItems(
ContentType.Note,
async (items, source) => {
await this.handleNoteStream(items as SNNote[], source);
}
);
}
private async handleNoteStream(notes: SNNote[], source?: PayloadSource) {
/** Update our note object reference whenever it changes */
const matchingNote = notes.find((item) => {
return item.uuid === this.note.uuid;
}) as SNNote;
if (matchingNote) {
this.isTemplateNote = false;
this.note = matchingNote;
this._onNoteValueChange!(matchingNote, source);
}
}
async insertTemplatedNote() {
return this.application.insertItem(this.note);
}
/**
* Reverts the editor to a blank state, removing any existing note from view,
* and creating a placeholder note.
*/
async reset(noteTitle?: string) {
const note = await this.application.createTemplateItem(
ContentType.Note,
{
text: '',
title: noteTitle || '',
references: []
}
);
this.isTemplateNote = true;
this.setNote(note as SNNote);
}
deinit() {
this.removeStreamObserver();
(this.removeStreamObserver as any) = undefined;
this._onNoteChange = undefined;
(this.application as any) = undefined;
this._onNoteChange = undefined;
this._onNoteValueChange = undefined;
}
/**
* Register to be notified when the editor's note changes.
*/
public onNoteChange(onNoteChange: () => void) {
this._onNoteChange = onNoteChange;
if (this.note) {
onNoteChange();
}
}
/**
* Register to be notified when the editor's note's values change
* (and thus a new object reference is created)
*/
public onNoteValueChange(
onNoteValueChange: (note: SNNote, source?: PayloadSource) => void
) {
this._onNoteValueChange = onNoteValueChange;
}
/**
* Sets the editor contents by setting its note.
*/
public setNote(note: SNNote) {
this.note = note;
if (this._onNoteChange) {
this._onNoteChange();
}
}
}

View File

@@ -0,0 +1,69 @@
import { removeFromArray } from 'snjs';
import { Editor } from './editor';
import { WebApplication } from './application';
type EditorGroupChangeCallback = () => void
export class EditorGroup {
public editors: Editor[] = []
private application: WebApplication
changeObservers: EditorGroupChangeCallback[] = []
constructor(application: WebApplication) {
this.application = application;
}
public deinit() {
(this.application as any) = undefined;
for (const editor of this.editors) {
this.deleteEditor(editor);
}
}
createEditor(noteUuid?: string, noteTitle?: string) {
const editor = new Editor(this.application, noteUuid, noteTitle);
this.editors.push(editor);
this.notifyObservers();
}
deleteEditor(editor: Editor) {
editor.deinit();
removeFromArray(this.editors, editor);
}
closeEditor(editor: Editor) {
this.deleteEditor(editor);
this.notifyObservers();
}
closeActiveEditor() {
this.deleteEditor(this.editors[0]);
}
closeAllEditors() {
for(const editor of this.editors) {
this.deleteEditor(editor);
}
}
get activeEditor() {
return this.editors[0];
}
/**
* Notifies observer when the active editor has changed.
*/
public addChangeObserver(callback: EditorGroupChangeCallback) {
this.changeObservers.push(callback);
if (this.activeEditor) {
callback();
}
}
private notifyObservers() {
for (const observer of this.changeObservers) {
observer();
}
}
}

View File

@@ -1,11 +1,10 @@
import { AppStateEvent } from '@/services/state';
import { WebApplication } from './../../application';
import { ApplicationEvent } from 'snjs';
import { WebApplication } from '@/ui_models/application';
export type CtrlState = Partial<Record<string, any>>
export type CtrlProps = Partial<Record<string, any>>
export class PureCtrl {
export class PureViewCtrl {
$timeout: ng.ITimeoutService
/** Passed through templates */
application?: WebApplication

View File

@@ -0,0 +1,17 @@
.main-ui-view(
ng-class='self.platformString'
)
#app.app(
ng-class='self.state.appClass',
ng-if='!self.state.needsUnlock && self.state.ready'
)
tags-view(application='self.application')
notes-view(application='self.application')
editor-group-view(
application='self.application'
)
footer-view(
ng-if='!self.state.needsUnlock && self.state.ready'
application='self.application'
)

View File

@@ -1,21 +1,21 @@
import { PanelPuppet, WebDirective, PermissionsModalScope, ModalComponentScope } from './../types';
import { WebDirective, PermissionsModalScope, ModalComponentScope } from '@/types';
import { getPlatformString } from '@/utils';
import template from '%/application-view.pug';
import template from './application-view.pug';
import { AppStateEvent } from '@/services/state';
import { ApplicationEvent, SNComponent } from 'snjs';
import angular from 'angular';
import {
PANEL_NAME_NOTES,
PANEL_NAME_TAGS
} from '@/controllers/constants';
} from '@/views/constants';
import {
STRING_SESSION_EXPIRED,
STRING_DEFAULT_FILE_ERROR
} from '@/strings';
import { PureCtrl } from './abstract/pure_ctrl';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import { PermissionDialog } from '@/../../../../snjs/dist/@types/services/component_manager';
class ApplicationViewCtrl extends PureCtrl {
class ApplicationViewCtrl extends PureViewCtrl {
private $compile?: ng.ICompileService
private $location?: ng.ILocationService
private $rootScope?: ng.IRootScopeService
@@ -47,7 +47,7 @@ class ApplicationViewCtrl extends PureCtrl {
this.presentPermissionsDialog = this.presentPermissionsDialog.bind(this);
this.addDragDropHandlers();
}
deinit() {
this.$location = undefined;
this.$rootScope = undefined;
@@ -61,7 +61,7 @@ class ApplicationViewCtrl extends PureCtrl {
(this.presentPermissionsDialog as any) = undefined;
super.deinit();
}
$onInit() {
super.$onInit();
this.loadApplication();
@@ -226,7 +226,7 @@ class ApplicationViewCtrl extends PureCtrl {
scope.component = dialog.component;
scope.callback = dialog.callback;
const el = this.$compile!(
"<permissions-modal component='component' permissions-string='permissionsString'"
"<permissions-modal component='component' permissions-string='permissionsString'"
+ " callback='callback' class='sk-modal'></permissions-modal>"
)(scope as any);
angular.element(document.body).append(el);
@@ -310,7 +310,8 @@ export class ApplicationView extends WebDirective {
this.controller = ApplicationViewCtrl;
this.replace = true;
this.controllerAs = 'self';
this.bindToController = {
this.bindToController = true;
this.scope = {
application: '='
};
}

View File

@@ -0,0 +1,4 @@
application-view(
ng-repeat='application in self.applications',
application='application'
)

View File

@@ -0,0 +1,40 @@
import { ApplicationGroup } from '@/ui_models/application_group';
import { WebDirective } from '@/types';
import template from './application-group-view.pug';
import { WebApplication } from '@/ui_models/application';
class ApplicationGroupViewCtrl {
private $timeout: ng.ITimeoutService
private applicationGroup: ApplicationGroup
public applications: WebApplication[] = []
/* @ngInject */
constructor(
$timeout: ng.ITimeoutService,
mainApplicationGroup: ApplicationGroup
) {
this.$timeout = $timeout;
this.applicationGroup = mainApplicationGroup;
this.applicationGroup.addApplicationChangeObserver(() => {
this.reload();
});
}
reload() {
this.$timeout(() => {
this.applications = this.applicationGroup.getApplications();
});
}
}
export class ApplicationGroupView extends WebDirective {
constructor() {
super();
this.template = template;
this.controller = ApplicationGroupViewCtrl;
this.replace = true;
this.controllerAs = 'self';
this.bindToController = true;
}
}

View File

@@ -0,0 +1,259 @@
#editor-column.section.editor.sn-component(aria-label='Note')
.sn-component
.sk-app-bar.no-edges(
ng-if='self.noteLocked',
ng-init="self.lockText = 'Note Locked'",
ng-mouseleave="self.lockText = 'Note Locked'",
ng-mouseover="self.lockText = 'Unlock'"
)
.left
.sk-app-bar-item(ng-click='self.toggleLockNote()')
.sk-label.warning
i.icon.ion-locked
| {{self.lockText}}
#editor-title-bar.section-title-bar(
ng-class="{'locked' : self.noteLocked}",
ng-show='self.note && !self.note.errorDecrypting'
)
.title
input#note-title-editor.input(
ng-blur='self.onTitleBlur()',
ng-change='self.onTitleChange()',
ng-disabled='self.noteLocked',
ng-focus='self.onTitleFocus()',
ng-keyup='$event.keyCode == 13 && self.onTitleEnter($event)',
ng-model='self.editorValues.title',
select-on-click='true',
spellcheck='false')
#save-status
.message(
ng-class="{'warning sk-bold': self.state.syncTakingTooLong, 'danger sk-bold': self.state.saveError}"
) {{self.state.noteStatus.message}}
.desc(ng-show='self.state.noteStatus.desc') {{self.state.noteStatus.desc}}
.editor-tags
#note-tags-component-container(ng-if='self.state.tagsComponent')
component-view.component-view(
component='self.state.tagsComponent',
ng-class="{'locked' : self.noteLocked}",
ng-style="self.noteLocked && {'pointer-events' : 'none'}",
application='self.application'
)
input.tags-input(
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.editorValues.tagsInputValue',
placeholder='#tags',
spellcheck='false',
type='text'
)
.sn-component(ng-if='self.note')
#editor-menu-bar.sk-app-bar.no-edges
.left
.sk-app-bar-item(
click-outside=`self.setMenuState('showOptionsMenu', false)`,
is-open='self.state.showOptionsMenu',
ng-class="{'selected' : self.state.showOptionsMenu}",
ng-click="self.toggleMenu('showOptionsMenu')"
)
.sk-label Options
.sk-menu-panel.dropdown-menu(ng-if='self.state.showOptionsMenu')
.sk-menu-panel-section
.sk-menu-panel-header
.sk-menu-panel-header-title Note Options
menu-row(
action='self.selectedMenuItem(true); self.togglePin()',
desc="'Pin or unpin a note from the top of your list'",
label="self.note.pinned ? 'Unpin' : 'Pin'"
)
menu-row(
action='self.selectedMenuItem(true); self.toggleArchiveNote()',
desc="'Archive or unarchive a note from your Archived system tag'",
label="self.note.archived ? 'Unarchive' : 'Archive'"
)
menu-row(
action='self.selectedMenuItem(true); self.toggleLockNote()',
desc="'Locking notes prevents unintentional editing'",
label="self.noteLocked ? 'Unlock' : 'Lock'"
)
menu-row(
action='self.selectedMenuItem(true); self.toggleProtectNote()',
desc=`'Protecting a note will require credentials to view
it (Manage Privileges via Account menu)'`,
label="self.note.protected ? 'Unprotect' : 'Protect'"
)
menu-row(
action='self.selectedMenuItem(true); self.toggleNotePreview()',
circle="self.note.hidePreview ? 'danger' : 'success'",
circle-align="'right'",
desc="'Hide or unhide the note preview from the list of notes'",
label="'Preview'"
)
menu-row(
action='self.selectedMenuItem(); self.deleteNote()',
desc="'Send this note to the trash'",
label="'Move to Trash'",
ng-show='!self.state.altKeyDown && !self.note.trashed && !self.note.errorDecrypting',
stylekit-class="'warning'"
)
menu-row(
action='self.selectedMenuItem(); self.deleteNotePermanantely()',
desc="'Delete this note permanently from all your devices'",
label="'Delete Permanently'",
ng-show='!self.note.trashed && self.note.errorDecrypting',
stylekit-class="'danger'"
)
div(ng-if='self.note.trashed || self.state.altKeyDown')
menu-row(
action='self.selectedMenuItem(true); self.restoreTrashedNote()',
desc="'Undelete this note and restore it back into your notes'",
label="'Restore'",
ng-show='self.note.trashed',
stylekit-class="'info'"
)
menu-row(
action='self.selectedMenuItem(true); self.deleteNotePermanantely()',
desc="'Delete this note permanently from all your devices'",
label="'Delete Permanently'",
stylekit-class="'danger'"
)
menu-row(
action='self.selectedMenuItem(true); self.emptyTrash()',
desc="'Permanently delete all notes in the trash'",
label="'Empty Trash'",
ng-show='self.note.trashed || !self.state.altKeyDown',
stylekit-class="'danger'",
subtitle="self.getTrashCount() + ' notes in trash'"
)
.sk-menu-panel-section
.sk-menu-panel-header
.sk-menu-panel-header-title Global Display
menu-row(
action="self.selectedMenuItem(true); self.toggleWebPrefKey(self.prefKeyMonospace)",
circle="self.state.monospaceEnabled ? 'success' : 'neutral'",
desc="'Toggles the font style for the default editor'",
disabled='self.state.selectedEditor',
label="'Monospace Font'",
subtitle="self.state.selectedEditor ? 'Not available with editor extensions' : null"
)
menu-row(
action="self.selectedMenuItem(true); self.toggleWebPrefKey(self.prefKeySpellcheck)",
circle="self.state.spellcheck ? 'success' : 'neutral'",
desc="'Toggles spellcheck for the default editor'",
disabled='self.state.selectedEditor',
label="'Spellcheck'",
subtitle=`
self.state.selectedEditor
? 'Not available with editor extensions'
: (self.state.isDesktop ? 'May degrade editor performance' : null)
`)
menu-row(
action="self.selectedMenuItem(true); self.toggleWebPrefKey(self.prefKeyMarginResizers)",
circle="self.state.marginResizersEnabled ? 'success' : 'neutral'",
desc="'Allows for editor left and right margins to be resized'",
faded='!self.state.marginResizersEnabled',
label="'Margin Resizers'"
)
.sk-app-bar-item(
click-outside=`self.setMenuState('showEditorMenu', false)`
is-open='self.state.showEditorMenu',
ng-class="{'selected' : self.state.showEditorMenu}",
ng-click="self.toggleMenu('showEditorMenu')"
)
.sk-label Editor
editor-menu(
callback='self.editorMenuOnSelect()',
current-item='self.note',
ng-if='self.state.showEditorMenu',
selected-editor='self.state.selectedEditor',
application='self.application'
)
.sk-app-bar-item(
click-outside=`self.setMenuState('showExtensions', false)`,
is-open='self.state.showExtensions',
ng-class="{'selected' : self.state.showExtensions}",
ng-click="self.toggleMenu('showExtensions')"
)
.sk-label Actions
actions-menu(
item='self.note',
ng-if='self.state.showExtensions',
application='self.application'
)
.sk-app-bar-item(
click-outside=`self.setMenuState('showSessionHistory', false)`,
is-open='self.state.showSessionHistory',
ng-click="self.toggleMenu('showSessionHistory')"
)
.sk-label Session History
session-history-menu(
item='self.note',
ng-if='self.state.showSessionHistory',
application='self.application'
)
#editor-content.editor-content(
ng-if='self.state.noteReady && !self.note.errorDecrypting'
)
panel-resizer.left(
control='self.leftPanelPuppet',
hoverable='true',
min-width='300',
ng-if='self.state.marginResizersEnabled',
on-resize-finish='self.onPanelResizeFinish()',
panel-id="'editor-content'",
property="'left'"
)
component-view.component-view(
component='self.state.selectedEditor',
ng-if='self.state.selectedEditor',
on-load='self.onEditorLoad',
application='self.application'
)
textarea#note-text-editor.editable(
dir='auto',
ng-attr-spellcheck='{{self.state.spellcheck}}',
ng-change='self.contentChanged()',
ng-click='self.clickedTextArea()',
ng-focus='self.onContentFocus()',
ng-if='!self.state.selectedEditor',
ng-model='self.editorValues.text',
ng-model-options='{ debounce: self.state.editorDebounce}',
ng-readonly='self.noteLocked',
ng-trim='false'
)
| {{self.onSystemEditorLoad()}}
panel-resizer(
control='self.rightPanelPuppet',
hoverable='true', min-width='300',
ng-if='self.state.marginResizersEnabled',
on-resize-finish='self.onPanelResizeFinish()',
panel-id="'editor-content'",
property="'right'"
)
.section(ng-show='self.note.errorDecrypting')
p.medium-padding(style='padding-top: 0 !important;')
| There was an error decrypting this item. Ensure you are running the
| latest version of this app, then sign out and sign back in to try again.
#editor-pane-component-stack(ng-show='self.note')
#component-stack-menu-bar.sk-app-bar.no-edges(ng-if='self.state.componentStack.length')
.left
.sk-app-bar-item(
ng-click='self.toggleStackComponentForCurrentItem(component)',
ng-repeat='component in self.state.componentStack track by component.uuid'
)
.sk-app-bar-item-column
.sk-circle.small(
ng-class="{'info' : !component.hidden && component.active, 'neutral' : component.hidden || !component.active}"
)
.sk-app-bar-item-column
.sk-label {{component.name}}
.sn-component
component-view.component-view.component-stack-item(
component='component',
manual-dealloc='true',
ng-if='component.active',
ng-repeat='component in self.state.componentStack track by component.uuid',
ng-show='!component.hidden',
application='self.application'
)

View File

@@ -1,5 +1,6 @@
import { WebApplication } from './../application';
import { PanelPuppet, WebDirective } from './../types';
import { Editor } from '@/ui_models/editor';
import { WebApplication } from '@/ui_models/application';
import { PanelPuppet, WebDirective } from '@/types';
import angular from 'angular';
import {
ApplicationEvent,
@@ -19,8 +20,8 @@ import {
import find from 'lodash/find';
import { isDesktopApplication } from '@/utils';
import { KeyboardModifier, KeyboardKey } from '@/services/keyboardManager';
import template from '%/editor.pug';
import { PureCtrl } from '@Controllers/abstract/pure_ctrl';
import template from './editor-view.pug';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import { AppStateEvent, EventSource } from '@/services/state';
import {
STRING_DELETED_NOTE,
@@ -39,12 +40,6 @@ const SAVE_TIMEOUT_DEBOUNCE = 350;
const SAVE_TIMEOUT_NO_DEBOUNCE = 100;
const EDITOR_DEBOUNCE = 200;
const AppDataKeys = {
Pinned: 'pinned',
Locked: 'locked',
Archived: 'archived',
PrefersPlainEditor: 'prefersPlainEditor'
};
const ElementIds = {
NoteTextEditor: 'note-text-editor',
NoteTitleEditor: 'note-title-editor',
@@ -63,9 +58,8 @@ type NoteStatus = {
}
type EditorState = {
note: SNNote
saveError?: any
selectedEditor?: SNComponent
editorComponent?: SNComponent
noteStatus?: NoteStatus
tagsAsStrings?: string
marginResizersEnabled?: boolean
@@ -73,6 +67,12 @@ type EditorState = {
isDesktop?: boolean
tagsComponent?: SNComponent
componentStack?: SNComponent[]
syncTakingTooLong: boolean
showExtensions: boolean
noteReady: boolean
showOptionsMenu: boolean
altKeyDown: boolean
spellcheck: boolean
/** Fields that can be directly mutated by the template */
mutable: {}
}
@@ -83,9 +83,16 @@ type EditorValues = {
tagsInputValue?: string
}
class EditorCtrl extends PureCtrl {
interface EditorViewScope {
application: WebApplication
editor: Editor
}
class EditorViewCtrl extends PureViewCtrl implements EditorViewScope {
/** Passed through template */
readonly application!: WebApplication
readonly editor!: Editor
private leftPanelPuppet?: PanelPuppet
private rightPanelPuppet?: PanelPuppet
private unregisterComponent: any
@@ -150,9 +157,23 @@ class EditorCtrl extends PureCtrl {
return this.state as EditorState;
}
get note() {
return this.editor.note;
}
$onInit() {
super.$onInit();
this.registerKeyboardShortcuts();
this.editor.onNoteChange(() => {
this.handleEditorNoteChange();
})
this.editor.onNoteValueChange((note, source) => {
if (isPayloadSourceRetrieved(source!)) {
this.editorValues.title = note.title;
this.editorValues.text = note.text;
this.reloadTagsString();
}
})
}
/** @override */
@@ -168,6 +189,10 @@ class EditorCtrl extends PureCtrl {
};
}
async setEditorState(state: Partial<EditorState>) {
return this.setState(state);
}
async onAppLaunch() {
await super.onAppLaunch();
this.streamItems();
@@ -176,29 +201,21 @@ class EditorCtrl extends PureCtrl {
/** @override */
onAppStateEvent(eventName: AppStateEvent, data: any) {
if (eventName === AppStateEvent.NoteChanged) {
this.handleNoteSelectionChange(
this.application.getAppState().getSelectedNote()!,
data.previousNote
);
} else if (eventName === AppStateEvent.PreferencesChanged) {
if (eventName === AppStateEvent.PreferencesChanged) {
this.reloadPreferences();
}
}
/** @override */
onAppEvent(eventName: ApplicationEvent) {
if (!this.getState().note) {
return;
}
if (eventName === ApplicationEvent.HighLatencySync) {
this.setState({ syncTakingTooLong: true });
this.setEditorState({ syncTakingTooLong: true });
} else if (eventName === ApplicationEvent.CompletedSync) {
this.setState({ syncTakingTooLong: false });
if (this.getState().note.dirty) {
this.setEditorState({ syncTakingTooLong: false });
if (this.note.dirty) {
/** if we're still dirty, don't change status, a sync is likely upcoming. */
} else {
const saved = this.getState().note.lastSyncEnd! > this.getState().note.lastSyncBegan!;
const saved = this.note.lastSyncEnd! > this.note.lastSyncBegan!;
const isInErrorState = this.getState().saveError;
if (isInErrorState || saved) {
this.showAllChangesSavedStatus();
@@ -210,7 +227,7 @@ class EditorCtrl extends PureCtrl {
* Otherwise, it means the originating sync came from somewhere else
* and we don't want to display an error here.
*/
if (this.getState().note.dirty) {
if (this.note.dirty) {
this.showErrorStatus();
}
} else if (eventName === ApplicationEvent.LocalDatabaseWriteError) {
@@ -221,6 +238,50 @@ class EditorCtrl extends PureCtrl {
}
}
async handleEditorNoteChange() {
const note = this.editor.note;
await this.setEditorState({
showExtensions: false,
showOptionsMenu: false,
altKeyDown: false,
noteStatus: undefined
});
this.editorValues.title = note.title;
this.editorValues.text = note.text;
if (!note) {
this.setEditorState({
noteReady: false
});
return;
}
const associatedEditor = this.editorForNote(note);
if (associatedEditor && associatedEditor !== this.getState().editorComponent) {
/**
* Setting note to not ready will remove the editor from view in a flash,
* so we only want to do this if switching between external editors
*/
this.setEditorState({
noteReady: false,
editorComponent: associatedEditor
});
} else if (!associatedEditor) {
/** No editor */
this.setEditorState({
editorComponent: undefined
});
}
await this.setEditorState({
noteReady: true,
});
this.reloadTagsString();
this.reloadPreferences();
if (note.safeText().length === 0) {
this.focusTitle();
}
this.reloadComponentContext();
}
/**
* Because note.locked accesses note.content.appData,
* we do not want to expose the template to direct access to note.locked,
@@ -230,57 +291,24 @@ class EditorCtrl extends PureCtrl {
* on a deleted note.
*/
get noteLocked() {
if (!this.getState().note || this.getState().note.deleted) {
if (!this.note || this.note.deleted) {
return false;
}
return this.getState().note.locked;
return this.note.locked;
}
streamItems() {
this.application.streamItems(
ContentType.Note,
async (items, source) => {
const currentNote = this.getState().note;
if (!currentNote) {
return;
}
const matchingNote = items.find((item) => {
return item.uuid === currentNote.uuid;
}) as SNNote;
if (!matchingNote) {
return;
}
if (matchingNote?.deleted) {
await this.setState({
note: undefined,
noteReady: false
});
return;
} else {
await this.setState({
note: matchingNote
});
}
if (!isPayloadSourceRetrieved(source!)) {
return;
}
this.editorValues.title = matchingNote.title;
this.editorValues.text = matchingNote.text;
this.reloadTagsString();
}
);
this.application.streamItems(
ContentType.Tag,
(items) => {
if (!this.getState().note) {
if (!this.note) {
return;
}
for (const tag of items) {
if (
!this.editorValues.tagsInputValue ||
tag.deleted ||
tag.hasRelationshipWithItem(this.getState().note)
tag.hasRelationshipWithItem(this.note)
) {
this.reloadTagsString();
break;
@@ -293,7 +321,7 @@ class EditorCtrl extends PureCtrl {
ContentType.Component,
async (items) => {
const components = items as SNComponent[];
if (!this.getState().note) {
if (!this.note) {
return;
}
/** Reload componentStack in case new ones were added or removed */
@@ -306,9 +334,9 @@ class EditorCtrl extends PureCtrl {
return;
}
/** Find the most recent editor for note */
const editor = this.editorForNote(this.getState().note);
this.setState({
selectedEditor: editor
const editor = this.editorForNote(this.note);
this.setEditorState({
editorComponent: editor
});
if (!editor) {
this.reloadFont();
@@ -317,62 +345,12 @@ class EditorCtrl extends PureCtrl {
);
}
async handleNoteSelectionChange(note: SNNote, previousNote?: SNNote) {
await this.setState({
note: note,
showExtensions: false,
showOptionsMenu: false,
altKeyDown: false,
noteStatus: null
});
this.editorValues.title = note.title;
this.editorValues.text = note.text;
if (!note) {
this.setState({
noteReady: false
});
return;
}
const associatedEditor = this.editorForNote(note);
if (associatedEditor && associatedEditor !== this.getState().selectedEditor) {
/**
* Setting note to not ready will remove the editor from view in a flash,
* so we only want to do this if switching between external editors
*/
this.setState({
noteReady: false,
selectedEditor: associatedEditor
});
} else if (!associatedEditor) {
/** No editor */
this.setState({
selectedEditor: null
});
}
await this.setState({
noteReady: true,
});
this.reloadTagsString();
this.reloadPreferences();
if (note.dummy) {
this.focusTitle();
}
if (previousNote && previousNote !== note) {
if (previousNote.dummy) {
this.performNoteDeletion(previousNote);
}
}
this.reloadComponentContext();
}
editorForNote(note: SNNote) {
return this.application.componentManager!.editorForNote(note);
}
setMenuState(menu: string, state: boolean) {
this.setState({
this.setEditorState({
[menu]: state
});
this.closeAllMenus(menu);
@@ -395,7 +373,7 @@ class EditorCtrl extends PureCtrl {
menuState[candidate] = false;
}
}
this.setState(menuState);
this.setEditorState(menuState);
}
editorMenuOnSelect(component: SNComponent) {
@@ -403,10 +381,10 @@ class EditorCtrl extends PureCtrl {
/** If plain editor or other editor */
this.setMenuState('showEditorMenu', false);
const editor = component;
if (this.getState().selectedEditor && editor !== this.getState().selectedEditor) {
this.disassociateComponentWithCurrentNote(this.getState().selectedEditor!);
if (this.getState().editorComponent && editor !== this.getState().editorComponent) {
this.disassociateComponentWithCurrentNote(this.getState().editorComponent!);
}
const note = this.getState().note;
const note = this.note;
if (editor) {
const prefersPlain = note.prefersPlainEditor;
if (prefersPlain) {
@@ -427,8 +405,8 @@ class EditorCtrl extends PureCtrl {
this.reloadFont();
}
this.setState({
selectedEditor: editor
this.setEditorState({
editorComponent: editor
});
} else if (component.area === 'editor-stack') {
this.toggleStackComponentForCurrentItem(component);
@@ -440,7 +418,7 @@ class EditorCtrl extends PureCtrl {
hasAvailableExtensions() {
return this.application.actionsManager!.
extensionsInContextOfItem(this.getState().note).length > 0;
extensionsInContextOfItem(this.note).length > 0;
}
performFirefoxPinnedTabFix() {
@@ -454,14 +432,14 @@ class EditorCtrl extends PureCtrl {
}
}
saveNote(
async saveNote(
bypassDebouncer = false,
isUserModified = false,
dontUpdatePreviews = false,
customMutate?: (mutator: NoteMutator) => void
) {
this.performFirefoxPinnedTabFix();
const note = this.getState().note;
const note = this.note;
if (note.deleted) {
this.application.alertService!.alert(
@@ -469,15 +447,16 @@ class EditorCtrl extends PureCtrl {
);
return;
}
if (this.editor.isTemplateNote) {
await this.editor.insertTemplatedNote();
}
if (!this.application.findItem(note.uuid)) {
this.application.alertService!.alert(
STRING_INVALID_NOTE
);
return;
}
this.showSavingStatus();
this.application.changeItem(note.uuid, (mutator) => {
const noteMutator = mutator as NoteMutator;
if (customMutate) {
@@ -516,7 +495,7 @@ class EditorCtrl extends PureCtrl {
}
showAllChangesSavedStatus() {
this.setState({
this.setEditorState({
saveError: false,
syncTakingTooLong: false
});
@@ -532,7 +511,7 @@ class EditorCtrl extends PureCtrl {
desc: "Changes saved offline"
};
}
this.setState({
this.setEditorState({
saveError: true,
syncTakingTooLong: false
});
@@ -541,11 +520,11 @@ class EditorCtrl extends PureCtrl {
setStatus(status: { message: string, date?: Date }, wait = true) {
let waitForMs;
if (!this.getState().noteStatus || !this.getState().noteStatus!.date) {
if (!this.state.noteStatus || !this.state.noteStatus!.date) {
waitForMs = 0;
} else {
waitForMs = MINIMUM_STATUS_DURATION - (
new Date().getTime() - this.getState().noteStatus!.date!.getTime()
new Date().getTime() - this.state.noteStatus!.date!.getTime()
);
}
if (!wait || waitForMs < 0) {
@@ -556,7 +535,7 @@ class EditorCtrl extends PureCtrl {
}
this.statusTimeout = this.$timeout(() => {
status.date = new Date();
this.setState({
this.setEditorState({
noteStatus: status
});
}, waitForMs);
@@ -619,21 +598,21 @@ class EditorCtrl extends PureCtrl {
}
async deleteNote(permanently: boolean) {
if (this.getState().note.dummy) {
if (this.editor.isTemplateNote) {
this.application.alertService!.alert(
STRING_DELETE_PLACEHOLDER_ATTEMPT
);
return;
}
const run = () => {
if (this.getState().note.locked) {
if (this.note.locked) {
this.application.alertService!.alert(
STRING_DELETE_LOCKED_ATTEMPT
);
return;
}
const title = this.getState().note.safeTitle().length
? `'${this.getState().note.title}'`
const title = this.note.safeTitle().length
? `'${this.note.title}'`
: "this note";
const text = StringDeleteNote(
title,
@@ -646,7 +625,7 @@ class EditorCtrl extends PureCtrl {
undefined,
() => {
if (permanently) {
this.performNoteDeletion(this.getState().note);
this.performNoteDeletion(this.note);
} else {
this.saveNote(
true,
@@ -657,8 +636,7 @@ class EditorCtrl extends PureCtrl {
}
);
}
this.application.getAppState().setSelectedNote(undefined);
this.setMenuState('showOptionsMenu', false);
this.appState.closeEditor(this.editor);
},
undefined,
true,
@@ -681,16 +659,6 @@ class EditorCtrl extends PureCtrl {
performNoteDeletion(note: SNNote) {
this.application.deleteItem(note);
if (note === this.getState().note) {
this.setState({
note: null
});
}
if (note.dummy) {
this.application.deleteItemLocally(note);
return;
}
this.application.sync();
}
restoreTrashedNote() {
@@ -702,7 +670,7 @@ class EditorCtrl extends PureCtrl {
mutator.trashed = false;
}
);
this.application.getAppState().setSelectedNote(undefined);
this.appState.closeEditor(this.editor);
}
deleteNotePermanantely() {
@@ -735,7 +703,7 @@ class EditorCtrl extends PureCtrl {
false,
true,
(mutator) => {
mutator.pinned = !this.getState().note.pinned
mutator.pinned = !this.note.pinned
}
);
}
@@ -746,7 +714,7 @@ class EditorCtrl extends PureCtrl {
false,
true,
(mutator) => {
mutator.locked = !this.getState().note.locked
mutator.locked = !this.note.locked
}
);
}
@@ -757,7 +725,7 @@ class EditorCtrl extends PureCtrl {
false,
true,
(mutator) => {
mutator.protected = !this.getState().note.protected
mutator.protected = !this.note.protected
}
);
/** Show privileges manager if protection is not yet set up */
@@ -776,7 +744,7 @@ class EditorCtrl extends PureCtrl {
false,
true,
(mutator) => {
mutator.hidePreview = !this.getState().note.hidePreview
mutator.hidePreview = !this.note.hidePreview
}
);
}
@@ -787,21 +755,21 @@ class EditorCtrl extends PureCtrl {
false,
true,
(mutator) => {
mutator.archived = !this.getState().note.archived
mutator.archived = !this.note.archived
}
);
}
reloadTagsString() {
const tags = this.appState.getNoteTags(this.getState().note);
const tags = this.appState.getNoteTags(this.note);
const string = SNTag.arrayToDisplayString(tags);
this.updateUI(() => {
this.editorValues.tagsInputValue = string;
})
}
addTag(tag: SNTag) {
const tags = this.appState.getNoteTags(this.getState().note);
private addTag(tag: SNTag) {
const tags = this.appState.getNoteTags(this.note);
const strings = tags.map((currentTag) => {
return currentTag.title;
});
@@ -810,7 +778,7 @@ class EditorCtrl extends PureCtrl {
}
removeTag(tag: SNTag) {
const tags = this.appState.getNoteTags(this.getState().note);
const tags = this.appState.getNoteTags(this.note);
const strings = tags.map((currentTag) => {
return currentTag.title;
}).filter((title) => {
@@ -819,7 +787,7 @@ class EditorCtrl extends PureCtrl {
this.saveTagsFromStrings(strings);
}
async saveTagsFromStrings(strings?: string[]) {
public async saveTagsFromStrings(strings?: string[]) {
if (
!strings
&& this.editorValues.tagsInputValue === this.getState().tagsAsStrings
@@ -836,10 +804,8 @@ class EditorCtrl extends PureCtrl {
return string.trim();
});
}
const note = this.getState().note;
const note = this.note;
const currentTags = this.appState.getNoteTags(note);
const removeTags = [];
for (const tag of currentTags) {
if (strings.indexOf(tag.title) === -1) {
@@ -907,7 +873,7 @@ class EditorCtrl extends PureCtrl {
WebPrefKey.EditorResizersEnabled,
true
);
this.setState({
this.setEditorState({
monospaceEnabled,
spellcheck,
marginResizersEnabled
@@ -973,10 +939,10 @@ class EditorCtrl extends PureCtrl {
if (key === WebPrefKey.EditorSpellcheck) {
/** Allows textarea to reload */
await this.setState({
await this.setEditorState({
noteReady: false
});
this.setState({
this.setEditorState({
noteReady: true
});
this.reloadFont();
@@ -1000,42 +966,42 @@ class EditorCtrl extends PureCtrl {
],
activationHandler: (component) => {
if (component.area === 'note-tags') {
this.setState({
tagsComponent: component.active ? component : null
this.setEditorState({
tagsComponent: component.active ? component : undefined
});
} else if (component.area === 'editor-editor') {
if (
component === this.getState().selectedEditor &&
component === this.getState().editorComponent &&
!component.active
) {
this.setState({ selectedEditor: null });
this.setEditorState({ editorComponent: undefined });
}
else if (this.getState().selectedEditor) {
if (this.getState().selectedEditor!.active && this.getState().note) {
else if (this.getState().editorComponent) {
if (this.getState().editorComponent!.active && this.note) {
if (
component.isExplicitlyEnabledForItem(this.getState().note)
&& !this.getState().selectedEditor!.isExplicitlyEnabledForItem(this.getState().note)
component.isExplicitlyEnabledForItem(this.note)
&& !this.getState().editorComponent!.isExplicitlyEnabledForItem(this.note)
) {
this.setState({ selectedEditor: component });
this.setEditorState({ editorComponent: component });
}
}
}
else if (this.getState().note) {
else if (this.note) {
const enableable = (
component.isExplicitlyEnabledForItem(this.getState().note)
component.isExplicitlyEnabledForItem(this.note)
|| component.isDefaultEditor()
);
if (
component.active
&& enableable
) {
this.setState({ selectedEditor: component });
this.setEditorState({ editorComponent: component });
} else {
/**
* Not a candidate, and no qualified editor.
* Disable the current editor.
*/
this.setState({ selectedEditor: null });
this.setEditorState({ editorComponent: undefined });
}
}
@@ -1045,11 +1011,11 @@ class EditorCtrl extends PureCtrl {
},
contextRequestHandler: (component) => {
if (
component === this.getState().selectedEditor ||
component === this.getState().editorComponent ||
component === this.getState().tagsComponent ||
this.getState().componentStack!.includes(component)
) {
return this.getState().note;
return this.note;
}
},
focusHandler: (component, focused) => {
@@ -1093,7 +1059,7 @@ class EditorCtrl extends PureCtrl {
else if (action === ComponentAction.SaveItems) {
const includesNote = data.items.map((item: RawPayload) => {
return item.uuid;
}).includes(this.getState().note.uuid);
}).includes(this.note.uuid);
if (includesNote) {
this.showSavingStatus();
}
@@ -1109,19 +1075,19 @@ class EditorCtrl extends PureCtrl {
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
});
this.setState({
this.setEditorState({
componentStack: components
});
}
reloadComponentContext() {
this.reloadComponentStackArray();
if (this.getState().note) {
if (this.note) {
for (const component of this.getState().componentStack!) {
if (component.active) {
this.application.componentManager!.setComponentHidden(
component,
!component.isExplicitlyEnabledForItem(this.getState().note)
!component.isExplicitlyEnabledForItem(this.note)
);
}
}
@@ -1148,7 +1114,7 @@ class EditorCtrl extends PureCtrl {
}
disassociateComponentWithCurrentNote(component: SNComponent) {
const note = this.getState().note;
const note = this.note;
this.application.changeAndSaveItem(component.uuid, (m) => {
const mutator = m as ComponentMutator;
mutator.removeAssociatedItemId(note.uuid);
@@ -1157,7 +1123,7 @@ class EditorCtrl extends PureCtrl {
}
associateComponentWithCurrentNote(component: SNComponent) {
const note = this.getState().note;
const note = this.note;
this.application.changeAndSaveItem(component.uuid, (m) => {
const mutator = m as ComponentMutator;
mutator.removeDisassociatedItemId(note.uuid);
@@ -1171,12 +1137,12 @@ class EditorCtrl extends PureCtrl {
KeyboardModifier.Alt
],
onKeyDown: () => {
this.setState({
this.setEditorState({
altKeyDown: true
});
},
onKeyUp: () => {
this.setState({
this.setEditorState({
altKeyDown: false
});
}
@@ -1223,7 +1189,7 @@ class EditorCtrl extends PureCtrl {
element: editor,
key: KeyboardKey.Tab,
onKeyDown: (event) => {
if (this.getState().note.locked || event.shiftKey) {
if (this.note.locked || event.shiftKey) {
return;
}
event.preventDefault();
@@ -1260,16 +1226,17 @@ class EditorCtrl extends PureCtrl {
}
}
export class EditorPanel extends WebDirective {
export class EditorView extends WebDirective {
constructor() {
super();
this.restrict = 'E';
this.scope = {
editor: '=',
application: '='
};
this.template = template;
this.replace = true;
this.controller = EditorCtrl;
this.controller = EditorViewCtrl;
this.controllerAs = 'self';
this.bindToController = true;
}

View File

@@ -0,0 +1,5 @@
editor-view(
ng-repeat='editor in self.editors'
application='self.application'
editor='editor'
)

View File

@@ -0,0 +1,35 @@
import { WebApplication } from '@/ui_models/application';
import { WebDirective } from './../../types';
import template from './editor-group-view.pug';
import { Editor } from '@/ui_models/editor';
class EditorGroupViewCtrl {
private application!: WebApplication
public editors: Editor[] = []
/* @ngInject */
constructor() {
}
$onInit() {
this.application.editorGroup.addChangeObserver(() => {
this.editors = this.application.editorGroup.editors;
})
}
}
export class EditorGroupView extends WebDirective {
constructor() {
super();
this.template = template;
this.controller = EditorGroupViewCtrl;
this.replace = true;
this.controllerAs = 'self';
this.bindToController = true;
this.scope = {
application: '='
};
}
}

View File

@@ -0,0 +1,97 @@
.sn-component
#footer-bar.sk-app-bar.no-edges.no-bottom-edge
.left
.sk-app-bar-item(
click-outside='ctrl.clickOutsideAccountMenu()',
is-open='ctrl.showAccountMenu',
ng-click='ctrl.accountMenuPressed()'
)
.sk-app-bar-item-column
.sk-circle.small(
ng-class="ctrl.hasError ? 'danger' : (ctrl.user ? 'info' : 'neutral')"
)
.sk-app-bar-item-column
.sk-label.title(ng-class='{red: ctrl.hasError}') Account
account-menu(
close-function='ctrl.closeAccountMenu()',
ng-click='$event.stopPropagation()',
ng-if='ctrl.showAccountMenu',
application='ctrl.application'
)
.sk-app-bar-item
a.no-decoration.sk-label.title(
href='https://standardnotes.org/help',
rel='noopener',
target='_blank'
)
| Help
.sk-app-bar-item.border
.sk-app-bar-item(ng-repeat='room in ctrl.rooms track by room.uuid')
.sk-app-bar-item-column(ng-click='ctrl.selectRoom(room)')
.sk-label {{room.name}}
component-modal(
component='room',
ng-if='ctrl.roomShowState[room.uuid]',
on-dismiss='ctrl.onRoomDismiss()',
application='ctrl.application'
)
.center
.sk-app-bar-item(ng-if='ctrl.arbitraryStatusMessage')
.sk-app-bar-item-column
span.neutral.sk-label {{ctrl.arbitraryStatusMessage}}
.right
.sk-app-bar-item(
ng-click='ctrl.openSecurityUpdate()',
ng-if='ctrl.state.dataUpgradeAvailable'
)
span.success.sk-label Encryption upgrade available.
.sk-app-bar-item(
ng-click='ctrl.clickedNewUpdateAnnouncement()',
ng-if='ctrl.newUpdateAvailable == true'
)
span.info.sk-label New update available.
.sk-app-bar-item.no-pointer(
ng-if='ctrl.lastSyncDate && !ctrl.isRefreshing'
)
.sk-label.subtle
| Last refreshed {{ctrl.lastSyncDate}}
.sk-app-bar-item(
ng-click='ctrl.toggleSyncResolutionMenu()',
ng-if='(ctrl.state.outOfSync && !ctrl.isRefreshing) || ctrl.showSyncResolution'
)
.sk-label.warning(ng-if='ctrl.state.outOfSync') Potentially Out of Sync
sync-resolution-menu(
close-function='ctrl.toggleSyncResolutionMenu()',
ng-click='$event.stopPropagation();',
ng-if='ctrl.showSyncResolution',
application='ctrl.application'
)
.sk-app-bar-item(ng-if='ctrl.lastSyncDate && ctrl.isRefreshing')
.sk-spinner.small
.sk-app-bar-item(ng-if='ctrl.offline')
.sk-label Offline
.sk-app-bar-item(ng-click='ctrl.refreshData()', ng-if='!ctrl.offline')
.sk-label Refresh
.sk-app-bar-item.border(ng-if='ctrl.dockShortcuts.length > 0')
.sk-app-bar-item.dock-shortcut(ng-repeat='shortcut in ctrl.dockShortcuts')
.sk-app-bar-item-column(
ng-class="{'underline': shortcut.component.active}",
ng-click='ctrl.selectShortcut(shortcut)'
)
.div(ng-if="shortcut.icon.type == 'circle'", title='{{shortcut.name}}')
.sk-circle.small(
ng-style="{'background-color': shortcut.icon.background_color, 'border-color': shortcut.icon.border_color}"
)
.div(ng-if="shortcut.icon.type == 'svg'", title='{{shortcut.name}}')
.svg-item(
elem-ready='ctrl.initSvgForShortcut(shortcut)',
ng-attr-id='dock-svg-{{shortcut.component.uuid}}'
)
.sk-app-bar-item.border(ng-if='ctrl.state.hasPasscode')
#lock-item.sk-app-bar-item(
ng-click='ctrl.lockApp()',
ng-if='ctrl.state.hasPasscode',
title='Locks application and wipes unencrypted data from memory.'
)
.sk-label
i#footer-lock-icon.icon.ion-locked

View File

@@ -1,4 +1,4 @@
import { FooterStatus, WebDirective } from './../types';
import { FooterStatus, WebDirective } from '@/types';
import { dateToLocalizedString } from '@/utils';
import {
ApplicationEvent,
@@ -10,13 +10,13 @@ import {
ComponentArea,
ComponentAction
} from 'snjs';
import template from '%/footer.pug';
import template from './footer-view.pug';
import { AppStateEvent, EventSource } from '@/services/state';
import {
STRING_GENERIC_SYNC_ERROR,
STRING_NEW_UPDATE_READY
} from '@/strings';
import { PureCtrl } from '@Controllers/abstract/pure_ctrl';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import { ComponentMutator } from '@/../../../../snjs/dist/@types/models';
type DockShortcut = {
@@ -29,7 +29,7 @@ type DockShortcut = {
}
}
class FooterCtrl extends PureCtrl {
class FooterViewCtrl extends PureViewCtrl {
private $rootScope: ng.IRootScopeService
private rooms: SNComponent[] = []
@@ -427,15 +427,16 @@ class FooterCtrl extends PureCtrl {
}
}
export class Footer extends WebDirective {
export class FooterView extends WebDirective {
constructor() {
super();
this.restrict = 'E';
this.template = template;
this.controller = FooterCtrl;
this.controller = FooterViewCtrl;
this.replace = true;
this.controllerAs = 'ctrl';
this.bindToController = {
this.bindToController = true;
this.scope = {
application: '='
};
}

View File

@@ -0,0 +1,11 @@
export { PureViewCtrl } from './abstract/pure_view_ctrl';
export { ApplicationGroupView } from './application_group/application_group_view';
export { ApplicationView } from './application/application_view';
export { EditorGroupView } from './editor_group/editor_group_view';
export { EditorView } from './editor/editor_view';
export { FooterView } from './footer/footer_view';
export { NotesView } from './notes/notes_view';
export { TagsView } from './tags/tags_view';

View File

@@ -111,10 +111,8 @@ export function sortNotes(
notes: SNNote[] = [],
sortBy: string,
reverse: boolean
) {
) {
const sortValueFn = (a: SNNote, b: SNNote, pinCheck = false): number => {
if (a.dummy) { return -1; }
if (b.dummy) { return 1; }
if (!pinCheck) {
if (a.pinned && b.pinned) {
return sortValueFn(a, b, true);
@@ -123,7 +121,7 @@ export function sortNotes(
if (b.pinned) { return 1; }
}
let aValue = (a as any)[sortBy] || '';
let bValue = (a as any)[sortBy] || '';
let bValue = (b as any)[sortBy] || '';
let vector = 1;
if (reverse) {
vector *= -1;

View File

@@ -0,0 +1,149 @@
#notes-column.sn-component.section.notes(aria-label='Notes')
.content
#notes-title-bar.section-title-bar
.padded
.section-title-bar-header
.title {{self.state.panelTitle}}
.sk-button.contrast.wide(
ng-click='self.createNewNote()',
title='Create a new note in the selected tag'
)
.sk-label
i.icon.ion-plus.add-button
.filter-section(role='search')
input#search-bar.filter-bar(
ng-blur='self.onFilterEnter()',
ng-change='self.filterTextChanged()',
ng-keyup='$event.keyCode == 13 && self.onFilterEnter();',
ng-model='self.state.noteFilter.text',
placeholder='Search',
select-on-click='true',
title='Searches notes in the currently selected tag'
)
#search-clear-button(
ng-click='self.clearFilterText();',
ng-show='self.state.noteFilter.text'
) ✕
#notes-menu-bar.sn-component
.sk-app-bar.no-edges
.left
.sk-app-bar-item(
ng-class="{'selected' : self.state.mutable.showMenu}",
ng-click='self.state.mutable.showMenu = !self.state.mutable.showMenu'
)
.sk-app-bar-item-column
.sk-label
| Options
.sk-app-bar-item-column
.sk-sublabel {{self.optionsSubtitle()}}
#notes-options-menu.sk-menu-panel.dropdown-menu(
ng-show='self.state.mutable.showMenu'
)
.sk-menu-panel-header
.sk-menu-panel-header-title Sort By
a.info.sk-h5(ng-click='self.toggleReverseSort()')
| {{self.state.sortReverse === true ? 'Disable Reverse Sort' : 'Enable Reverse Sort'}}
menu-row(
action="self.selectedMenuItem(); self.selectedSortByCreated()"
circle="self.state.sortBy == 'created_at' && 'success'"
desc="'Sort notes by newest first'"
label="'Date Added'"
)
menu-row(
action="self.selectedMenuItem(); self.selectedSortByUpdated()"
circle="self.state.sortBy == 'client_updated_at' && 'success'"
desc="'Sort notes with the most recently updated first'"
label="'Date Modified'"
)
menu-row(
action="self.selectedMenuItem(); self.selectedSortByTitle()"
circle="self.state.sortBy == 'title' && 'success'"
desc="'Sort notes alphabetically by their title'"
label="'Title'"
)
.sk-menu-panel-section
.sk-menu-panel-header
.sk-menu-panel-header-title Display
menu-row(
action="self.selectedMenuItem(); self.toggleWebPrefKey('showArchived')"
circle="self.state.showArchived ? 'success' : 'danger'"
desc=`'Archived notes are usually hidden.
You can explicitly show them with this option.'`
faded="!self.state.showArchived"
label="'Archived Notes'"
)
menu-row(
action="self.selectedMenuItem(); self.toggleWebPrefKey('hidePinned')"
circle="self.state.hidePinned ? 'danger' : 'success'"
desc=`'Pinned notes always appear on top. You can hide them temporarily
with this option so you can focus on other notes in the list.'`
faded="self.state.hidePinned"
label="'Pinned Notes'"
)
menu-row(
action="self.selectedMenuItem(); self.toggleWebPrefKey('hideNotePreview')"
circle="self.state.hideNotePreview ? 'danger' : 'success'"
desc="'Hide the note preview for a more condensed list of notes'"
faded="self.state.hideNotePreview"
label="'Note Preview'"
)
menu-row(
action="self.selectedMenuItem(); self.toggleWebPrefKey('hideDate')"
circle="self.state.hideDate ? 'danger' : 'success'"
desc="'Hide the date displayed in each row'"
faded="self.state.hideDate"
label="'Date'"
)
menu-row(
action="self.selectedMenuItem(); self.toggleWebPrefKey('hideTags')"
circle="self.state.hideTags ? 'danger' : 'success'"
desc="'Hide the list of tags associated with each note'"
faded="self.state.hideTags"
label="'Tags'"
)
.scrollable
#notes-scrollable.infinite-scroll(
can-load='true',
infinite-scroll='self.paginate()',
threshold='200'
)
.note(
ng-repeat='note in self.state.renderedNotes track by note.uuid'
ng-class="{'selected' : self.activeEditorNote == note}"
ng-click='self.selectNote(note)'
)
.note-flags(ng-show='self.noteFlags[note.uuid].length > 0')
.flag(ng-class='flag.class', ng-repeat='flag in self.noteFlags[note.uuid]')
.label {{flag.text}}
.name(ng-show='note.title')
| {{note.title}}
.note-preview(
ng-if=`
!self.state.hideNotePreview &&
!note.hidePreview &&
!note.protected`
)
.html-preview(
ng-bind-html='note.preview_html',
ng-show='note.preview_html'
)
.plain-preview(
ng-show='!note.preview_html && note.preview_plain'
) {{note.preview_plain}}
.default-preview(
ng-show='!note.preview_html && !note.preview_plain'
) {{note.text}}
.date.faded(ng-show='!self.state.hideDate')
span(ng-show="self.state.sortBy == 'client_updated_at'")
| Modified {{note.updatedAtString || 'Now'}}
span(ng-show="self.state.sortBy != 'client_updated_at'")
| {{note.createdAtString || 'Now'}}
panel-resizer(
collapsable="true"
control="self.panelPuppet"
default-width="300"
hoverable="true"
on-resize-finish="self.onPanelResize()"
panel-id="'notes-column'"
)

View File

@@ -1,13 +1,20 @@
import { PanelPuppet, WebDirective } from './../../types';
import angular from 'angular';
import template from '%/notes.pug';
import { ApplicationEvent, ContentType, removeFromArray, SNNote, SNTag, WebPrefKey } from 'snjs';
import { PureCtrl } from '@Controllers/abstract/pure_ctrl';
import template from './notes-view.pug';
import {
ApplicationEvent,
ContentType,
removeFromArray,
SNNote,
SNTag,
WebPrefKey
} from 'snjs';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import { AppStateEvent } from '@/services/state';
import { KeyboardModifier, KeyboardKey } from '@/services/keyboardManager';
import {
PANEL_NAME_NOTES
} from '@/controllers/constants';
} from '@/views/constants';
import {
NoteSortKey,
filterAndSortNotes
@@ -15,6 +22,7 @@ import {
import { UuidString } from '@/../../../../snjs/dist/@types/types';
type NotesState = {
panelTitle: string
tag?: SNTag
notes?: SNNote[]
renderedNotes?: SNNote[]
@@ -43,7 +51,7 @@ const DEFAULT_LIST_NUM_NOTES = 20;
const ELEMENT_ID_SEARCH_BAR = 'search-bar';
const ELEMENT_ID_SCROLL_CONTAINER = 'notes-scrollable';
class NotesCtrl extends PureCtrl {
class NotesViewCtrl extends PureViewCtrl {
private panelPuppet?: PanelPuppet
private reloadNotesPromise?: any
@@ -101,13 +109,17 @@ class NotesCtrl extends PureCtrl {
return this.state as NotesState;
}
async setNotesState(state: Partial<NotesState>) {
return this.setState(state);
}
getInitialState() {
return {
notes: [],
renderedNotes: [],
mutable: { showMenu: false },
noteFilter: { text: '' },
} as NotesState;
} as Partial<NotesState>;
}
async onAppLaunch() {
@@ -120,13 +132,10 @@ class NotesCtrl extends PureCtrl {
onAppStateEvent(eventName: AppStateEvent, data?: any) {
if (eventName === AppStateEvent.TagChanged) {
this.handleTagChange(
this.application!.getAppState().getSelectedTag()!,
data.previousTag
);
} else if (eventName === AppStateEvent.NoteChanged) {
this.handleNoteSelection(
this.application!.getAppState().getSelectedNote()!
this.application!.getAppState().getSelectedTag()!
);
} else if (eventName === AppStateEvent.ActiveEditorChanged) {
this.handleEditorChange();
} else if (eventName === AppStateEvent.PreferencesChanged) {
this.reloadPreferences();
this.reloadNotes();
@@ -135,19 +144,19 @@ class NotesCtrl extends PureCtrl {
}
}
get selectedNote() {
return this.appState.getSelectedNote();
get activeEditorNote() {
const activeEditor = this.appState.getActiveEditor();
return activeEditor && activeEditor.note;
}
public get editorNotes() {
return this.appState.getEditors().map((editor) => editor.note);
}
/** @override */
async onAppEvent(eventName: ApplicationEvent) {
if (eventName === ApplicationEvent.SignedIn) {
/** Delete dummy note if applicable */
if (this.selectedNote && this.selectedNote!.dummy) {
this.application!.deleteItemLocally(this.selectedNote!);
await this.selectNote(undefined);
await this.reloadNotes();
}
this.appState.closeAllEditors();
} else if (eventName === ApplicationEvent.CompletedSync) {
this.getMostValidNotes().then((notes) => {
if (notes.length === 0) {
@@ -196,9 +205,9 @@ class NotesCtrl extends PureCtrl {
[ContentType.Note, ContentType.Tag],
async (items) => {
await this.reloadNotes();
const selectedNote = this.selectedNote;
if (selectedNote) {
const discarded = selectedNote.deleted || selectedNote.trashed;
const activeNote = this.activeEditorNote;
if (activeNote) {
const discarded = activeNote.deleted || activeNote.trashed;
if (discarded) {
this.selectNextOrCreateNew();
}
@@ -218,51 +227,20 @@ class NotesCtrl extends PureCtrl {
);
}
async selectNote(note?: SNNote) {
return this.application!.getAppState().setSelectedNote(note);
async selectNote(note: SNNote) {
this.appState.openEditor(note.uuid);
}
async createNewNote() {
const selectedTag = this.application!.getAppState().getSelectedTag();
if (!selectedTag) {
throw 'Attempting to create note with no selected tag';
}
let title;
let isDummyNote = true;
let title = `Note ${this.getState().notes!.length + 1}`;
if (this.isFiltering()) {
title = this.getState().noteFilter.text;
isDummyNote = false;
} else if (this.selectedNote && this.selectedNote!.dummy) {
return;
} else {
title = `Note ${this.getState().notes!.length + 1}`;
}
const newNote = await this.application!.createManagedItem(
ContentType.Note,
{
text: '',
title: title,
references: []
},
true,
{
dummy: isDummyNote
}
) as SNNote;
if (!selectedTag.isSmartTag()) {
this.application!.changeItem(selectedTag.uuid, (mutator) => {
mutator.addItemAsRelationship(newNote);
});
}
this.selectNote(newNote);
this.appState.createEditor(title);
}
async handleTagChange(tag: SNTag, previousTag?: SNTag) {
if (this.selectedNote && this.selectedNote!.dummy) {
await this.application!.deleteItemLocally(this.selectedNote!);
await this.selectNote(undefined);
}
await this.setState({ tag: tag });
async handleTagChange(tag: SNTag) {
await this.setNotesState({ tag });
this.resetScrollPosition();
this.setShowMenuFalse();
@@ -270,7 +248,8 @@ class NotesCtrl extends PureCtrl {
this.application!.getDesktopService().searchText();
this.resetPagination();
/* Capture db load state before beginning reloadNotes, since this status may change during reload */
/* Capture db load state before beginning reloadNotes,
since this status may change during reload */
const dbLoaded = this.application!.isDatabaseLoaded();
await this.reloadNotes();
@@ -280,10 +259,10 @@ class NotesCtrl extends PureCtrl {
if (!tag.isSmartTag() || tag.isAllTag) {
this.createPlaceholderNote();
} else if (
this.selectedNote &&
!this.getState().notes!.includes(this.selectedNote!)
this.activeEditorNote &&
!this.getState().notes!.includes(this.activeEditorNote!)
) {
this.selectNote(undefined);
this.appState.closeActiveEditor();
}
}
}
@@ -299,7 +278,7 @@ class NotesCtrl extends PureCtrl {
async removeNoteFromList(note: SNNote) {
const notes = this.getState().notes!;
removeFromArray(notes, note);
await this.setState({
await this.setNotesState({
notes: notes,
renderedNotes: notes.slice(0, this.notesToDisplay)
});
@@ -330,7 +309,7 @@ class NotesCtrl extends PureCtrl {
this.loadFlagsForNote(note);
}
}
await this.setState({
await this.setNotesState({
notes: notes,
renderedNotes: notes.slice(0, this.notesToDisplay)
});
@@ -338,7 +317,7 @@ class NotesCtrl extends PureCtrl {
}
setShowMenuFalse() {
this.setState({
this.setNotesState({
mutable: {
...this.getState().mutable,
showMenu: false
@@ -346,20 +325,10 @@ class NotesCtrl extends PureCtrl {
});
}
async handleNoteSelection(note: SNNote) {
const previousNote = this.selectedNote;
if (previousNote === note) {
return;
}
if (previousNote && previousNote.dummy) {
await this.application!.deleteItemLocally(previousNote);
this.removeNoteFromList(previousNote);
}
if (!note) {
return;
}
if (note.conflictOf) {
this.application!.changeAndSaveItem(note.uuid, (mutator) => {
async handleEditorChange() {
const activeNote = this.appState.getActiveEditor().note;
if (activeNote && activeNote.conflictOf) {
this.application!.changeAndSaveItem(activeNote.uuid, (mutator) => {
mutator.conflictOf = undefined;
})
}
@@ -404,7 +373,7 @@ class NotesCtrl extends PureCtrl {
WebPrefKey.NotesHideTags,
false
);
this.setState({
this.setNotesState({
...viewOptions
});
if (prevSortValue && prevSortValue !== sortBy) {
@@ -469,7 +438,7 @@ class NotesCtrl extends PureCtrl {
} else if (this.getState().tag) {
title = `${this.getState().tag!.title}`;
}
this.setState({
this.setNotesState({
panelTitle: title
});
}
@@ -584,7 +553,7 @@ class NotesCtrl extends PureCtrl {
selectNextNote() {
const displayableNotes = this.displayableNotes();
const currentIndex = displayableNotes.findIndex((candidate) => {
return candidate.uuid === this.selectedNote!.uuid
return candidate.uuid === this.activeEditorNote!.uuid
});
if (currentIndex + 1 < displayableNotes.length) {
this.selectNote(displayableNotes[currentIndex + 1]);
@@ -598,13 +567,13 @@ class NotesCtrl extends PureCtrl {
} else if (!this.getState().tag || !this.getState().tag!.isSmartTag()) {
this.createPlaceholderNote();
} else {
this.selectNote(undefined);
this.appState.closeActiveEditor();
}
}
selectPreviousNote() {
const displayableNotes = this.displayableNotes();
const currentIndex = displayableNotes.indexOf(this.selectedNote!);
const currentIndex = displayableNotes.indexOf(this.activeEditorNote!);
if (currentIndex - 1 >= 0) {
this.selectNote(displayableNotes[currentIndex - 1]);
return true;
@@ -619,7 +588,7 @@ class NotesCtrl extends PureCtrl {
}
async setNoteFilterText(text: string) {
await this.setState({
await this.setNotesState({
noteFilter: {
...this.getState().noteFilter,
text: text
@@ -748,12 +717,12 @@ class NotesCtrl extends PureCtrl {
}
}
export class NotesPanel extends WebDirective {
export class NotesView extends WebDirective {
constructor() {
super();
this.template = template;
this.replace = true;
this.controller = NotesCtrl;
this.controller = NotesViewCtrl;
this.controllerAs = 'self';
this.bindToController = true;
this.scope = {

View File

@@ -0,0 +1,72 @@
#tags-column.sn-component.section.tags(aria-label='Tags')
.component-view-container(ng-if='self.component.active')
component-view.component-view(
component='self.component',
application='self.application'
)
#tags-content.content(ng-if='!(self.component && self.component.active)')
.tags-title-section.section-title-bar
.section-title-bar-header
.sk-h3.title
span.sk-bold Views
.sk-button.sk-secondary-contrast.wide(
ng-click='self.clickedAddNewTag()',
title='Create a new tag'
)
.sk-label
i.icon.ion-plus.add-button
.scrollable
.infinite-scroll
.tag(
ng-class="{'selected' : self.state.selectedTag == tag, 'faded' : !tag.isAllTag}",
ng-click='self.selectTag(tag)',
ng-repeat='tag in self.state.smartTags track by tag.uuid'
)
.tag-info
input.title(
ng-disabled='true',
ng-change='self.onTagTitleChange(tag)'
ng-model='tag.title'
)
.count(ng-show='tag.isAllTag') {{self.state.noteCounts[tag.uuid]}}
.tags-title-section.section-title-bar
.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
.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.newTag || self.state.editingTag == tag',
sn-autofocus='true',
spellcheck='false'
)
.count {{self.state.noteCounts[tag.uuid]}}
.danger.small-text.bold(ng-show='tag.conflictOf') Conflicted Copy
.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(tag)' ng-show='!self.state.editingTag') Rename
a.item(ng-click='self.saveTag($event, tag)' ng-show='self.state.editingTag') Save
a.item(ng-click='self.selectedDeleteTag(tag)') Delete
.no-tags-placeholder(ng-show='self.state.tags.length == 0')
| No tags. Create one using the add button above.
panel-resizer(
collapsable='true',
control='self.panelPuppet',
default-width='150',
hoverable='true',
on-resize-finish='self.onPanelResize()',
panel-id="'tags-column'"
)

View File

@@ -1,7 +1,6 @@
import { WebDirective, PanelPuppet } from './../types';
import { WebApplication } from './../application';
import { WebDirective, PanelPuppet } from '@/types';
import { WebApplication } from '@/ui_models/application';
import {
SNNote,
SNTag,
ContentType,
ApplicationEvent,
@@ -11,17 +10,17 @@ import {
SNComponent,
WebPrefKey
} from 'snjs';
import template from '%/tags.pug';
import template from './tags-view.pug';
import { AppStateEvent } from '@/services/state';
import { PANEL_NAME_TAGS } from '@/controllers/constants';
import { PANEL_NAME_TAGS } from '@/views/constants';
import { STRING_DELETE_TAG } from '@/strings';
import { PureCtrl } from '@Controllers/abstract/pure_ctrl';
import { PureViewCtrl } from '@Views/abstract/pure_view_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 TagsViewCtrl extends PureViewCtrl {
/** Passed through template */
readonly application!: WebApplication
@@ -106,6 +105,10 @@ class TagsPanelCtrl extends PureCtrl {
});
if (!matchingTag || matchingTag.deleted) {
this.selectTag(this.state.smartTags[0]);
} else {
this.setState({
selectedTag: matchingTag
})
}
}
}
@@ -342,7 +345,7 @@ class TagsPanelCtrl extends PureCtrl {
}
}
export class TagsPanel extends WebDirective {
export class TagsView extends WebDirective {
constructor() {
super();
this.restrict = 'E';
@@ -351,7 +354,7 @@ export class TagsPanel extends WebDirective {
};
this.template = template;
this.replace = true;
this.controller = TagsPanelCtrl;
this.controller = TagsViewCtrl;
this.controllerAs = 'self';
this.bindToController = true;
}