mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
using ES6 modules for whole frontend SPA app
This commit is contained in:
parent
b3c32a39e9
commit
a699210a29
35
src/public/javascripts/bootstrap.js
vendored
35
src/public/javascripts/bootstrap.js
vendored
@ -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 => {
|
||||||
|
@ -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
|
||||||
|
};
|
@ -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
|
|
||||||
}
|
|
||||||
})();
|
|
@ -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
|
||||||
|
};
|
@ -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
|
||||||
|
};
|
@ -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
|
||||||
|
};
|
||||||
|
@ -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
|
||||||
|
};
|
@ -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
|
||||||
|
};
|
@ -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
|
||||||
};
|
};
|
||||||
})();
|
|
@ -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
|
||||||
|
};
|
@ -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
|
||||||
|
};
|
@ -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
|
||||||
|
};
|
@ -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");
|
||||||
|
@ -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
|
||||||
|
};
|
@ -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;
|
@ -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
|
||||||
|
};
|
@ -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);
|
||||||
|
});
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
})();
|
|
@ -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
|
||||||
|
};
|
@ -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> </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> </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
@ -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();
|
||||||
|
}
|
||||||
|
};
|
@ -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
|
||||||
|
};
|
@ -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;
|
@ -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;
|
@ -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");
|
||||||
|
@ -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
|
||||||
|
};
|
@ -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
|
||||||
|
};
|
@ -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
|
||||||
|
};
|
@ -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,
|
||||||
|
};
|
@ -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
|
|
||||||
};
|
|
||||||
})();
|
|
@ -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.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user