929 lines
29 KiB
JavaScript
929 lines
29 KiB
JavaScript
|
|
import angular from 'angular';
|
|
import { SFModelManager } from 'snjs';
|
|
import { isDesktopApplication } from '@/utils';
|
|
import { KeyboardManager } from '@/services/keyboardManager';
|
|
import { PrivilegesManager } from '@/services/privilegesManager';
|
|
import template from '%/editor.pug';
|
|
|
|
export class EditorPanel {
|
|
constructor() {
|
|
this.restrict = 'E';
|
|
this.scope = {
|
|
remove: '&',
|
|
note: '=',
|
|
updateTags: '&'
|
|
};
|
|
|
|
this.template = template;
|
|
this.replace = true;
|
|
this.controllerAs = 'ctrl';
|
|
this.bindToController = true;
|
|
}
|
|
|
|
link(scope, elem, attrs, ctrl) {
|
|
scope.$watch('ctrl.note', (note, oldNote) => {
|
|
if (note) {
|
|
ctrl.noteDidChange(note, oldNote);
|
|
}
|
|
});
|
|
}
|
|
|
|
/* @ngInject */
|
|
controller(
|
|
$timeout,
|
|
authManager,
|
|
$rootScope,
|
|
actionsManager,
|
|
syncManager,
|
|
modelManager,
|
|
themeManager,
|
|
componentManager,
|
|
storageManager,
|
|
sessionHistory,
|
|
privilegesManager,
|
|
keyboardManager,
|
|
desktopManager,
|
|
alertManager
|
|
) {
|
|
this.spellcheck = true;
|
|
this.componentManager = componentManager;
|
|
this.componentStack = [];
|
|
this.isDesktop = isDesktopApplication();
|
|
|
|
const MinimumStatusDurationMs = 400;
|
|
|
|
syncManager.addEventHandler((eventName, data) => {
|
|
if(!this.note) {
|
|
return;
|
|
}
|
|
|
|
if(eventName == "sync:taking-too-long") {
|
|
this.syncTakingTooLong = true;
|
|
}
|
|
|
|
else if(eventName == "sync:completed") {
|
|
this.syncTakingTooLong = false;
|
|
if(this.note.dirty) {
|
|
// if we're still dirty, don't change status, a sync is likely upcoming.
|
|
} else {
|
|
let savedItem = data.savedItems.find((item) => item.uuid == this.note.uuid);
|
|
let isInErrorState = this.saveError;
|
|
if(isInErrorState || savedItem) {
|
|
this.showAllChangesSavedStatus();
|
|
}
|
|
}
|
|
} else if(eventName == "sync:error") {
|
|
// only show error status in editor if the note is dirty. Otherwise, it means the originating sync
|
|
// came from somewhere else and we don't want to display an error here.
|
|
if(this.note.dirty){
|
|
this.showErrorStatus();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Right now this only handles offline saving status changes.
|
|
this.syncStatusObserver = syncManager.registerSyncStatusObserver((status) => {
|
|
if(status.localError) {
|
|
$timeout(() => {
|
|
this.showErrorStatus({
|
|
message: "Offline Saving Issue",
|
|
desc: "Changes not saved"
|
|
});
|
|
}, 500)
|
|
} else {
|
|
}
|
|
})
|
|
|
|
modelManager.addItemSyncObserver("editor-note-observer", "Note", (allItems, validItems, deletedItems, source) => {
|
|
if(!this.note) { return; }
|
|
|
|
// Before checking if isMappingSourceRetrieved, we check if this item was deleted via a local source,
|
|
// such as alternating uuids during sign in. Otherwise, we only want to make interface updates if it's a
|
|
// remote retrieved source.
|
|
if(this.note.deleted || this.note.content.trashed) {
|
|
$rootScope.notifyDelete();
|
|
return;
|
|
}
|
|
|
|
if(!SFModelManager.isMappingSourceRetrieved(source)) {
|
|
return;
|
|
}
|
|
|
|
var matchingNote = allItems.find((item) => {
|
|
return item.uuid == this.note.uuid;
|
|
});
|
|
|
|
if(!matchingNote) {
|
|
return;
|
|
}
|
|
|
|
// Update tags
|
|
this.loadTagsString();
|
|
});
|
|
|
|
modelManager.addItemSyncObserver("editor-tag-observer", "Tag", (allItems, validItems, deletedItems, source) => {
|
|
if(!this.note) { return; }
|
|
|
|
for(var tag of allItems) {
|
|
// If a tag is deleted then we'll have lost references to notes. Reload anyway.
|
|
if(this.note.savedTagsString == null || tag.deleted || tag.hasRelationshipWithItem(this.note)) {
|
|
this.loadTagsString();
|
|
return;
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
modelManager.addItemSyncObserver("editor-component-observer", "SN|Component", (allItems, validItems, deletedItems, source) => {
|
|
if(!this.note) { return; }
|
|
|
|
// Reload componentStack in case new ones were added or removed
|
|
this.reloadComponentStackArray();
|
|
|
|
// Observe editor changes to see if the current note should update its editor
|
|
var editors = allItems.filter(function(item) {
|
|
return item.isEditor();
|
|
});
|
|
|
|
// If no editors have changed
|
|
if(editors.length == 0) {
|
|
return;
|
|
}
|
|
|
|
// Look through editors again and find the most proper one
|
|
var editor = this.editorForNote(this.note);
|
|
this.selectedEditor = editor;
|
|
if(!editor) {
|
|
this.reloadFont();
|
|
}
|
|
});
|
|
|
|
this.noteDidChange = function(note, oldNote) {
|
|
this.setNote(note, oldNote);
|
|
this.reloadComponentContext();
|
|
}
|
|
|
|
this.setNote = function(note, oldNote) {
|
|
this.showExtensions = false;
|
|
this.showMenu = false;
|
|
this.noteStatus = null;
|
|
// When setting alt key down and deleting note, an alert will come up and block the key up event when alt is released.
|
|
// We reset it on set note so that the alt menu restores to default.
|
|
this.altKeyDown = false;
|
|
this.loadTagsString();
|
|
|
|
let onReady = () => {
|
|
this.noteReady = true;
|
|
$timeout(() => {
|
|
this.loadPreferences();
|
|
})
|
|
}
|
|
|
|
let associatedEditor = this.editorForNote(note);
|
|
if(associatedEditor && associatedEditor != this.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.noteReady = false;
|
|
// switch after timeout, so that note data isnt posted to current editor
|
|
$timeout(() => {
|
|
this.selectedEditor = associatedEditor;
|
|
onReady();
|
|
})
|
|
} else if(associatedEditor) {
|
|
// Same editor as currently active
|
|
onReady();
|
|
} else {
|
|
// No editor
|
|
this.selectedEditor = null;
|
|
onReady();
|
|
}
|
|
|
|
if(note.safeText().length == 0 && note.dummy) {
|
|
this.focusTitle(100);
|
|
}
|
|
|
|
if(oldNote && oldNote != note) {
|
|
if(oldNote.dummy) {
|
|
this.remove()(oldNote);
|
|
}
|
|
}
|
|
}
|
|
|
|
this.editorForNote = function(note) {
|
|
return componentManager.editorForNote(note);
|
|
}
|
|
|
|
this.closeAllMenus = function() {
|
|
this.showEditorMenu = false;
|
|
this.showMenu = false;
|
|
this.showExtensions = false;
|
|
}
|
|
|
|
this.toggleMenu = function(menu) {
|
|
this[menu] = !this[menu];
|
|
|
|
let allMenus = ['showMenu', 'showEditorMenu', 'showExtensions', 'showSessionHistory'];
|
|
for(var candidate of allMenus) {
|
|
if(candidate != menu) {
|
|
this[candidate] = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.editorMenuOnSelect = function(component) {
|
|
if(!component || component.area == "editor-editor") {
|
|
// if plain editor or other editor
|
|
this.showEditorMenu = false;
|
|
var editor = component;
|
|
if(this.selectedEditor && editor !== this.selectedEditor) {
|
|
this.disassociateComponentWithCurrentNote(this.selectedEditor);
|
|
}
|
|
if(editor) {
|
|
if(this.note.getAppDataItem("prefersPlainEditor") == true) {
|
|
this.note.setAppDataItem("prefersPlainEditor", false);
|
|
modelManager.setItemDirty(this.note, true);
|
|
}
|
|
this.associateComponentWithCurrentNote(editor);
|
|
} else {
|
|
// Note prefers plain editor
|
|
if(!this.note.getAppDataItem("prefersPlainEditor")) {
|
|
this.note.setAppDataItem("prefersPlainEditor", true);
|
|
modelManager.setItemDirty(this.note, true);
|
|
}
|
|
$timeout(() => {
|
|
this.reloadFont();
|
|
})
|
|
}
|
|
|
|
this.selectedEditor = editor;
|
|
} else if(component.area == "editor-stack") {
|
|
// If component stack item
|
|
this.toggleStackComponentForCurrentItem(component);
|
|
}
|
|
|
|
// Lots of dirtying can happen above, so we'll sync
|
|
syncManager.sync();
|
|
}.bind(this)
|
|
|
|
this.hasAvailableExtensions = function() {
|
|
return actionsManager.extensionsInContextOfItem(this.note).length > 0;
|
|
}
|
|
|
|
this.focusEditor = function(delay) {
|
|
setTimeout(function(){
|
|
var element = document.getElementById("note-text-editor");
|
|
if(element) {
|
|
element.focus();
|
|
}
|
|
}, delay)
|
|
}
|
|
|
|
this.focusTitle = function(delay) {
|
|
setTimeout(function(){
|
|
document.getElementById("note-title-editor").focus();
|
|
}, delay)
|
|
}
|
|
|
|
this.clickedTextArea = function() {
|
|
this.showMenu = false;
|
|
}
|
|
|
|
this.EditorNgDebounce = 200;
|
|
const SyncDebouce = 350;
|
|
const SyncNoDebounce = 100;
|
|
|
|
this.saveNote = function({bypassDebouncer, updateClientModified, dontUpdatePreviews}) {
|
|
let note = this.note;
|
|
note.dummy = false;
|
|
|
|
if(note.deleted) {
|
|
alertManager.alert({text: "The note you are attempting to edit has been deleted, and is awaiting sync. Changes you make will be disregarded."});
|
|
return;
|
|
}
|
|
|
|
if(!modelManager.findItem(note.uuid)) {
|
|
alertManager.alert({text: "The note you are attempting to save can not be found or has been deleted. Changes you make will not be synced. Please copy this note's text and start a new note."});
|
|
return;
|
|
}
|
|
|
|
this.showSavingStatus();
|
|
|
|
if(!dontUpdatePreviews) {
|
|
let limit = 80;
|
|
var text = note.text || "";
|
|
var truncate = text.length > limit;
|
|
note.content.preview_plain = text.substring(0, limit) + (truncate ? "..." : "");
|
|
// Clear dynamic previews if using plain editor
|
|
note.content.preview_html = null;
|
|
}
|
|
|
|
modelManager.setItemDirty(note, true, updateClientModified);
|
|
|
|
if(this.saveTimeout) {
|
|
$timeout.cancel(this.saveTimeout);
|
|
}
|
|
|
|
let syncDebouceMs;
|
|
if(authManager.offline() || bypassDebouncer) {
|
|
syncDebouceMs = SyncNoDebounce;
|
|
} else {
|
|
syncDebouceMs = SyncDebouce;
|
|
}
|
|
|
|
this.saveTimeout = $timeout(() => {
|
|
syncManager.sync().then((response) => {
|
|
if(response && response.error && !this.didShowErrorAlert) {
|
|
this.didShowErrorAlert = true;
|
|
alertManager.alert({text: "There was an error saving your note. Please try again."});
|
|
}
|
|
})
|
|
}, syncDebouceMs)
|
|
}
|
|
|
|
this.showSavingStatus = function() {
|
|
this.setStatus({message: "Saving..."}, false);
|
|
}
|
|
|
|
this.showAllChangesSavedStatus = function() {
|
|
this.saveError = false;
|
|
this.syncTakingTooLong = false;
|
|
|
|
var status = "All changes saved";
|
|
if(authManager.offline()) {
|
|
status += " (offline)";
|
|
}
|
|
|
|
this.setStatus({message: status});
|
|
}
|
|
|
|
this.showErrorStatus = function(error) {
|
|
if(!error) {
|
|
error = {
|
|
message: "Sync Unreachable",
|
|
desc: "Changes saved offline"
|
|
}
|
|
}
|
|
this.saveError = true;
|
|
this.syncTakingTooLong = false;
|
|
this.setStatus(error);
|
|
}
|
|
|
|
this.setStatus = function(status, wait = true) {
|
|
// Keep every status up for a minimum duration so it doesnt flash crazily.
|
|
let waitForMs;
|
|
if(!this.noteStatus || !this.noteStatus.date) {
|
|
waitForMs = 0;
|
|
} else {
|
|
waitForMs = MinimumStatusDurationMs - (new Date() - this.noteStatus.date);
|
|
}
|
|
if(!wait || waitForMs < 0) {waitForMs = 0;}
|
|
if(this.statusTimeout) $timeout.cancel(this.statusTimeout);
|
|
this.statusTimeout = $timeout(() => {
|
|
status.date = new Date();
|
|
this.noteStatus = status;
|
|
}, waitForMs)
|
|
}
|
|
|
|
this.contentChanged = function() {
|
|
this.saveNote({updateClientModified: true});
|
|
}
|
|
|
|
this.onTitleEnter = function($event) {
|
|
$event.target.blur();
|
|
this.onTitleChange();
|
|
this.focusEditor();
|
|
}
|
|
|
|
this.onTitleChange = function() {
|
|
this.saveNote({dontUpdatePreviews: true, updateClientModified: true});
|
|
}
|
|
|
|
this.onNameFocus = function() {
|
|
this.editingName = true;
|
|
}
|
|
|
|
this.onContentFocus = function() {
|
|
$rootScope.$broadcast("editorFocused");
|
|
}
|
|
|
|
this.onNameBlur = function() {
|
|
this.editingName = false;
|
|
}
|
|
|
|
this.selectedMenuItem = function(hide) {
|
|
if(hide) {
|
|
this.showMenu = false;
|
|
}
|
|
}
|
|
|
|
this.deleteNote = async function(permanently) {
|
|
if(this.note.dummy) {
|
|
alertManager.alert({text: "This note is a placeholder and cannot be deleted. To remove from your list, simply navigate to a different note."});
|
|
return;
|
|
}
|
|
|
|
let run = () => {
|
|
$timeout(() => {
|
|
if(this.note.locked) {
|
|
alertManager.alert("This note is locked. If you'd like to delete it, unlock it, and try again.");
|
|
return;
|
|
}
|
|
|
|
let title = this.note.safeTitle().length ? `'${this.note.title}'` : "this note";
|
|
let text = permanently ? `Are you sure you want to permanently delete ${title}?`
|
|
: `Are you sure you want to move ${title} to the trash?`
|
|
|
|
alertManager.confirm({text, destructive: true, onConfirm: () => {
|
|
if(permanently) {
|
|
this.remove()(this.note);
|
|
} else {
|
|
this.note.content.trashed = true;
|
|
this.saveNote({bypassDebouncer: true, dontUpdatePreviews: true});
|
|
}
|
|
this.showMenu = false;
|
|
}})
|
|
});
|
|
}
|
|
|
|
if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionDeleteNote)) {
|
|
privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionDeleteNote, () => {
|
|
run();
|
|
});
|
|
} else {
|
|
run();
|
|
}
|
|
}
|
|
|
|
this.restoreTrashedNote = function() {
|
|
this.note.content.trashed = false;
|
|
this.saveNote({bypassDebouncer: true, dontUpdatePreviews: true});
|
|
}
|
|
|
|
this.deleteNotePermanantely = function() {
|
|
this.deleteNote(true);
|
|
}
|
|
|
|
this.getTrashCount = function() {
|
|
return modelManager.trashedItems().length;
|
|
}
|
|
|
|
this.emptyTrash = function() {
|
|
let count = this.getTrashCount();
|
|
alertManager.confirm({text: `Are you sure you want to permanently delete ${count} note(s)?`, destructive: true, onConfirm: () => {
|
|
modelManager.emptyTrash();
|
|
syncManager.sync();
|
|
}})
|
|
}
|
|
|
|
this.togglePin = function() {
|
|
this.note.setAppDataItem("pinned", !this.note.pinned);
|
|
this.saveNote({bypassDebouncer: true, dontUpdatePreviews: true});
|
|
}
|
|
|
|
this.toggleLockNote = function() {
|
|
this.note.setAppDataItem("locked", !this.note.locked);
|
|
this.saveNote({bypassDebouncer: true, dontUpdatePreviews: true});
|
|
}
|
|
|
|
this.toggleProtectNote = function() {
|
|
this.note.content.protected = !this.note.content.protected;
|
|
this.saveNote({bypassDebouncer: true, dontUpdatePreviews: true});
|
|
|
|
// Show privilegesManager if Protection is not yet set up
|
|
privilegesManager.actionHasPrivilegesConfigured(PrivilegesManager.ActionViewProtectedNotes).then((configured) => {
|
|
if(!configured) {
|
|
privilegesManager.presentPrivilegesManagementModal();
|
|
}
|
|
})
|
|
}
|
|
|
|
this.toggleNotePreview = function() {
|
|
this.note.content.hidePreview = !this.note.content.hidePreview;
|
|
this.saveNote({bypassDebouncer: true, dontUpdatePreviews: true});
|
|
}
|
|
|
|
this.toggleArchiveNote = function() {
|
|
this.note.setAppDataItem("archived", !this.note.archived);
|
|
this.saveNote({bypassDebouncer: true, dontUpdatePreviews: true});
|
|
$rootScope.$broadcast("noteArchived");
|
|
}
|
|
|
|
this.clickedEditNote = function() {
|
|
this.focusEditor(100);
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
Tags
|
|
*/
|
|
|
|
this.loadTagsString = function() {
|
|
this.tagsString = this.note.tagsString();
|
|
}
|
|
|
|
this.addTag = function(tag) {
|
|
var tags = this.note.tags;
|
|
var strings = tags.map(function(_tag){
|
|
return _tag.title;
|
|
})
|
|
strings.push(tag.title);
|
|
this.updateTags()(this.note, strings);
|
|
this.loadTagsString();
|
|
}
|
|
|
|
this.removeTag = function(tag) {
|
|
var tags = this.note.tags;
|
|
var strings = tags.map(function(_tag){
|
|
return _tag.title;
|
|
}).filter(function(_tag){
|
|
return _tag !== tag.title;
|
|
})
|
|
this.updateTags()(this.note, strings);
|
|
this.loadTagsString();
|
|
}
|
|
|
|
this.updateTagsFromTagsString = function() {
|
|
if(this.tagsString == this.note.tagsString()) {
|
|
return;
|
|
}
|
|
|
|
var strings = this.tagsString.split("#").filter((string) => {
|
|
return string.length > 0;
|
|
}).map((string) => {
|
|
return string.trim();
|
|
})
|
|
|
|
this.note.dummy = false;
|
|
this.updateTags()(this.note, strings);
|
|
}
|
|
|
|
|
|
/* Resizability */
|
|
|
|
this.leftResizeControl = {};
|
|
this.rightResizeControl = {};
|
|
|
|
this.onPanelResizeFinish = (width, left, isMaxWidth) => {
|
|
if(isMaxWidth) {
|
|
authManager.setUserPrefValue("editorWidth", null);
|
|
} else {
|
|
if(width !== undefined && width !== null) {
|
|
authManager.setUserPrefValue("editorWidth", width);
|
|
this.leftResizeControl.setWidth(width);
|
|
}
|
|
}
|
|
|
|
if(left !== undefined && left !== null) {
|
|
authManager.setUserPrefValue("editorLeft", left);
|
|
this.rightResizeControl.setLeft(left);
|
|
}
|
|
authManager.syncUserPreferences();
|
|
}
|
|
|
|
$rootScope.$on("user-preferences-changed", () => {
|
|
this.loadPreferences();
|
|
});
|
|
|
|
this.loadPreferences = function() {
|
|
this.monospaceFont = authManager.getUserPrefValue("monospaceFont", "monospace");
|
|
|
|
// On desktop application, disable spellcheck by default, as it is not performant.
|
|
let defaultSpellcheckStatus = isDesktopApplication() ? false : true;
|
|
this.spellcheck = authManager.getUserPrefValue("spellcheck", defaultSpellcheckStatus);
|
|
|
|
this.marginResizersEnabled = authManager.getUserPrefValue("marginResizersEnabled", true);
|
|
|
|
if(!document.getElementById("editor-content")) {
|
|
// Elements have not yet loaded due to ng-if around wrapper
|
|
return;
|
|
}
|
|
|
|
this.reloadFont();
|
|
|
|
if(this.marginResizersEnabled) {
|
|
let width = authManager.getUserPrefValue("editorWidth", null);
|
|
if(width !== null) {
|
|
this.leftResizeControl.setWidth(width);
|
|
this.rightResizeControl.setWidth(width);
|
|
}
|
|
|
|
let left = authManager.getUserPrefValue("editorLeft", null);
|
|
if(left !== null) {
|
|
this.leftResizeControl.setLeft(left);
|
|
this.rightResizeControl.setLeft(left);
|
|
}
|
|
}
|
|
}
|
|
|
|
this.reloadFont = function() {
|
|
var editable = document.getElementById("note-text-editor");
|
|
|
|
if(!editable) {
|
|
return;
|
|
}
|
|
|
|
if(this.monospaceFont) {
|
|
if(isDesktopApplication()) {
|
|
editable.style.fontFamily = "Menlo, Consolas, 'DejaVu Sans Mono', monospace";
|
|
} else {
|
|
editable.style.fontFamily = "monospace";
|
|
}
|
|
} else {
|
|
editable.style.fontFamily = "inherit";
|
|
}
|
|
}
|
|
|
|
this.toggleKey = function(key) {
|
|
this[key] = !this[key];
|
|
authManager.setUserPrefValue(key, this[key], true);
|
|
this.reloadFont();
|
|
|
|
if(key == "spellcheck") {
|
|
// Allows textarea to reload
|
|
this.noteReady = false;
|
|
$timeout(() => {
|
|
this.noteReady = true;
|
|
$timeout(() => {
|
|
this.reloadFont();
|
|
})
|
|
}, 0)
|
|
} else if(key == "marginResizersEnabled" && this[key] == true) {
|
|
$timeout(() => {
|
|
this.leftResizeControl.flash();
|
|
this.rightResizeControl.flash();
|
|
})
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
Components
|
|
*/
|
|
|
|
this.onEditorLoad = function(editor) {
|
|
desktopManager.redoSearch();
|
|
}
|
|
|
|
componentManager.registerHandler({identifier: "editor", areas: ["note-tags", "editor-stack", "editor-editor"], activationHandler: (component) => {
|
|
if(component.area === "note-tags") {
|
|
// Autocomplete Tags
|
|
this.tagsComponent = component.active ? component : null;
|
|
} else if(component.area == "editor-editor") {
|
|
// An editor is already active, ensure the potential replacement is explicitely enabled for this item
|
|
// We also check if the selectedEditor is active. If it's inactive, we want to treat it as an external reference wishing to deactivate this editor (i.e componentView)
|
|
if(this.selectedEditor && this.selectedEditor == component && component.active == false) {
|
|
this.selectedEditor = null;
|
|
}
|
|
else if(this.selectedEditor) {
|
|
if(this.selectedEditor.active) {
|
|
// In the case where an editor is duplicated, then you'll have two editors who are explicitely enabled for the same note.
|
|
// This will cause an infinite loop, where as soon as the first is enabled, the second will come in, pass the `isExplicitlyEnabledForItem` check,
|
|
// and replace the previous one. So we now check to make the current editor isn't also explicitely enabled, and if it is, then we'll just keep that one active.
|
|
if(component.isExplicitlyEnabledForItem(this.note) && !this.selectedEditor.isExplicitlyEnabledForItem(this.note)) {
|
|
this.selectedEditor = component;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// If no selected editor, let's see if the incoming one is a candidate
|
|
if(component.active && this.note && (component.isExplicitlyEnabledForItem(this.note) || component.isDefaultEditor())) {
|
|
this.selectedEditor = component;
|
|
} else {
|
|
// Not a candidate, and no selected editor. Disable the current editor by setting selectedEditor to null
|
|
this.selectedEditor = null;
|
|
}
|
|
}
|
|
|
|
} else if(component.area == "editor-stack") {
|
|
this.reloadComponentContext();
|
|
}
|
|
}, contextRequestHandler: (component) => {
|
|
if(component == this.selectedEditor || component == this.tagsComponent || this.componentStack.includes(component)) {
|
|
return this.note;
|
|
}
|
|
}, focusHandler: (component, focused) => {
|
|
if(component.isEditor() && focused) {
|
|
this.closeAllMenus();
|
|
}
|
|
}, actionHandler: (component, action, data) => {
|
|
if(action === "set-size") {
|
|
var setSize = function(element, size) {
|
|
var widthString = typeof size.width === 'string' ? size.width : `${data.width}px`;
|
|
var heightString = typeof size.height === 'string' ? size.height : `${data.height}px`;
|
|
element.setAttribute("style", `width:${widthString}; height:${heightString}; `);
|
|
}
|
|
|
|
if(data.type == "container") {
|
|
if(component.area == "note-tags") {
|
|
var container = document.getElementById("note-tags-component-container");
|
|
setSize(container, data);
|
|
}
|
|
}
|
|
}
|
|
|
|
else if(action === "associate-item") {
|
|
if(data.item.content_type == "Tag") {
|
|
var tag = modelManager.findItem(data.item.uuid);
|
|
this.addTag(tag);
|
|
}
|
|
}
|
|
|
|
else if(action === "deassociate-item") {
|
|
var tag = modelManager.findItem(data.item.uuid);
|
|
this.removeTag(tag);
|
|
}
|
|
|
|
else if(action === "save-items") {
|
|
if(data.items.map((item) => {return item.uuid}).includes(this.note.uuid)) {
|
|
this.showSavingStatus();
|
|
}
|
|
}
|
|
}});
|
|
|
|
this.reloadComponentStackArray = function() {
|
|
this.componentStack = componentManager.componentsForArea("editor-stack").sort((a, b) => {
|
|
// Careful here. For some reason (probably because re-assigning array everytime quickly destroys componentView elements, causing deallocs),
|
|
// sorting by updated_at (or any other property that may always be changing)
|
|
// causes weird problems with ext communication when changing notes or activating/deactivating in quick succession
|
|
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
|
|
});
|
|
}
|
|
|
|
this.reloadComponentContext = function() {
|
|
// componentStack is used by the template to ng-repeat
|
|
this.reloadComponentStackArray();
|
|
/*
|
|
In the past, we were doing this looping code even if the note wasn't currently defined.
|
|
The problem is if an editor stack item loaded first, requested to stream items, and the note was undefined,
|
|
we would set component.hidden = true. Which means messages would not be sent to the component.
|
|
Theoretically, upon the note loading, we would run this code again, and unhide the extension.
|
|
However, if you had requested to stream items when it was hidden, and then it unhid, it would never
|
|
resend those items upon unhiding.
|
|
|
|
Our solution here is to check that the note is defined before setting hidden. The question remains, when
|
|
would note really ever be undefined? Maybe temprarily when you're deleting a note?
|
|
*/
|
|
if(this.note) {
|
|
for(var component of this.componentStack) {
|
|
if(component.active) {
|
|
componentManager.setComponentHidden(component, !component.isExplicitlyEnabledForItem(this.note));
|
|
}
|
|
}
|
|
}
|
|
|
|
componentManager.contextItemDidChangeInArea("note-tags");
|
|
componentManager.contextItemDidChangeInArea("editor-stack");
|
|
componentManager.contextItemDidChangeInArea("editor-editor");
|
|
}
|
|
|
|
this.toggleStackComponentForCurrentItem = function(component) {
|
|
// If it's hidden, we want to show it
|
|
// If it's not active, then hidden won't be set, and we mean to activate and show it.
|
|
if(component.hidden || !component.active) {
|
|
// Unhide, associate with current item
|
|
componentManager.setComponentHidden(component, false);
|
|
this.associateComponentWithCurrentNote(component);
|
|
if(!component.active) {
|
|
componentManager.activateComponent(component);
|
|
}
|
|
componentManager.contextItemDidChangeInArea("editor-stack");
|
|
} else {
|
|
// not hidden, hide
|
|
componentManager.setComponentHidden(component, true);
|
|
this.disassociateComponentWithCurrentNote(component);
|
|
}
|
|
}
|
|
|
|
this.disassociateComponentWithCurrentNote = function(component) {
|
|
component.associatedItemIds = component.associatedItemIds.filter((id) => {return id !== this.note.uuid});
|
|
|
|
if(!component.disassociatedItemIds.includes(this.note.uuid)) {
|
|
component.disassociatedItemIds.push(this.note.uuid);
|
|
}
|
|
|
|
modelManager.setItemDirty(component, true);
|
|
syncManager.sync();
|
|
}
|
|
|
|
this.associateComponentWithCurrentNote = function(component) {
|
|
component.disassociatedItemIds = component.disassociatedItemIds.filter((id) => {return id !== this.note.uuid});
|
|
|
|
if(!component.associatedItemIds.includes(this.note.uuid)) {
|
|
component.associatedItemIds.push(this.note.uuid);
|
|
}
|
|
|
|
modelManager.setItemDirty(component, true);
|
|
syncManager.sync();
|
|
}
|
|
|
|
this.altKeyObserver = keyboardManager.addKeyObserver({
|
|
modifiers: [KeyboardManager.KeyModifierAlt],
|
|
onKeyDown: () => {
|
|
$timeout(() => {
|
|
this.altKeyDown = true;
|
|
})
|
|
},
|
|
onKeyUp: () => {
|
|
$timeout(() => {
|
|
this.altKeyDown = false;
|
|
});
|
|
}
|
|
})
|
|
|
|
this.trashKeyObserver = keyboardManager.addKeyObserver({
|
|
key: KeyboardManager.KeyBackspace,
|
|
notElementIds: ["note-text-editor", "note-title-editor"],
|
|
modifiers: [KeyboardManager.KeyModifierMeta],
|
|
onKeyDown: () => {
|
|
$timeout(() => {
|
|
this.deleteNote();
|
|
});
|
|
},
|
|
})
|
|
|
|
this.deleteKeyObserver = keyboardManager.addKeyObserver({
|
|
key: KeyboardManager.KeyBackspace,
|
|
modifiers: [KeyboardManager.KeyModifierMeta, KeyboardManager.KeyModifierShift, KeyboardManager.KeyModifierAlt],
|
|
onKeyDown: (event) => {
|
|
event.preventDefault();
|
|
$timeout(() => {
|
|
this.deleteNote(true);
|
|
});
|
|
},
|
|
})
|
|
|
|
/*
|
|
Editor Customization
|
|
*/
|
|
|
|
this.onSystemEditorLoad = function() {
|
|
if(this.loadedTabListener) {
|
|
return;
|
|
}
|
|
this.loadedTabListener = true;
|
|
|
|
/**
|
|
* Insert 4 spaces when a tab key is pressed,
|
|
* only used when inside of the text editor.
|
|
* If the shift key is pressed first, this event is
|
|
* not fired.
|
|
*/
|
|
|
|
const editor = document.getElementById("note-text-editor");
|
|
this.tabObserver = keyboardManager.addKeyObserver({
|
|
element: editor,
|
|
key: KeyboardManager.KeyTab,
|
|
onKeyDown: (event) => {
|
|
if(event.shiftKey) {
|
|
return;
|
|
}
|
|
|
|
if(this.note.locked) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
|
|
// Using document.execCommand gives us undo support
|
|
let insertSuccessful = document.execCommand("insertText", false, "\t");
|
|
if(!insertSuccessful) {
|
|
// document.execCommand works great on Chrome/Safari but not Firefox
|
|
var start = editor.selectionStart;
|
|
var end = editor.selectionEnd;
|
|
var spaces = " ";
|
|
|
|
// Insert 4 spaces
|
|
editor.value = editor.value.substring(0, start)
|
|
+ spaces + editor.value.substring(end);
|
|
|
|
// Place cursor 4 spaces away from where
|
|
// the tab key was pressed
|
|
editor.selectionStart = editor.selectionEnd = start + 4;
|
|
}
|
|
|
|
$timeout(() => {
|
|
this.note.text = editor.value;
|
|
this.saveNote({bypassDebouncer: true});
|
|
})
|
|
},
|
|
})
|
|
|
|
// This handles when the editor itself is destroyed, and not when our controller is destroyed.
|
|
angular.element(editor).on('$destroy', () => {
|
|
if(this.tabObserver) {
|
|
keyboardManager.removeKeyObserver(this.tabObserver);
|
|
this.loadedTabListener = false;
|
|
}
|
|
});
|
|
};
|
|
}
|
|
}
|