using ES6 modules for whole frontend SPA app

This commit is contained in:
azivner 2018-03-25 11:09:17 -04:00
parent b3c32a39e9
commit a699210a29
32 changed files with 3452 additions and 3383 deletions

View File

@ -1,9 +1,38 @@
import searchTree from './search_tree.js'; import addLink from './dialogs/add_link.js';
import editTreePrefix from './dialogs/edit_tree_prefix.js';
import eventLog from './dialogs/event_log.js';
import jumpToNote from './dialogs/jump_to_note.js';
import labelsDialog from './dialogs/labels.js';
import noteHistory from './dialogs/note_history.js';
import noteSource from './dialogs/note_source.js';
import recentChanges from './dialogs/recent_changes.js';
import recentNotes from './dialogs/recent_notes.js';
import settings from './dialogs/settings.js';
import sqlConsole from './dialogs/sql_console.js';
import cloning from './cloning.js';
import contextMenu from './context_menu.js';
import dragAndDropSetup from './drag_and_drop.js';
import exportService from './export.js';
import link from './link.js';
import messaging from './messaging.js';
import noteEditor from './note_editor.js';
import noteType from './note_type.js';
import protected_session from './protected_session.js';
import ScriptApi from './script_api.js';
import ScriptContext from './script_context.js';
import sync from './sync.js';
import treeChanges from './tree_changes.js';
import treeUtils from './tree_utils.js';
import utils from './utils.js';
import searchTreeService from './search_tree.js';
import './init.js';
import treeService from './note_tree.js';
const $toggleSearchButton = $("#toggle-search-button"); const $toggleSearchButton = $("#toggle-search-button");
$toggleSearchButton.click(searchTree.toggleSearch); $toggleSearchButton.click(searchTreeService.toggleSearch);
bindShortcut('ctrl+s', searchTree.toggleSearch); bindShortcut('ctrl+s', searchTreeService.toggleSearch);
function bindShortcut(keyboardShortcut, handler) { function bindShortcut(keyboardShortcut, handler) {
$(document).bind('keydown', keyboardShortcut, e => { $(document).bind('keydown', keyboardShortcut, e => {

View File

@ -1,33 +1,33 @@
"use strict"; "use strict";
const cloning = (function() { import treeService from './note_tree.js';
async function cloneNoteTo(childNoteId, parentNoteId, prefix) {
const resp = await server.put('notes/' + childNoteId + '/clone-to/' + parentNoteId, {
prefix: prefix
});
if (!resp.success) { async function cloneNoteTo(childNoteId, parentNoteId, prefix) {
alert(resp.message); const resp = await server.put('notes/' + childNoteId + '/clone-to/' + parentNoteId, {
return; prefix: prefix
} });
await treeService.reload(); if (!resp.success) {
alert(resp.message);
return;
} }
// beware that first arg is noteId and second is branchId! await treeService.reload();
async function cloneNoteAfter(noteId, afterBranchId) { }
const resp = await server.put('notes/' + noteId + '/clone-after/' + afterBranchId);
if (!resp.success) { // beware that first arg is noteId and second is branchId!
alert(resp.message); async function cloneNoteAfter(noteId, afterBranchId) {
return; const resp = await server.put('notes/' + noteId + '/clone-after/' + afterBranchId);
}
await treeService.reload(); if (!resp.success) {
alert(resp.message);
return;
} }
return { await treeService.reload();
cloneNoteAfter, }
cloneNoteTo
}; export default {
})(); cloneNoteAfter,
cloneNoteTo
};

View File

@ -1,181 +1,188 @@
"use strict"; "use strict";
const contextMenu = (function() { import treeService from './note_tree.js';
const $tree = $("#tree"); import cloning from './cloning.js';
import exportService from './export.js';
import messaging from './messaging.js';
import protected_session from './protected_session.js';
import treeChanges from './tree_changes.js';
import treeUtils from './tree_utils.js';
import utils from './utils.js';
let clipboardIds = []; const $tree = $("#tree");
let clipboardMode = null;
async function pasteAfter(node) { let clipboardIds = [];
if (clipboardMode === 'cut') { let clipboardMode = null;
const nodes = clipboardIds.map(nodeKey => treeUtils.getNodeByKey(nodeKey));
await treeChanges.moveAfterNode(nodes, node); async function pasteAfter(node) {
if (clipboardMode === 'cut') {
const nodes = clipboardIds.map(nodeKey => treeUtils.getNodeByKey(nodeKey));
clipboardIds = []; await treeChanges.moveAfterNode(nodes, node);
clipboardMode = null;
clipboardIds = [];
clipboardMode = null;
}
else if (clipboardMode === 'copy') {
for (const noteId of clipboardIds) {
await cloning.cloneNoteAfter(noteId, node.data.branchId);
} }
else if (clipboardMode === 'copy') {
for (const noteId of clipboardIds) {
await cloning.cloneNoteAfter(noteId, node.data.branchId);
}
// copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places // copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places
}
else if (clipboardIds.length === 0) {
// just do nothing
}
else {
utils.throwError("Unrecognized clipboard mode=" + clipboardMode);
}
}
async function pasteInto(node) {
if (clipboardMode === 'cut') {
const nodes = clipboardIds.map(nodeKey => treeUtils.getNodeByKey(nodeKey));
await treeChanges.moveToNode(nodes, node);
clipboardIds = [];
clipboardMode = null;
}
else if (clipboardMode === 'copy') {
for (const noteId of clipboardIds) {
await cloning.cloneNoteTo(noteId, node.data.noteId);
} }
else if (clipboardIds.length === 0) { // copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places
// just do nothing }
else if (clipboardIds.length === 0) {
// just do nothing
}
else {
utils.throwError("Unrecognized clipboard mode=" + mode);
}
}
function copy(nodes) {
clipboardIds = nodes.map(node => node.data.noteId);
clipboardMode = 'copy';
utils.showMessage("Note(s) have been copied into clipboard.");
}
function cut(nodes) {
clipboardIds = nodes.map(node => node.key);
clipboardMode = 'cut';
utils.showMessage("Note(s) have been cut into clipboard.");
}
const contextMenuSettings = {
delegate: "span.fancytree-title",
autoFocus: true,
menu: [
{title: "Insert note here <kbd>Ctrl+O</kbd>", cmd: "insertNoteHere", uiIcon: "ui-icon-plus"},
{title: "Insert child note <kbd>Ctrl+P</kbd>", cmd: "insertChildNote", uiIcon: "ui-icon-plus"},
{title: "Delete <kbd>Ctrl+Del</kbd>", cmd: "delete", uiIcon: "ui-icon-trash"},
{title: "----"},
{title: "Edit tree prefix <kbd>F2</kbd>", cmd: "editTreePrefix", uiIcon: "ui-icon-pencil"},
{title: "----"},
{title: "Protect sub-tree", cmd: "protectSubTree", uiIcon: "ui-icon-locked"},
{title: "Unprotect sub-tree", cmd: "unprotectSubTree", uiIcon: "ui-icon-unlocked"},
{title: "----"},
{title: "Copy / clone <kbd>Ctrl+C</kbd>", cmd: "copy", uiIcon: "ui-icon-copy"},
{title: "Cut <kbd>Ctrl+X</kbd>", cmd: "cut", uiIcon: "ui-icon-scissors"},
{title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"},
{title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"},
{title: "----"},
{title: "Export sub-tree", cmd: "exportSubTree", uiIcon: " ui-icon-arrowthick-1-ne"},
{title: "Import sub-tree into", cmd: "importSubTree", uiIcon: "ui-icon-arrowthick-1-sw"},
{title: "----"},
{title: "Collapse sub-tree <kbd>Alt+-</kbd>", cmd: "collapseSubTree", uiIcon: "ui-icon-minus"},
{title: "Force note sync", cmd: "forceNoteSync", uiIcon: "ui-icon-refresh"},
{title: "Sort alphabetically <kbd>Alt+S</kbd>", cmd: "sortAlphabetically", uiIcon: " ui-icon-arrowthick-2-n-s"}
],
beforeOpen: (event, ui) => {
const node = $.ui.fancytree.getNode(ui.target);
const branch = treeService.getBranch(node.data.branchId);
const note = treeService.getNote(node.data.noteId);
const parentNote = treeService.getNote(branch.parentNoteId);
// Modify menu entries depending on node status
$tree.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0 && (!parentNote || parentNote.type !== 'search'));
$tree.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0 && note.type !== 'search');
$tree.contextmenu("enableEntry", "insertNoteHere", !parentNote || parentNote.type !== 'search');
$tree.contextmenu("enableEntry", "insertChildNote", note.type !== 'search');
$tree.contextmenu("enableEntry", "importSubTree", note.type !== 'search');
$tree.contextmenu("enableEntry", "exportSubTree", note.type !== 'search');
// Activate node on right-click
node.setActive();
// Disable tree keyboard handling
ui.menu.prevKeyboard = node.tree.options.keyboard;
node.tree.options.keyboard = false;
},
close: (event, ui) => {},
select: (event, ui) => {
const node = $.ui.fancytree.getNode(ui.target);
if (ui.cmd === "insertNoteHere") {
const parentNoteId = node.data.parentNoteId;
const isProtected = treeUtils.getParentProtectedStatus(node);
treeService.createNote(node, parentNoteId, 'after', isProtected);
}
else if (ui.cmd === "insertChildNote") {
treeService.createNote(node, node.data.noteId, 'into');
}
else if (ui.cmd === "editTreePrefix") {
editTreePrefix.showDialog(node);
}
else if (ui.cmd === "protectSubTree") {
protected_session.protectSubTree(node.data.noteId, true);
}
else if (ui.cmd === "unprotectSubTree") {
protected_session.protectSubTree(node.data.noteId, false);
}
else if (ui.cmd === "copy") {
copy(treeService.getSelectedNodes());
}
else if (ui.cmd === "cut") {
cut(treeService.getSelectedNodes());
}
else if (ui.cmd === "pasteAfter") {
pasteAfter(node);
}
else if (ui.cmd === "pasteInto") {
pasteInto(node);
}
else if (ui.cmd === "delete") {
treeChanges.deleteNodes(treeService.getSelectedNodes(true));
}
else if (ui.cmd === "exportSubTree") {
exportService.exportSubTree(node.data.noteId);
}
else if (ui.cmd === "importSubTree") {
exportService.importSubTree(node.data.noteId);
}
else if (ui.cmd === "collapseSubTree") {
treeService.collapseTree(node);
}
else if (ui.cmd === "forceNoteSync") {
syncService.forceNoteSync(node.data.noteId);
}
else if (ui.cmd === "sortAlphabetically") {
treeService.sortAlphabetically(node.data.noteId);
} }
else { else {
utils.throwError("Unrecognized clipboard mode=" + clipboardMode); messaging.logError("Unknown command: " + ui.cmd);
} }
} }
};
async function pasteInto(node) { export default {
if (clipboardMode === 'cut') { pasteAfter,
const nodes = clipboardIds.map(nodeKey => treeUtils.getNodeByKey(nodeKey)); pasteInto,
cut,
await treeChanges.moveToNode(nodes, node); copy,
contextMenuSettings
clipboardIds = []; };
clipboardMode = null;
}
else if (clipboardMode === 'copy') {
for (const noteId of clipboardIds) {
await cloning.cloneNoteTo(noteId, node.data.noteId);
}
// copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places
}
else if (clipboardIds.length === 0) {
// just do nothing
}
else {
utils.throwError("Unrecognized clipboard mode=" + mode);
}
}
function copy(nodes) {
clipboardIds = nodes.map(node => node.data.noteId);
clipboardMode = 'copy';
utils.showMessage("Note(s) have been copied into clipboard.");
}
function cut(nodes) {
clipboardIds = nodes.map(node => node.key);
clipboardMode = 'cut';
utils.showMessage("Note(s) have been cut into clipboard.");
}
const contextMenuSettings = {
delegate: "span.fancytree-title",
autoFocus: true,
menu: [
{title: "Insert note here <kbd>Ctrl+O</kbd>", cmd: "insertNoteHere", uiIcon: "ui-icon-plus"},
{title: "Insert child note <kbd>Ctrl+P</kbd>", cmd: "insertChildNote", uiIcon: "ui-icon-plus"},
{title: "Delete <kbd>Ctrl+Del</kbd>", cmd: "delete", uiIcon: "ui-icon-trash"},
{title: "----"},
{title: "Edit tree prefix <kbd>F2</kbd>", cmd: "editTreePrefix", uiIcon: "ui-icon-pencil"},
{title: "----"},
{title: "Protect sub-tree", cmd: "protectSubTree", uiIcon: "ui-icon-locked"},
{title: "Unprotect sub-tree", cmd: "unprotectSubTree", uiIcon: "ui-icon-unlocked"},
{title: "----"},
{title: "Copy / clone <kbd>Ctrl+C</kbd>", cmd: "copy", uiIcon: "ui-icon-copy"},
{title: "Cut <kbd>Ctrl+X</kbd>", cmd: "cut", uiIcon: "ui-icon-scissors"},
{title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"},
{title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"},
{title: "----"},
{title: "Export sub-tree", cmd: "exportSubTree", uiIcon: " ui-icon-arrowthick-1-ne"},
{title: "Import sub-tree into", cmd: "importSubTree", uiIcon: "ui-icon-arrowthick-1-sw"},
{title: "----"},
{title: "Collapse sub-tree <kbd>Alt+-</kbd>", cmd: "collapseSubTree", uiIcon: "ui-icon-minus"},
{title: "Force note sync", cmd: "forceNoteSync", uiIcon: "ui-icon-refresh"},
{title: "Sort alphabetically <kbd>Alt+S</kbd>", cmd: "sortAlphabetically", uiIcon: " ui-icon-arrowthick-2-n-s"}
],
beforeOpen: (event, ui) => {
const node = $.ui.fancytree.getNode(ui.target);
const branch = treeService.getBranch(node.data.branchId);
const note = treeService.getNote(node.data.noteId);
const parentNote = treeService.getNote(branch.parentNoteId);
// Modify menu entries depending on node status
$tree.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0 && (!parentNote || parentNote.type !== 'search'));
$tree.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0 && note.type !== 'search');
$tree.contextmenu("enableEntry", "insertNoteHere", !parentNote || parentNote.type !== 'search');
$tree.contextmenu("enableEntry", "insertChildNote", note.type !== 'search');
$tree.contextmenu("enableEntry", "importSubTree", note.type !== 'search');
$tree.contextmenu("enableEntry", "exportSubTree", note.type !== 'search');
// Activate node on right-click
node.setActive();
// Disable tree keyboard handling
ui.menu.prevKeyboard = node.tree.options.keyboard;
node.tree.options.keyboard = false;
},
close: (event, ui) => {},
select: (event, ui) => {
const node = $.ui.fancytree.getNode(ui.target);
if (ui.cmd === "insertNoteHere") {
const parentNoteId = node.data.parentNoteId;
const isProtected = treeUtils.getParentProtectedStatus(node);
treeService.createNote(node, parentNoteId, 'after', isProtected);
}
else if (ui.cmd === "insertChildNote") {
treeService.createNote(node, node.data.noteId, 'into');
}
else if (ui.cmd === "editTreePrefix") {
editTreePrefix.showDialog(node);
}
else if (ui.cmd === "protectSubTree") {
protected_session.protectSubTree(node.data.noteId, true);
}
else if (ui.cmd === "unprotectSubTree") {
protected_session.protectSubTree(node.data.noteId, false);
}
else if (ui.cmd === "copy") {
copy(treeService.getSelectedNodes());
}
else if (ui.cmd === "cut") {
cut(treeService.getSelectedNodes());
}
else if (ui.cmd === "pasteAfter") {
pasteAfter(node);
}
else if (ui.cmd === "pasteInto") {
pasteInto(node);
}
else if (ui.cmd === "delete") {
treeChanges.deleteNodes(treeService.getSelectedNodes(true));
}
else if (ui.cmd === "exportSubTree") {
exportService.exportSubTree(node.data.noteId);
}
else if (ui.cmd === "importSubTree") {
exportService.importSubTree(node.data.noteId);
}
else if (ui.cmd === "collapseSubTree") {
treeService.collapseTree(node);
}
else if (ui.cmd === "forceNoteSync") {
syncService.forceNoteSync(node.data.noteId);
}
else if (ui.cmd === "sortAlphabetically") {
treeService.sortAlphabetically(node.data.noteId);
}
else {
messaging.logError("Unknown command: " + ui.cmd);
}
}
};
return {
pasteAfter,
pasteInto,
cut,
copy,
contextMenuSettings
}
})();

View File

@ -1,137 +1,141 @@
"use strict"; "use strict";
const addLink = (function() { import treeService from '../note_tree.js';
const $dialog = $("#add-link-dialog"); import cloning from '../cloning.js';
const $form = $("#add-link-form"); import link from '../link.js';
const $autoComplete = $("#note-autocomplete"); import noteEditor from '../note_editor.js';
const $linkTitle = $("#link-title"); import treeUtils from '../tree_utils.js';
const $clonePrefix = $("#clone-prefix");
const $linkTitleFormGroup = $("#add-link-title-form-group");
const $prefixFormGroup = $("#add-link-prefix-form-group");
const $linkTypes = $("input[name='add-link-type']");
const $linkTypeHtml = $linkTypes.filter('input[value="html"]');
function setLinkType(linkType) { const $dialog = $("#add-link-dialog");
$linkTypes.each(function () { const $form = $("#add-link-form");
$(this).prop('checked', $(this).val() === linkType); const $autoComplete = $("#note-autocomplete");
}); const $linkTitle = $("#link-title");
const $clonePrefix = $("#clone-prefix");
const $linkTitleFormGroup = $("#add-link-title-form-group");
const $prefixFormGroup = $("#add-link-prefix-form-group");
const $linkTypes = $("input[name='add-link-type']");
const $linkTypeHtml = $linkTypes.filter('input[value="html"]');
linkTypeChanged(); function setLinkType(linkType) {
$linkTypes.each(function () {
$(this).prop('checked', $(this).val() === linkType);
});
linkTypeChanged();
}
async function showDialog() {
glob.activeDialog = $dialog;
if (noteEditor.getCurrentNoteType() === 'text') {
$linkTypeHtml.prop('disabled', false);
setLinkType('html');
}
else {
$linkTypeHtml.prop('disabled', true);
setLinkType('selected-to-current');
} }
async function showDialog() { $dialog.dialog({
glob.activeDialog = $dialog; modal: true,
width: 700
});
if (noteEditor.getCurrentNoteType() === 'text') { $autoComplete.val('').focus();
$linkTypeHtml.prop('disabled', false); $clonePrefix.val('');
$linkTitle.val('');
setLinkType('html'); function setDefaultLinkTitle(noteId) {
} const noteTitle = treeService.getNoteTitle(noteId);
else {
$linkTypeHtml.prop('disabled', true);
setLinkType('selected-to-current'); $linkTitle.val(noteTitle);
} }
$dialog.dialog({ $autoComplete.autocomplete({
modal: true, source: await treeService.getAutocompleteItems(),
width: 700 minLength: 0,
}); change: () => {
const val = $autoComplete.val();
const notePath = link.getNodePathFromLabel(val);
if (!notePath) {
return;
}
$autoComplete.val('').focus(); const noteId = treeUtils.getNoteIdFromNotePath(notePath);
$clonePrefix.val('');
$linkTitle.val('');
function setDefaultLinkTitle(noteId) {
const noteTitle = treeService.getNoteTitle(noteId);
$linkTitle.val(noteTitle);
}
$autoComplete.autocomplete({
source: await treeService.getAutocompleteItems(),
minLength: 0,
change: () => {
const val = $autoComplete.val();
const notePath = link.getNodePathFromLabel(val);
if (!notePath) {
return;
}
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
if (noteId) {
setDefaultLinkTitle(noteId);
}
},
// this is called when user goes through autocomplete list with keyboard
// at this point the item isn't selected yet so we use supplied ui.item to see WHERE the cursor is
focus: (event, ui) => {
const notePath = link.getNodePathFromLabel(ui.item.value);
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
if (noteId) {
setDefaultLinkTitle(noteId); setDefaultLinkTitle(noteId);
} }
}); },
} // this is called when user goes through autocomplete list with keyboard
// at this point the item isn't selected yet so we use supplied ui.item to see WHERE the cursor is
focus: (event, ui) => {
const notePath = link.getNodePathFromLabel(ui.item.value);
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
$form.submit(() => { setDefaultLinkTitle(noteId);
const value = $autoComplete.val();
const notePath = link.getNodePathFromLabel(value);
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
if (notePath) {
const linkType = $("input[name='add-link-type']:checked").val();
if (linkType === 'html') {
const linkTitle = $linkTitle.val();
$dialog.dialog("close");
link.addLinkToEditor(linkTitle, '#' + notePath);
}
else if (linkType === 'selected-to-current') {
const prefix = $clonePrefix.val();
cloning.cloneNoteTo(noteId, noteEditor.getCurrentNoteId(), prefix);
$dialog.dialog("close");
}
else if (linkType === 'current-to-selected') {
const prefix = $clonePrefix.val();
cloning.cloneNoteTo(noteEditor.getCurrentNoteId(), noteId, prefix);
$dialog.dialog("close");
}
} }
return false;
}); });
}
function linkTypeChanged() { $form.submit(() => {
const value = $linkTypes.filter(":checked").val(); const value = $autoComplete.val();
if (value === 'html') { const notePath = link.getNodePathFromLabel(value);
$linkTitleFormGroup.show(); const noteId = treeUtils.getNoteIdFromNotePath(notePath);
$prefixFormGroup.hide();
if (notePath) {
const linkType = $("input[name='add-link-type']:checked").val();
if (linkType === 'html') {
const linkTitle = $linkTitle.val();
$dialog.dialog("close");
link.addLinkToEditor(linkTitle, '#' + notePath);
} }
else { else if (linkType === 'selected-to-current') {
$linkTitleFormGroup.hide(); const prefix = $clonePrefix.val();
$prefixFormGroup.show();
cloning.cloneNoteTo(noteId, noteEditor.getCurrentNoteId(), prefix);
$dialog.dialog("close");
}
else if (linkType === 'current-to-selected') {
const prefix = $clonePrefix.val();
cloning.cloneNoteTo(noteEditor.getCurrentNoteId(), noteId, prefix);
$dialog.dialog("close");
} }
} }
$linkTypes.change(linkTypeChanged); return false;
});
$(document).bind('keydown', 'ctrl+l', e => { function linkTypeChanged() {
showDialog(); const value = $linkTypes.filter(":checked").val();
e.preventDefault(); if (value === 'html') {
}); $linkTitleFormGroup.show();
$prefixFormGroup.hide();
}
else {
$linkTitleFormGroup.hide();
$prefixFormGroup.show();
}
}
return { $linkTypes.change(linkTypeChanged);
showDialog
}; $(document).bind('keydown', 'ctrl+l', e => {
})(); showDialog();
e.preventDefault();
});
export default {
showDialog
};

View File

@ -1,46 +1,46 @@
"use strict"; "use strict";
const editTreePrefix = (function() { import treeService from '../note_tree.js';
const $dialog = $("#edit-tree-prefix-dialog");
const $form = $("#edit-tree-prefix-form");
const $treePrefixInput = $("#tree-prefix-input");
const $noteTitle = $('#tree-prefix-note-title');
let branchId; const $dialog = $("#edit-tree-prefix-dialog");
const $form = $("#edit-tree-prefix-form");
const $treePrefixInput = $("#tree-prefix-input");
const $noteTitle = $('#tree-prefix-note-title');
async function showDialog() { let branchId;
glob.activeDialog = $dialog;
await $dialog.dialog({ async function showDialog() {
modal: true, glob.activeDialog = $dialog;
width: 500
});
const currentNode = treeService.getCurrentNode(); await $dialog.dialog({
modal: true,
branchId = currentNode.data.branchId; width: 500
const nt = treeService.getBranch(branchId);
$treePrefixInput.val(nt.prefix).focus();
const noteTitle = treeService.getNoteTitle(currentNode.data.noteId);
$noteTitle.html(noteTitle);
}
$form.submit(() => {
const prefix = $treePrefixInput.val();
server.put('tree/' + branchId + '/set-prefix', {
prefix: prefix
}).then(() => treeService.setPrefix(branchId, prefix));
$dialog.dialog("close");
return false;
}); });
return { const currentNode = treeService.getCurrentNode();
showDialog
}; branchId = currentNode.data.branchId;
})(); const nt = treeService.getBranch(branchId);
$treePrefixInput.val(nt.prefix).focus();
const noteTitle = treeService.getNoteTitle(currentNode.data.noteId);
$noteTitle.html(noteTitle);
}
$form.submit(() => {
const prefix = $treePrefixInput.val();
server.put('tree/' + branchId + '/set-prefix', {
prefix: prefix
}).then(() => treeService.setPrefix(branchId, prefix));
$dialog.dialog("close");
return false;
});
export default {
showDialog
};

View File

@ -1,38 +1,39 @@
"use strict"; "use strict";
const eventLog = (function() { import link from '../link.js';
const $dialog = $("#event-log-dialog"); import utils from '../utils.js';
const $list = $("#event-log-list");
async function showDialog() { const $dialog = $("#event-log-dialog");
glob.activeDialog = $dialog; const $list = $("#event-log-list");
$dialog.dialog({ async function showDialog() {
modal: true, glob.activeDialog = $dialog;
width: 800,
height: 700
});
const result = await server.get('event-log'); $dialog.dialog({
modal: true,
width: 800,
height: 700
});
$list.html(''); const result = await server.get('event-log');
for (const event of result) { $list.html('');
const dateTime = utils.formatDateTime(utils.parseDate(event.dateAdded));
if (event.noteId) { for (const event of result) {
const noteLink = link.createNoteLink(event.noteId).prop('outerHTML'); const dateTime = utils.formatDateTime(utils.parseDate(event.dateAdded));
event.comment = event.comment.replace('<note>', noteLink); if (event.noteId) {
} const noteLink = link.createNoteLink(event.noteId).prop('outerHTML');
const eventEl = $('<li>').html(dateTime + " - " + event.comment); event.comment = event.comment.replace('<note>', noteLink);
$list.append(eventEl);
} }
}
return { const eventEl = $('<li>').html(dateTime + " - " + event.comment);
showDialog
}; $list.append(eventEl);
})(); }
}
export default {
showDialog
};

View File

@ -1,59 +1,61 @@
"use strict"; "use strict";
const jumpToNote = (function() { import treeService from '../note_tree.js';
const $showDialogButton = $("#jump-to-note-button"); import link from '../link.js';
const $dialog = $("#jump-to-note-dialog"); import utils from '../utils.js';
const $autoComplete = $("#jump-to-note-autocomplete");
const $form = $("#jump-to-note-form");
async function showDialog() { const $showDialogButton = $("#jump-to-note-button");
glob.activeDialog = $dialog; const $dialog = $("#jump-to-note-dialog");
const $autoComplete = $("#jump-to-note-autocomplete");
const $form = $("#jump-to-note-form");
$autoComplete.val(''); async function showDialog() {
glob.activeDialog = $dialog;
$dialog.dialog({ $autoComplete.val('');
modal: true,
width: 800
});
await $autoComplete.autocomplete({ $dialog.dialog({
source: await utils.stopWatch("building autocomplete", treeService.getAutocompleteItems), modal: true,
minLength: 0 width: 800
});
}
function getSelectedNotePath() {
const val = $autoComplete.val();
return link.getNodePathFromLabel(val);
}
function goToNote() {
const notePath = getSelectedNotePath();
if (notePath) {
treeService.activateNode(notePath);
$dialog.dialog('close');
}
}
$(document).bind('keydown', 'ctrl+j', e => {
showDialog();
e.preventDefault();
}); });
$form.submit(() => { await $autoComplete.autocomplete({
const action = $dialog.find("button:focus").val(); source: await utils.stopWatch("building autocomplete", treeService.getAutocompleteItems),
minLength: 0
goToNote();
return false;
}); });
}
$showDialogButton.click(showDialog); function getSelectedNotePath() {
const val = $autoComplete.val();
return link.getNodePathFromLabel(val);
}
return { function goToNote() {
showDialog const notePath = getSelectedNotePath();
};
})(); if (notePath) {
treeService.activateNode(notePath);
$dialog.dialog('close');
}
}
$(document).bind('keydown', 'ctrl+j', e => {
showDialog();
e.preventDefault();
});
$form.submit(() => {
const action = $dialog.find("button:focus").val();
goToNote();
return false;
});
$showDialogButton.click(showDialog);
export default {
showDialog
};

View File

@ -1,227 +1,228 @@
"use strict"; "use strict";
const labelsDialog = (function() { import noteEditor from '../note_editor.js';
const $showDialogButton = $(".show-labels-button"); import utils from '../utils.js';
const $dialog = $("#labels-dialog");
const $saveLabelsButton = $("#save-labels-button");
const $labelsBody = $('#labels-table tbody');
const labelsModel = new LabelsModel(); const $showDialogButton = $(".show-labels-button");
let labelNames = []; const $dialog = $("#labels-dialog");
const $saveLabelsButton = $("#save-labels-button");
const $labelsBody = $('#labels-table tbody');
function LabelsModel() { const labelsModel = new LabelsModel();
const self = this; let labelNames = [];
this.labels = ko.observableArray(); function LabelsModel() {
const self = this;
this.loadLabels = async function() { this.labels = ko.observableArray();
const noteId = noteEditor.getCurrentNoteId();
const labels = await server.get('notes/' + noteId + '/labels'); this.loadLabels = async function() {
const noteId = noteEditor.getCurrentNoteId();
self.labels(labels.map(ko.observable)); const labels = await server.get('notes/' + noteId + '/labels');
self.labels(labels.map(ko.observable));
addLastEmptyRow();
labelNames = await server.get('labels/names');
// label might not be rendered immediatelly so could not focus
setTimeout(() => $(".label-name:last").focus(), 100);
$labelsBody.sortable({
handle: '.handle',
containment: $labelsBody,
update: function() {
let position = 0;
// we need to update positions by searching in the DOM, because order of the
// labels in the viewmodel (self.labels()) stays the same
$labelsBody.find('input[name="position"]').each(function() {
const attr = self.getTargetLabel(this);
attr().position = position++;
});
}
});
};
this.deleteLabel = function(data, event) {
const attr = self.getTargetLabel(event.target);
const attrData = attr();
if (attrData) {
attrData.isDeleted = 1;
attr(attrData);
addLastEmptyRow(); addLastEmptyRow();
labelNames = await server.get('labels/names');
// label might not be rendered immediatelly so could not focus
setTimeout(() => $(".label-name:last").focus(), 100);
$labelsBody.sortable({
handle: '.handle',
containment: $labelsBody,
update: function() {
let position = 0;
// we need to update positions by searching in the DOM, because order of the
// labels in the viewmodel (self.labels()) stays the same
$labelsBody.find('input[name="position"]').each(function() {
const attr = self.getTargetLabel(this);
attr().position = position++;
});
}
});
};
this.deleteLabel = function(data, event) {
const attr = self.getTargetLabel(event.target);
const attrData = attr();
if (attrData) {
attrData.isDeleted = 1;
attr(attrData);
addLastEmptyRow();
}
};
function isValid() {
for (let attrs = self.labels(), i = 0; i < attrs.length; i++) {
if (self.isEmptyName(i)) {
return false;
}
}
return true;
} }
};
this.save = async function() { function isValid() {
// we need to defocus from input (in case of enter-triggered save) because value is updated for (let attrs = self.labels(), i = 0; i < attrs.length; i++) {
// on blur event (because of conflict with jQuery UI Autocomplete). Without this, input would if (self.isEmptyName(i)) {
// stay in focus, blur wouldn't be triggered and change wouldn't be updated in the viewmodel.
$saveLabelsButton.focus();
if (!isValid()) {
alert("Please fix all validation errors and try saving again.");
return;
}
const noteId = noteEditor.getCurrentNoteId();
const labelsToSave = self.labels()
.map(attr => attr())
.filter(attr => attr.labelId !== "" || attr.name !== "");
const labels = await server.put('notes/' + noteId + '/labels', labelsToSave);
self.labels(labels.map(ko.observable));
addLastEmptyRow();
utils.showMessage("Labels have been saved.");
noteEditor.loadLabelList();
};
function addLastEmptyRow() {
const attrs = self.labels().filter(attr => attr().isDeleted === 0);
const last = attrs.length === 0 ? null : attrs[attrs.length - 1]();
if (!last || last.name.trim() !== "" || last.value !== "") {
self.labels.push(ko.observable({
labelId: '',
name: '',
value: '',
isDeleted: 0,
position: 0
}));
}
}
this.labelChanged = function (data, event) {
addLastEmptyRow();
const attr = self.getTargetLabel(event.target);
attr.valueHasMutated();
};
this.isNotUnique = function(index) {
const cur = self.labels()[index]();
if (cur.name.trim() === "") {
return false; return false;
} }
}
for (let attrs = self.labels(), i = 0; i < attrs.length; i++) { return true;
const attr = attrs[i](); }
if (index !== i && cur.name === attr.name) { this.save = async function() {
return true; // we need to defocus from input (in case of enter-triggered save) because value is updated
} // on blur event (because of conflict with jQuery UI Autocomplete). Without this, input would
} // stay in focus, blur wouldn't be triggered and change wouldn't be updated in the viewmodel.
$saveLabelsButton.focus();
return false; if (!isValid()) {
}; alert("Please fix all validation errors and try saving again.");
return;
}
this.isEmptyName = function(index) { const noteId = noteEditor.getCurrentNoteId();
const cur = self.labels()[index]();
return cur.name.trim() === "" && (cur.labelId !== "" || cur.value !== ""); const labelsToSave = self.labels()
}; .map(attr => attr())
.filter(attr => attr.labelId !== "" || attr.name !== "");
this.getTargetLabel = function(target) { const labels = await server.put('notes/' + noteId + '/labels', labelsToSave);
const context = ko.contextFor(target);
const index = context.$index();
return self.labels()[index]; self.labels(labels.map(ko.observable));
addLastEmptyRow();
utils.showMessage("Labels have been saved.");
noteEditor.loadLabelList();
};
function addLastEmptyRow() {
const attrs = self.labels().filter(attr => attr().isDeleted === 0);
const last = attrs.length === 0 ? null : attrs[attrs.length - 1]();
if (!last || last.name.trim() !== "" || last.value !== "") {
self.labels.push(ko.observable({
labelId: '',
name: '',
value: '',
isDeleted: 0,
position: 0
}));
} }
} }
async function showDialog() { this.labelChanged = function (data, event) {
glob.activeDialog = $dialog; addLastEmptyRow();
await labelsModel.loadLabels(); const attr = self.getTargetLabel(event.target);
$dialog.dialog({ attr.valueHasMutated();
modal: true, };
width: 800,
height: 500 this.isNotUnique = function(index) {
const cur = self.labels()[index]();
if (cur.name.trim() === "") {
return false;
}
for (let attrs = self.labels(), i = 0; i < attrs.length; i++) {
const attr = attrs[i]();
if (index !== i && cur.name === attr.name) {
return true;
}
}
return false;
};
this.isEmptyName = function(index) {
const cur = self.labels()[index]();
return cur.name.trim() === "" && (cur.labelId !== "" || cur.value !== "");
};
this.getTargetLabel = function(target) {
const context = ko.contextFor(target);
const index = context.$index();
return self.labels()[index];
}
}
async function showDialog() {
glob.activeDialog = $dialog;
await labelsModel.loadLabels();
$dialog.dialog({
modal: true,
width: 800,
height: 500
});
}
$(document).bind('keydown', 'alt+a', e => {
showDialog();
e.preventDefault();
});
ko.applyBindings(labelsModel, document.getElementById('labels-dialog'));
$(document).on('focus', '.label-name', function (e) {
if (!$(this).hasClass("ui-autocomplete-input")) {
$(this).autocomplete({
// shouldn't be required and autocomplete should just accept array of strings, but that fails
// because we have overriden filter() function in init.js
source: labelNames.map(attr => {
return {
label: attr,
value: attr
}
}),
minLength: 0
}); });
} }
$(document).bind('keydown', 'alt+a', e => { $(this).autocomplete("search", $(this).val());
showDialog(); });
e.preventDefault(); $(document).on('focus', '.label-value', async function (e) {
}); if (!$(this).hasClass("ui-autocomplete-input")) {
const labelName = $(this).parent().parent().find('.label-name').val();
ko.applyBindings(labelsModel, document.getElementById('labels-dialog')); if (labelName.trim() === "") {
return;
$(document).on('focus', '.label-name', function (e) {
if (!$(this).hasClass("ui-autocomplete-input")) {
$(this).autocomplete({
// shouldn't be required and autocomplete should just accept array of strings, but that fails
// because we have overriden filter() function in init.js
source: labelNames.map(attr => {
return {
label: attr,
value: attr
}
}),
minLength: 0
});
} }
$(this).autocomplete("search", $(this).val()); const labelValues = await server.get('labels/values/' + encodeURIComponent(labelName));
});
$(document).on('focus', '.label-value', async function (e) { if (labelValues.length === 0) {
if (!$(this).hasClass("ui-autocomplete-input")) { return;
const labelName = $(this).parent().parent().find('.label-name').val();
if (labelName.trim() === "") {
return;
}
const labelValues = await server.get('labels/values/' + encodeURIComponent(labelName));
if (labelValues.length === 0) {
return;
}
$(this).autocomplete({
// shouldn't be required and autocomplete should just accept array of strings, but that fails
// because we have overriden filter() function in init.js
source: labelValues.map(attr => {
return {
label: attr,
value: attr
}
}),
minLength: 0
});
} }
$(this).autocomplete("search", $(this).val()); $(this).autocomplete({
}); // shouldn't be required and autocomplete should just accept array of strings, but that fails
// because we have overriden filter() function in init.js
source: labelValues.map(attr => {
return {
label: attr,
value: attr
}
}),
minLength: 0
});
}
$showDialogButton.click(showDialog); $(this).autocomplete("search", $(this).val());
});
return { $showDialogButton.click(showDialog);
showDialog
}; export default {
})(); showDialog
};

View File

@ -1,81 +1,82 @@
"use strict"; "use strict";
const noteHistory = (function() { import noteEditor from '../note_editor.js';
const $showDialogButton = $("#show-history-button"); import utils from '../utils.js';
const $dialog = $("#note-history-dialog");
const $list = $("#note-history-list");
const $content = $("#note-history-content");
const $title = $("#note-history-title");
let historyItems = []; const $showDialogButton = $("#show-history-button");
const $dialog = $("#note-history-dialog");
const $list = $("#note-history-list");
const $content = $("#note-history-content");
const $title = $("#note-history-title");
async function showCurrentNoteHistory() { let historyItems = [];
await showNoteHistoryDialog(noteEditor.getCurrentNoteId());
async function showCurrentNoteHistory() {
await showNoteHistoryDialog(noteEditor.getCurrentNoteId());
}
async function showNoteHistoryDialog(noteId, noteRevisionId) {
glob.activeDialog = $dialog;
$dialog.dialog({
modal: true,
width: 800,
height: 700
});
$list.empty();
$content.empty();
historyItems = await server.get('notes-history/' + noteId);
for (const item of historyItems) {
const dateModified = utils.parseDate(item.dateModifiedFrom);
$list.append($('<option>', {
value: item.noteRevisionId,
text: utils.formatDateTime(dateModified)
}));
} }
async function showNoteHistoryDialog(noteId, noteRevisionId) { if (historyItems.length > 0) {
glob.activeDialog = $dialog; if (!noteRevisionId) {
noteRevisionId = $list.find("option:first").val();
$dialog.dialog({
modal: true,
width: 800,
height: 700
});
$list.empty();
$content.empty();
historyItems = await server.get('notes-history/' + noteId);
for (const item of historyItems) {
const dateModified = utils.parseDate(item.dateModifiedFrom);
$list.append($('<option>', {
value: item.noteRevisionId,
text: utils.formatDateTime(dateModified)
}));
} }
if (historyItems.length > 0) { $list.val(noteRevisionId).trigger('change');
if (!noteRevisionId) {
noteRevisionId = $list.find("option:first").val();
}
$list.val(noteRevisionId).trigger('change');
}
else {
$title.text("No history for this note yet...");
}
} }
else {
$title.text("No history for this note yet...");
}
}
$(document).bind('keydown', 'alt+h', e => { $(document).bind('keydown', 'alt+h', e => {
showCurrentNoteHistory(); showCurrentNoteHistory();
e.preventDefault(); e.preventDefault();
}); });
$list.on('change', () => { $list.on('change', () => {
const optVal = $list.find(":selected").val(); const optVal = $list.find(":selected").val();
const historyItem = historyItems.find(r => r.noteRevisionId === optVal); const historyItem = historyItems.find(r => r.noteRevisionId === optVal);
$title.html(historyItem.title); $title.html(historyItem.title);
$content.html(historyItem.content); $content.html(historyItem.content);
}); });
$(document).on('click', "a[action='note-history']", event => { $(document).on('click', "a[action='note-history']", event => {
const linkEl = $(event.target); const linkEl = $(event.target);
const noteId = linkEl.attr('note-path'); const noteId = linkEl.attr('note-path');
const noteRevisionId = linkEl.attr('note-history-id'); const noteRevisionId = linkEl.attr('note-history-id');
showNoteHistoryDialog(noteId, noteRevisionId); showNoteHistoryDialog(noteId, noteRevisionId);
return false; return false;
}); });
$showDialogButton.click(showCurrentNoteHistory); $showDialogButton.click(showCurrentNoteHistory);
return { export default {
showCurrentNoteHistory showCurrentNoteHistory
}; };
})();

View File

@ -1,60 +1,60 @@
"use strict"; "use strict";
const noteSource = (function() { import noteEditor from '../note_editor.js';
const $showDialogButton = $("#show-source-button");
const $dialog = $("#note-source-dialog");
const $noteSource = $("#note-source");
function showDialog() { const $showDialogButton = $("#show-source-button");
glob.activeDialog = $dialog; const $dialog = $("#note-source-dialog");
const $noteSource = $("#note-source");
$dialog.dialog({ function showDialog() {
modal: true, glob.activeDialog = $dialog;
width: 800,
height: 500
});
const noteText = noteEditor.getCurrentNote().detail.content; $dialog.dialog({
modal: true,
$noteSource.text(formatHtml(noteText)); width: 800,
} height: 500
function formatHtml(str) {
const div = document.createElement('div');
div.innerHTML = str.trim();
return formatNode(div, 0).innerHTML.trim();
}
function formatNode(node, level) {
const indentBefore = new Array(level++ + 1).join(' ');
const indentAfter = new Array(level - 1).join(' ');
let textNode;
for (let i = 0; i < node.children.length; i++) {
textNode = document.createTextNode('\n' + indentBefore);
node.insertBefore(textNode, node.children[i]);
formatNode(node.children[i], level);
if (node.lastElementChild === node.children[i]) {
textNode = document.createTextNode('\n' + indentAfter);
node.appendChild(textNode);
}
}
return node;
}
$(document).bind('keydown', 'ctrl+u', e => {
showDialog();
e.preventDefault();
}); });
$showDialogButton.click(showDialog); const noteText = noteEditor.getCurrentNote().detail.content;
return { $noteSource.text(formatHtml(noteText));
showDialog }
};
})(); function formatHtml(str) {
const div = document.createElement('div');
div.innerHTML = str.trim();
return formatNode(div, 0).innerHTML.trim();
}
function formatNode(node, level) {
const indentBefore = new Array(level++ + 1).join(' ');
const indentAfter = new Array(level - 1).join(' ');
let textNode;
for (let i = 0; i < node.children.length; i++) {
textNode = document.createTextNode('\n' + indentBefore);
node.insertBefore(textNode, node.children[i]);
formatNode(node.children[i], level);
if (node.lastElementChild === node.children[i]) {
textNode = document.createTextNode('\n' + indentAfter);
node.appendChild(textNode);
}
}
return node;
}
$(document).bind('keydown', 'ctrl+u', e => {
showDialog();
e.preventDefault();
});
$showDialogButton.click(showDialog);
export default {
showDialog
};

View File

@ -1,92 +1,93 @@
"use strict"; "use strict";
const recentChanges = (function() { import link from '../link.js';
const $showDialogButton = $("#recent-changes-button"); import utils from '../utils.js';
const $dialog = $("#recent-changes-dialog");
async function showDialog() { const $showDialogButton = $("#recent-changes-button");
glob.activeDialog = $dialog; const $dialog = $("#recent-changes-dialog");
$dialog.dialog({ async function showDialog() {
modal: true, glob.activeDialog = $dialog;
width: 800,
height: 700
});
const result = await server.get('recent-changes/'); $dialog.dialog({
modal: true,
width: 800,
height: 700
});
$dialog.html(''); const result = await server.get('recent-changes/');
const groupedByDate = groupByDate(result); $dialog.html('');
for (const [dateDay, dayChanges] of groupedByDate) { const groupedByDate = groupByDate(result);
const changesListEl = $('<ul>');
const dayEl = $('<div>').append($('<b>').html(utils.formatDate(dateDay))).append(changesListEl); for (const [dateDay, dayChanges] of groupedByDate) {
const changesListEl = $('<ul>');
for (const change of dayChanges) { const dayEl = $('<div>').append($('<b>').html(utils.formatDate(dateDay))).append(changesListEl);
const formattedTime = utils.formatTime(utils.parseDate(change.dateModifiedTo));
const revLink = $("<a>", { for (const change of dayChanges) {
href: 'javascript:', const formattedTime = utils.formatTime(utils.parseDate(change.dateModifiedTo));
text: 'rev'
}).attr('action', 'note-history')
.attr('note-path', change.noteId)
.attr('note-history-id', change.noteRevisionId);
let noteLink; const revLink = $("<a>", {
href: 'javascript:',
text: 'rev'
}).attr('action', 'note-history')
.attr('note-path', change.noteId)
.attr('note-history-id', change.noteRevisionId);
if (change.current_isDeleted) { let noteLink;
noteLink = change.current_title;
}
else {
noteLink = link.createNoteLink(change.noteId, change.title);
}
changesListEl.append($('<li>') if (change.current_isDeleted) {
.append(formattedTime + ' - ') noteLink = change.current_title;
.append(noteLink)
.append(' (').append(revLink).append(')'));
}
$dialog.append(dayEl);
}
}
function groupByDate(result) {
const groupedByDate = new Map();
const dayCache = {};
for (const row of result) {
let dateDay = utils.parseDate(row.dateModifiedTo);
dateDay.setHours(0);
dateDay.setMinutes(0);
dateDay.setSeconds(0);
dateDay.setMilliseconds(0);
// this stupidity is to make sure that we always use the same day object because Map uses only
// reference equality
if (dayCache[dateDay]) {
dateDay = dayCache[dateDay];
} }
else { else {
dayCache[dateDay] = dateDay; noteLink = link.createNoteLink(change.noteId, change.title);
} }
if (!groupedByDate.has(dateDay)) { changesListEl.append($('<li>')
groupedByDate.set(dateDay, []); .append(formattedTime + ' - ')
} .append(noteLink)
.append(' (').append(revLink).append(')'));
groupedByDate.get(dateDay).push(row);
} }
return groupedByDate;
$dialog.append(dayEl);
} }
}
$(document).bind('keydown', 'alt+r', showDialog); function groupByDate(result) {
const groupedByDate = new Map();
const dayCache = {};
$showDialogButton.click(showDialog); for (const row of result) {
let dateDay = utils.parseDate(row.dateModifiedTo);
dateDay.setHours(0);
dateDay.setMinutes(0);
dateDay.setSeconds(0);
dateDay.setMilliseconds(0);
return { // this stupidity is to make sure that we always use the same day object because Map uses only
showDialog // reference equality
}; if (dayCache[dateDay]) {
})(); dateDay = dayCache[dateDay];
}
else {
dayCache[dateDay] = dateDay;
}
if (!groupedByDate.has(dateDay)) {
groupedByDate.set(dateDay, []);
}
groupedByDate.get(dateDay).push(row);
}
return groupedByDate;
}
$(document).bind('keydown', 'alt+r', showDialog);
$showDialogButton.click(showDialog);
export default {
showDialog
};

View File

@ -1,105 +1,107 @@
"use strict"; "use strict";
const recentNotes = (function() { import treeService from '../note_tree.js';
const $showDialogButton = $("#recent-notes-button"); import server from '../server.js';
const $dialog = $("#recent-notes-dialog"); import messaging from '../messaging.js';
const $searchInput = $('#recent-notes-search-input');
// list of recent note paths const $showDialogButton = $("#recent-notes-button");
let list = []; const $dialog = $("#recent-notes-dialog");
const $searchInput = $('#recent-notes-search-input');
async function reload() { // list of recent note paths
const result = await server.get('recent-notes'); let list = [];
list = result.map(r => r.notePath); async function reload() {
} const result = await server.get('recent-notes');
function addRecentNote(branchId, notePath) { list = result.map(r => r.notePath);
setTimeout(async () => { }
// we include the note into recent list only if the user stayed on the note at least 5 seconds
if (notePath && notePath === treeService.getCurrentNotePath()) {
const result = await server.put('recent-notes/' + branchId + '/' + encodeURIComponent(notePath));
list = result.map(r => r.notePath); function addRecentNote(branchId, notePath) {
} setTimeout(async () => {
}, 1500); // we include the note into recent list only if the user stayed on the note at least 5 seconds
} if (notePath && notePath === treeService.getCurrentNotePath()) {
const result = await server.put('recent-notes/' + branchId + '/' + encodeURIComponent(notePath));
function showDialog() { list = result.map(r => r.notePath);
glob.activeDialog = $dialog; }
}, 1500);
}
$dialog.dialog({ function showDialog() {
modal: true, glob.activeDialog = $dialog;
width: 800,
height: 100,
position: { my: "center top+100", at: "top", of: window }
});
$searchInput.val(''); $dialog.dialog({
modal: true,
// remove the current note width: 800,
const recNotes = list.filter(note => note !== treeService.getCurrentNotePath()); height: 100,
position: { my: "center top+100", at: "top", of: window }
$searchInput.autocomplete({
source: recNotes.map(notePath => {
let noteTitle;
try {
noteTitle = treeService.getNotePathTitle(notePath);
}
catch (e) {
noteTitle = "[error - can't find note title]";
messaging.logError("Could not find title for notePath=" + notePath + ", stack=" + e.stack);
}
return {
label: noteTitle,
value: notePath
}
}),
minLength: 0,
autoFocus: true,
select: function (event, ui) {
treeService.activateNode(ui.item.value);
$searchInput.autocomplete('destroy');
$dialog.dialog('close');
},
focus: function (event, ui) {
event.preventDefault();
},
close: function (event, ui) {
if (event.keyCode === 27) { // escape closes dialog
$searchInput.autocomplete('destroy');
$dialog.dialog('close');
}
else {
// keep autocomplete open
// we're kind of abusing autocomplete to work in a way which it's not designed for
$searchInput.autocomplete("search", "");
}
},
create: () => $searchInput.autocomplete("search", ""),
classes: {
"ui-autocomplete": "recent-notes-autocomplete"
}
});
}
reload();
$(document).bind('keydown', 'ctrl+e', e => {
showDialog();
e.preventDefault();
}); });
$showDialogButton.click(showDialog); $searchInput.val('');
return { // remove the current note
showDialog, const recNotes = list.filter(note => note !== treeService.getCurrentNotePath());
addRecentNote,
reload $searchInput.autocomplete({
}; source: recNotes.map(notePath => {
})(); let noteTitle;
try {
noteTitle = treeService.getNotePathTitle(notePath);
}
catch (e) {
noteTitle = "[error - can't find note title]";
messaging.logError("Could not find title for notePath=" + notePath + ", stack=" + e.stack);
}
return {
label: noteTitle,
value: notePath
}
}),
minLength: 0,
autoFocus: true,
select: function (event, ui) {
treeService.activateNode(ui.item.value);
$searchInput.autocomplete('destroy');
$dialog.dialog('close');
},
focus: function (event, ui) {
event.preventDefault();
},
close: function (event, ui) {
if (event.keyCode === 27) { // escape closes dialog
$searchInput.autocomplete('destroy');
$dialog.dialog('close');
}
else {
// keep autocomplete open
// we're kind of abusing autocomplete to work in a way which it's not designed for
$searchInput.autocomplete("search", "");
}
},
create: () => $searchInput.autocomplete("search", ""),
classes: {
"ui-autocomplete": "recent-notes-autocomplete"
}
});
}
reload();
$(document).bind('keydown', 'ctrl+e', e => {
showDialog();
e.preventDefault();
});
$showDialogButton.click(showDialog);
export default {
showDialog,
addRecentNote,
reload
};

View File

@ -1,54 +1,56 @@
"use strict"; "use strict";
const settings = (function() { import protected_session from '../protected_session.js';
const $showDialogButton = $("#settings-button"); import utils from '../utils.js';
const $dialog = $("#settings-dialog"); import server from '../server.js';
const $tabs = $("#settings-tabs");
const settingModules = []; const $showDialogButton = $("#settings-button");
const $dialog = $("#settings-dialog");
const $tabs = $("#settings-tabs");
function addModule(module) { const settingModules = [];
settingModules.push(module);
}
async function showDialog() { function addModule(module) {
glob.activeDialog = $dialog; settingModules.push(module);
}
const settings = await server.get('settings'); async function showDialog() {
glob.activeDialog = $dialog;
$dialog.dialog({ const settings = await server.get('settings');
modal: true,
width: 900
});
$tabs.tabs(); $dialog.dialog({
modal: true,
width: 900
});
for (const module of settingModules) { $tabs.tabs();
if (module.settingsLoaded) {
module.settingsLoaded(settings); for (const module of settingModules) {
} if (module.settingsLoaded) {
module.settingsLoaded(settings);
} }
} }
}
async function saveSettings(settingName, settingValue) { async function saveSettings(settingName, settingValue) {
await server.post('settings', { await server.post('settings', {
name: settingName, name: settingName,
value: settingValue value: settingValue
}); });
utils.showMessage("Settings change have been saved."); utils.showMessage("Settings change have been saved.");
} }
$showDialogButton.click(showDialog); $showDialogButton.click(showDialog);
return { export default {
showDialog, showDialog,
saveSettings, saveSettings,
addModule addModule
}; };
})();
settings.addModule((function() { addModule((function() {
const $form = $("#change-password-form"); const $form = $("#change-password-form");
const $oldPassword = $("#old-password"); const $oldPassword = $("#old-password");
const $newPassword1 = $("#new-password1"); const $newPassword1 = $("#new-password1");
@ -94,7 +96,7 @@ settings.addModule((function() {
}; };
})()); })());
settings.addModule((function() { addModule((function() {
const $form = $("#protected-session-timeout-form"); const $form = $("#protected-session-timeout-form");
const $protectedSessionTimeout = $("#protected-session-timeout-in-seconds"); const $protectedSessionTimeout = $("#protected-session-timeout-in-seconds");
const settingName = 'protected_session_timeout'; const settingName = 'protected_session_timeout';
@ -118,7 +120,7 @@ settings.addModule((function() {
}; };
})()); })());
settings.addModule((function () { addModule((function () {
const $form = $("#history-snapshot-time-interval-form"); const $form = $("#history-snapshot-time-interval-form");
const $timeInterval = $("#history-snapshot-time-interval-in-seconds"); const $timeInterval = $("#history-snapshot-time-interval-in-seconds");
const settingName = 'history_snapshot_time_interval'; const settingName = 'history_snapshot_time_interval';
@ -138,7 +140,7 @@ settings.addModule((function () {
}; };
})()); })());
settings.addModule((async function () { addModule((async function () {
const $appVersion = $("#app-version"); const $appVersion = $("#app-version");
const $dbVersion = $("#db-version"); const $dbVersion = $("#db-version");
const $buildDate = $("#build-date"); const $buildDate = $("#build-date");
@ -155,7 +157,7 @@ settings.addModule((async function () {
return {}; return {};
})()); })());
settings.addModule((async function () { addModule((async function () {
const $forceFullSyncButton = $("#force-full-sync-button"); const $forceFullSyncButton = $("#force-full-sync-button");
const $fillSyncRowsButton = $("#fill-sync-rows-button"); const $fillSyncRowsButton = $("#fill-sync-rows-button");
const $anonymizeButton = $("#anonymize-button"); const $anonymizeButton = $("#anonymize-button");

View File

@ -1,106 +1,106 @@
"use strict"; "use strict";
const sqlConsole = (function() { import utils from '../utils.js';
const $dialog = $("#sql-console-dialog");
const $query = $('#sql-console-query');
const $executeButton = $('#sql-console-execute');
const $resultHead = $('#sql-console-results thead');
const $resultBody = $('#sql-console-results tbody');
let codeEditor; const $dialog = $("#sql-console-dialog");
const $query = $('#sql-console-query');
const $executeButton = $('#sql-console-execute');
const $resultHead = $('#sql-console-results thead');
const $resultBody = $('#sql-console-results tbody');
function showDialog() { let codeEditor;
glob.activeDialog = $dialog;
$dialog.dialog({ function showDialog() {
modal: true, glob.activeDialog = $dialog;
width: $(window).width(),
height: $(window).height(),
open: function() {
initEditor();
}
});
}
async function initEditor() { $dialog.dialog({
if (!codeEditor) { modal: true,
await utils.requireLibrary(utils.CODE_MIRROR); width: $(window).width(),
height: $(window).height(),
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess"; open: function() {
CodeMirror.keyMap.default["Tab"] = "indentMore"; initEditor();
// removing Escape binding so that Escape will propagate to the dialog (which will close on escape)
delete CodeMirror.keyMap.basic["Esc"];
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
codeEditor = CodeMirror($query[0], {
value: "",
viewportMargin: Infinity,
indentUnit: 4,
highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: false}
});
codeEditor.setOption("mode", "text/x-sqlite");
CodeMirror.autoLoadMode(codeEditor, "sql");
} }
});
}
codeEditor.focus(); async function initEditor() {
} if (!codeEditor) {
await utils.requireLibrary(utils.CODE_MIRROR);
async function execute(e) { CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
// stop from propagating upwards (dangerous especially with ctrl+enter executable javascript notes) CodeMirror.keyMap.default["Tab"] = "indentMore";
e.preventDefault();
e.stopPropagation();
const sqlQuery = codeEditor.getValue(); // removing Escape binding so that Escape will propagate to the dialog (which will close on escape)
delete CodeMirror.keyMap.basic["Esc"];
const result = await server.post("sql/execute", { CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
query: sqlQuery
codeEditor = CodeMirror($query[0], {
value: "",
viewportMargin: Infinity,
indentUnit: 4,
highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: false}
}); });
if (!result.success) { codeEditor.setOption("mode", "text/x-sqlite");
utils.showError(result.error); CodeMirror.autoLoadMode(codeEditor, "sql");
return;
}
else {
utils.showMessage("Query was executed successfully.");
}
const rows = result.rows;
$resultHead.empty();
$resultBody.empty();
if (rows.length > 0) {
const result = rows[0];
const rowEl = $("<tr>");
for (const key in result) {
rowEl.append($("<th>").html(key));
}
$resultHead.append(rowEl);
}
for (const result of rows) {
const rowEl = $("<tr>");
for (const key in result) {
rowEl.append($("<td>").html(result[key]));
}
$resultBody.append(rowEl);
}
} }
$(document).bind('keydown', 'alt+o', showDialog); codeEditor.focus();
}
$query.bind('keydown', 'ctrl+return', execute); async function execute(e) {
// stop from propagating upwards (dangerous especially with ctrl+enter executable javascript notes)
e.preventDefault();
e.stopPropagation();
$executeButton.click(execute); const sqlQuery = codeEditor.getValue();
return { const result = await server.post("sql/execute", {
showDialog query: sqlQuery
}; });
})();
if (!result.success) {
utils.showError(result.error);
return;
}
else {
utils.showMessage("Query was executed successfully.");
}
const rows = result.rows;
$resultHead.empty();
$resultBody.empty();
if (rows.length > 0) {
const result = rows[0];
const rowEl = $("<tr>");
for (const key in result) {
rowEl.append($("<th>").html(key));
}
$resultHead.append(rowEl);
}
for (const result of rows) {
const rowEl = $("<tr>");
for (const key in result) {
rowEl.append($("<td>").html(result[key]));
}
$resultBody.append(rowEl);
}
}
$(document).bind('keydown', 'alt+o', showDialog);
$query.bind('keydown', 'ctrl+return', execute);
$executeButton.click(execute);
export default {
showDialog
};

View File

@ -1,5 +1,8 @@
"use strict"; "use strict";
import treeService from './note_tree.js';
import treeChanges from './tree_changes.js';
const dragAndDropSetup = { const dragAndDropSetup = {
autoExpandMS: 600, autoExpandMS: 600,
draggable: { // modify default jQuery draggable options draggable: { // modify default jQuery draggable options
@ -65,3 +68,5 @@ const dragAndDropSetup = {
} }
} }
}; };
export default dragAndDropSetup;

View File

@ -1,39 +1,41 @@
"use strict"; "use strict";
const exportService = (function () { import treeService from './note_tree.js';
function exportSubTree(noteId) { import protected_session from './protected_session.js';
const url = utils.getHost() + "/api/export/" + noteId + "?protectedSessionId=" import utils from './utils.js';
+ encodeURIComponent(protected_session.getProtectedSessionId());
utils.download(url); function exportSubTree(noteId) {
} const url = utils.getHost() + "/api/export/" + noteId + "?protectedSessionId="
+ encodeURIComponent(protected_session.getProtectedSessionId());
let importNoteId; utils.download(url);
}
function importSubTree(noteId) { let importNoteId;
importNoteId = noteId;
$("#import-upload").trigger('click'); function importSubTree(noteId) {
} importNoteId = noteId;
$("#import-upload").change(async function() { $("#import-upload").trigger('click');
const formData = new FormData(); }
formData.append('upload', this.files[0]);
await $.ajax({ $("#import-upload").change(async function() {
url: baseApiUrl + 'import/' + importNoteId, const formData = new FormData();
headers: server.getHeaders(), formData.append('upload', this.files[0]);
data: formData,
type: 'POST',
contentType: false, // NEEDED, DON'T OMIT THIS
processData: false, // NEEDED, DON'T OMIT THIS
});
await treeService.reload(); await $.ajax({
url: baseApiUrl + 'import/' + importNoteId,
headers: server.getHeaders(),
data: formData,
type: 'POST',
contentType: false, // NEEDED, DON'T OMIT THIS
processData: false, // NEEDED, DON'T OMIT THIS
}); });
return { await treeService.reload();
exportSubTree, });
importSubTree
}; export default {
})(); exportSubTree,
importSubTree
};

View File

@ -1,252 +1,258 @@
"use strict"; "use strict";
const initService = (function() { import treeService from './note_tree.js';
// hot keys are active also inside inputs and content editables import link from './link.js';
jQuery.hotkeys.options.filterInputAcceptingElements = false; import messaging from './messaging.js';
jQuery.hotkeys.options.filterContentEditable = false; import noteEditor from './note_editor.js';
jQuery.hotkeys.options.filterTextInputs = false; import treeUtils from './tree_utils.js';
import utils from './utils.js';
import server from './server.js';
$(document).bind('keydown', 'alt+m', e => { // hot keys are active also inside inputs and content editables
$(".hide-toggle").toggleClass("suppressed"); jQuery.hotkeys.options.filterInputAcceptingElements = false;
jQuery.hotkeys.options.filterContentEditable = false;
jQuery.hotkeys.options.filterTextInputs = false;
e.preventDefault(); $(document).bind('keydown', 'alt+m', e => {
}); $(".hide-toggle").toggleClass("suppressed");
// hide (toggle) everything except for the note content for distraction free writing e.preventDefault();
$(document).bind('keydown', 'alt+t', e => { });
const date = new Date();
const dateString = utils.formatDateTime(date);
link.addTextToEditor(dateString); // hide (toggle) everything except for the note content for distraction free writing
$(document).bind('keydown', 'alt+t', e => {
const date = new Date();
const dateString = utils.formatDateTime(date);
e.preventDefault(); link.addTextToEditor(dateString);
});
$(document).bind('keydown', 'f5', () => { e.preventDefault();
utils.reloadApp(); });
$(document).bind('keydown', 'f5', () => {
utils.reloadApp();
return false;
});
$(document).bind('keydown', 'ctrl+r', () => {
utils.reloadApp();
return false;
});
$(document).bind('keydown', 'ctrl+shift+i', () => {
if (utils.isElectron()) {
require('electron').remote.getCurrentWindow().toggleDevTools();
return false; return false;
}); }
});
$(document).bind('keydown', 'ctrl+r', () => { $(document).bind('keydown', 'ctrl+f', () => {
utils.reloadApp(); if (utils.isElectron()) {
const searchInPage = require('electron-in-page-search').default;
const remote = require('electron').remote;
const inPageSearch = searchInPage(remote.getCurrentWebContents());
inPageSearch.openSearchWindow();
return false; return false;
}); }
});
$(document).bind('keydown', 'ctrl+shift+i', () => { $(document).bind('keydown', "ctrl+shift+up", () => {
if (utils.isElectron()) { const node = treeService.getCurrentNode();
require('electron').remote.getCurrentWindow().toggleDevTools(); node.navigate($.ui.keyCode.UP, true);
return false; $("#note-detail").focus();
return false;
});
$(document).bind('keydown', "ctrl+shift+down", () => {
const node = treeService.getCurrentNode();
node.navigate($.ui.keyCode.DOWN, true);
$("#note-detail").focus();
return false;
});
$(document).bind('keydown', 'ctrl+-', () => {
if (utils.isElectron()) {
const webFrame = require('electron').webFrame;
if (webFrame.getZoomFactor() > 0.2) {
webFrame.setZoomFactor(webFrame.getZoomFactor() - 0.1);
} }
});
$(document).bind('keydown', 'ctrl+f', () => {
if (utils.isElectron()) {
const searchInPage = require('electron-in-page-search').default;
const remote = require('electron').remote;
const inPageSearch = searchInPage(remote.getCurrentWebContents());
inPageSearch.openSearchWindow();
return false;
}
});
$(document).bind('keydown', "ctrl+shift+up", () => {
const node = treeService.getCurrentNode();
node.navigate($.ui.keyCode.UP, true);
$("#note-detail").focus();
return false; return false;
}); }
});
$(document).bind('keydown', "ctrl+shift+down", () => { $(document).bind('keydown', 'ctrl+=', () => {
const node = treeService.getCurrentNode(); if (utils.isElectron()) {
node.navigate($.ui.keyCode.DOWN, true); const webFrame = require('electron').webFrame;
$("#note-detail").focus(); webFrame.setZoomFactor(webFrame.getZoomFactor() + 0.1);
return false; return false;
}); }
});
$(document).bind('keydown', 'ctrl+-', () => { $("#note-title").bind('keydown', 'return', () => $("#note-detail").focus());
if (utils.isElectron()) {
const webFrame = require('electron').webFrame;
if (webFrame.getZoomFactor() > 0.2) { $(window).on('beforeunload', () => {
webFrame.setZoomFactor(webFrame.getZoomFactor() - 0.1); // this makes sure that when user e.g. reloads the page or navigates away from the page, the note's content is saved
} // this sends the request asynchronously and doesn't wait for result
noteEditor.saveNoteIfChanged();
});
return false; // Overrides the default autocomplete filter function to search for matched on atleast 1 word in each of the input term's words
} $.ui.autocomplete.filter = (array, terms) => {
}); if (!terms) {
return array;
}
$(document).bind('keydown', 'ctrl+=', () => { const startDate = new Date();
if (utils.isElectron()) {
const webFrame = require('electron').webFrame;
webFrame.setZoomFactor(webFrame.getZoomFactor() + 0.1); const results = [];
const tokens = terms.toLowerCase().split(" ");
return false; for (const item of array) {
} const lcLabel = item.label.toLowerCase();
});
$("#note-title").bind('keydown', 'return', () => $("#note-detail").focus()); const found = tokens.every(token => lcLabel.indexOf(token) !== -1);
if (!found) {
$(window).on('beforeunload', () => { continue;
// this makes sure that when user e.g. reloads the page or navigates away from the page, the note's content is saved
// this sends the request asynchronously and doesn't wait for result
noteEditor.saveNoteIfChanged();
});
// Overrides the default autocomplete filter function to search for matched on atleast 1 word in each of the input term's words
$.ui.autocomplete.filter = (array, terms) => {
if (!terms) {
return array;
} }
const startDate = new Date(); // this is not completely correct and might cause minor problems with note with names containing this " / "
const lastSegmentIndex = lcLabel.lastIndexOf(" / ");
const results = []; if (lastSegmentIndex !== -1) {
const tokens = terms.toLowerCase().split(" "); const lastSegment = lcLabel.substr(lastSegmentIndex + 3);
for (const item of array) { // at least some token needs to be in the last segment (leaf note), otherwise this
const lcLabel = item.label.toLowerCase(); // particular note is not that interesting (query is satisfied by parent note)
const foundInLastSegment = tokens.some(token => lastSegment.indexOf(token) !== -1);
const found = tokens.every(token => lcLabel.indexOf(token) !== -1); if (!foundInLastSegment) {
if (!found) {
continue; continue;
} }
// this is not completely correct and might cause minor problems with note with names containing this " / "
const lastSegmentIndex = lcLabel.lastIndexOf(" / ");
if (lastSegmentIndex !== -1) {
const lastSegment = lcLabel.substr(lastSegmentIndex + 3);
// at least some token needs to be in the last segment (leaf note), otherwise this
// particular note is not that interesting (query is satisfied by parent note)
const foundInLastSegment = tokens.some(token => lastSegment.indexOf(token) !== -1);
if (!foundInLastSegment) {
continue;
}
}
results.push(item);
if (results.length > 100) {
break;
}
} }
console.log("Search took " + (new Date().getTime() - startDate.getTime()) + "ms"); results.push(item);
return results; if (results.length > 100) {
}; break;
}
}
$(document).tooltip({ console.log("Search took " + (new Date().getTime() - startDate.getTime()) + "ms");
items: "#note-detail a",
content: function(callback) {
const notePath = link.getNotePathFromLink($(this).attr("href"));
if (notePath !== null) { return results;
const noteId = treeUtils.getNoteIdFromNotePath(notePath); };
noteEditor.loadNote(noteId).then(note => callback(note.detail.content)); $(document).tooltip({
} items: "#note-detail a",
}, content: function(callback) {
close: function(event, ui) const notePath = link.getNotePathFromLink($(this).attr("href"));
{
ui.tooltip.hover(function() if (notePath !== null) {
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
noteEditor.loadNote(noteId).then(note => callback(note.detail.content));
}
},
close: function(event, ui)
{
ui.tooltip.hover(function()
{
$(this).stop(true).fadeTo(400, 1);
},
function()
{
$(this).fadeOut('400', function()
{ {
$(this).stop(true).fadeTo(400, 1); $(this).remove();
},
function()
{
$(this).fadeOut('400', function()
{
$(this).remove();
});
}); });
} });
}); }
});
window.onerror = function (msg, url, lineNo, columnNo, error) { window.onerror = function (msg, url, lineNo, columnNo, error) {
const string = msg.toLowerCase(); const string = msg.toLowerCase();
let message = "Uncaught error: "; let message = "Uncaught error: ";
if (string.indexOf("script error") > -1){ if (string.indexOf("script error") > -1){
message += 'No details available'; message += 'No details available';
} }
else { else {
message += [ message += [
'Message: ' + msg, 'Message: ' + msg,
'URL: ' + url, 'URL: ' + url,
'Line: ' + lineNo, 'Line: ' + lineNo,
'Column: ' + columnNo, 'Column: ' + columnNo,
'Error object: ' + JSON.stringify(error) 'Error object: ' + JSON.stringify(error)
].join(' - '); ].join(' - ');
}
messaging.logError(message);
return false;
};
$("#logout-button").toggle(!utils.isElectron());
$(document).ready(() => {
server.get("script/startup").then(scriptBundles => {
for (const bundle of scriptBundles) {
utils.executeBundle(bundle);
}
});
});
if (utils.isElectron()) {
require('electron').ipcRenderer.on('create-day-sub-note', async function(event, parentNoteId) {
// this might occur when day note had to be created
if (!await treeService.noteExists(parentNoteId)) {
await treeService.reload();
}
await treeService.activateNode(parentNoteId);
setTimeout(() => {
const node = treeService.getCurrentNode();
treeService.createNote(node, node.data.noteId, 'into', node.data.isProtected);
}, 500);
});
} }
function uploadAttachment() { messaging.logError(message);
$("#attachment-upload").trigger('click');
}
$("#upload-attachment-button").click(uploadAttachment); return false;
};
$("#attachment-upload").change(async function() { $("#logout-button").toggle(!utils.isElectron());
const formData = new FormData();
formData.append('upload', this.files[0]);
const resp = await $.ajax({ $(document).ready(() => {
url: baseApiUrl + 'attachments/upload/' + noteEditor.getCurrentNoteId(), server.get("script/startup").then(scriptBundles => {
headers: server.getHeaders(), for (const bundle of scriptBundles) {
data: formData, utils.executeBundle(bundle);
type: 'POST', }
contentType: false, // NEEDED, DON'T OMIT THIS
processData: false, // NEEDED, DON'T OMIT THIS
});
await treeService.reload();
await treeService.activateNode(resp.noteId);
}); });
})(); });
if (utils.isElectron()) {
require('electron').ipcRenderer.on('create-day-sub-note', async function(event, parentNoteId) {
// this might occur when day note had to be created
if (!await treeService.noteExists(parentNoteId)) {
await treeService.reload();
}
await treeService.activateNode(parentNoteId);
setTimeout(() => {
const node = treeService.getCurrentNode();
treeService.createNote(node, node.data.noteId, 'into', node.data.isProtected);
}, 500);
});
}
function uploadAttachment() {
$("#attachment-upload").trigger('click');
}
$("#upload-attachment-button").click(uploadAttachment);
$("#attachment-upload").change(async function() {
const formData = new FormData();
formData.append('upload', this.files[0]);
const resp = await $.ajax({
url: baseApiUrl + 'attachments/upload/' + noteEditor.getCurrentNoteId(),
headers: server.getHeaders(),
data: formData,
type: 'POST',
contentType: false, // NEEDED, DON'T OMIT THIS
processData: false, // NEEDED, DON'T OMIT THIS
});
await treeService.reload();
await treeService.activateNode(resp.noteId);
});

View File

@ -1,103 +1,105 @@
"use strict"; "use strict";
const link = (function() { import treeService from './note_tree.js';
function getNotePathFromLink(url) { import noteEditor from './note_editor.js';
const notePathMatch = /#([A-Za-z0-9/]+)$/.exec(url); import treeUtils from './tree_utils.js';
if (notePathMatch === null) { function getNotePathFromLink(url) {
return null; const notePathMatch = /#([A-Za-z0-9/]+)$/.exec(url);
}
else {
return notePathMatch[1];
}
}
function getNodePathFromLabel(label) {
const notePathMatch = / \(([A-Za-z0-9/]+)\)/.exec(label);
if (notePathMatch !== null) {
return notePathMatch[1];
}
if (notePathMatch === null) {
return null; return null;
} }
else {
return notePathMatch[1];
}
}
function createNoteLink(notePath, noteTitle) { function getNodePathFromLabel(label) {
if (!noteTitle) { const notePathMatch = / \(([A-Za-z0-9/]+)\)/.exec(label);
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
noteTitle = treeService.getNoteTitle(noteId); if (notePathMatch !== null) {
return notePathMatch[1];
}
return null;
}
function createNoteLink(notePath, noteTitle) {
if (!noteTitle) {
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
noteTitle = treeService.getNoteTitle(noteId);
}
const noteLink = $("<a>", {
href: 'javascript:',
text: noteTitle
}).attr('action', 'note')
.attr('note-path', notePath);
return noteLink;
}
function goToLink(e) {
e.preventDefault();
const $link = $(e.target);
let notePath = $link.attr("note-path");
if (!notePath) {
const address = $link.attr("note-path") ? $link.attr("note-path") : $link.attr('href');
if (!address) {
return;
} }
const noteLink = $("<a>", { if (address.startsWith('http')) {
href: 'javascript:', window.open(address, '_blank');
text: noteTitle
}).attr('action', 'note')
.attr('note-path', notePath);
return noteLink; return;
}
function goToLink(e) {
e.preventDefault();
const $link = $(e.target);
let notePath = $link.attr("note-path");
if (!notePath) {
const address = $link.attr("note-path") ? $link.attr("note-path") : $link.attr('href');
if (!address) {
return;
}
if (address.startsWith('http')) {
window.open(address, '_blank');
return;
}
notePath = getNotePathFromLink(address);
} }
treeService.activateNode(notePath); notePath = getNotePathFromLink(address);
}
// this is quite ugly hack, but it seems like we can't close the tooltip otherwise treeService.activateNode(notePath);
$("[role='tooltip']").remove();
if (glob.activeDialog) { // this is quite ugly hack, but it seems like we can't close the tooltip otherwise
try { $("[role='tooltip']").remove();
glob.activeDialog.dialog('close');
} if (glob.activeDialog) {
catch (e) {} try {
glob.activeDialog.dialog('close');
} }
catch (e) {}
} }
}
function addLinkToEditor(linkTitle, linkHref) { function addLinkToEditor(linkTitle, linkHref) {
const editor = noteEditor.getEditor(); const editor = noteEditor.getEditor();
const doc = editor.document; const doc = editor.document;
doc.enqueueChanges(() => editor.data.insertLink(linkTitle, linkHref), doc.selection); doc.enqueueChanges(() => editor.data.insertLink(linkTitle, linkHref), doc.selection);
} }
function addTextToEditor(text) { function addTextToEditor(text) {
const editor = noteEditor.getEditor(); const editor = noteEditor.getEditor();
const doc = editor.document; const doc = editor.document;
doc.enqueueChanges(() => editor.data.insertText(text), doc.selection); doc.enqueueChanges(() => editor.data.insertText(text), doc.selection);
} }
// when click on link popup, in case of internal link, just go the the referenced note instead of default behavior // when click on link popup, in case of internal link, just go the the referenced note instead of default behavior
// of opening the link in new window/tab // of opening the link in new window/tab
$(document).on('click', "a[action='note']", goToLink); $(document).on('click', "a[action='note']", goToLink);
$(document).on('click', 'div.popover-content a, div.ui-tooltip-content a', goToLink); $(document).on('click', 'div.popover-content a, div.ui-tooltip-content a', goToLink);
$(document).on('dblclick', '#note-detail a', goToLink); $(document).on('dblclick', '#note-detail a', goToLink);
return { export default {
getNodePathFromLabel, getNodePathFromLabel,
getNotePathFromLink, getNotePathFromLink,
createNoteLink, createNoteLink,
addLinkToEditor, addLinkToEditor,
addTextToEditor addTextToEditor
}; };
})();

View File

@ -1,115 +1,118 @@
"use strict"; "use strict";
const messaging = (function() { import treeService from './note_tree.js';
const $changesToPushCount = $("#changes-to-push-count"); import noteEditor from './note_editor.js';
import sync from './sync.js';
import utils from './utils.js';
function logError(message) { const $changesToPushCount = $("#changes-to-push-count");
console.log(utils.now(), message); // needs to be separate from .trace()
console.trace();
if (ws && ws.readyState === 1) { function logError(message) {
ws.send(JSON.stringify({ console.log(utils.now(), message); // needs to be separate from .trace()
type: 'log-error', console.trace();
error: message
}));
}
}
function messageHandler(event) {
const message = JSON.parse(event.data);
if (message.type === 'sync') {
lastPingTs = new Date().getTime();
if (message.data.length > 0) {
console.log(utils.now(), "Sync data: ", message.data);
lastSyncId = message.data[message.data.length - 1].id;
}
const syncData = message.data.filter(sync => sync.sourceId !== glob.sourceId);
if (syncData.some(sync => sync.entityName === 'branches')
|| syncData.some(sync => sync.entityName === 'notes')) {
console.log(utils.now(), "Reloading tree because of background changes");
treeService.reload();
}
if (syncData.some(sync => sync.entityName === 'notes' && sync.entityId === noteEditor.getCurrentNoteId())) {
utils.showMessage('Reloading note because of background changes');
noteEditor.reload();
}
if (syncData.some(sync => sync.entityName === 'recent_notes')) {
console.log(utils.now(), "Reloading recent notes because of background changes");
recentNotes.reload();
}
// we don't detect image changes here since images themselves are immutable and references should be
// updated in note detail as well
$changesToPushCount.html(message.changesToPushCount);
}
else if (message.type === 'sync-hash-check-failed') {
utils.utils.showError("Sync check failed!", 60000);
}
else if (message.type === 'consistency-checks-failed') {
utils.showError("Consistency checks failed! See logs for details.", 50 * 60000);
}
}
function connectWebSocket() {
const protocol = document.location.protocol === 'https:' ? 'wss' : 'ws';
// use wss for secure messaging
const ws = new WebSocket(protocol + "://" + location.host);
ws.onopen = event => console.log(utils.now(), "Connected to server with WebSocket");
ws.onmessage = messageHandler;
ws.onclose = function(){
// Try to reconnect in 5 seconds
setTimeout(() => connectWebSocket(), 5000);
};
return ws;
}
const ws = connectWebSocket();
let lastSyncId = glob.maxSyncIdAtLoad;
let lastPingTs = new Date().getTime();
let connectionBrokenNotification = null;
setInterval(async () => {
if (new Date().getTime() - lastPingTs > 30000) {
if (!connectionBrokenNotification) {
connectionBrokenNotification = $.notify({
// options
message: "Lost connection to server"
},{
// settings
type: 'danger',
delay: 100000000 // keep it until we explicitly close it
});
}
}
else if (connectionBrokenNotification) {
await connectionBrokenNotification.close();
connectionBrokenNotification = null;
utils.showMessage("Re-connected to server");
}
if (ws && ws.readyState === 1) {
ws.send(JSON.stringify({ ws.send(JSON.stringify({
type: 'ping', type: 'log-error',
lastSyncId: lastSyncId error: message
})); }));
}, 1000); }
}
return { function messageHandler(event) {
logError const message = JSON.parse(event.data);
if (message.type === 'sync') {
lastPingTs = new Date().getTime();
if (message.data.length > 0) {
console.log(utils.now(), "Sync data: ", message.data);
lastSyncId = message.data[message.data.length - 1].id;
}
const syncData = message.data.filter(sync => sync.sourceId !== glob.sourceId);
if (syncData.some(sync => sync.entityName === 'branches')
|| syncData.some(sync => sync.entityName === 'notes')) {
console.log(utils.now(), "Reloading tree because of background changes");
treeService.reload();
}
if (syncData.some(sync => sync.entityName === 'notes' && sync.entityId === noteEditor.getCurrentNoteId())) {
utils.showMessage('Reloading note because of background changes');
noteEditor.reload();
}
if (syncData.some(sync => sync.entityName === 'recent_notes')) {
console.log(utils.now(), "Reloading recent notes because of background changes");
recentNotes.reload();
}
// we don't detect image changes here since images themselves are immutable and references should be
// updated in note detail as well
$changesToPushCount.html(message.changesToPushCount);
}
else if (message.type === 'sync-hash-check-failed') {
utils.utils.showError("Sync check failed!", 60000);
}
else if (message.type === 'consistency-checks-failed') {
utils.showError("Consistency checks failed! See logs for details.", 50 * 60000);
}
}
function connectWebSocket() {
const protocol = document.location.protocol === 'https:' ? 'wss' : 'ws';
// use wss for secure messaging
const ws = new WebSocket(protocol + "://" + location.host);
ws.onopen = event => console.log(utils.now(), "Connected to server with WebSocket");
ws.onmessage = messageHandler;
ws.onclose = function(){
// Try to reconnect in 5 seconds
setTimeout(() => connectWebSocket(), 5000);
}; };
})();
return ws;
}
const ws = connectWebSocket();
let lastSyncId = glob.maxSyncIdAtLoad;
let lastPingTs = new Date().getTime();
let connectionBrokenNotification = null;
setInterval(async () => {
if (new Date().getTime() - lastPingTs > 30000) {
if (!connectionBrokenNotification) {
connectionBrokenNotification = $.notify({
// options
message: "Lost connection to server"
},{
// settings
type: 'danger',
delay: 100000000 // keep it until we explicitly close it
});
}
}
else if (connectionBrokenNotification) {
await connectionBrokenNotification.close();
connectionBrokenNotification = null;
utils.showMessage("Re-connected to server");
}
ws.send(JSON.stringify({
type: 'ping',
lastSyncId: lastSyncId
}));
}, 1000);
export default {
logError
};

View File

@ -1,400 +1,404 @@
"use strict"; "use strict";
const noteEditor = (function() { import treeService from './note_tree.js';
const $noteTitle = $("#note-title"); import noteType from './note_type.js';
import protected_session from './protected_session.js';
import utils from './utils.js';
import server from './server.js';
const $noteDetail = $('#note-detail'); const $noteTitle = $("#note-title");
const $noteDetailCode = $('#note-detail-code');
const $noteDetailSearch = $('#note-detail-search');
const $noteDetailRender = $('#note-detail-render');
const $noteDetailAttachment = $('#note-detail-attachment');
const $protectButton = $("#protect-button"); const $noteDetail = $('#note-detail');
const $unprotectButton = $("#unprotect-button"); const $noteDetailCode = $('#note-detail-code');
const $noteDetailWrapper = $("#note-detail-wrapper"); const $noteDetailSearch = $('#note-detail-search');
const $noteIdDisplay = $("#note-id-display"); const $noteDetailRender = $('#note-detail-render');
const $labelList = $("#label-list"); const $noteDetailAttachment = $('#note-detail-attachment');
const $labelListInner = $("#label-list-inner");
const $attachmentFileName = $("#attachment-filename");
const $attachmentFileType = $("#attachment-filetype");
const $attachmentFileSize = $("#attachment-filesize");
const $attachmentDownload = $("#attachment-download");
const $attachmentOpen = $("#attachment-open");
const $searchString = $("#search-string");
const $executeScriptButton = $("#execute-script-button"); const $protectButton = $("#protect-button");
const $unprotectButton = $("#unprotect-button");
const $noteDetailWrapper = $("#note-detail-wrapper");
const $noteIdDisplay = $("#note-id-display");
const $labelList = $("#label-list");
const $labelListInner = $("#label-list-inner");
const $attachmentFileName = $("#attachment-filename");
const $attachmentFileType = $("#attachment-filetype");
const $attachmentFileSize = $("#attachment-filesize");
const $attachmentDownload = $("#attachment-download");
const $attachmentOpen = $("#attachment-open");
const $searchString = $("#search-string");
let editor = null; const $executeScriptButton = $("#execute-script-button");
let codeEditor = null;
let currentNote = null; let editor = null;
let codeEditor = null;
let noteChangeDisabled = false; let currentNote = null;
let isNoteChanged = false; let noteChangeDisabled = false;
function getCurrentNote() { let isNoteChanged = false;
return currentNote;
function getCurrentNote() {
return currentNote;
}
function getCurrentNoteId() {
return currentNote ? currentNote.detail.noteId : null;
}
function noteChanged() {
if (noteChangeDisabled) {
return;
} }
function getCurrentNoteId() { isNoteChanged = true;
return currentNote ? currentNote.detail.noteId : null; }
async function reload() {
// no saving here
await loadNoteToEditor(getCurrentNoteId());
}
async function switchToNote(noteId) {
if (getCurrentNoteId() !== noteId) {
await saveNoteIfChanged();
await loadNoteToEditor(noteId);
}
}
async function saveNoteIfChanged() {
if (!isNoteChanged) {
return;
} }
function noteChanged() { const note = getCurrentNote();
if (noteChangeDisabled) {
return; updateNoteFromInputs(note);
await saveNoteToServer(note);
if (note.detail.isProtected) {
protected_session.touchProtectedSession();
}
}
function updateNoteFromInputs(note) {
if (note.detail.type === 'text') {
let content = editor.getData();
// if content is only tags/whitespace (typically <p>&nbsp;</p>), then just make it empty
// this is important when setting new note to code
if (jQuery(content).text().trim() === '' && !content.includes("<img")) {
content = '';
} }
isNoteChanged = true; note.detail.content = content;
}
else if (note.detail.type === 'code') {
note.detail.content = codeEditor.getValue();
}
else if (note.detail.type === 'search') {
note.detail.content = JSON.stringify({
searchString: $searchString.val()
});
}
else if (note.detail.type === 'render' || note.detail.type === 'file') {
// nothing
}
else {
utils.throwError("Unrecognized type: " + note.detail.type);
} }
async function reload() { const title = $noteTitle.val();
// no saving here
await loadNoteToEditor(getCurrentNoteId()); note.detail.title = title;
treeService.setNoteTitle(note.detail.noteId, title);
}
async function saveNoteToServer(note) {
await server.put('notes/' + note.detail.noteId, note);
isNoteChanged = false;
utils.showMessage("Saved!");
}
function setNoteBackgroundIfProtected(note) {
const isProtected = !!note.detail.isProtected;
$noteDetailWrapper.toggleClass("protected", isProtected);
$protectButton.toggle(!isProtected);
$unprotectButton.toggle(isProtected);
}
let isNewNoteCreated = false;
function newNoteCreated() {
isNewNoteCreated = true;
}
async function setContent(content) {
if (currentNote.detail.type === 'text') {
if (!editor) {
await utils.requireLibrary(utils.CKEDITOR);
editor = await BalloonEditor.create($noteDetail[0], {});
editor.document.on('change', noteChanged);
}
// temporary workaround for https://github.com/ckeditor/ckeditor5-enter/issues/49
editor.setData(content ? content : "<p></p>");
$noteDetail.show();
} }
else if (currentNote.detail.type === 'code') {
if (!codeEditor) {
await utils.requireLibrary(utils.CODE_MIRROR);
async function switchToNote(noteId) { CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
if (getCurrentNoteId() !== noteId) { CodeMirror.keyMap.default["Tab"] = "indentMore";
await saveNoteIfChanged();
await loadNoteToEditor(noteId); CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
}
}
async function saveNoteIfChanged() { codeEditor = CodeMirror($("#note-detail-code")[0], {
if (!isNoteChanged) { value: "",
return; viewportMargin: Infinity,
} indentUnit: 4,
matchBrackets: true,
const note = noteEditor.getCurrentNote(); matchTags: { bothTags: true },
highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: false },
updateNoteFromInputs(note); lint: true,
gutters: ["CodeMirror-lint-markers"],
await saveNoteToServer(note); lineNumbers: true
if (note.detail.isProtected) {
protected_session.touchProtectedSession();
}
}
function updateNoteFromInputs(note) {
if (note.detail.type === 'text') {
let content = editor.getData();
// if content is only tags/whitespace (typically <p>&nbsp;</p>), then just make it empty
// this is important when setting new note to code
if (jQuery(content).text().trim() === '' && !content.includes("<img")) {
content = '';
}
note.detail.content = content;
}
else if (note.detail.type === 'code') {
note.detail.content = codeEditor.getValue();
}
else if (note.detail.type === 'search') {
note.detail.content = JSON.stringify({
searchString: $searchString.val()
}); });
}
else if (note.detail.type === 'render' || note.detail.type === 'file') { codeEditor.on('change', noteChanged);
// nothing
}
else {
utils.throwError("Unrecognized type: " + note.detail.type);
} }
const title = $noteTitle.val(); $noteDetailCode.show();
note.detail.title = title; // this needs to happen after the element is shown, otherwise the editor won't be refresheds
codeEditor.setValue(content);
treeService.setNoteTitle(note.detail.noteId, title); const info = CodeMirror.findModeByMIME(currentNote.detail.mime);
if (info) {
codeEditor.setOption("mode", info.mime);
CodeMirror.autoLoadMode(codeEditor, info.mode);
}
codeEditor.refresh();
}
else if (currentNote.detail.type === 'search') {
$noteDetailSearch.show();
try {
const json = JSON.parse(content);
$searchString.val(json.searchString);
}
catch (e) {
console.log(e);
$searchString.val('');
}
$searchString.on('input', noteChanged);
}
}
async function loadNoteToEditor(noteId) {
currentNote = await loadNote(noteId);
if (isNewNoteCreated) {
isNewNoteCreated = false;
$noteTitle.focus().select();
} }
async function saveNoteToServer(note) { $noteIdDisplay.html(noteId);
await server.put('notes/' + note.detail.noteId, note);
isNoteChanged = false; await protected_session.ensureProtectedSession(currentNote.detail.isProtected, false);
utils.showMessage("Saved!"); if (currentNote.detail.isProtected) {
protected_session.touchProtectedSession();
} }
function setNoteBackgroundIfProtected(note) { // this might be important if we focused on protected note when not in protected note and we got a dialog
const isProtected = !!note.detail.isProtected; // to login, but we chose instead to come to another node - at that point the dialog is still visible and this will close it.
protected_session.ensureDialogIsClosed();
$noteDetailWrapper.toggleClass("protected", isProtected); $noteDetailWrapper.show();
$protectButton.toggle(!isProtected);
$unprotectButton.toggle(isProtected); noteChangeDisabled = true;
$noteTitle.val(currentNote.detail.title);
noteType.setNoteType(currentNote.detail.type);
noteType.setNoteMime(currentNote.detail.mime);
$noteDetail.hide();
$noteDetailSearch.hide();
$noteDetailCode.hide();
$noteDetailRender.html('').hide();
$noteDetailAttachment.hide();
if (currentNote.detail.type === 'render') {
$noteDetailRender.show();
const bundle = await server.get('script/bundle/' + getCurrentNoteId());
$noteDetailRender.html(bundle.html);
utils.executeBundle(bundle);
}
else if (currentNote.detail.type === 'file') {
$noteDetailAttachment.show();
$attachmentFileName.text(currentNote.labels.original_file_name);
$attachmentFileSize.text(currentNote.labels.file_size + " bytes");
$attachmentFileType.text(currentNote.detail.mime);
}
else {
await setContent(currentNote.detail.content);
} }
let isNewNoteCreated = false; noteChangeDisabled = false;
function newNoteCreated() { setNoteBackgroundIfProtected(currentNote);
isNewNoteCreated = true; treeService.setBranchBackgroundBasedOnProtectedStatus(noteId);
// after loading new note make sure editor is scrolled to the top
$noteDetailWrapper.scrollTop(0);
loadLabelList();
}
async function loadLabelList() {
const noteId = getCurrentNoteId();
const labels = await server.get('notes/' + noteId + '/labels');
$labelListInner.html('');
if (labels.length > 0) {
for (const attr of labels) {
$labelListInner.append(utils.formatLabel(attr) + " ");
}
$labelList.show();
} }
else {
async function setContent(content) { $labelList.hide();
if (currentNote.detail.type === 'text') {
if (!editor) {
await utils.requireLibrary(utils.CKEDITOR);
editor = await BalloonEditor.create($noteDetail[0], {});
editor.document.on('change', noteChanged);
}
// temporary workaround for https://github.com/ckeditor/ckeditor5-enter/issues/49
editor.setData(content ? content : "<p></p>");
$noteDetail.show();
}
else if (currentNote.detail.type === 'code') {
if (!codeEditor) {
await utils.requireLibrary(utils.CODE_MIRROR);
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
CodeMirror.keyMap.default["Tab"] = "indentMore";
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
codeEditor = CodeMirror($("#note-detail-code")[0], {
value: "",
viewportMargin: Infinity,
indentUnit: 4,
matchBrackets: true,
matchTags: { bothTags: true },
highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: false },
lint: true,
gutters: ["CodeMirror-lint-markers"],
lineNumbers: true
});
codeEditor.on('change', noteChanged);
}
$noteDetailCode.show();
// this needs to happen after the element is shown, otherwise the editor won't be refresheds
codeEditor.setValue(content);
const info = CodeMirror.findModeByMIME(currentNote.detail.mime);
if (info) {
codeEditor.setOption("mode", info.mime);
CodeMirror.autoLoadMode(codeEditor, info.mode);
}
codeEditor.refresh();
}
else if (currentNote.detail.type === 'search') {
$noteDetailSearch.show();
try {
const json = JSON.parse(content);
$searchString.val(json.searchString);
}
catch (e) {
console.log(e);
$searchString.val('');
}
$searchString.on('input', noteChanged);
}
} }
}
async function loadNoteToEditor(noteId) { async function loadNote(noteId) {
currentNote = await loadNote(noteId); return await server.get('notes/' + noteId);
}
if (isNewNoteCreated) { function getEditor() {
isNewNoteCreated = false; return editor;
}
$noteTitle.focus().select(); function focus() {
} const note = getCurrentNote();
$noteIdDisplay.html(noteId); if (note.detail.type === 'text') {
$noteDetail.focus();
}
else if (note.detail.type === 'code') {
codeEditor.focus();
}
else if (note.detail.type === 'render' || note.detail.type === 'file' || note.detail.type === 'search') {
// do nothing
}
else {
utils.throwError('Unrecognized type: ' + note.detail.type);
}
}
await protected_session.ensureProtectedSession(currentNote.detail.isProtected, false); function getCurrentNoteType() {
const currentNote = getCurrentNote();
if (currentNote.detail.isProtected) { return currentNote ? currentNote.detail.type : null;
protected_session.touchProtectedSession(); }
}
// this might be important if we focused on protected note when not in protected note and we got a dialog async function executeCurrentNote() {
// to login, but we chose instead to come to another node - at that point the dialog is still visible and this will close it. if (getCurrentNoteType() === 'code') {
protected_session.ensureDialogIsClosed(); // make sure note is saved so we load latest changes
await saveNoteIfChanged();
$noteDetailWrapper.show();
noteChangeDisabled = true;
$noteTitle.val(currentNote.detail.title);
noteType.setNoteType(currentNote.detail.type);
noteType.setNoteMime(currentNote.detail.mime);
$noteDetail.hide();
$noteDetailSearch.hide();
$noteDetailCode.hide();
$noteDetailRender.html('').hide();
$noteDetailAttachment.hide();
if (currentNote.detail.type === 'render') {
$noteDetailRender.show();
if (currentNote.detail.mime.endsWith("env=frontend")) {
const bundle = await server.get('script/bundle/' + getCurrentNoteId()); const bundle = await server.get('script/bundle/' + getCurrentNoteId());
$noteDetailRender.html(bundle.html);
utils.executeBundle(bundle); utils.executeBundle(bundle);
} }
else if (currentNote.detail.type === 'file') {
$noteDetailAttachment.show();
$attachmentFileName.text(currentNote.labels.original_file_name); if (currentNote.detail.mime.endsWith("env=backend")) {
$attachmentFileSize.text(currentNote.labels.file_size + " bytes"); await server.post('script/run/' + getCurrentNoteId());
$attachmentFileType.text(currentNote.detail.mime);
}
else {
await setContent(currentNote.detail.content);
} }
noteChangeDisabled = false; utils.showMessage("Note executed");
setNoteBackgroundIfProtected(currentNote);
treeService.setBranchBackgroundBasedOnProtectedStatus(noteId);
// after loading new note make sure editor is scrolled to the top
$noteDetailWrapper.scrollTop(0);
loadLabelList();
} }
}
async function loadLabelList() { $attachmentDownload.click(() => utils.download(getAttachmentUrl()));
const noteId = getCurrentNoteId();
const labels = await server.get('notes/' + noteId + '/labels'); $attachmentOpen.click(() => {
if (utils.isElectron()) {
const open = require("open");
$labelListInner.html(''); open(getAttachmentUrl());
if (labels.length > 0) {
for (const attr of labels) {
$labelListInner.append(utils.formatLabel(attr) + " ");
}
$labelList.show();
}
else {
$labelList.hide();
}
} }
else {
async function loadNote(noteId) { window.location.href = getAttachmentUrl();
return await server.get('notes/' + noteId);
} }
});
function getEditor() { function getAttachmentUrl() {
return editor; // electron needs absolute URL so we extract current host, port, protocol
} return utils.getHost() + "/api/attachments/download/" + getCurrentNoteId()
+ "?protectedSessionId=" + encodeURIComponent(protected_session.getProtectedSessionId());
}
function focus() { $(document).ready(() => {
const note = getCurrentNote(); $noteTitle.on('input', () => {
noteChanged();
if (note.detail.type === 'text') { const title = $noteTitle.val();
$noteDetail.focus();
}
else if (note.detail.type === 'code') {
codeEditor.focus();
}
else if (note.detail.type === 'render' || note.detail.type === 'file' || note.detail.type === 'search') {
// do nothing
}
else {
utils.throwError('Unrecognized type: ' + note.detail.type);
}
}
function getCurrentNoteType() { treeService.setNoteTitle(getCurrentNoteId(), title);
const currentNote = getCurrentNote();
return currentNote ? currentNote.detail.type : null;
}
async function executeCurrentNote() {
if (getCurrentNoteType() === 'code') {
// make sure note is saved so we load latest changes
await saveNoteIfChanged();
if (currentNote.detail.mime.endsWith("env=frontend")) {
const bundle = await server.get('script/bundle/' + getCurrentNoteId());
utils.executeBundle(bundle);
}
if (currentNote.detail.mime.endsWith("env=backend")) {
await server.post('script/run/' + getCurrentNoteId());
}
utils.showMessage("Note executed");
}
}
$attachmentDownload.click(() => utils.download(getAttachmentUrl()));
$attachmentOpen.click(() => {
if (utils.isElectron()) {
const open = require("open");
open(getAttachmentUrl());
}
else {
window.location.href = getAttachmentUrl();
}
}); });
function getAttachmentUrl() { // so that tab jumps from note title (which has tabindex 1)
// electron needs absolute URL so we extract current host, port, protocol $noteDetail.attr("tabindex", 2);
return utils.getHost() + "/api/attachments/download/" + getCurrentNoteId() });
+ "?protectedSessionId=" + encodeURIComponent(protected_session.getProtectedSessionId());
}
$(document).ready(() => { $(document).bind('keydown', "ctrl+return", executeCurrentNote);
$noteTitle.on('input', () => {
noteChanged();
const title = $noteTitle.val(); $executeScriptButton.click(executeCurrentNote());
treeService.setNoteTitle(getCurrentNoteId(), title); setInterval(saveNoteIfChanged, 5000);
});
// so that tab jumps from note title (which has tabindex 1) export default {
$noteDetail.attr("tabindex", 2); reload,
}); switchToNote,
saveNoteIfChanged,
$(document).bind('keydown', "ctrl+return", executeCurrentNote); updateNoteFromInputs,
saveNoteToServer,
$executeScriptButton.click(executeCurrentNote()); setNoteBackgroundIfProtected,
loadNote,
setInterval(saveNoteIfChanged, 5000); getCurrentNote,
getCurrentNoteType,
return { getCurrentNoteId,
reload, newNoteCreated,
switchToNote, getEditor,
saveNoteIfChanged, focus,
updateNoteFromInputs, executeCurrentNote,
saveNoteToServer, loadLabelList,
setNoteBackgroundIfProtected, setContent
loadNote, };
getCurrentNote,
getCurrentNoteType,
getCurrentNoteId,
newNoteCreated,
getEditor,
focus,
executeCurrentNote,
loadLabelList,
setContent
};
})();

File diff suppressed because it is too large Load Diff

View File

@ -1,145 +1,147 @@
"use strict"; "use strict";
const noteType = (function() { import treeService from './note_tree.js';
const $executeScriptButton = $("#execute-script-button"); import noteEditor from './note_editor.js';
const noteTypeModel = new NoteTypeModel(); import utils from './utils.js';
function NoteTypeModel() { const $executeScriptButton = $("#execute-script-button");
const self = this; const noteTypeModel = new NoteTypeModel();
this.type = ko.observable('text'); function NoteTypeModel() {
this.mime = ko.observable(''); const self = this;
this.codeMimeTypes = ko.observableArray([ this.type = ko.observable('text');
{ mime: 'text/x-csrc', title: 'C' }, this.mime = ko.observable('');
{ mime: 'text/x-c++src', title: 'C++' },
{ mime: 'text/x-csharp', title: 'C#' },
{ mime: 'text/x-clojure', title: 'Clojure' },
{ mime: 'text/css', title: 'CSS' },
{ mime: 'text/x-dockerfile', title: 'Dockerfile' },
{ mime: 'text/x-erlang', title: 'Erlang' },
{ mime: 'text/x-feature', title: 'Gherkin' },
{ mime: 'text/x-go', title: 'Go' },
{ mime: 'text/x-groovy', title: 'Groovy' },
{ mime: 'text/x-haskell', title: 'Haskell' },
{ mime: 'text/html', title: 'HTML' },
{ mime: 'message/http', title: 'HTTP' },
{ mime: 'text/x-java', title: 'Java' },
{ mime: 'application/javascript;env=frontend', title: 'JavaScript frontend' },
{ mime: 'application/javascript;env=backend', title: 'JavaScript backend' },
{ mime: 'application/json', title: 'JSON' },
{ mime: 'text/x-kotlin', title: 'Kotlin' },
{ mime: 'text/x-lua', title: 'Lua' },
{ mime: 'text/x-markdown', title: 'Markdown' },
{ mime: 'text/x-objectivec', title: 'Objective C' },
{ mime: 'text/x-pascal', title: 'Pascal' },
{ mime: 'text/x-perl', title: 'Perl' },
{ mime: 'text/x-php', title: 'PHP' },
{ mime: 'text/x-python', title: 'Python' },
{ mime: 'text/x-ruby', title: 'Ruby' },
{ mime: 'text/x-rustsrc', title: 'Rust' },
{ mime: 'text/x-scala', title: 'Scala' },
{ mime: 'text/x-sh', title: 'Shell' },
{ mime: 'text/x-sql', title: 'SQL' },
{ mime: 'text/x-swift', title: 'Swift' },
{ mime: 'text/xml', title: 'XML' },
{ mime: 'text/x-yaml', title: 'YAML' }
]);
this.typeString = function() { this.codeMimeTypes = ko.observableArray([
const type = self.type(); { mime: 'text/x-csrc', title: 'C' },
const mime = self.mime(); { mime: 'text/x-c++src', title: 'C++' },
{ mime: 'text/x-csharp', title: 'C#' },
{ mime: 'text/x-clojure', title: 'Clojure' },
{ mime: 'text/css', title: 'CSS' },
{ mime: 'text/x-dockerfile', title: 'Dockerfile' },
{ mime: 'text/x-erlang', title: 'Erlang' },
{ mime: 'text/x-feature', title: 'Gherkin' },
{ mime: 'text/x-go', title: 'Go' },
{ mime: 'text/x-groovy', title: 'Groovy' },
{ mime: 'text/x-haskell', title: 'Haskell' },
{ mime: 'text/html', title: 'HTML' },
{ mime: 'message/http', title: 'HTTP' },
{ mime: 'text/x-java', title: 'Java' },
{ mime: 'application/javascript;env=frontend', title: 'JavaScript frontend' },
{ mime: 'application/javascript;env=backend', title: 'JavaScript backend' },
{ mime: 'application/json', title: 'JSON' },
{ mime: 'text/x-kotlin', title: 'Kotlin' },
{ mime: 'text/x-lua', title: 'Lua' },
{ mime: 'text/x-markdown', title: 'Markdown' },
{ mime: 'text/x-objectivec', title: 'Objective C' },
{ mime: 'text/x-pascal', title: 'Pascal' },
{ mime: 'text/x-perl', title: 'Perl' },
{ mime: 'text/x-php', title: 'PHP' },
{ mime: 'text/x-python', title: 'Python' },
{ mime: 'text/x-ruby', title: 'Ruby' },
{ mime: 'text/x-rustsrc', title: 'Rust' },
{ mime: 'text/x-scala', title: 'Scala' },
{ mime: 'text/x-sh', title: 'Shell' },
{ mime: 'text/x-sql', title: 'SQL' },
{ mime: 'text/x-swift', title: 'Swift' },
{ mime: 'text/xml', title: 'XML' },
{ mime: 'text/x-yaml', title: 'YAML' }
]);
if (type === 'text') { this.typeString = function() {
return 'Text'; const type = self.type();
} const mime = self.mime();
else if (type === 'code') {
if (!mime) {
return 'Code';
}
else {
const found = self.codeMimeTypes().find(x => x.mime === mime);
return found ? found.title : mime; if (type === 'text') {
} return 'Text';
} }
else if (type === 'render') { else if (type === 'code') {
return 'Render HTML note'; if (!mime) {
} return 'Code';
else if (type === 'file') {
return 'Attachment';
}
else if (type === 'search') {
// ignore and do nothing, "type" will be hidden since it's not possible to switch to and from search
} }
else { else {
utils.throwError('Unrecognized type: ' + type); const found = self.codeMimeTypes().find(x => x.mime === mime);
return found ? found.title : mime;
} }
};
this.isDisabled = function() {
return self.type() === "file";
};
async function save() {
const note = noteEditor.getCurrentNote();
await server.put('notes/' + note.detail.noteId
+ '/type/' + encodeURIComponent(self.type())
+ '/mime/' + encodeURIComponent(self.mime()));
await noteEditor.reload();
// for the note icon to be updated in the tree
await treeService.reload();
self.updateExecuteScriptButtonVisibility();
} }
else if (type === 'render') {
this.selectText = function() { return 'Render HTML note';
self.type('text');
self.mime('');
save();
};
this.selectRender = function() {
self.type('render');
self.mime('');
save();
};
this.selectCode = function() {
self.type('code');
self.mime('');
save();
};
this.selectCodeMime = function(el) {
self.type('code');
self.mime(el.mime);
save();
};
this.updateExecuteScriptButtonVisibility = function() {
$executeScriptButton.toggle(self.mime().startsWith('application/javascript'));
} }
} else if (type === 'file') {
return 'Attachment';
ko.applyBindings(noteTypeModel, document.getElementById('note-type')); }
else if (type === 'search') {
return { // ignore and do nothing, "type" will be hidden since it's not possible to switch to and from search
getNoteType: () => noteTypeModel.type(), }
setNoteType: type => noteTypeModel.type(type), else {
utils.throwError('Unrecognized type: ' + type);
getNoteMime: () => noteTypeModel.mime(),
setNoteMime: mime => {
noteTypeModel.mime(mime);
noteTypeModel.updateExecuteScriptButtonVisibility();
} }
}; };
})();
this.isDisabled = function() {
return self.type() === "file";
};
async function save() {
const note = noteEditor.getCurrentNote();
await server.put('notes/' + note.detail.noteId
+ '/type/' + encodeURIComponent(self.type())
+ '/mime/' + encodeURIComponent(self.mime()));
await noteEditor.reload();
// for the note icon to be updated in the tree
await treeService.reload();
self.updateExecuteScriptButtonVisibility();
}
this.selectText = function() {
self.type('text');
self.mime('');
save();
};
this.selectRender = function() {
self.type('render');
self.mime('');
save();
};
this.selectCode = function() {
self.type('code');
self.mime('');
save();
};
this.selectCodeMime = function(el) {
self.type('code');
self.mime(el.mime);
save();
};
this.updateExecuteScriptButtonVisibility = function() {
$executeScriptButton.toggle(self.mime().startsWith('application/javascript'));
}
}
ko.applyBindings(noteTypeModel, document.getElementById('note-type'));
export default {
getNoteType: () => noteTypeModel.type(),
setNoteType: type => noteTypeModel.type(type),
getNoteMime: () => noteTypeModel.mime(),
setNoteMime: mime => {
noteTypeModel.mime(mime);
noteTypeModel.updateExecuteScriptButtonVisibility();
}
};

View File

@ -1,189 +1,192 @@
"use strict"; "use strict";
const protected_session = (function() { import treeService from './note_tree.js';
const $dialog = $("#protected-session-password-dialog"); import noteEditor from './note_editor.js';
const $passwordForm = $("#protected-session-password-form"); import utils from './utils.js';
const $password = $("#protected-session-password"); import server from './server.js';
const $noteDetailWrapper = $("#note-detail-wrapper");
const $protectButton = $("#protect-button");
const $unprotectButton = $("#unprotect-button");
let protectedSessionDeferred = null; const $dialog = $("#protected-session-password-dialog");
let lastProtectedSessionOperationDate = null; const $passwordForm = $("#protected-session-password-form");
let protectedSessionTimeout = null; const $password = $("#protected-session-password");
let protectedSessionId = null; const $noteDetailWrapper = $("#note-detail-wrapper");
const $protectButton = $("#protect-button");
const $unprotectButton = $("#unprotect-button");
$(document).ready(() => { let protectedSessionDeferred = null;
server.get('settings/all').then(settings => protectedSessionTimeout = settings.protected_session_timeout); let lastProtectedSessionOperationDate = null;
}); let protectedSessionTimeout = null;
let protectedSessionId = null;
function setProtectedSessionTimeout(encSessTimeout) { $(document).ready(() => {
protectedSessionTimeout = encSessTimeout; server.get('settings/all').then(settings => protectedSessionTimeout = settings.protected_session_timeout);
} });
function ensureProtectedSession(requireProtectedSession, modal) { function setProtectedSessionTimeout(encSessTimeout) {
const dfd = $.Deferred(); protectedSessionTimeout = encSessTimeout;
}
if (requireProtectedSession && !isProtectedSessionAvailable()) { function ensureProtectedSession(requireProtectedSession, modal) {
protectedSessionDeferred = dfd; const dfd = $.Deferred();
if (treeService.getCurrentNode().data.isProtected) { if (requireProtectedSession && !isProtectedSessionAvailable()) {
$noteDetailWrapper.hide(); protectedSessionDeferred = dfd;
}
$dialog.dialog({ if (treeService.getCurrentNode().data.isProtected) {
modal: modal, $noteDetailWrapper.hide();
width: 400, }
open: () => {
if (!modal) { $dialog.dialog({
// dialog steals focus for itself, which is not what we want for non-modal (viewing) modal: modal,
treeService.getCurrentNode().setFocus(); width: 400,
} open: () => {
if (!modal) {
// dialog steals focus for itself, which is not what we want for non-modal (viewing)
treeService.getCurrentNode().setFocus();
} }
}); }
}
else {
dfd.resolve();
}
return dfd.promise();
}
async function setupProtectedSession() {
const password = $password.val();
$password.val("");
const response = await enterProtectedSession(password);
if (!response.success) {
utils.showError("Wrong password.");
return;
}
protectedSessionId = response.protectedSessionId;
$dialog.dialog("close");
noteEditor.reload();
treeService.reload();
if (protectedSessionDeferred !== null) {
ensureDialogIsClosed($dialog, $password);
$noteDetailWrapper.show();
protectedSessionDeferred.resolve();
protectedSessionDeferred = null;
}
}
function ensureDialogIsClosed() {
// this may fal if the dialog has not been previously opened
try {
$dialog.dialog('close');
}
catch (e) {}
$password.val('');
}
async function enterProtectedSession(password) {
return await server.post('login/protected', {
password: password
}); });
} }
else {
function getProtectedSessionId() { dfd.resolve();
return protectedSessionId;
} }
function resetProtectedSession() { return dfd.promise();
protectedSessionId = null; }
// most secure solution - guarantees nothing remained in memory async function setupProtectedSession() {
// since this expires because user doesn't use the app, it shouldn't be disruptive const password = $password.val();
utils.reloadApp(); $password.val("");
const response = await enterProtectedSession(password);
if (!response.success) {
utils.showError("Wrong password.");
return;
} }
function isProtectedSessionAvailable() { protectedSessionId = response.protectedSessionId;
return protectedSessionId !== null;
$dialog.dialog("close");
noteEditor.reload();
treeService.reload();
if (protectedSessionDeferred !== null) {
ensureDialogIsClosed($dialog, $password);
$noteDetailWrapper.show();
protectedSessionDeferred.resolve();
protectedSessionDeferred = null;
} }
}
async function protectNoteAndSendToServer() { function ensureDialogIsClosed() {
await ensureProtectedSession(true, true); // this may fal if the dialog has not been previously opened
try {
const note = noteEditor.getCurrentNote(); $dialog.dialog('close');
noteEditor.updateNoteFromInputs(note);
note.detail.isProtected = true;
await noteEditor.saveNoteToServer(note);
treeService.setProtected(note.detail.noteId, note.detail.isProtected);
noteEditor.setNoteBackgroundIfProtected(note);
} }
catch (e) {}
async function unprotectNoteAndSendToServer() { $password.val('');
await ensureProtectedSession(true, true); }
const note = noteEditor.getCurrentNote(); async function enterProtectedSession(password) {
return await server.post('login/protected', {
noteEditor.updateNoteFromInputs(note); password: password
note.detail.isProtected = false;
await noteEditor.saveNoteToServer(note);
treeService.setProtected(note.detail.noteId, note.detail.isProtected);
noteEditor.setNoteBackgroundIfProtected(note);
}
function touchProtectedSession() {
if (isProtectedSessionAvailable()) {
lastProtectedSessionOperationDate = new Date();
}
}
async function protectSubTree(noteId, protect) {
await ensureProtectedSession(true, true);
await server.put('notes/' + noteId + "/protect-sub-tree/" + (protect ? 1 : 0));
utils.showMessage("Request to un/protect sub tree has finished successfully");
treeService.reload();
noteEditor.reload();
}
$passwordForm.submit(() => {
setupProtectedSession();
return false;
}); });
}
setInterval(() => { function getProtectedSessionId() {
if (lastProtectedSessionOperationDate !== null && new Date().getTime() - lastProtectedSessionOperationDate.getTime() > protectedSessionTimeout * 1000) { return protectedSessionId;
resetProtectedSession(); }
}
}, 5000);
$protectButton.click(protectNoteAndSendToServer); function resetProtectedSession() {
$unprotectButton.click(unprotectNoteAndSendToServer); protectedSessionId = null;
return { // most secure solution - guarantees nothing remained in memory
setProtectedSessionTimeout, // since this expires because user doesn't use the app, it shouldn't be disruptive
ensureProtectedSession, utils.reloadApp();
resetProtectedSession, }
isProtectedSessionAvailable,
protectNoteAndSendToServer, function isProtectedSessionAvailable() {
unprotectNoteAndSendToServer, return protectedSessionId !== null;
getProtectedSessionId, }
touchProtectedSession,
protectSubTree, async function protectNoteAndSendToServer() {
ensureDialogIsClosed await ensureProtectedSession(true, true);
};
})(); const note = noteEditor.getCurrentNote();
noteEditor.updateNoteFromInputs(note);
note.detail.isProtected = true;
await noteEditor.saveNoteToServer(note);
treeService.setProtected(note.detail.noteId, note.detail.isProtected);
noteEditor.setNoteBackgroundIfProtected(note);
}
async function unprotectNoteAndSendToServer() {
await ensureProtectedSession(true, true);
const note = noteEditor.getCurrentNote();
noteEditor.updateNoteFromInputs(note);
note.detail.isProtected = false;
await noteEditor.saveNoteToServer(note);
treeService.setProtected(note.detail.noteId, note.detail.isProtected);
noteEditor.setNoteBackgroundIfProtected(note);
}
function touchProtectedSession() {
if (isProtectedSessionAvailable()) {
lastProtectedSessionOperationDate = new Date();
}
}
async function protectSubTree(noteId, protect) {
await ensureProtectedSession(true, true);
await server.put('notes/' + noteId + "/protect-sub-tree/" + (protect ? 1 : 0));
utils.showMessage("Request to un/protect sub tree has finished successfully");
treeService.reload();
noteEditor.reload();
}
$passwordForm.submit(() => {
setupProtectedSession();
return false;
});
setInterval(() => {
if (lastProtectedSessionOperationDate !== null && new Date().getTime() - lastProtectedSessionOperationDate.getTime() > protectedSessionTimeout * 1000) {
resetProtectedSession();
}
}, 5000);
$protectButton.click(protectNoteAndSendToServer);
$unprotectButton.click(unprotectNoteAndSendToServer);
export default {
setProtectedSessionTimeout,
ensureProtectedSession,
resetProtectedSession,
isProtectedSessionAvailable,
protectNoteAndSendToServer,
unprotectNoteAndSendToServer,
getProtectedSessionId,
touchProtectedSession,
protectSubTree,
ensureDialogIsClosed
};

View File

@ -1,3 +1,5 @@
import treeService from './note_tree.js';
function ScriptApi(startNote, currentNote) { function ScriptApi(startNote, currentNote) {
const $pluginButtons = $("#plugin-buttons"); const $pluginButtons = $("#plugin-buttons");
@ -52,3 +54,5 @@ function ScriptApi(startNote, currentNote) {
runOnServer runOnServer
} }
} }
export default ScriptApi;

View File

@ -1,3 +1,8 @@
"use strict";
import ScriptApi from './script_api.js';
import utils from './utils.js';
function ScriptContext(startNote, allNotes) { function ScriptContext(startNote, allNotes) {
const modules = {}; const modules = {};
@ -19,3 +24,5 @@ function ScriptContext(startNote, allNotes) {
} }
}; };
} }
export default ScriptContext;

View File

@ -1,5 +1,7 @@
"use strict"; "use strict";
import treeService from './note_tree.js';
const $tree = $("#tree"); const $tree = $("#tree");
const $searchInput = $("input[name='search-text']"); const $searchInput = $("input[name='search-text']");
const $resetSearchButton = $("#reset-search-button"); const $resetSearchButton = $("#reset-search-button");

View File

@ -1,101 +1,104 @@
const server = (function() { "use strict";
function getHeaders() {
let protectedSessionId = null;
try { // this is because protected session might not be declared in some cases - like when it's included in migration page import protected_session from './protected_session.js';
protectedSessionId = protected_session.getProtectedSessionId(); import utils from './utils.js';
}
catch(e) {}
// headers need to be lowercase because node.js automatically converts them to lower case function getHeaders() {
// so hypothetical protectedSessionId becomes protectedsessionid on the backend let protectedSessionId = null;
return {
protected_session_id: protectedSessionId, try { // this is because protected session might not be declared in some cases - like when it's included in migration page
source_id: glob.sourceId protectedSessionId = protected_session.getProtectedSessionId();
};
} }
catch(e) {}
async function get(url) { // headers need to be lowercase because node.js automatically converts them to lower case
return await call('GET', url); // so hypothetical protectedSessionId becomes protectedsessionid on the backend
} return {
protected_session_id: protectedSessionId,
source_id: glob.sourceId
};
}
async function post(url, data) { async function get(url) {
return await call('POST', url, data); return await call('GET', url);
} }
async function put(url, data) { async function post(url, data) {
return await call('PUT', url, data); return await call('POST', url, data);
} }
async function remove(url) { async function put(url, data) {
return await call('DELETE', url); return await call('PUT', url, data);
} }
let i = 1; async function remove(url) {
const reqResolves = {}; return await call('DELETE', url);
}
async function call(method, url, data) { let i = 1;
if (utils.isElectron()) { const reqResolves = {};
const ipc = require('electron').ipcRenderer;
const requestId = i++;
return new Promise((resolve, reject) => {
reqResolves[requestId] = resolve;
console.log(utils.now(), "Request #" + requestId + " to " + method + " " + url);
ipc.send('server-request', {
requestId: requestId,
headers: getHeaders(),
method: method,
url: "/" + baseApiUrl + url,
data: data
});
});
}
else {
return await ajax(url, method, data);
}
}
async function call(method, url, data) {
if (utils.isElectron()) { if (utils.isElectron()) {
const ipc = require('electron').ipcRenderer; const ipc = require('electron').ipcRenderer;
const requestId = i++;
ipc.on('server-response', (event, arg) => { return new Promise((resolve, reject) => {
console.log(utils.now(), "Response #" + arg.requestId + ": " + arg.statusCode); reqResolves[requestId] = resolve;
reqResolves[arg.requestId](arg.body); console.log(utils.now(), "Request #" + requestId + " to " + method + " " + url);
delete reqResolves[arg.requestId]; ipc.send('server-request', {
requestId: requestId,
headers: getHeaders(),
method: method,
url: "/" + baseApiUrl + url,
data: data
});
}); });
} }
else {
return await ajax(url, method, data);
}
}
async function ajax(url, method, data) { if (utils.isElectron()) {
const options = { const ipc = require('electron').ipcRenderer;
url: baseApiUrl + url,
type: method,
headers: getHeaders()
};
if (data) { ipc.on('server-response', (event, arg) => {
options.data = JSON.stringify(data); console.log(utils.now(), "Response #" + arg.requestId + ": " + arg.statusCode);
options.contentType = "application/json";
}
return await $.ajax(options).catch(e => { reqResolves[arg.requestId](arg.body);
const message = "Error when calling " + method + " " + url + ": " + e.status + " - " + e.statusText;
utils.showError(message); delete reqResolves[arg.requestId];
utils.throwError(message); });
}); }
async function ajax(url, method, data) {
const options = {
url: baseApiUrl + url,
type: method,
headers: getHeaders()
};
if (data) {
options.data = JSON.stringify(data);
options.contentType = "application/json";
} }
return { return await $.ajax(options).catch(e => {
get, const message = "Error when calling " + method + " " + url + ": " + e.status + " - " + e.statusText;
post, utils.showError(message);
put, utils.throwError(message);
remove, });
ajax, }
// don't remove, used from CKEditor image upload!
getHeaders export default {
} get,
})(); post,
put,
remove,
ajax,
// don't remove, used from CKEditor image upload!
getHeaders
};

View File

@ -1,31 +1,31 @@
"use strict"; "use strict";
const syncService = (function() { import utils from './utils.js';
async function syncNow() {
const result = await server.post('sync/now');
if (result.success) { async function syncNow() {
utils.showMessage("Sync finished successfully."); const result = await server.post('sync/now');
}
else {
if (result.message.length > 50) {
result.message = result.message.substr(0, 50);
}
utils.showError("Sync failed: " + result.message); if (result.success) {
} utils.showMessage("Sync finished successfully.");
} }
else {
if (result.message.length > 50) {
result.message = result.message.substr(0, 50);
}
$("#sync-now-button").click(syncNow); utils.showError("Sync failed: " + result.message);
async function forceNoteSync(noteId) {
const result = await server.post('sync/force-note-sync/' + noteId);
utils.showMessage("Note added to sync queue.");
} }
}
return { $("#sync-now-button").click(syncNow);
syncNow,
forceNoteSync async function forceNoteSync(noteId) {
}; const result = await server.post('sync/force-note-sync/' + noteId);
})();
utils.showMessage("Note added to sync queue.");
}
export default {
syncNow,
forceNoteSync
};

View File

@ -1,132 +1,133 @@
"use strict"; "use strict";
const treeChanges = (function() { import treeService from './note_tree.js';
async function moveBeforeNode(nodesToMove, beforeNode) { import utils from './utils.js';
for (const nodeToMove of nodesToMove) {
const resp = await server.put('tree/' + nodeToMove.data.branchId + '/move-before/' + beforeNode.data.branchId);
if (!resp.success) { async function moveBeforeNode(nodesToMove, beforeNode) {
alert(resp.message); for (const nodeToMove of nodesToMove) {
return; const resp = await server.put('tree/' + nodeToMove.data.branchId + '/move-before/' + beforeNode.data.branchId);
}
changeNode(nodeToMove, node => node.moveTo(beforeNode, 'before'));
}
}
async function moveAfterNode(nodesToMove, afterNode) {
nodesToMove.reverse(); // need to reverse to keep the note order
for (const nodeToMove of nodesToMove) {
const resp = await server.put('tree/' + nodeToMove.data.branchId + '/move-after/' + afterNode.data.branchId);
if (!resp.success) {
alert(resp.message);
return;
}
changeNode(nodeToMove, node => node.moveTo(afterNode, 'after'));
}
}
async function moveToNode(nodesToMove, toNode) {
for (const nodeToMove of nodesToMove) {
const resp = await server.put('tree/' + nodeToMove.data.branchId + '/move-to/' + toNode.data.noteId);
if (!resp.success) {
alert(resp.message);
return;
}
changeNode(nodeToMove, node => {
// first expand which will force lazy load and only then move the node
// if this is not expanded before moving, then lazy load won't happen because it already contains node
// this doesn't work if this isn't a folder yet, that's why we expand second time below
toNode.setExpanded(true);
node.moveTo(toNode);
toNode.folder = true;
toNode.renderTitle();
// this expands the note in case it become the folder only after the move
toNode.setExpanded(true);
});
}
}
async function deleteNodes(nodes) {
if (nodes.length === 0 || !confirm('Are you sure you want to delete select note(s) and all the sub-notes?')) {
return;
}
for (const node of nodes) {
await server.remove('tree/' + node.data.branchId);
}
// following code assumes that nodes contain only top-most selected nodes - getSelectedNodes has been
// called with stopOnParent=true
let next = nodes[nodes.length - 1].getNextSibling();
if (!next) {
next = nodes[0].getPrevSibling();
}
if (!next && !utils.isTopLevelNode(nodes[0])) {
next = nodes[0].getParent();
}
if (next) {
// activate next element after this one is deleted so we don't lose focus
next.setActive();
treeService.setCurrentNotePathToHash(next);
}
treeService.reload();
utils.showMessage("Note(s) has been deleted.");
}
async function moveNodeUpInHierarchy(node) {
if (utils.isTopLevelNode(node)) {
return;
}
const resp = await server.put('tree/' + node.data.branchId + '/move-after/' + node.getParent().data.branchId);
if (!resp.success) { if (!resp.success) {
alert(resp.message); alert(resp.message);
return; return;
} }
if (!utils.isTopLevelNode(node) && node.getParent().getChildren().length <= 1) { changeNode(nodeToMove, node => node.moveTo(beforeNode, 'before'));
node.getParent().folder = false; }
node.getParent().renderTitle(); }
async function moveAfterNode(nodesToMove, afterNode) {
nodesToMove.reverse(); // need to reverse to keep the note order
for (const nodeToMove of nodesToMove) {
const resp = await server.put('tree/' + nodeToMove.data.branchId + '/move-after/' + afterNode.data.branchId);
if (!resp.success) {
alert(resp.message);
return;
} }
changeNode(node, node => node.moveTo(node.getParent(), 'after')); changeNode(nodeToMove, node => node.moveTo(afterNode, 'after'));
}
}
async function moveToNode(nodesToMove, toNode) {
for (const nodeToMove of nodesToMove) {
const resp = await server.put('tree/' + nodeToMove.data.branchId + '/move-to/' + toNode.data.noteId);
if (!resp.success) {
alert(resp.message);
return;
}
changeNode(nodeToMove, node => {
// first expand which will force lazy load and only then move the node
// if this is not expanded before moving, then lazy load won't happen because it already contains node
// this doesn't work if this isn't a folder yet, that's why we expand second time below
toNode.setExpanded(true);
node.moveTo(toNode);
toNode.folder = true;
toNode.renderTitle();
// this expands the note in case it become the folder only after the move
toNode.setExpanded(true);
});
}
}
async function deleteNodes(nodes) {
if (nodes.length === 0 || !confirm('Are you sure you want to delete select note(s) and all the sub-notes?')) {
return;
} }
function changeNode(node, func) { for (const node of nodes) {
utils.assertArguments(node.data.parentNoteId, node.data.noteId); await server.remove('tree/' + node.data.branchId);
treeService.removeParentChildRelation(node.data.parentNoteId, node.data.noteId);
func(node);
node.data.parentNoteId = utils.isTopLevelNode(node) ? 'root' : node.getParent().data.noteId;
treeService.setParentChildRelation(node.data.branchId, node.data.parentNoteId, node.data.noteId);
treeService.setCurrentNotePathToHash(node);
} }
return { // following code assumes that nodes contain only top-most selected nodes - getSelectedNodes has been
moveBeforeNode, // called with stopOnParent=true
moveAfterNode, let next = nodes[nodes.length - 1].getNextSibling();
moveToNode,
deleteNodes, if (!next) {
moveNodeUpInHierarchy next = nodes[0].getPrevSibling();
}; }
})();
if (!next && !utils.isTopLevelNode(nodes[0])) {
next = nodes[0].getParent();
}
if (next) {
// activate next element after this one is deleted so we don't lose focus
next.setActive();
treeService.setCurrentNotePathToHash(next);
}
treeService.reload();
utils.showMessage("Note(s) has been deleted.");
}
async function moveNodeUpInHierarchy(node) {
if (utils.isTopLevelNode(node)) {
return;
}
const resp = await server.put('tree/' + node.data.branchId + '/move-after/' + node.getParent().data.branchId);
if (!resp.success) {
alert(resp.message);
return;
}
if (!utils.isTopLevelNode(node) && node.getParent().getChildren().length <= 1) {
node.getParent().folder = false;
node.getParent().renderTitle();
}
changeNode(node, node => node.moveTo(node.getParent(), 'after'));
}
function changeNode(node, func) {
utils.assertArguments(node.data.parentNoteId, node.data.noteId);
treeService.removeParentChildRelation(node.data.parentNoteId, node.data.noteId);
func(node);
node.data.parentNoteId = utils.isTopLevelNode(node) ? 'root' : node.getParent().data.noteId;
treeService.setParentChildRelation(node.data.branchId, node.data.parentNoteId, node.data.noteId);
treeService.setCurrentNotePathToHash(node);
}
export default {
moveBeforeNode,
moveAfterNode,
moveToNode,
deleteNodes,
moveNodeUpInHierarchy
};

View File

@ -1,40 +1,40 @@
"use strict"; "use strict";
const treeUtils = (function() { import utils from './utils.js';
const $tree = $("#tree");
function getParentProtectedStatus(node) { const $tree = $("#tree");
return utils.isTopLevelNode(node) ? 0 : node.getParent().data.isProtected;
}
function getNodeByKey(key) { function getParentProtectedStatus(node) {
return $tree.fancytree('getNodeByKey', key); return utils.isTopLevelNode(node) ? 0 : node.getParent().data.isProtected;
} }
function getNoteIdFromNotePath(notePath) { function getNodeByKey(key) {
const path = notePath.split("/"); return $tree.fancytree('getNodeByKey', key);
}
return path[path.length - 1]; function getNoteIdFromNotePath(notePath) {
} const path = notePath.split("/");
function getNotePath(node) { return path[path.length - 1];
const path = []; }
while (node && !utils.isRootNode(node)) { function getNotePath(node) {
if (node.data.noteId) { const path = [];
path.push(node.data.noteId);
}
node = node.getParent(); while (node && !utils.isRootNode(node)) {
if (node.data.noteId) {
path.push(node.data.noteId);
} }
return path.reverse().join("/"); node = node.getParent();
} }
return { return path.reverse().join("/");
getParentProtectedStatus, }
getNodeByKey,
getNotePath, export default {
getNoteIdFromNotePath, getParentProtectedStatus,
}; getNodeByKey,
})(); getNotePath,
getNoteIdFromNotePath,
};

View File

@ -1,270 +1,272 @@
"use strict"; "use strict";
const utils = (function() { import link from './link.js';
function reloadApp() { import messaging from './messaging.js';
window.location.reload(true); import ScriptContext from './script_context.js';
function reloadApp() {
window.location.reload(true);
}
function showMessage(message) {
console.log(now(), "message: ", message);
$.notify({
// options
message: message
}, {
// settings
type: 'success',
delay: 3000
});
}
function showError(message, delay = 10000) {
console.log(now(), "error: ", message);
$.notify({
// options
message: message
}, {
// settings
type: 'danger',
delay: delay
});
}
function throwError(message) {
messaging.logError(message);
throw new Error(message);
}
function parseDate(str) {
try {
return new Date(Date.parse(str));
} }
catch (e) {
function showMessage(message) { throw new Error("Can't parse date from " + str + ": " + e.stack);
console.log(now(), "message: ", message);
$.notify({
// options
message: message
}, {
// settings
type: 'success',
delay: 3000
});
} }
}
function showError(message, delay = 10000) { function padNum(num) {
console.log(now(), "error: ", message); return (num <= 9 ? "0" : "") + num;
}
$.notify({ function formatTime(date) {
// options return padNum(date.getHours()) + ":" + padNum(date.getMinutes());
message: message }
}, {
// settings
type: 'danger',
delay: delay
});
}
function throwError(message) { function formatTimeWithSeconds(date) {
messaging.logError(message); return padNum(date.getHours()) + ":" + padNum(date.getMinutes()) + ":" + padNum(date.getSeconds());
}
throw new Error(message); function formatDate(date) {
} return padNum(date.getDate()) + ". " + padNum(date.getMonth() + 1) + ". " + date.getFullYear();
}
function parseDate(str) { function formatDateISO(date) {
try { return date.getFullYear() + "-" + padNum(date.getMonth() + 1) + "-" + padNum(date.getDate());
return new Date(Date.parse(str)); }
}
catch (e) { function formatDateTime(date) {
throw new Error("Can't parse date from " + str + ": " + e.stack); return formatDate(date) + " " + formatTime(date);
}
function now() {
return formatTimeWithSeconds(new Date());
}
function isElectron() {
return window && window.process && window.process.type;
}
function assertArguments() {
for (const i in arguments) {
if (!arguments[i]) {
throwError(`Argument idx#${i} should not be falsy: ${arguments[i]}`);
} }
} }
}
function padNum(num) { function assert(expr, message) {
return (num <= 9 ? "0" : "") + num; if (!expr) {
throwError(message);
}
}
function isTopLevelNode(node) {
return isRootNode(node.getParent());
}
function isRootNode(node) {
return node.key === "root_1";
}
function escapeHtml(str) {
return $('<div/>').text(str).html();
}
async function stopWatch(what, func) {
const start = new Date();
const ret = await func();
const tookMs = new Date().getTime() - start.getTime();
console.log(`${what} took ${tookMs}ms`);
return ret;
}
async function executeBundle(bundle) {
const apiContext = ScriptContext(bundle.note, bundle.allNotes);
return await (function () {
return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`);
}.call(apiContext));
}
function formatValueWithWhitespace(val) {
return /[^\w_-]/.test(val) ? '"' + val + '"' : val;
}
function formatLabel(attr) {
let str = "@" + formatValueWithWhitespace(attr.name);
if (attr.value !== "") {
str += "=" + formatValueWithWhitespace(attr.value);
} }
function formatTime(date) { return str;
return padNum(date.getHours()) + ":" + padNum(date.getMinutes()); }
const CKEDITOR = {"js": ["libraries/ckeditor/ckeditor.js"]};
const CODE_MIRROR = {
js: [
"libraries/codemirror/codemirror.js",
"libraries/codemirror/addon/mode/loadmode.js",
"libraries/codemirror/addon/fold/xml-fold.js",
"libraries/codemirror/addon/edit/matchbrackets.js",
"libraries/codemirror/addon/edit/matchtags.js",
"libraries/codemirror/addon/search/match-highlighter.js",
"libraries/codemirror/mode/meta.js",
"libraries/codemirror/addon/lint/lint.js",
"libraries/codemirror/addon/lint/eslint.js"
],
css: [
"libraries/codemirror/codemirror.css",
"libraries/codemirror/addon/lint/lint.css"
]
};
const ESLINT = {js: ["libraries/eslint.js"]};
async function requireLibrary(library) {
if (library.css) {
library.css.map(cssUrl => requireCss(cssUrl));
} }
function formatTimeWithSeconds(date) { if (library.js) {
return padNum(date.getHours()) + ":" + padNum(date.getMinutes()) + ":" + padNum(date.getSeconds()); for (const scriptUrl of library.js) {
} await requireScript(scriptUrl);
function formatDate(date) {
return padNum(date.getDate()) + ". " + padNum(date.getMonth() + 1) + ". " + date.getFullYear();
}
function formatDateISO(date) {
return date.getFullYear() + "-" + padNum(date.getMonth() + 1) + "-" + padNum(date.getDate());
}
function formatDateTime(date) {
return formatDate(date) + " " + formatTime(date);
}
function now() {
return formatTimeWithSeconds(new Date());
}
function isElectron() {
return window && window.process && window.process.type;
}
function assertArguments() {
for (const i in arguments) {
if (!arguments[i]) {
throwError(`Argument idx#${i} should not be falsy: ${arguments[i]}`);
}
} }
} }
}
function assert(expr, message) { const dynamicallyLoadedScripts = [];
if (!expr) {
throwError(message); async function requireScript(url) {
} if (!dynamicallyLoadedScripts.includes(url)) {
dynamicallyLoadedScripts.push(url);
return await $.ajax({
url: url,
dataType: "script",
cache: true
})
}
}
async function requireCss(url) {
const css = Array
.from(document.querySelectorAll('link'))
.map(scr => scr.href);
if (!css.includes(url)) {
$('head').append($('<link rel="stylesheet" type="text/css" />').attr('href', url));
}
}
function getHost() {
const url = new URL(window.location.href);
return url.protocol + "//" + url.hostname + ":" + url.port;
}
function download(url) {
if (isElectron()) {
const remote = require('electron').remote;
remote.getCurrentWebContents().downloadURL(url);
}
else {
window.location.href = url;
}
}
function toObject(array, fn) {
const obj = {};
for (const item of array) {
const ret = fn(item);
obj[ret[0]] = ret[1];
} }
function isTopLevelNode(node) { return obj;
return isRootNode(node.getParent()); }
function randomString(len) {
let text = "";
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < len; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
} }
function isRootNode(node) { return text;
return node.key === "root_1"; }
}
function escapeHtml(str) { export default {
return $('<div/>').text(str).html(); reloadApp,
} showMessage,
showError,
async function stopWatch(what, func) { throwError,
const start = new Date(); parseDate,
padNum,
const ret = await func(); formatTime,
formatTimeWithSeconds,
const tookMs = new Date().getTime() - start.getTime(); formatDate,
formatDateISO,
console.log(`${what} took ${tookMs}ms`); formatDateTime,
now,
return ret; isElectron,
} assertArguments,
assert,
async function executeBundle(bundle) { isTopLevelNode,
const apiContext = ScriptContext(bundle.note, bundle.allNotes); isRootNode,
escapeHtml,
return await (function () { stopWatch,
return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`); executeBundle,
}.call(apiContext)); formatValueWithWhitespace,
} formatLabel,
requireLibrary,
function formatValueWithWhitespace(val) { CKEDITOR,
return /[^\w_-]/.test(val) ? '"' + val + '"' : val; CODE_MIRROR,
} ESLINT,
getHost,
function formatLabel(attr) { download,
let str = "@" + formatValueWithWhitespace(attr.name); toObject,
randomString
if (attr.value !== "") { };
str += "=" + formatValueWithWhitespace(attr.value);
}
return str;
}
const CKEDITOR = {"js": ["libraries/ckeditor/ckeditor.js"]};
const CODE_MIRROR = {
js: [
"libraries/codemirror/codemirror.js",
"libraries/codemirror/addon/mode/loadmode.js",
"libraries/codemirror/addon/fold/xml-fold.js",
"libraries/codemirror/addon/edit/matchbrackets.js",
"libraries/codemirror/addon/edit/matchtags.js",
"libraries/codemirror/addon/search/match-highlighter.js",
"libraries/codemirror/mode/meta.js",
"libraries/codemirror/addon/lint/lint.js",
"libraries/codemirror/addon/lint/eslint.js"
],
css: [
"libraries/codemirror/codemirror.css",
"libraries/codemirror/addon/lint/lint.css"
]
};
const ESLINT = {js: ["libraries/eslint.js"]};
async function requireLibrary(library) {
if (library.css) {
library.css.map(cssUrl => requireCss(cssUrl));
}
if (library.js) {
for (const scriptUrl of library.js) {
await requireScript(scriptUrl);
}
}
}
const dynamicallyLoadedScripts = [];
async function requireScript(url) {
if (!dynamicallyLoadedScripts.includes(url)) {
dynamicallyLoadedScripts.push(url);
return await $.ajax({
url: url,
dataType: "script",
cache: true
})
}
}
async function requireCss(url) {
const css = Array
.from(document.querySelectorAll('link'))
.map(scr => scr.href);
if (!css.includes(url)) {
$('head').append($('<link rel="stylesheet" type="text/css" />').attr('href', url));
}
}
function getHost() {
const url = new URL(window.location.href);
return url.protocol + "//" + url.hostname + ":" + url.port;
}
function download(url) {
if (isElectron()) {
const remote = require('electron').remote;
remote.getCurrentWebContents().downloadURL(url);
}
else {
window.location.href = url;
}
}
function toObject(array, fn) {
const obj = {};
for (const item of array) {
const ret = fn(item);
obj[ret[0]] = ret[1];
}
return obj;
}
function randomString(len) {
let text = "";
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < len; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
return {
reloadApp,
showMessage,
showError,
throwError,
parseDate,
padNum,
formatTime,
formatTimeWithSeconds,
formatDate,
formatDateISO,
formatDateTime,
now,
isElectron,
assertArguments,
assert,
isTopLevelNode,
isRootNode,
escapeHtml,
stopWatch,
executeBundle,
formatValueWithWhitespace,
formatLabel,
requireLibrary,
CKEDITOR,
CODE_MIRROR,
ESLINT,
getHost,
download,
toObject,
randomString
};
})();

View File

@ -521,43 +521,6 @@
<script src="/javascripts/bootstrap.js" type="module"></script> <script src="/javascripts/bootstrap.js" type="module"></script>
<script src="/javascripts/utils.js"></script>
<script src="/javascripts/init.js"></script>
<script src="/javascripts/server.js"></script>
<!-- Tree scripts -->
<script src="/javascripts/note_tree.js"></script>
<script src="/javascripts/tree_changes.js"></script>
<script src="/javascripts/cloning.js"></script>
<script src="/javascripts/tree_utils.js"></script>
<script src="/javascripts/drag_and_drop.js"></script>
<script src="/javascripts/context_menu.js"></script>
<script src="/javascripts/export.js"></script>
<!-- Note detail -->
<script src="/javascripts/note_editor.js"></script>
<script src="/javascripts/protected_session.js"></script>
<script src="/javascripts/note_type.js"></script>
<!-- dialogs -->
<script src="/javascripts/dialogs/recent_notes.js"></script>
<script src="/javascripts/dialogs/add_link.js"></script>
<script src="/javascripts/dialogs/jump_to_note.js"></script>
<script src="/javascripts/dialogs/settings.js"></script>
<script src="/javascripts/dialogs/note_history.js"></script>
<script src="/javascripts/dialogs/recent_changes.js"></script>
<script src="/javascripts/dialogs/event_log.js"></script>
<script src="/javascripts/dialogs/edit_tree_prefix.js"></script>
<script src="/javascripts/dialogs/sql_console.js"></script>
<script src="/javascripts/dialogs/note_source.js"></script>
<script src="/javascripts/dialogs/labels.js"></script>
<script src="/javascripts/link.js"></script>
<script src="/javascripts/sync.js"></script>
<script src="/javascripts/messaging.js"></script>
<script src="/javascripts/script_context.js"></script>
<script src="/javascripts/script_api.js"></script>
<script type="text/javascript"> <script type="text/javascript">
// we hide container initally because otherwise it is rendered first without CSS and then flickers into // we hide container initally because otherwise it is rendered first without CSS and then flickers into
// final form which is pretty ugly. // final form which is pretty ugly.