Compare commits

...

34 Commits

Author SHA1 Message Date
azivner
31d5ac05ff release 0.8.1 2018-03-01 23:08:53 -05:00
azivner
72d91d1571 don't use eslint on JSON notes, closes #70 2018-03-01 22:42:51 -05:00
azivner
f4b57f4c57 Allow attachments to be included in the scripts, closes #66 2018-03-01 22:30:06 -05:00
azivner
ee0833390a fix export in electron (auth problem) 2018-02-27 09:47:05 -05:00
azivner
2acff07368 release 0.8.0-beta 2018-02-26 22:57:15 -05:00
azivner
bea1d24f07 tweaks to eslint 2018-02-26 22:55:58 -05:00
azivner
adc270c59f removed reference to reddit plugin 2018-02-26 22:31:35 -05:00
azivner
66064f7a94 Script API changes, finished porting reddit plugin, reddit importer tar file 2018-02-26 20:47:34 -05:00
azivner
1501fa8dbf import notes from tar archive, closes #63 2018-02-26 00:07:43 -05:00
azivner
60bba46d80 export subtree to tar file 2018-02-25 10:55:21 -05:00
azivner
12c06ae97e manual transaction handling for jobs 2018-02-24 22:44:45 -05:00
azivner
f0bea9cf71 API changes necessary to port reddit plugin, closes #58 2018-02-24 21:23:04 -05:00
azivner
a555b6319c support for backend jobs and other script API changes 2018-02-24 14:42:52 -05:00
azivner
5dd93e4cdc eslint for javascript inside HTML (htmlmixed mode), closes #62 2018-02-24 00:58:11 -05:00
azivner
3b4509d833 support encryption for files, closes #60 2018-02-23 22:58:24 -05:00
azivner
19308bbfbd small changes to linting and protected session 2018-02-23 20:10:29 -05:00
azivner
4acc5432c3 autocomplete returns items which have at least one of the tokens in the leaf note title, closes #59 2018-02-22 19:52:08 -05:00
azivner
08b8141fdf upgrade to codemirror 5.35 2018-02-21 23:09:52 -05:00
azivner
e1200aa308 lazy loading of eslint only for JS code 2018-02-21 20:30:15 -05:00
azivner
89666eb078 paperclip icon for attachment, closes #61 2018-02-21 19:53:46 -05:00
azivner
d5605aa64d initial support for eslint backed JS linting 2018-02-20 23:24:55 -05:00
azivner
2582b016f9 increased "connection lost" timeout from 5 seconds to 30, it was way to common and mostly false positive 2018-02-20 07:52:39 -05:00
azivner
e8c52e25f0 release 0.7.0-beta 2018-02-19 23:03:30 -05:00
azivner
a149c6a105 lazy / dynamic loading of CKEditor and Code mirror 2018-02-19 22:02:03 -05:00
azivner
131af9ab12 fix attachment sync 2018-02-18 22:55:36 -05:00
azivner
aa2bbc6575 attachment download now works also in electron, added option to open the attachment 2018-02-18 22:19:07 -05:00
azivner
78e8c15786 attachment upload and download now works for browser 2018-02-18 21:28:24 -05:00
azivner
fda4146150 correct handling of inclusion of dependencies 2018-02-18 10:47:02 -05:00
azivner
ddc885066e support passing functions to the backend as parameters 2018-02-18 09:53:36 -05:00
azivner
08bc2afb49 now it's possible to add comment to the weight, closes #54 2018-02-17 11:47:22 -05:00
azivner
1d0220b03d add weight causes updating old chart instead of creating new chart, closes #53 2018-02-17 10:45:00 -05:00
azivner
3033f7cc08 attribute value is now non-null, fixes #52 2018-02-16 19:07:59 -05:00
azivner
6b9ff47c88 Merge branch 'stable' 2018-02-15 23:24:02 -05:00
azivner
cdde6a4d8e file/attachment upload, wiP 2018-02-14 23:31:20 -05:00
58 changed files with 106685 additions and 4331 deletions

View File

@@ -1,3 +1,7 @@
[General]
# Instance name can be used to distinguish between different instances
instanceName=
[Network] [Network]
port=8080 port=8080
# true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure). # true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure).

View File

@@ -0,0 +1,23 @@
UPDATE attributes SET value = '' WHERE value IS NULL;
CREATE TABLE IF NOT EXISTS "attributes_mig"
(
attributeId TEXT PRIMARY KEY NOT NULL,
noteId TEXT NOT NULL,
name TEXT NOT NULL,
value TEXT NOT NULL DEFAULT '',
position INT NOT NULL DEFAULT 0,
dateCreated TEXT NOT NULL,
dateModified TEXT NOT NULL,
isDeleted INT NOT NULL
);
INSERT INTO attributes_mig (attributeId, noteId, name, value, position, dateCreated, dateModified, isDeleted)
SELECT attributeId, noteId, name, value, position, dateCreated, dateModified, isDeleted FROM attributes;
DROP TABLE attributes;
ALTER TABLE attributes_mig RENAME TO attributes;
CREATE INDEX IDX_attributes_noteId ON attributes (noteId);
CREATE INDEX IDX_attributes_name_value ON attributes (name, value);

View File

@@ -15,6 +15,8 @@ require('electron-debug')();
// Prevent window being garbage collected // Prevent window being garbage collected
let mainWindow; let mainWindow;
require('electron-dl')({ saveAs: true });
function onClosed() { function onClosed() {
// Dereference the window // Dereference the window
// For multiple windows store them in an array // For multiple windows store them in an array

72
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "trilium", "name": "trilium",
"version": "0.6.1", "version": "0.7.0-beta",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -3206,6 +3206,16 @@
"electron-localshortcut": "3.1.0" "electron-localshortcut": "3.1.0"
} }
}, },
"electron-dl": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/electron-dl/-/electron-dl-1.11.0.tgz",
"integrity": "sha512-iL9qHzzWOuL9bus+UT+P72SwrDQcFTV6QHqcbhwgqjCC9/K5jhdRzG0dIMB3TzYlk6rmApanPqh9DvWykwIH1Q==",
"requires": {
"ext-name": "5.0.0",
"pupa": "1.0.0",
"unused-filename": "1.0.0"
}
},
"electron-download": { "electron-download": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/electron-download/-/electron-download-3.3.0.tgz", "resolved": "https://registry.npmjs.org/electron-download/-/electron-download-3.3.0.tgz",
@@ -4374,6 +4384,23 @@
} }
} }
}, },
"ext-list": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz",
"integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==",
"requires": {
"mime-db": "1.30.0"
}
},
"ext-name": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz",
"integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==",
"requires": {
"ext-list": "2.2.2",
"sort-keys-length": "1.0.1"
}
},
"extend": { "extend": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
@@ -5922,8 +5949,7 @@
"is-plain-obj": { "is-plain-obj": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
"integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4="
"dev": true
}, },
"is-png": { "is-png": {
"version": "1.1.0", "version": "1.1.0",
@@ -7152,6 +7178,11 @@
} }
} }
}, },
"modify-filename": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/modify-filename/-/modify-filename-1.1.0.tgz",
"integrity": "sha1-mi3sg4Bvuy2XXyK+7IWcoms5OqE="
},
"moment": { "moment": {
"version": "2.20.1", "version": "2.20.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz", "resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz",
@@ -7564,6 +7595,11 @@
"mimic-fn": "1.1.0" "mimic-fn": "1.1.0"
} }
}, },
"open": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/open/-/open-0.0.5.tgz",
"integrity": "sha1-QsPhjslUZra/DcQvOilFw/DK2Pw="
},
"optimist": { "optimist": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
@@ -8391,6 +8427,11 @@
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
}, },
"pupa": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/pupa/-/pupa-1.0.0.tgz",
"integrity": "sha1-mpVopa9+ZXuEYqbp1TKHQ1YM7/Y="
},
"q": { "q": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
@@ -9186,11 +9227,18 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz",
"integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=",
"dev": true,
"requires": { "requires": {
"is-plain-obj": "1.1.0" "is-plain-obj": "1.1.0"
} }
}, },
"sort-keys-length": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz",
"integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=",
"requires": {
"sort-keys": "1.1.2"
}
},
"source-map": { "source-map": {
"version": "0.5.7", "version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -10963,6 +11011,22 @@
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
}, },
"unused-filename": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unused-filename/-/unused-filename-1.0.0.tgz",
"integrity": "sha1-00CID3GuIRXrqhMlvvBcxmhEacY=",
"requires": {
"modify-filename": "1.1.0",
"path-exists": "3.0.0"
},
"dependencies": {
"path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
}
}
},
"unzip-response": { "unzip-response": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz",

View File

@@ -1,7 +1,7 @@
{ {
"name": "trilium", "name": "trilium",
"description": "Trilium Notes", "description": "Trilium Notes",
"version": "0.6.2", "version": "0.8.1",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"main": "electron.js", "main": "electron.js",
"repository": { "repository": {
@@ -29,6 +29,7 @@
"ejs": "~2.5.7", "ejs": "~2.5.7",
"electron": "^1.8.2", "electron": "^1.8.2",
"electron-debug": "^1.5.0", "electron-debug": "^1.5.0",
"electron-dl": "^1.11.0",
"electron-in-page-search": "^1.2.4", "electron-in-page-search": "^1.2.4",
"express": "~4.16.2", "express": "~4.16.2",
"express-promise-wrap": "^0.2.2", "express-promise-wrap": "^0.2.2",
@@ -45,6 +46,7 @@
"jimp": "^0.2.28", "jimp": "^0.2.28",
"moment": "^2.20.1", "moment": "^2.20.1",
"multer": "^1.3.0", "multer": "^1.3.0",
"open": "0.0.5",
"rand-token": "^0.4.0", "rand-token": "^0.4.0",
"request": "^2.83.0", "request": "^2.83.0",
"request-promise": "^4.2.2", "request-promise": "^4.2.2",
@@ -55,6 +57,7 @@
"session-file-store": "^1.1.2", "session-file-store": "^1.1.2",
"simple-node-logger": "^0.93.30", "simple-node-logger": "^0.93.30",
"sqlite": "^2.9.0", "sqlite": "^2.9.0",
"tar-stream": "^1.5.5",
"unescape": "^1.0.1", "unescape": "^1.0.1",
"ws": "^3.3.2" "ws": "^3.3.2"
}, },

View File

@@ -73,8 +73,6 @@ require('./services/backup');
// trigger consistency checks timer // trigger consistency checks timer
require('./services/consistency_checks'); require('./services/consistency_checks');
require('./plugins/reddit');
module.exports = { module.exports = {
app, app,
sessionParser sessionParser

View File

@@ -23,6 +23,11 @@ class Note extends Entity {
return this.type === "code" && this.mime === "application/json"; return this.type === "code" && this.mime === "application/json";
} }
isJavaScript() {
return (this.type === "code" || this.type === "file")
&& (this.mime === "application/javascript" || this.mime === "application/x-javascript");
}
async getAttributes() { async getAttributes() {
return this.repository.getEntities("SELECT * FROM attributes WHERE noteId = ? AND isDeleted = 0", [this.noteId]); return this.repository.getEntities("SELECT * FROM attributes WHERE noteId = ? AND isDeleted = 0", [this.noteId]);
} }
@@ -39,6 +44,48 @@ class Note extends Entity {
return this.repository.getEntities("SELECT * FROM note_tree WHERE isDeleted = 0 AND noteId = ?", [this.noteId]); return this.repository.getEntities("SELECT * FROM note_tree WHERE isDeleted = 0 AND noteId = ?", [this.noteId]);
} }
async getChild(name) {
return this.repository.getEntity(`
SELECT notes.*
FROM note_tree
JOIN notes USING(noteId)
WHERE notes.isDeleted = 0
AND note_tree.isDeleted = 0
AND note_tree.parentNoteId = ?
AND notes.title = ?`, [this.noteId, name]);
}
async getChildren() {
return this.repository.getEntities(`
SELECT notes.*
FROM note_tree
JOIN notes USING(noteId)
WHERE notes.isDeleted = 0
AND note_tree.isDeleted = 0
AND note_tree.parentNoteId = ?`, [this.noteId]);
}
async getParents() {
return this.repository.getEntities(`
SELECT parent_notes.*
FROM
note_tree AS child_tree
JOIN notes AS parent_notes ON parent_notes.noteId = child_tree.parentNoteId
WHERE child_tree.noteId = ?
AND child_tree.isDeleted = 0
AND parent_notes.isDeleted = 0`, [this.noteId]);
}
async getNoteTree() {
return this.repository.getEntities(`
SELECT note_tree.*
FROM note_tree
JOIN notes USING(noteId)
WHERE notes.isDeleted = 0
AND note_tree.isDeleted = 0
AND note_tree.noteId = ?`, [this.noteId]);
}
beforeSaving() { beforeSaving() {
this.content = JSON.stringify(this.jsonContent, null, '\t'); this.content = JSON.stringify(this.jsonContent, null, '\t');

View File

@@ -1,144 +0,0 @@
"use strict";
const sql = require('../services/sql');
const notes = require('../services/notes');
const axios = require('axios');
const log = require('../services/log');
const utils = require('../services/utils');
const unescape = require('unescape');
const attributes = require('../services/attributes');
const sync_mutex = require('../services/sync_mutex');
const config = require('../services/config');
const date_notes = require('../services/date_notes');
// "reddit" date note is subnote of date note which contains all reddit comments from that date
const REDDIT_DATE_ATTRIBUTE = 'reddit_date_note';
async function createNote(parentNoteId, noteTitle, noteText) {
return (await notes.createNewNote(parentNoteId, {
title: noteTitle,
content: noteText,
target: 'into',
isProtected: false
})).noteId;
}
function redditId(kind, id) {
return kind + "_" + id;
}
async function getDateNoteIdForReddit(dateTimeStr, rootNoteId) {
const dateStr = dateTimeStr.substr(0, 10);
let redditDateNoteId = await attributes.getNoteIdWithAttribute(REDDIT_DATE_ATTRIBUTE, dateStr);
if (!redditDateNoteId) {
const dateNoteId = await date_notes.getDateNoteId(dateTimeStr, rootNoteId);
redditDateNoteId = await createNote(dateNoteId, "Reddit");
await attributes.createAttribute(redditDateNoteId, REDDIT_DATE_ATTRIBUTE, dateStr);
await attributes.createAttribute(redditDateNoteId, "hide_in_autocomplete");
}
return redditDateNoteId;
}
async function importComments(rootNoteId, accountName, afterId = null) {
let url = `https://www.reddit.com/user/${accountName}.json`;
if (afterId) {
url += "?after=" + afterId;
}
const response = await axios.get(url);
const listing = response.data;
if (listing.kind !== 'Listing') {
log.info(`Reddit: Unknown object kind ${listing.kind}`);
return;
}
const children = listing.data.children;
let importedComments = 0;
for (const child of children) {
const comment = child.data;
let commentNoteId = await attributes.getNoteIdWithAttribute('reddit_id', redditId(child.kind, comment.id));
if (commentNoteId) {
continue;
}
const dateTimeStr = utils.dateStr(new Date(comment.created_utc * 1000));
const permaLink = 'https://reddit.com' + comment.permalink;
const noteText =
`<p><a href="${permaLink}">${permaLink}</a></p>
<p>author: <a href="https://reddit.com/u/${comment.author}">${comment.author}</a>,
subreddit: <a href="https://reddit.com/r/${comment.subreddit}">${comment.subreddit}</a>,
karma: ${comment.score}, created at ${dateTimeStr}</p><p></p>`
+ unescape(comment.body_html);
let parentNoteId = await getDateNoteIdForReddit(dateTimeStr, rootNoteId);
await sql.doInTransaction(async () => {
commentNoteId = await createNote(parentNoteId, comment.link_title, noteText);
log.info("Reddit: Imported comment to note " + commentNoteId);
importedComments++;
await attributes.createAttribute(commentNoteId, "reddit_kind", child.kind);
await attributes.createAttribute(commentNoteId, "reddit_id", redditId(child.kind, comment.id));
await attributes.createAttribute(commentNoteId, "reddit_created_utc", comment.created_utc);
});
}
// if there have been no imported comments on this page, there shouldn't be any to import
// on the next page since those are older
if (listing.data.after && importedComments > 0) {
importedComments += await importComments(rootNoteId, accountName, listing.data.after);
}
return importedComments;
}
let redditAccounts = [];
async function runImport() {
const rootNoteId = await date_notes.getRootNoteId();
// technically mutex shouldn't be necessary but we want to avoid doing potentially expensive import
// concurrently with sync
await sync_mutex.doExclusively(async () => {
let importedComments = 0;
for (const account of redditAccounts) {
importedComments += await importComments(rootNoteId, account);
}
log.info(`Reddit: Imported ${importedComments} comments.`);
});
}
sql.dbReady.then(async () => {
if (!config['Reddit'] || config['Reddit']['enabled'] !== true) {
return;
}
const redditAccountsStr = config['Reddit']['accounts'];
if (!redditAccountsStr) {
log.info("Reddit: No reddit accounts defined in option 'reddit_accounts'");
}
redditAccounts = redditAccountsStr.split(",").map(s => s.trim());
const pollingIntervalInSeconds = config['Reddit']['pollingIntervalInSeconds'] || (4 * 3600);
setInterval(runImport, pollingIntervalInSeconds * 1000);
setTimeout(runImport, 10000); // 10 seconds after startup - intentionally after initial sync
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 B

View File

@@ -1,5 +1,5 @@
const api = (function() { const api = (function() {
const pluginButtonsEl = $("#plugin-buttons"); const $pluginButtons = $("#plugin-buttons");
async function activateNote(notePath) { async function activateNote(notePath) {
await noteTree.activateNode(notePath); await noteTree.activateNode(notePath);
@@ -10,12 +10,12 @@ const api = (function() {
button.attr('id', buttonId); button.attr('id', buttonId);
pluginButtonsEl.append(button); $pluginButtons.append(button);
} }
return { return {
addButtonToToolbar, addButtonToToolbar,
activateNote activateNote,
getInstanceName: noteTree.getInstanceName
} }
})(); })();

View File

@@ -1,7 +1,7 @@
"use strict"; "use strict";
const contextMenu = (function() { const contextMenu = (function() {
const treeEl = $("#tree"); const $tree = $("#tree");
let clipboardIds = []; let clipboardIds = [];
let clipboardMode = null; let clipboardMode = null;
@@ -85,16 +85,19 @@ const contextMenu = (function() {
{title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"}, {title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"},
{title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"}, {title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"},
{title: "----"}, {title: "----"},
{title: "Collapse sub-tree <kbd>Alt+-</kbd>", cmd: "collapse-sub-tree", uiIcon: "ui-icon-minus"}, {title: "Export sub-tree", cmd: "exportSubTree", uiIcon: " ui-icon-arrowthick-1-ne"},
{title: "Force note sync", cmd: "force-note-sync", uiIcon: "ui-icon-refresh"}, {title: "Import sub-tree into", cmd: "importSubTree", uiIcon: "ui-icon-arrowthick-1-sw"},
{title: "Sort alphabetically <kbd>Alt+S</kbd>", cmd: "sort-alphabetically", uiIcon: " ui-icon-arrowthick-2-n-s"} {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) => { beforeOpen: (event, ui) => {
const node = $.ui.fancytree.getNode(ui.target); const node = $.ui.fancytree.getNode(ui.target);
// Modify menu entries depending on node status // Modify menu entries depending on node status
treeEl.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0); $tree.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0);
treeEl.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0); $tree.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0);
// Activate node on right-click // Activate node on right-click
node.setActive(); node.setActive();
@@ -139,13 +142,19 @@ const contextMenu = (function() {
else if (ui.cmd === "delete") { else if (ui.cmd === "delete") {
treeChanges.deleteNodes(noteTree.getSelectedNodes(true)); treeChanges.deleteNodes(noteTree.getSelectedNodes(true));
} }
else if (ui.cmd === "collapse-sub-tree") { else if (ui.cmd === "exportSubTree") {
exportSubTree(node.data.noteId);
}
else if (ui.cmd === "importSubTree") {
importSubTree(node.data.noteId);
}
else if (ui.cmd === "collapseSubTree") {
noteTree.collapseTree(node); noteTree.collapseTree(node);
} }
else if (ui.cmd === "force-note-sync") { else if (ui.cmd === "forceNoteSync") {
forceNoteSync(node.data.noteId); forceNoteSync(node.data.noteId);
} }
else if (ui.cmd === "sort-alphabetically") { else if (ui.cmd === "sortAlphabetically") {
noteTree.sortAlphabetically(node.data.noteId); noteTree.sortAlphabetically(node.data.noteId);
} }
else { else {

View File

@@ -17,26 +17,34 @@ const sqlConsole = (function() {
width: $(window).width(), width: $(window).width(),
height: $(window).height(), height: $(window).height(),
open: function() { open: function() {
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess"; initEditor();
CodeMirror.keyMap.default["Tab"] = "indentMore";
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 requireLibrary(CODE_MIRROR);
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
CodeMirror.keyMap.default["Tab"] = "indentMore";
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 execute() { async function execute() {
const sqlQuery = codeEditor.getValue(); const sqlQuery = codeEditor.getValue();

View File

@@ -0,0 +1,32 @@
"use strict";
function exportSubTree(noteId) {
const url = getHost() + "/api/export/" + noteId + "?protectedSessionId="
+ encodeURIComponent(protected_session.getProtectedSessionId());
download(url);
}
let importNoteId;
function importSubTree(noteId) {
importNoteId = noteId;
$("#import-upload").trigger('click');
}
$("#import-upload").change(async function() {
const formData = new FormData();
formData.append('upload', this.files[0]);
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
});
await noteTree.reload();
});

View File

@@ -114,22 +114,32 @@ $.ui.autocomplete.filter = (array, terms) => {
const tokens = terms.toLowerCase().split(" "); const tokens = terms.toLowerCase().split(" ");
for (const item of array) { for (const item of array) {
let found = true;
const lcLabel = item.label.toLowerCase(); const lcLabel = item.label.toLowerCase();
for (const token of tokens) { const found = tokens.every(token => lcLabel.indexOf(token) !== -1);
if (lcLabel.indexOf(token) === -1) { if (!found) {
found = false; continue;
break; }
// 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;
} }
} }
if (found) { results.push(item);
results.push(item);
if (results.length > 100) { if (results.length > 100) {
break; break;
}
} }
} }
@@ -214,3 +224,25 @@ if (isElectron()) {
}, 500); }, 500);
}); });
} }
function uploadAttachment() {
$("#attachment-upload").trigger('click');
}
$("#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 noteTree.reload();
await noteTree.activateNode(resp.noteId);
});

View File

@@ -41,11 +41,11 @@ const link = (function() {
function goToLink(e) { function goToLink(e) {
e.preventDefault(); e.preventDefault();
const linkEl = $(e.target); const $link = $(e.target);
let notePath = linkEl.attr("note-path"); let notePath = $link.attr("note-path");
if (!notePath) { if (!notePath) {
const address = linkEl.attr("note-path") ? linkEl.attr("note-path") : linkEl.attr('href'); const address = $link.attr("note-path") ? $link.attr("note-path") : $link.attr('href');
if (!address) { if (!address) {
return; return;

View File

@@ -1,7 +1,7 @@
"use strict"; "use strict";
const messaging = (function() { const messaging = (function() {
const changesToPushCountEl = $("#changes-to-push-count"); const $changesToPushCount = $("#changes-to-push-count");
function logError(message) { function logError(message) {
console.log(now(), message); // needs to be separate from .trace() console.log(now(), message); // needs to be separate from .trace()
@@ -52,7 +52,7 @@ const messaging = (function() {
// we don't detect image changes here since images themselves are immutable and references should be // we don't detect image changes here since images themselves are immutable and references should be
// updated in note detail as well // updated in note detail as well
changesToPushCountEl.html(message.changesToPushCount); $changesToPushCount.html(message.changesToPushCount);
} }
else if (message.type === 'sync-hash-check-failed') { else if (message.type === 'sync-hash-check-failed') {
showError("Sync check failed!", 60000); showError("Sync check failed!", 60000);
@@ -84,7 +84,7 @@ const messaging = (function() {
let connectionBrokenNotification = null; let connectionBrokenNotification = null;
setInterval(async () => { setInterval(async () => {
if (new Date().getTime() - lastPingTs > 5000) { if (new Date().getTime() - lastPingTs > 30000) {
if (!connectionBrokenNotification) { if (!connectionBrokenNotification) {
connectionBrokenNotification = $.notify({ connectionBrokenNotification = $.notify({
// options // options

View File

@@ -1,16 +1,24 @@
"use strict"; "use strict";
const noteEditor = (function() { const noteEditor = (function() {
const noteTitleEl = $("#note-title"); const $noteTitle = $("#note-title");
const noteDetailEl = $('#note-detail');
const noteDetailCodeEl = $('#note-detail-code'); const $noteDetail = $('#note-detail');
const noteDetailRenderEl = $('#note-detail-render'); const $noteDetailCode = $('#note-detail-code');
const protectButton = $("#protect-button"); const $noteDetailRender = $('#note-detail-render');
const unprotectButton = $("#unprotect-button"); const $noteDetailAttachment = $('#note-detail-attachment');
const noteDetailWrapperEl = $("#note-detail-wrapper");
const noteIdDisplayEl = $("#note-id-display"); const $protectButton = $("#protect-button");
const attributeListEl = $("#attribute-list"); const $unprotectButton = $("#unprotect-button");
const attributeListInnerEl = $("#attribute-list-inner"); const $noteDetailWrapper = $("#note-detail-wrapper");
const $noteIdDisplay = $("#note-id-display");
const $attributeList = $("#attribute-list");
const $attributeListInner = $("#attribute-list-inner");
const $attachmentFileName = $("#attachment-filename");
const $attachmentFileType = $("#attachment-filetype");
const $attachmentFileSize = $("#attachment-filesize");
const $attachmentDownload = $("#attachment-download");
const $attachmentOpen = $("#attachment-open");
let editor = null; let editor = null;
let codeEditor = null; let codeEditor = null;
@@ -80,14 +88,14 @@ const noteEditor = (function() {
else if (note.detail.type === 'code') { else if (note.detail.type === 'code') {
note.detail.content = codeEditor.getValue(); note.detail.content = codeEditor.getValue();
} }
else if (note.detail.type === 'render') { else if (note.detail.type === 'render' || note.detail.type === 'file') {
// nothing // nothing
} }
else { else {
throwError("Unrecognized type: " + note.detail.type); throwError("Unrecognized type: " + note.detail.type);
} }
const title = noteTitleEl.val(); const title = $noteTitle.val();
note.detail.title = title; note.detail.title = title;
@@ -105,9 +113,9 @@ const noteEditor = (function() {
function setNoteBackgroundIfProtected(note) { function setNoteBackgroundIfProtected(note) {
const isProtected = !!note.detail.isProtected; const isProtected = !!note.detail.isProtected;
noteDetailWrapperEl.toggleClass("protected", isProtected); $noteDetailWrapper.toggleClass("protected", isProtected);
protectButton.toggle(!isProtected); $protectButton.toggle(!isProtected);
unprotectButton.toggle(isProtected); $unprotectButton.toggle(isProtected);
} }
let isNewNoteCreated = false; let isNewNoteCreated = false;
@@ -116,19 +124,46 @@ const noteEditor = (function() {
isNewNoteCreated = true; isNewNoteCreated = true;
} }
function setContent(content) { async function setContent(content) {
if (currentNote.detail.type === 'text') { if (currentNote.detail.type === 'text') {
if (!editor) {
await requireLibrary(CKEDITOR);
editor = await BalloonEditor.create($noteDetail[0], {});
editor.document.on('change', noteChanged);
}
// temporary workaround for https://github.com/ckeditor/ckeditor5-enter/issues/49 // temporary workaround for https://github.com/ckeditor/ckeditor5-enter/issues/49
editor.setData(content ? content : "<p></p>"); editor.setData(content ? content : "<p></p>");
noteDetailEl.show(); $noteDetail.show();
noteDetailCodeEl.hide();
noteDetailRenderEl.html('').hide();
} }
else if (currentNote.detail.type === 'code') { else if (currentNote.detail.type === 'code') {
noteDetailEl.hide(); if (!codeEditor) {
noteDetailCodeEl.show(); await requireLibrary(CODE_MIRROR);
noteDetailRenderEl.html('').hide();
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 // this needs to happen after the element is shown, otherwise the editor won't be refresheds
codeEditor.setValue(content); codeEditor.setValue(content);
@@ -139,6 +174,8 @@ const noteEditor = (function() {
codeEditor.setOption("mode", info.mime); codeEditor.setOption("mode", info.mime);
CodeMirror.autoLoadMode(codeEditor, info.mode); CodeMirror.autoLoadMode(codeEditor, info.mode);
} }
codeEditor.refresh();
} }
} }
@@ -148,10 +185,10 @@ const noteEditor = (function() {
if (isNewNoteCreated) { if (isNewNoteCreated) {
isNewNoteCreated = false; isNewNoteCreated = false;
noteTitleEl.focus().select(); $noteTitle.focus().select();
} }
noteIdDisplayEl.html(noteId); $noteIdDisplay.html(noteId);
await protected_session.ensureProtectedSession(currentNote.detail.isProtected, false); await protected_session.ensureProtectedSession(currentNote.detail.isProtected, false);
@@ -163,26 +200,36 @@ const noteEditor = (function() {
// to login, but we chose instead to come to another node - at that point the dialog is still visible and this will close it. // 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(); protected_session.ensureDialogIsClosed();
noteDetailWrapperEl.show(); $noteDetailWrapper.show();
noteChangeDisabled = true; noteChangeDisabled = true;
noteTitleEl.val(currentNote.detail.title); $noteTitle.val(currentNote.detail.title);
noteType.setNoteType(currentNote.detail.type); noteType.setNoteType(currentNote.detail.type);
noteType.setNoteMime(currentNote.detail.mime); noteType.setNoteMime(currentNote.detail.mime);
$noteDetail.hide();
$noteDetailCode.hide();
$noteDetailRender.html('').hide();
$noteDetailAttachment.hide();
if (currentNote.detail.type === 'render') { if (currentNote.detail.type === 'render') {
noteDetailEl.hide(); $noteDetailRender.show();
noteDetailCodeEl.hide();
noteDetailRenderEl.html('').show();
const subTree = await server.get('script/subtree/' + getCurrentNoteId()); const subTree = await server.get('script/subtree/' + getCurrentNoteId());
noteDetailRenderEl.html(subTree); $noteDetailRender.html(subTree);
}
else if (currentNote.detail.type === 'file') {
$noteDetailAttachment.show();
$attachmentFileName.text(currentNote.attributes.original_file_name);
$attachmentFileSize.text(currentNote.attributes.file_size + " bytes");
$attachmentFileType.text(currentNote.detail.mime);
} }
else { else {
setContent(currentNote.detail.content); await setContent(currentNote.detail.content);
} }
noteChangeDisabled = false; noteChangeDisabled = false;
@@ -191,7 +238,7 @@ const noteEditor = (function() {
noteTree.setNoteTreeBackgroundBasedOnProtectedStatus(noteId); noteTree.setNoteTreeBackgroundBasedOnProtectedStatus(noteId);
// after loading new note make sure editor is scrolled to the top // after loading new note make sure editor is scrolled to the top
noteDetailWrapperEl.scrollTop(0); $noteDetailWrapper.scrollTop(0);
loadAttributeList(); loadAttributeList();
} }
@@ -201,17 +248,17 @@ const noteEditor = (function() {
const attributes = await server.get('notes/' + noteId + '/attributes'); const attributes = await server.get('notes/' + noteId + '/attributes');
attributeListInnerEl.html(''); $attributeListInner.html('');
if (attributes.length > 0) { if (attributes.length > 0) {
for (const attr of attributes) { for (const attr of attributes) {
attributeListInnerEl.append(formatAttribute(attr) + " "); $attributeListInner.append(formatAttribute(attr) + " ");
} }
attributeListEl.show(); $attributeList.show();
} }
else { else {
attributeListEl.hide(); $attributeList.hide();
} }
} }
@@ -227,12 +274,12 @@ const noteEditor = (function() {
const note = getCurrentNote(); const note = getCurrentNote();
if (note.detail.type === 'text') { if (note.detail.type === 'text') {
noteDetailEl.focus(); $noteDetail.focus();
} }
else if (note.detail.type === 'code') { else if (note.detail.type === 'code') {
codeEditor.focus(); codeEditor.focus();
} }
else if (note.detail.type === 'render') { else if (note.detail.type === 'render' || note.detail.type === 'file') {
// do nothing // do nothing
} }
else { else {
@@ -257,45 +304,36 @@ const noteEditor = (function() {
} }
} }
$attachmentDownload.click(() => download(getAttachmentUrl()));
$attachmentOpen.click(() => {
if (isElectron()) {
const open = require("open");
open(getAttachmentUrl());
}
else {
window.location.href = getAttachmentUrl();
}
});
function getAttachmentUrl() {
// electron needs absolute URL so we extract current host, port, protocol
return getHost() + "/api/attachments/download/" + getCurrentNoteId()
+ "?protectedSessionId=" + encodeURIComponent(protected_session.getProtectedSessionId());
}
$(document).ready(() => { $(document).ready(() => {
noteTitleEl.on('input', () => { $noteTitle.on('input', () => {
noteChanged(); noteChanged();
const title = noteTitleEl.val(); const title = $noteTitle.val();
noteTree.setNoteTitle(getCurrentNoteId(), title); noteTree.setNoteTitle(getCurrentNoteId(), title);
}); });
BalloonEditor
.create(document.querySelector('#note-detail'), {
})
.then(edit => {
editor = edit;
editor.document.on('change', noteChanged);
})
.catch(error => {
console.error(error);
});
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 }
});
codeEditor.on('change', noteChanged);
// so that tab jumps from note title (which has tabindex 1) // so that tab jumps from note title (which has tabindex 1)
noteDetailEl.attr("tabindex", 2); $noteDetail.attr("tabindex", 2);
}); });
$(document).bind('keydown', "ctrl+return", executeCurrentNote); $(document).bind('keydown', "ctrl+return", executeCurrentNote);

View File

@@ -1,9 +1,11 @@
"use strict"; "use strict";
const noteTree = (function() { const noteTree = (function() {
const treeEl = $("#tree"); const $tree = $("#tree");
const parentListEl = $("#parent-list"); const $parentList = $("#parent-list");
const parentListListEl = $("#parent-list-inner"); const $parentListList = $("#parent-list-inner");
let instanceName = null; // should have better place
let startNotePath = null; let startNotePath = null;
let notesTreeMap = {}; let notesTreeMap = {};
@@ -52,7 +54,7 @@ const noteTree = (function() {
// note that if you want to access data like noteId or isProtected, you need to go into "data" property // note that if you want to access data like noteId or isProtected, you need to go into "data" property
function getCurrentNode() { function getCurrentNode() {
return treeEl.fancytree("getActiveNode"); return $tree.fancytree("getActiveNode");
} }
function getCurrentNotePath() { function getCurrentNotePath() {
@@ -155,6 +157,9 @@ const noteTree = (function() {
if (note.type === 'code') { if (note.type === 'code') {
extraClasses.push("code"); extraClasses.push("code");
} }
else if (note.type === 'file') {
extraClasses.push('attachment');
}
return extraClasses.join(" "); return extraClasses.join(" ");
} }
@@ -314,11 +319,11 @@ const noteTree = (function() {
} }
if (parents.length <= 1) { if (parents.length <= 1) {
parentListEl.hide(); $parentList.hide();
} }
else { else {
parentListEl.show(); $parentList.show();
parentListListEl.empty(); $parentListList.empty();
for (const parentNoteId of parents) { for (const parentNoteId of parents) {
const parentNotePath = getSomeNotePath(parentNoteId); const parentNotePath = getSomeNotePath(parentNoteId);
@@ -335,7 +340,7 @@ const noteTree = (function() {
item = link.createNoteLink(notePath, title); item = link.createNoteLink(notePath, title);
} }
parentListListEl.append($("<li/>").append(item)); $parentListList.append($("<li/>").append(item));
} }
} }
} }
@@ -543,7 +548,7 @@ const noteTree = (function() {
} }
}; };
treeEl.fancytree({ $tree.fancytree({
autoScroll: true, autoScroll: true,
keyboard: false, // we takover keyboard handling in the hotkeys plugin keyboard: false, // we takover keyboard handling in the hotkeys plugin
extensions: ["hotkeys", "filter", "dnd", "clones"], extensions: ["hotkeys", "filter", "dnd", "clones"],
@@ -624,11 +629,11 @@ const noteTree = (function() {
} }
}); });
treeEl.contextmenu(contextMenu.contextMenuSettings); $tree.contextmenu(contextMenu.contextMenuSettings);
} }
function getTree() { function getTree() {
return treeEl.fancytree('getTree'); return $tree.fancytree('getTree');
} }
async function reload() { async function reload() {
@@ -645,6 +650,7 @@ const noteTree = (function() {
async function loadTree() { async function loadTree() {
const resp = await server.get('tree'); const resp = await server.get('tree');
startNotePath = resp.start_note_path; startNotePath = resp.start_note_path;
instanceName = resp.instanceName;
if (document.location.hash) { if (document.location.hash) {
startNotePath = getNotePathFromAddress(); startNotePath = getNotePathFromAddress();
@@ -663,7 +669,7 @@ const noteTree = (function() {
function collapseTree(node = null) { function collapseTree(node = null) {
if (!node) { if (!node) {
node = treeEl.fancytree("getRootNode"); node = $tree.fancytree("getRootNode");
} }
node.setExpanded(false); node.setExpanded(false);
@@ -710,6 +716,9 @@ const noteTree = (function() {
titlePath = ''; titlePath = '';
} }
// https://github.com/zadam/trilium/issues/46
// unfortunately not easy to implement because we don't have an easy access to note's isProtected property
const autocompleteItems = []; const autocompleteItems = [];
for (const childNoteId of parentToChildren[parentNoteId]) { for (const childNoteId of parentToChildren[parentNoteId]) {
@@ -744,7 +753,7 @@ const noteTree = (function() {
} }
async function createNewTopLevelNote() { async function createNewTopLevelNote() {
const rootNode = treeEl.fancytree("getRootNode"); const rootNode = $tree.fancytree("getRootNode");
await createNote(rootNode, "root", "into"); await createNote(rootNode, "root", "into");
} }
@@ -820,6 +829,10 @@ const noteTree = (function() {
return !!childToParents[noteId]; return !!childToParents[noteId];
} }
function getInstanceName() {
return instanceName;
}
$(document).bind('keydown', 'ctrl+o', e => { $(document).bind('keydown', 'ctrl+o', e => {
const node = getCurrentNode(); const node = getCurrentNode();
const parentNoteId = node.data.parentNoteId; const parentNoteId = node.data.parentNoteId;
@@ -894,6 +907,7 @@ const noteTree = (function() {
setParentChildRelation, setParentChildRelation,
getSelectedNodes, getSelectedNodes,
sortAlphabetically, sortAlphabetically,
noteExists noteExists,
getInstanceName
}; };
})(); })();

View File

@@ -1,7 +1,7 @@
"use strict"; "use strict";
const noteType = (function() { const noteType = (function() {
const executeScriptButton = $("#execute-script-button"); const $executeScriptButton = $("#execute-script-button");
const noteTypeModel = new NoteTypeModel(); const noteTypeModel = new NoteTypeModel();
function NoteTypeModel() { function NoteTypeModel() {
@@ -65,11 +65,18 @@ const noteType = (function() {
else if (type === 'render') { else if (type === 'render') {
return 'Render HTML note'; return 'Render HTML note';
} }
else if (type === 'file') {
return 'Attachment';
}
else { else {
throwError('Unrecognized type: ' + type); throwError('Unrecognized type: ' + type);
} }
}; };
this.isDisabled = function() {
return self.type() === "file";
};
async function save() { async function save() {
const note = noteEditor.getCurrentNote(); const note = noteEditor.getCurrentNote();
@@ -114,7 +121,7 @@ const noteType = (function() {
}; };
this.updateExecuteScriptButtonVisibility = function() { this.updateExecuteScriptButtonVisibility = function() {
executeScriptButton.toggle(self.mime() === 'application/javascript'); $executeScriptButton.toggle(self.mime() === 'application/javascript');
} }
} }

View File

@@ -1,10 +1,10 @@
"use strict"; "use strict";
const protected_session = (function() { const protected_session = (function() {
const dialogEl = $("#protected-session-password-dialog"); const $dialog = $("#protected-session-password-dialog");
const passwordFormEl = $("#protected-session-password-form"); const $passwordForm = $("#protected-session-password-form");
const passwordEl = $("#protected-session-password"); const $password = $("#protected-session-password");
const noteDetailWrapperEl = $("#note-detail-wrapper"); const $noteDetailWrapper = $("#note-detail-wrapper");
let protectedSessionDeferred = null; let protectedSessionDeferred = null;
let lastProtectedSessionOperationDate = null; let lastProtectedSessionOperationDate = null;
@@ -25,9 +25,11 @@ const protected_session = (function() {
if (requireProtectedSession && !isProtectedSessionAvailable()) { if (requireProtectedSession && !isProtectedSessionAvailable()) {
protectedSessionDeferred = dfd; protectedSessionDeferred = dfd;
noteDetailWrapperEl.hide(); if (noteTree.getCurrentNode().data.isProtected) {
$noteDetailWrapper.hide();
}
dialogEl.dialog({ $dialog.dialog({
modal: modal, modal: modal,
width: 400, width: 400,
open: () => { open: () => {
@@ -46,8 +48,8 @@ const protected_session = (function() {
} }
async function setupProtectedSession() { async function setupProtectedSession() {
const password = passwordEl.val(); const password = $password.val();
passwordEl.val(""); $password.val("");
const response = await enterProtectedSession(password); const response = await enterProtectedSession(password);
@@ -58,15 +60,15 @@ const protected_session = (function() {
protectedSessionId = response.protectedSessionId; protectedSessionId = response.protectedSessionId;
dialogEl.dialog("close"); $dialog.dialog("close");
noteEditor.reload(); noteEditor.reload();
noteTree.reload(); noteTree.reload();
if (protectedSessionDeferred !== null) { if (protectedSessionDeferred !== null) {
ensureDialogIsClosed(dialogEl, passwordEl); ensureDialogIsClosed($dialog, $password);
noteDetailWrapperEl.show(); $noteDetailWrapper.show();
protectedSessionDeferred.resolve(); protectedSessionDeferred.resolve();
@@ -77,11 +79,11 @@ const protected_session = (function() {
function ensureDialogIsClosed() { function ensureDialogIsClosed() {
// this may fal if the dialog has not been previously opened // this may fal if the dialog has not been previously opened
try { try {
dialogEl.dialog('close'); $dialog.dialog('close');
} }
catch (e) {} catch (e) {}
passwordEl.val(''); $password.val('');
} }
async function enterProtectedSession(password) { async function enterProtectedSession(password) {
@@ -155,7 +157,7 @@ const protected_session = (function() {
noteEditor.reload(); noteEditor.reload();
} }
passwordFormEl.submit(() => { $passwordForm.submit(() => {
setupProtectedSession(); setupProtectedSession();
return false; return false;

View File

@@ -1,40 +1,40 @@
"use strict"; "use strict";
const searchTree = (function() { const searchTree = (function() {
const treeEl = $("#tree"); const $tree = $("#tree");
const searchInputEl = $("input[name='search-text']"); const $searchInput = $("input[name='search-text']");
const resetSearchButton = $("button#reset-search-button"); const $resetSearchButton = $("button#reset-search-button");
const searchBoxEl = $("#search-box"); const $searchBox = $("#search-box");
resetSearchButton.click(resetSearch); $resetSearchButton.click(resetSearch);
function toggleSearch() { function toggleSearch() {
if (searchBoxEl.is(":hidden")) { if ($searchBox.is(":hidden")) {
searchBoxEl.show(); $searchBox.show();
searchInputEl.focus(); $searchInput.focus();
} }
else { else {
resetSearch(); resetSearch();
searchBoxEl.hide(); $searchBox.hide();
} }
} }
function resetSearch() { function resetSearch() {
searchInputEl.val(""); $searchInput.val("");
getTree().clearFilter(); getTree().clearFilter();
} }
function getTree() { function getTree() {
return treeEl.fancytree('getTree'); return $tree.fancytree('getTree');
} }
searchInputEl.keyup(async e => { $searchInput.keyup(async e => {
const searchText = searchInputEl.val(); const searchText = $searchInput.val();
if (e && e.which === $.ui.keyCode.ESCAPE || $.trim(searchText) === "") { if (e && e.which === $.ui.keyCode.ESCAPE || $.trim(searchText) === "") {
resetSearchButton.click(); $resetSearchButton.click();
return; return;
} }

View File

@@ -31,16 +31,38 @@ const server = (function() {
return await call('DELETE', url); return await call('DELETE', url);
} }
function prepareParams(params) {
if (!params) {
return params;
}
return params.map(p => {
if (typeof p === "function") {
return "!@#Function: " + p.toString();
}
else {
return p;
}
});
}
async function exec(params, script) { async function exec(params, script) {
if (typeof script === "function") { if (typeof script === "function") {
script = script.toString(); script = script.toString();
} }
const ret = await post('script/exec', { script: script, params: params }); const ret = await post('script/exec', { script: script, params: prepareParams(params) });
return ret.executionResult; return ret.executionResult;
} }
async function setJob(opts) {
opts.job = opts.job.toString();
opts.params = prepareParams(opts.params);
await post('script/job', opts);
}
let i = 1; let i = 1;
const reqResolves = {}; const reqResolves = {};
@@ -105,6 +127,8 @@ const server = (function() {
put, put,
remove, remove,
exec, exec,
setJob,
ajax,
// don't remove, used from CKEditor image upload! // don't remove, used from CKEditor image upload!
getHeaders getHeaders
} }

View File

@@ -1,14 +1,14 @@
"use strict"; "use strict";
const treeUtils = (function() { const treeUtils = (function() {
const treeEl = $("#tree"); const $tree = $("#tree");
function getParentProtectedStatus(node) { function getParentProtectedStatus(node) {
return isTopLevelNode(node) ? 0 : node.getParent().data.isProtected; return isTopLevelNode(node) ? 0 : node.getParent().data.isProtected;
} }
function getNodeByKey(key) { function getNodeByKey(key) {
return treeEl.fancytree('getNodeByKey', key); return $tree.fancytree('getNodeByKey', key);
} }
function getNoteIdFromNotePath(notePath) { function getNoteIdFromNotePath(notePath) {

View File

@@ -116,8 +116,7 @@ async function stopWatch(what, func) {
} }
function executeScript(script) { function executeScript(script) {
// last \r\n is necessary if script contains line comment on its last line eval(script);
eval("(async function() {" + script + "\r\n})()");
} }
function formatValueWithWhitespace(val) { function formatValueWithWhitespace(val) {
@@ -133,3 +132,77 @@ function formatAttribute(attr) {
return str; 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;
}
}

View File

@@ -102,18 +102,23 @@
} }
} }
var currentlyHighlighted = null;
function doMatchBrackets(cm) { function doMatchBrackets(cm) {
cm.operation(function() { cm.operation(function() {
if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;} if (cm.state.matchBrackets.currentlyHighlighted) {
currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets); cm.state.matchBrackets.currentlyHighlighted();
cm.state.matchBrackets.currentlyHighlighted = null;
}
cm.state.matchBrackets.currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets);
}); });
} }
CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) { CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) {
if (old && old != CodeMirror.Init) { if (old && old != CodeMirror.Init) {
cm.off("cursorActivity", doMatchBrackets); cm.off("cursorActivity", doMatchBrackets);
if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;} if (cm.state.matchBrackets && cm.state.matchBrackets.currentlyHighlighted) {
cm.state.matchBrackets.currentlyHighlighted();
cm.state.matchBrackets.currentlyHighlighted = null;
}
} }
if (val) { if (val) {
cm.state.matchBrackets = typeof val == "object" ? val : {}; cm.state.matchBrackets = typeof val == "object" ? val : {};

View File

@@ -138,7 +138,7 @@
var iter = new Iter(cm, start.line, 0); var iter = new Iter(cm, start.line, 0);
for (;;) { for (;;) {
var openTag = toNextTag(iter), end; var openTag = toNextTag(iter), end;
if (!openTag || iter.line != start.line || !(end = toTagEnd(iter))) return; if (!openTag || !(end = toTagEnd(iter)) || iter.line != start.line) return;
if (!openTag[1] && end != "selfClose") { if (!openTag[1] && end != "selfClose") {
var startPos = Pos(iter.line, iter.ch); var startPos = Pos(iter.line, iter.ch);
var endPos = findMatchingClose(iter, openTag[2]); var endPos = findMatchingClose(iter, openTag[2]);

View File

@@ -0,0 +1,92 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
async function validatorHtml(text, options) {
const result = /<script[^>]*>([\s\S]+)<\/script>/ig.exec(text);
if (result !== null) {
// preceding code is copied over but any (non-newline) character is replaced with space
// this will preserve line numbers etc.
const prefix = text.substr(0, result.index).replace(/./g, " ");
const js = prefix + result[1];
return await validatorJavaScript(js, options);
}
return [];
}
async function validatorJavaScript(text, options) {
if (noteEditor.getCurrentNote().detail.mime === 'application/json') {
// eslint doesn't seem to validate pure JSON well
return [];
}
await requireLibrary(ESLINT);
if (text.length > 20000) {
console.log("Skipping linting because of large size: ", text.length);
return [];
}
const errors = new eslint().verify(text, {
root: true,
parserOptions: {
ecmaVersion: 2017
},
extends: ['eslint:recommended', 'airbnb-base'],
env: {
'node': true
},
rules: {
'import/no-unresolved': 'off',
'func-names': 'off',
'comma-dangle': ['warn'],
'padded-blocks': 'off',
'linebreak-style': 'off',
'class-methods-use-this': 'off',
'no-unused-vars': ['warn', { vars: 'local', args: 'after-used' }],
'no-nested-ternary': 'off',
'no-underscore-dangle': ['error', {'allow': ['_super', '_lookupFactory']}]
}
});
const result = [];
if (errors) {
parseErrors(errors, result);
}
return result;
}
CodeMirror.registerHelper("lint", "javascript", validatorJavaScript);
CodeMirror.registerHelper("lint", "html", validatorHtml);
function parseErrors(errors, output) {
for (const error of errors) {
const startLine = error.line - 1;
const endLine = error.endLine !== undefined ? error.endLine - 1 : startLine;
const startCol = error.column - 1;
const endCol = error.endColumn !== undefined ? error.endColumn - 1 : startCol + 1;
output.push({
message: error.message,
severity: error.severity === 1 ? "warning" : "error",
from: CodeMirror.Pos(startLine, startCol),
to: CodeMirror.Pos(endLine, endCol)
});
}
}
});

View File

@@ -0,0 +1,73 @@
/* The lint marker gutter */
.CodeMirror-lint-markers {
width: 16px;
}
.CodeMirror-lint-tooltip {
background-color: #ffd;
border: 1px solid black;
border-radius: 4px 4px 4px 4px;
color: black;
font-family: monospace;
font-size: 10pt;
overflow: hidden;
padding: 2px 5px;
position: fixed;
white-space: pre;
white-space: pre-wrap;
z-index: 100;
max-width: 600px;
opacity: 0;
transition: opacity .4s;
-moz-transition: opacity .4s;
-webkit-transition: opacity .4s;
-o-transition: opacity .4s;
-ms-transition: opacity .4s;
}
.CodeMirror-lint-mark-error, .CodeMirror-lint-mark-warning {
background-position: left bottom;
background-repeat: repeat-x;
}
.CodeMirror-lint-mark-error {
background-image:
url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==")
;
}
.CodeMirror-lint-mark-warning {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII=");
}
.CodeMirror-lint-marker-error, .CodeMirror-lint-marker-warning {
background-position: center center;
background-repeat: no-repeat;
cursor: pointer;
display: inline-block;
height: 16px;
width: 16px;
vertical-align: middle;
position: relative;
}
.CodeMirror-lint-message-error, .CodeMirror-lint-message-warning {
padding-left: 18px;
background-position: top left;
background-repeat: no-repeat;
}
.CodeMirror-lint-marker-error, .CodeMirror-lint-message-error {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII=");
}
.CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII=");
}
.CodeMirror-lint-marker-multiple {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC");
background-repeat: no-repeat;
background-position: right bottom;
width: 100%; height: 100%;
}

View File

@@ -0,0 +1,252 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
var GUTTER_ID = "CodeMirror-lint-markers";
function showTooltip(e, content) {
var tt = document.createElement("div");
tt.className = "CodeMirror-lint-tooltip";
tt.appendChild(content.cloneNode(true));
document.body.appendChild(tt);
function position(e) {
if (!tt.parentNode) return CodeMirror.off(document, "mousemove", position);
tt.style.top = Math.max(0, e.clientY - tt.offsetHeight - 5) + "px";
tt.style.left = (e.clientX + 5) + "px";
}
CodeMirror.on(document, "mousemove", position);
position(e);
if (tt.style.opacity != null) tt.style.opacity = 1;
return tt;
}
function rm(elt) {
if (elt.parentNode) elt.parentNode.removeChild(elt);
}
function hideTooltip(tt) {
if (!tt.parentNode) return;
if (tt.style.opacity == null) rm(tt);
tt.style.opacity = 0;
setTimeout(function() { rm(tt); }, 600);
}
function showTooltipFor(e, content, node) {
var tooltip = showTooltip(e, content);
function hide() {
CodeMirror.off(node, "mouseout", hide);
if (tooltip) { hideTooltip(tooltip); tooltip = null; }
}
var poll = setInterval(function() {
if (tooltip) for (var n = node;; n = n.parentNode) {
if (n && n.nodeType == 11) n = n.host;
if (n == document.body) return;
if (!n) { hide(); break; }
}
if (!tooltip) return clearInterval(poll);
}, 400);
CodeMirror.on(node, "mouseout", hide);
}
function LintState(cm, options, hasGutter) {
this.marked = [];
this.options = options;
this.timeout = null;
this.hasGutter = hasGutter;
this.onMouseOver = function(e) { onMouseOver(cm, e); };
this.waitingFor = 0
}
function parseOptions(_cm, options) {
if (options instanceof Function) return {getAnnotations: options};
if (!options || options === true) options = {};
return options;
}
function clearMarks(cm) {
var state = cm.state.lint;
if (state.hasGutter) cm.clearGutter(GUTTER_ID);
for (var i = 0; i < state.marked.length; ++i)
state.marked[i].clear();
state.marked.length = 0;
}
function makeMarker(labels, severity, multiple, tooltips) {
var marker = document.createElement("div"), inner = marker;
marker.className = "CodeMirror-lint-marker-" + severity;
if (multiple) {
inner = marker.appendChild(document.createElement("div"));
inner.className = "CodeMirror-lint-marker-multiple";
}
if (tooltips != false) CodeMirror.on(inner, "mouseover", function(e) {
showTooltipFor(e, labels, inner);
});
return marker;
}
function getMaxSeverity(a, b) {
if (a == "error") return a;
else return b;
}
function groupByLine(annotations) {
var lines = [];
for (var i = 0; i < annotations.length; ++i) {
var ann = annotations[i], line = ann.from.line;
(lines[line] || (lines[line] = [])).push(ann);
}
return lines;
}
function annotationTooltip(ann) {
var severity = ann.severity;
if (!severity) severity = "error";
var tip = document.createElement("div");
tip.className = "CodeMirror-lint-message-" + severity;
if (typeof ann.messageHTML != 'undefined') {
tip.innerHTML = ann.messageHTML;
} else {
tip.appendChild(document.createTextNode(ann.message));
}
return tip;
}
function lintAsync(cm, getAnnotations, passOptions) {
var state = cm.state.lint
var id = ++state.waitingFor
function abort() {
id = -1
cm.off("change", abort)
}
cm.on("change", abort)
getAnnotations(cm.getValue(), function(annotations, arg2) {
cm.off("change", abort)
if (state.waitingFor != id) return
if (arg2 && annotations instanceof CodeMirror) annotations = arg2
cm.operation(function() {updateLinting(cm, annotations)})
}, passOptions, cm);
}
function startLinting(cm) {
var state = cm.state.lint, options = state.options;
/*
* Passing rules in `options` property prevents JSHint (and other linters) from complaining
* about unrecognized rules like `onUpdateLinting`, `delay`, `lintOnChange`, etc.
*/
var passOptions = options.options || options;
var getAnnotations = options.getAnnotations || cm.getHelper(CodeMirror.Pos(0, 0), "lint");
if (!getAnnotations) return;
if (options.async || getAnnotations.async) {
lintAsync(cm, getAnnotations, passOptions)
} else {
var annotations = getAnnotations(cm.getValue(), passOptions, cm);
if (!annotations) return;
if (annotations.then) annotations.then(function(issues) {
cm.operation(function() {updateLinting(cm, issues)})
});
else cm.operation(function() {updateLinting(cm, annotations)})
}
}
function updateLinting(cm, annotationsNotSorted) {
clearMarks(cm);
var state = cm.state.lint, options = state.options;
var annotations = groupByLine(annotationsNotSorted);
for (var line = 0; line < annotations.length; ++line) {
var anns = annotations[line];
if (!anns) continue;
var maxSeverity = null;
var tipLabel = state.hasGutter && document.createDocumentFragment();
for (var i = 0; i < anns.length; ++i) {
var ann = anns[i];
var severity = ann.severity;
if (!severity) severity = "error";
maxSeverity = getMaxSeverity(maxSeverity, severity);
if (options.formatAnnotation) ann = options.formatAnnotation(ann);
if (state.hasGutter) tipLabel.appendChild(annotationTooltip(ann));
if (ann.to) state.marked.push(cm.markText(ann.from, ann.to, {
className: "CodeMirror-lint-mark-" + severity,
__annotation: ann
}));
}
if (state.hasGutter)
cm.setGutterMarker(line, GUTTER_ID, makeMarker(tipLabel, maxSeverity, anns.length > 1,
state.options.tooltips));
}
if (options.onUpdateLinting) options.onUpdateLinting(annotationsNotSorted, annotations, cm);
}
function onChange(cm) {
var state = cm.state.lint;
if (!state) return;
clearTimeout(state.timeout);
state.timeout = setTimeout(function(){startLinting(cm);}, state.options.delay || 500);
}
function popupTooltips(annotations, e) {
var target = e.target || e.srcElement;
var tooltip = document.createDocumentFragment();
for (var i = 0; i < annotations.length; i++) {
var ann = annotations[i];
tooltip.appendChild(annotationTooltip(ann));
}
showTooltipFor(e, tooltip, target);
}
function onMouseOver(cm, e) {
var target = e.target || e.srcElement;
if (!/\bCodeMirror-lint-mark-/.test(target.className)) return;
var box = target.getBoundingClientRect(), x = (box.left + box.right) / 2, y = (box.top + box.bottom) / 2;
var spans = cm.findMarksAt(cm.coordsChar({left: x, top: y}, "client"));
var annotations = [];
for (var i = 0; i < spans.length; ++i) {
var ann = spans[i].__annotation;
if (ann) annotations.push(ann);
}
if (annotations.length) popupTooltips(annotations, e);
}
CodeMirror.defineOption("lint", false, function(cm, val, old) {
if (old && old != CodeMirror.Init) {
clearMarks(cm);
if (cm.state.lint.options.lintOnChange !== false)
cm.off("change", onChange);
CodeMirror.off(cm.getWrapperElement(), "mouseover", cm.state.lint.onMouseOver);
clearTimeout(cm.state.lint.timeout);
delete cm.state.lint;
}
if (val) {
var gutters = cm.getOption("gutters"), hasLintGutter = false;
for (var i = 0; i < gutters.length; ++i) if (gutters[i] == GUTTER_ID) hasLintGutter = true;
var state = cm.state.lint = new LintState(cm, parseOptions(cm, val), hasLintGutter);
if (state.options.lintOnChange !== false)
cm.on("change", onChange);
if (state.options.tooltips != false && state.options.tooltips != "gutter")
CodeMirror.on(cm.getWrapperElement(), "mouseover", state.onMouseOver);
startLinting(cm);
}
});
CodeMirror.defineExtension("performLint", function() {
if (this.state.lint) startLinting(this);
});
});

View File

@@ -90,7 +90,7 @@
var state = cm.state.matchHighlighter; var state = cm.state.matchHighlighter;
cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style)); cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style));
if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) { if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) {
var searchFor = hasBoundary ? new RegExp("\\b" + query + "\\b") : query; var searchFor = hasBoundary ? new RegExp("\\b" + query.replace(/[\\\[+*?(){|^$]/g, "\\$&") + "\\b") : query;
state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false, state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false,
{className: "CodeMirror-selection-highlight-scrollbar"}); {className: "CodeMirror-selection-highlight-scrollbar"});
} }

File diff suppressed because it is too large Load Diff

101349
src/public/libraries/eslint.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -72,6 +72,11 @@ span.fancytree-node.fancytree-folder.code > span.fancytree-icon {
background-image: url("../images/icons/code-folder.png"); background-image: url("../images/icons/code-folder.png");
} }
span.fancytree-node.attachment > span.fancytree-icon {
background-position: 0 0;
background-image: url("../images/icons/paperclip.png");
}
span.fancytree-node.protected > span.fancytree-icon { span.fancytree-node.protected > span.fancytree-icon {
filter: drop-shadow(2px 2px 2px black); filter: drop-shadow(2px 2px 2px black);
} }
@@ -269,3 +274,8 @@ div.ui-tooltip {
padding: 2px; padding: 2px;
margin-right: 5px; margin-right: 5px;
} }
#attachment-table th, #attachment-table td {
padding: 10px;
font-size: large;
}

View File

@@ -0,0 +1,74 @@
"use strict";
const express = require('express');
const router = express.Router();
const sql = require('../../services/sql');
const auth = require('../../services/auth');
const notes = require('../../services/notes');
const attributes = require('../../services/attributes');
const protected_session = require('../../services/protected_session');
const multer = require('multer')();
const wrap = require('express-promise-wrap').wrap;
router.post('/upload/:parentNoteId', auth.checkApiAuthOrElectron, multer.single('upload'), wrap(async (req, res, next) => {
const sourceId = req.headers.source_id;
const parentNoteId = req.params.parentNoteId;
const file = req.file;
const originalName = file.originalname;
const size = file.size;
const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [parentNoteId]);
if (!note) {
return res.status(404).send(`Note ${parentNoteId} doesn't exist.`);
}
await sql.doInTransaction(async () => {
const noteId = (await notes.createNewNote(parentNoteId, {
title: originalName,
content: file.buffer,
target: 'into',
isProtected: false,
type: 'file',
mime: file.mimetype
}, req, sourceId)).noteId;
await attributes.createAttribute(noteId, "original_file_name", originalName, sourceId);
await attributes.createAttribute(noteId, "file_size", size, sourceId);
res.send({
noteId: noteId
});
});
}));
router.get('/download/:noteId', auth.checkApiAuthOrElectron, wrap(async (req, res, next) => {
const noteId = req.params.noteId;
const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
const protectedSessionId = req.query.protectedSessionId;
if (!note) {
return res.status(404).send(`Note ${noteId} doesn't exist.`);
}
if (note.isProtected) {
const dataKey = protected_session.getDataKeyForProtectedSessionId(protectedSessionId);
if (!dataKey) {
res.status(401).send("Protected session not available");
return;
}
protected_session.decryptNote(dataKey, note);
}
const attributeMap = await attributes.getNoteAttributeMap(noteId);
const fileName = attributeMap.original_file_name ? attributeMap.original_file_name : note.title;
res.setHeader('Content-Disposition', 'attachment; filename=' + fileName);
res.setHeader('Content-Type', note.mime);
res.send(note.content);
}));
module.exports = router;

View File

@@ -2,56 +2,72 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const rimraf = require('rimraf');
const fs = require('fs');
const sql = require('../../services/sql'); const sql = require('../../services/sql');
const data_dir = require('../../services/data_dir'); const attributes = require('../../services/attributes');
const html = require('html'); const html = require('html');
const auth = require('../../services/auth'); const auth = require('../../services/auth');
const wrap = require('express-promise-wrap').wrap; const wrap = require('express-promise-wrap').wrap;
const tar = require('tar-stream');
const sanitize = require("sanitize-filename");
router.get('/:noteId/to/:directory', auth.checkApiAuth, wrap(async (req, res, next) => { router.get('/:noteId/', auth.checkApiAuthOrElectron, wrap(async (req, res, next) => {
const noteId = req.params.noteId; const noteId = req.params.noteId;
const directory = req.params.directory.replace(/[^0-9a-zA-Z_-]/gi, '');
if (!fs.existsSync(data_dir.EXPORT_DIR)) {
fs.mkdirSync(data_dir.EXPORT_DIR);
}
const completeExportDir = data_dir.EXPORT_DIR + '/' + directory;
if (fs.existsSync(completeExportDir)) {
rimraf.sync(completeExportDir);
}
fs.mkdirSync(completeExportDir);
const noteTreeId = await sql.getValue('SELECT noteTreeId FROM note_tree WHERE noteId = ?', [noteId]); const noteTreeId = await sql.getValue('SELECT noteTreeId FROM note_tree WHERE noteId = ?', [noteId]);
await exportNote(noteTreeId, completeExportDir); const pack = tar.pack();
res.send({}); const name = await exportNote(noteTreeId, '', pack);
pack.finalize();
res.setHeader('Content-Disposition', 'attachment; filename="' + name + '.tar"');
res.setHeader('Content-Type', 'application/tar');
pack.pipe(res);
})); }));
async function exportNote(noteTreeId, dir) { async function exportNote(noteTreeId, directory, pack) {
const noteTree = await sql.getRow("SELECT * FROM note_tree WHERE noteTreeId = ?", [noteTreeId]); const noteTree = await sql.getRow("SELECT * FROM note_tree WHERE noteTreeId = ?", [noteTreeId]);
const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteTree.noteId]); const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteTree.noteId]);
const pos = (noteTree.notePosition + '').padStart(4, '0'); if (note.isProtected) {
return;
}
fs.writeFileSync(dir + '/' + pos + '-' + note.title + '.html', html.prettyPrint(note.content, {indent_size: 2})); const metadata = await getMetadata(note);
if ('exclude_from_export' in metadata.attributes) {
return;
}
const metadataJson = JSON.stringify(metadata, null, '\t');
const childFileName = directory + sanitize(note.title);
pack.entry({ name: childFileName + ".meta", size: metadataJson.length }, metadataJson);
const content = note.type === 'text' ? html.prettyPrint(note.content, {indent_size: 2}) : note.content;
pack.entry({ name: childFileName + ".dat", size: content.length }, content);
const children = await sql.getRows("SELECT * FROM note_tree WHERE parentNoteId = ? AND isDeleted = 0", [note.noteId]); const children = await sql.getRows("SELECT * FROM note_tree WHERE parentNoteId = ? AND isDeleted = 0", [note.noteId]);
if (children.length > 0) { if (children.length > 0) {
const childrenDir = dir + '/' + pos + '-' + note.title;
fs.mkdirSync(childrenDir);
for (const child of children) { for (const child of children) {
await exportNote(child.noteTreeId, childrenDir); await exportNote(child.noteTreeId, childFileName + "/", pack);
} }
} }
return childFileName;
}
async function getMetadata(note) {
return {
title: note.title,
type: note.type,
mime: note.mime,
attributes: await attributes.getNoteAttributeMap(note.noteId)
};
} }
module.exports = router; module.exports = router;

View File

@@ -2,104 +2,128 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const fs = require('fs');
const sql = require('../../services/sql'); const sql = require('../../services/sql');
const data_dir = require('../../services/data_dir');
const utils = require('../../services/utils');
const sync_table = require('../../services/sync_table');
const auth = require('../../services/auth'); const auth = require('../../services/auth');
const notes = require('../../services/notes');
const wrap = require('express-promise-wrap').wrap; const wrap = require('express-promise-wrap').wrap;
const tar = require('tar-stream');
const multer = require('multer')();
const stream = require('stream');
const path = require('path');
router.get('/:directory/to/:parentNoteId', auth.checkApiAuth, wrap(async (req, res, next) => { function getFileName(name) {
const directory = req.params.directory.replace(/[^0-9a-zA-Z_-]/gi, ''); let key;
if (name.endsWith(".dat")) {
key = "data";
name = name.substr(0, name.length - 4);
}
else if (name.endsWith((".meta"))) {
key = "meta";
name = name.substr(0, name.length - 5);
}
else {
throw new Error("Unknown file type in import archive: " + name);
}
return {name, key};
}
async function parseImportFile(file) {
const fileMap = {};
const files = [];
const extract = tar.extract();
extract.on('entry', function(header, stream, next) {
let {name, key} = getFileName(header.name);
let file = fileMap[name];
if (!file) {
file = fileMap[name] = {
children: []
};
let parentFileName = path.dirname(header.name);
if (parentFileName && parentFileName !== '.') {
fileMap[parentFileName].children.push(file);
}
else {
files.push(file);
}
}
const chunks = [];
stream.on("data", function (chunk) {
chunks.push(chunk);
});
// header is the tar header
// stream is the content body (might be an empty stream)
// call next when you are done with this entry
stream.on('end', function() {
file[key] = Buffer.concat(chunks);
if (key === "meta") {
file[key] = JSON.parse(file[key].toString("UTF-8"));
}
next(); // ready for next entry
});
stream.resume(); // just auto drain the stream
});
return new Promise(resolve => {
extract.on('finish', function() {
resolve(files);
});
const bufferStream = new stream.PassThrough();
bufferStream.end(file.buffer);
bufferStream.pipe(extract);
});
}
router.post('/:parentNoteId', auth.checkApiAuthOrElectron, multer.single('upload'), wrap(async (req, res, next) => {
const sourceId = req.headers.source_id;
const parentNoteId = req.params.parentNoteId; const parentNoteId = req.params.parentNoteId;
const file = req.file;
const dir = data_dir.EXPORT_DIR + '/' + directory; const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [parentNoteId]);
await sql.doInTransaction(async () => await importNotes(dir, parentNoteId)); if (!note) {
return res.status(404).send(`Note ${parentNoteId} doesn't exist.`);
}
const files = await parseImportFile(file);
await sql.doInTransaction(async () => {
await importNotes(files, parentNoteId, sourceId);
});
res.send({}); res.send({});
})); }));
async function importNotes(dir, parentNoteId) { async function importNotes(files, parentNoteId, sourceId) {
const parent = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [parentNoteId]); for (const file of files) {
if (file.meta.type !== 'file') {
if (!parent) { file.data = file.data.toString("UTF-8");
return;
}
const fileList = fs.readdirSync(dir);
for (const file of fileList) {
const path = dir + '/' + file;
if (fs.lstatSync(path).isDirectory()) {
continue;
} }
if (!file.endsWith('.html')) { const noteId = await notes.createNote(parentNoteId, file.meta.title, file.data, {
continue; type: file.meta.type,
} mime: file.meta.mime,
attributes: file.meta.attributes,
const fileNameWithoutExt = file.substr(0, file.length - 5); sourceId: sourceId
let noteTitle;
let notePos;
const match = fileNameWithoutExt.match(/^([0-9]{4})-(.*)$/);
if (match) {
notePos = parseInt(match[1]);
noteTitle = match[2];
}
else {
let maxPos = await sql.getValue("SELECT MAX(notePosition) FROM note_tree WHERE parentNoteId = ? AND isDeleted = 0", [parentNoteId]);
if (maxPos) {
notePos = maxPos + 1;
}
else {
notePos = 0;
}
noteTitle = fileNameWithoutExt;
}
const noteText = fs.readFileSync(path, "utf8");
const noteId = utils.newNoteId();
const noteTreeId = utils.newNoteRevisionId();
const now = utils.nowDate();
await sql.insert('note_tree', {
noteTreeId: noteTreeId,
noteId: noteId,
parentNoteId: parentNoteId,
notePosition: notePos,
isExpanded: 0,
isDeleted: 0,
dateModified: now
}); });
await sync_table.addNoteTreeSync(noteTreeId); if (file.children.length > 0) {
await importNotes(file.children, noteId, sourceId);
await sql.insert('notes', {
noteId: noteId,
title: noteTitle,
content: noteText,
isDeleted: 0,
isProtected: 0,
type: 'text',
mime: 'text/html',
dateCreated: now,
dateModified: now
});
await sync_table.addNoteSync(noteId);
const noteDir = dir + '/' + fileNameWithoutExt;
if (fs.existsSync(noteDir) && fs.lstatSync(noteDir).isDirectory()) {
await importNotes(noteDir, noteId);
} }
} }
} }

View File

@@ -5,6 +5,7 @@ const router = express.Router();
const auth = require('../../services/auth'); const auth = require('../../services/auth');
const sql = require('../../services/sql'); const sql = require('../../services/sql');
const notes = require('../../services/notes'); const notes = require('../../services/notes');
const attributes = require('../../services/attributes');
const log = require('../../services/log'); const log = require('../../services/log');
const utils = require('../../services/utils'); const utils = require('../../services/utils');
const protected_session = require('../../services/protected_session'); const protected_session = require('../../services/protected_session');
@@ -25,8 +26,19 @@ router.get('/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
protected_session.decryptNote(req, detail); protected_session.decryptNote(req, detail);
let attributeMap = null;
if (detail.type === 'file') {
// no need to transfer attachment payload for this request
detail.content = null;
// attributes contain important attachment metadata - filename and size
attributeMap = await attributes.getNoteAttributeMap(noteId);
}
res.send({ res.send({
detail: detail detail: detail,
attributes: attributeMap
}); });
})); }));

View File

@@ -17,6 +17,12 @@ router.post('/exec', auth.checkApiAuth, wrap(async (req, res, next) => {
}); });
})); }));
router.post('/job', auth.checkApiAuth, wrap(async (req, res, next) => {
await script.setJob(req.body);
res.send({});
}));
router.get('/startup', auth.checkApiAuth, wrap(async (req, res, next) => { router.get('/startup', auth.checkApiAuth, wrap(async (req, res, next) => {
const noteIds = await attributes.getNoteIdsWithAttribute("run_on_startup"); const noteIds = await attributes.getNoteIdsWithAttribute("run_on_startup");
const repository = new Repository(req); const repository = new Repository(req);
@@ -31,32 +37,36 @@ router.get('/startup', auth.checkApiAuth, wrap(async (req, res, next) => {
})); }));
router.get('/subtree/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => { router.get('/subtree/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
const repository = new Repository(req);
const noteId = req.params.noteId; const noteId = req.params.noteId;
const repository = new Repository(req); res.send(await getNoteWithSubtreeScript(noteId, repository));
const noteScript = (await repository.getNote(noteId)).content;
const subTreeScripts = await getSubTreeScripts(noteId, [noteId], repository);
res.send(subTreeScripts + noteScript);
})); }));
async function getNoteWithSubtreeScript(noteId, repository) { async function getNoteWithSubtreeScript(noteId, repository) {
const noteScript = (await repository.getNote(noteId)).content; const note = await repository.getNote(noteId);
const subTreeScripts = await getSubTreeScripts(noteId, [noteId], repository); let noteScript = note.content;
if (note.isJavaScript()) {
// last \r\n is necessary if script contains line comment on its last line
noteScript = "(async function() {" + noteScript + "\r\n})()";
}
const subTreeScripts = await getSubTreeScripts(noteId, [noteId], repository, note.isJavaScript());
return subTreeScripts + noteScript; return subTreeScripts + noteScript;
} }
async function getSubTreeScripts(parentId, includedNoteIds, repository) { async function getSubTreeScripts(parentId, includedNoteIds, repository, isJavaScript) {
const children = await repository.getEntities(` const children = await repository.getEntities(`
SELECT notes.* SELECT notes.*
FROM notes JOIN note_tree USING(noteId) FROM notes JOIN note_tree USING(noteId)
WHERE note_tree.isDeleted = 0 AND notes.isDeleted = 0 WHERE note_tree.isDeleted = 0 AND notes.isDeleted = 0
AND note_tree.parentNoteId = ? AND notes.type = 'code' AND note_tree.parentNoteId = ? AND (notes.type = 'code' OR notes.type = 'file')
AND (notes.mime = 'application/javascript' OR notes.mime = 'text/html')`, [parentId]); AND (notes.mime = 'application/javascript'
OR notes.mime = 'application/x-javascript'
OR notes.mime = 'text/html')`, [parentId]);
let script = "\r\n"; let script = "\r\n";
@@ -69,7 +79,7 @@ async function getSubTreeScripts(parentId, includedNoteIds, repository) {
script += await getSubTreeScripts(child.noteId, includedNoteIds, repository); script += await getSubTreeScripts(child.noteId, includedNoteIds, repository);
if (child.mime === 'application/javascript') { if (!isJavaScript && child.isJavaScript()) {
child.content = '<script>' + child.content + '</script>'; child.content = '<script>' + child.content + '</script>';
} }

View File

@@ -79,9 +79,12 @@ router.get('/changed', auth.checkApiAuth, wrap(async (req, res, next) => {
router.get('/notes/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => { router.get('/notes/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
const noteId = req.params.noteId; const noteId = req.params.noteId;
const entity = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
sync.serializeNoteContentBuffer(entity);
res.send({ res.send({
entity: await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]) entity: entity
}); });
})); }));

View File

@@ -6,6 +6,7 @@ const sql = require('../../services/sql');
const options = require('../../services/options'); const options = require('../../services/options');
const utils = require('../../services/utils'); const utils = require('../../services/utils');
const auth = require('../../services/auth'); const auth = require('../../services/auth');
const config = require('../../services/config');
const protected_session = require('../../services/protected_session'); const protected_session = require('../../services/protected_session');
const sync_table = require('../../services/sync_table'); const sync_table = require('../../services/sync_table');
const wrap = require('express-promise-wrap').wrap; const wrap = require('express-promise-wrap').wrap;
@@ -41,6 +42,7 @@ router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => {
AND notes.isDeleted = 0`); AND notes.isDeleted = 0`);
res.send({ res.send({
instanceName: config.General ? config.General.instanceName : null,
notes: notes, notes: notes,
hiddenInAutocomplete: hiddenInAutocomplete, hiddenInAutocomplete: hiddenInAutocomplete,
start_note_path: await options.getOption('start_note_path') start_note_path: await options.getOption('start_note_path')

View File

@@ -29,6 +29,7 @@ const imageRoute = require('./api/image');
const attributesRoute = require('./api/attributes'); const attributesRoute = require('./api/attributes');
const scriptRoute = require('./api/script'); const scriptRoute = require('./api/script');
const senderRoute = require('./api/sender'); const senderRoute = require('./api/sender');
const attachmentsRoute = require('./api/attachments');
function register(app) { function register(app) {
app.use('/', indexRoute); app.use('/', indexRoute);
@@ -61,6 +62,7 @@ function register(app) {
app.use('/api/images', imageRoute); app.use('/api/images', imageRoute);
app.use('/api/script', scriptRoute); app.use('/api/script', scriptRoute);
app.use('/api/sender', senderRoute); app.use('/api/sender', senderRoute);
app.use('/api/attachments', attachmentsRoute);
} }
module.exports = { module.exports = {

Binary file not shown.

View File

@@ -1,4 +1,4 @@
<form id="weight-form" style="display: flex; width: 500px; justify-content: space-around; align-items: flex-end;"> <form id="weight-form" style="display: flex; width: 700px; justify-content: space-around; align-items: flex-end;">
<div> <div>
<label for="weight-date">Date</label> <label for="weight-date">Date</label>
<input type="text" id="weight-date" class="form-control" style="width: 150px; text-align: center;" /> <input type="text" id="weight-date" class="form-control" style="width: 150px; text-align: center;" />
@@ -7,6 +7,10 @@
<label for="weight">Weight</label> <label for="weight">Weight</label>
<input type="number" id="weight" value="80.0" step="0.1" class="form-control" style="text-align: center; width: 100px;" /> <input type="number" id="weight" value="80.0" step="0.1" class="form-control" style="text-align: center; width: 100px;" />
</div> </div>
<div>
<label for="comment">Comment</label>
<input type="text" id="comment" class="form-control" style="width: 200px;" />
</div>
<button type="submit" class="btn btn-primary">Add</button> <button type="submit" class="btn btn-primary">Add</button>
</form> </form>
@@ -16,84 +20,127 @@
<canvas id="canvas"></canvas> <canvas id="canvas"></canvas>
<script> <script>
(async function() { (async function() {
const dateEl = $("#weight-date"); const $form = $("#weight-form");
const weightEl = $("#weight"); const $date = $("#weight-date");
const $weight = $("#weight");
const $comment = $("#comment");
let chart;
dateEl.datepicker(); $date.datepicker();
dateEl.datepicker('option', 'dateFormat', 'yy-mm-dd'); $date.datepicker('option', 'dateFormat', 'yy-mm-dd');
dateEl.datepicker('setDate', new Date()); $date.datepicker('setDate', new Date());
async function saveWeight() { async function saveWeight() {
await server.exec([dateEl.val(), weightEl.val()], async (date, weight) => { await server.exec([$date.val(), parseFloat($weight.val()), $comment.val()], async (date, weight, comment) => {
const dataNote = await this.getNoteWithAttribute('date_data', date); const dataNote = await this.getNoteWithAttribute('date_data', date);
if (dataNote) { if (dataNote) {
dataNote.jsonContent.weight = weight; dataNote.jsonContent.weight = weight;
await this.updateEntity(dataNote); if (comment) {
} dataNote.jsonContent.weight_comment = comment;
else {
const parentNoteId = await this.getDateNoteId(date);
const jsonContent = { weight: weight };
await this.createNote(parentNoteId, 'data', jsonContent, {
json: true,
attributes: {
date_data: date,
hide_in_autocomplete: null
} }
});
}
});
showMessage("Weight has been saved"); await this.updateEntity(dataNote);
}
else {
const parentNoteId = await this.getDateNoteId(date);
const jsonContent = { weight: weight };
if (comment) {
jsonContent.weight_comment = comment;
}
await this.createNote(parentNoteId, 'data', jsonContent, {
json: true,
attributes: {
date_data: date,
hide_in_autocomplete: null
}
});
}
});
showMessage("Weight has been saved");
chart.data = await getData();
chart.update();
}
async function drawChart() {
const data = await getData();
const ctx = $("#canvas")[0].getContext("2d");
chart = new Chart(ctx, {
type: 'line',
data: data,
options: {
tooltips: {
enabled: true,
mode: 'single',
callbacks: {
label: function (tooltipItem, data) {
const multistringText = [tooltipItem.yLabel];
const comment = data.comments[tooltipItem['index']];
if (comment) {
multistringText.push(comment);
}
return multistringText;
}
}
},
}
});
}
async function getData() {
const data = await server.exec([], async () => {
const notes = await this.getNotesWithAttribute('date_data');
const data = [];
for (const note of notes) {
const dateAttr = await note.getAttribute('date_data');
data.push({
date: dateAttr.value,
weight: note.jsonContent.weight,
comment: note.jsonContent.weight_comment
});
}
data.sort((a, b) => a.date < b.date ? -1 : +1);
return data;
});
const datasets = [{
label: "Weight",
backgroundColor: 'red',
borderColor: 'red',
data: data.map(row => row.weight),
fill: false
}];
const labels = data.map(row => row.date);
const comments = data.map(row => row.comment);
return {
labels: labels,
datasets: datasets,
comments: comments
};
}
$form.submit(event => {
saveWeight();
event.preventDefault();
});
drawChart(); drawChart();
} })();
async function drawChart() {
const data = await server.exec([], async () => {
const notes = await this.getNotesWithAttribute('date_data');
const data = [];
for (const note of notes) {
const dateAttr = await note.getAttribute('date_data');
data.push({
date: dateAttr.value,
weight: note.jsonContent.weight
});
}
data.sort((a, b) => a.date < b.date ? -1 : +1);
return data;
});
const ctx = $("#canvas")[0].getContext("2d");
new Chart(ctx, {
type: 'line',
data: {
labels: data.map(row => row.date),
datasets: [{
label: "Weight",
backgroundColor: 'red',
borderColor: 'red',
data: data.map(row => row.weight),
fill: false
}]
}
});
}
$("#weight-form").submit(event => {
saveWeight();
event.preventDefault();
});
drawChart();
})();
</script> </script>

View File

@@ -3,7 +3,7 @@
const build = require('./build'); const build = require('./build');
const packageJson = require('../../package'); const packageJson = require('../../package');
const APP_DB_VERSION = 76; const APP_DB_VERSION = 77;
module.exports = { module.exports = {
app_version: packageJson.version, app_version: packageJson.version,

View File

@@ -9,11 +9,12 @@ const BUILTIN_ATTRIBUTES = [
'run_on_startup', 'run_on_startup',
'disable_versioning', 'disable_versioning',
'calendar_root', 'calendar_root',
'hide_in_autocomplete' 'hide_in_autocomplete',
'exclude_from_export'
]; ];
async function getNoteAttributeMap(noteId) { async function getNoteAttributeMap(noteId) {
return await sql.getMap(`SELECT name, value FROM attributes WHERE noteId = ?`, [noteId]); return await sql.getMap(`SELECT name, value FROM attributes WHERE noteId = ? AND isDeleted = 0`, [noteId]);
} }
async function getNoteIdWithAttribute(name, value) { async function getNoteIdWithAttribute(name, value) {
@@ -52,7 +53,11 @@ async function getNoteIdsWithAttribute(name) {
WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ? AND attributes.isDeleted = 0`, [name]); WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ? AND attributes.isDeleted = 0`, [name]);
} }
async function createAttribute(noteId, name, value = null, sourceId = null) { async function createAttribute(noteId, name, value = "", sourceId = null) {
if (value === null || value === undefined) {
value = "";
}
const now = utils.nowDate(); const now = utils.nowDate();
const attributeId = utils.newAttributeId(); const attributeId = utils.newAttributeId();

View File

@@ -214,7 +214,7 @@ async function runAllChecks() {
FROM FROM
notes notes
WHERE WHERE
type != 'text' AND type != 'code' AND type != 'render'`, type != 'text' AND type != 'code' AND type != 'render' AND type != 'file'`,
"Note has invalid type", errorList); "Note has invalid type", errorList);
await runSyncRowChecks("notes", "noteId", errorList); await runSyncRowChecks("notes", "noteId", errorList);

View File

@@ -20,6 +20,5 @@ module.exports = {
DOCUMENT_PATH, DOCUMENT_PATH,
BACKUP_DIR, BACKUP_DIR,
LOG_DIR, LOG_DIR,
EXPORT_DIR,
ANONYMIZED_DB_DIR ANONYMIZED_DB_DIR
}; };

View File

@@ -88,7 +88,7 @@ function noteTitleIv(iv) {
return "0" + iv; return "0" + iv;
} }
function noteTextIv(iv) { function noteContentIv(iv) {
return "1" + iv; return "1" + iv;
} }
@@ -97,5 +97,5 @@ module.exports = {
decrypt, decrypt,
decryptString, decryptString,
noteTitleIv, noteTitleIv,
noteTextIv noteContentIv
}; };

View File

@@ -29,7 +29,7 @@ async function getNoteStartingWith(parentNoteId, startsWith) {
AND note_tree.isDeleted = 0`, [parentNoteId]); AND note_tree.isDeleted = 0`, [parentNoteId]);
} }
async function getRootNoteId() { async function getRootCalendarNoteId() {
let rootNoteId = await sql.getValue(`SELECT notes.noteId FROM notes JOIN attributes USING(noteId) let rootNoteId = await sql.getValue(`SELECT notes.noteId FROM notes JOIN attributes USING(noteId)
WHERE attributes.name = '${CALENDAR_ROOT_ATTRIBUTE}' AND notes.isDeleted = 0`); WHERE attributes.name = '${CALENDAR_ROOT_ATTRIBUTE}' AND notes.isDeleted = 0`);
@@ -91,7 +91,7 @@ async function getMonthNoteId(dateTimeStr, rootNoteId) {
async function getDateNoteId(dateTimeStr, rootNoteId = null) { async function getDateNoteId(dateTimeStr, rootNoteId = null) {
if (!rootNoteId) { if (!rootNoteId) {
rootNoteId = await getRootNoteId(); rootNoteId = await getRootCalendarNoteId();
} }
const dateStr = dateTimeStr.substr(0, 10); const dateStr = dateTimeStr.substr(0, 10);
@@ -119,7 +119,7 @@ async function getDateNoteId(dateTimeStr, rootNoteId = null) {
} }
module.exports = { module.exports = {
getRootNoteId, getRootCalendarNoteId,
getYearNoteId, getYearNoteId,
getMonthNoteId, getMonthNoteId,
getDateNoteId getDateNoteId

View File

@@ -83,6 +83,40 @@ async function createNewNote(parentNoteId, noteOpts, dataKey, sourceId) {
}; };
} }
async function createNote(parentNoteId, title, content = "", extraOptions = {}) {
if (!parentNoteId) throw new Error("Empty parentNoteId");
if (!title) throw new Error("Empty title");
const note = {
title: title,
content: extraOptions.json ? JSON.stringify(content, null, '\t') : content,
target: 'into',
isProtected: extraOptions.isProtected !== undefined ? extraOptions.isProtected : false,
type: extraOptions.type,
mime: extraOptions.mime
};
if (extraOptions.json) {
note.type = "code";
note.mime = "application/json";
}
if (!note.type) {
note.type = "text";
note.mime = "text/html";
}
const {noteId} = await createNewNote(parentNoteId, note, extraOptions.dataKey, extraOptions.sourceId);
if (extraOptions.attributes) {
for (const attrName in extraOptions.attributes) {
await attributes.createAttribute(noteId, attrName, extraOptions.attributes[attrName]);
}
}
return noteId;
}
async function protectNoteRecursively(noteId, dataKey, protect, sourceId) { async function protectNoteRecursively(noteId, dataKey, protect, sourceId) {
const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
@@ -148,10 +182,14 @@ async function protectNoteHistory(noteId, dataKey, protect, sourceId) {
async function saveNoteHistory(noteId, dataKey, sourceId, nowStr) { async function saveNoteHistory(noteId, dataKey, sourceId, nowStr) {
const oldNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); const oldNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
if (oldNote.type === 'file') {
return;
}
if (oldNote.isProtected) { if (oldNote.isProtected) {
protected_session.decryptNote(dataKey, oldNote); protected_session.decryptNote(dataKey, oldNote);
note.isProtected = false; oldNote.isProtected = false;
} }
const newNoteRevisionId = utils.newNoteRevisionId(); const newNoteRevisionId = utils.newNoteRevisionId();
@@ -217,7 +255,21 @@ async function saveNoteImages(noteId, noteText, sourceId) {
} }
} }
async function loadFile(noteId, newNote, dataKey) {
const oldNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
if (oldNote.isProtected) {
await protected_session.decryptNote(dataKey, oldNote);
}
newNote.detail.content = oldNote.content;
}
async function updateNote(noteId, newNote, dataKey, sourceId) { async function updateNote(noteId, newNote, dataKey, sourceId) {
if (newNote.detail.type === 'file') {
await loadFile(noteId, newNote, dataKey);
}
if (newNote.detail.isProtected) { if (newNote.detail.isProtected) {
await protected_session.encryptNote(dataKey, newNote.detail); await protected_session.encryptNote(dataKey, newNote.detail);
} }
@@ -289,6 +341,7 @@ async function deleteNote(noteTreeId, sourceId) {
module.exports = { module.exports = {
createNewNote, createNewNote,
createNote,
updateNote, updateNote,
deleteNote, deleteNote,
protectNoteRecursively protectNoteRecursively

View File

@@ -26,6 +26,10 @@ function getDataKey(obj) {
const protectedSessionId = getProtectedSessionId(obj); const protectedSessionId = getProtectedSessionId(obj);
return getDataKeyForProtectedSessionId(protectedSessionId);
}
function getDataKeyForProtectedSessionId(protectedSessionId) {
if (protectedSessionId && session.protectedSessionId === protectedSessionId) { if (protectedSessionId && session.protectedSessionId === protectedSessionId) {
return session.decryptedDataKey; return session.decryptedDataKey;
} }
@@ -52,7 +56,14 @@ function decryptNote(dataKey, note) {
} }
if (note.content) { if (note.content) {
note.content = data_encryption.decryptString(dataKey, data_encryption.noteTextIv(note.noteId), note.content); const contentIv = data_encryption.noteContentIv(note.noteId);
if (note.type === 'file') {
note.content = data_encryption.decrypt(dataKey, contentIv, note.content);
}
else {
note.content = data_encryption.decryptString(dataKey, contentIv, note.content);
}
} }
} }
@@ -76,7 +87,7 @@ function decryptNoteHistoryRow(dataKey, hist) {
} }
if (hist.content) { if (hist.content) {
hist.content = data_encryption.decryptString(dataKey, data_encryption.noteTextIv(hist.noteRevisionId), hist.content); hist.content = data_encryption.decryptString(dataKey, data_encryption.noteContentIv(hist.noteRevisionId), hist.content);
} }
} }
@@ -92,19 +103,20 @@ function encryptNote(dataKey, note) {
dataKey = getDataKey(dataKey); dataKey = getDataKey(dataKey);
note.title = data_encryption.encrypt(dataKey, data_encryption.noteTitleIv(note.noteId), note.title); note.title = data_encryption.encrypt(dataKey, data_encryption.noteTitleIv(note.noteId), note.title);
note.content = data_encryption.encrypt(dataKey, data_encryption.noteTextIv(note.noteId), note.content); note.content = data_encryption.encrypt(dataKey, data_encryption.noteContentIv(note.noteId), note.content);
} }
function encryptNoteHistoryRow(dataKey, history) { function encryptNoteHistoryRow(dataKey, history) {
dataKey = getDataKey(dataKey); dataKey = getDataKey(dataKey);
history.title = data_encryption.encrypt(dataKey, data_encryption.noteTitleIv(history.noteRevisionId), history.title); history.title = data_encryption.encrypt(dataKey, data_encryption.noteTitleIv(history.noteRevisionId), history.title);
history.content = data_encryption.encrypt(dataKey, data_encryption.noteTextIv(history.noteRevisionId), history.content); history.content = data_encryption.encrypt(dataKey, data_encryption.noteContentIv(history.noteRevisionId), history.content);
} }
module.exports = { module.exports = {
setDataKey, setDataKey,
getDataKey, getDataKey,
getDataKeyForProtectedSessionId,
isProtectedSessionAvailable, isProtectedSessionAvailable,
decryptNote, decryptNote,
decryptNotes, decryptNotes,

View File

@@ -1,27 +1,78 @@
const log = require('./log');
const sql = require('./sql'); const sql = require('./sql');
const ScriptContext = require('./script_context'); const ScriptContext = require('./script_context');
async function executeScript(dataKey, script, params) { async function executeScript(dataKey, script, params) {
log.info('Executing script: ' + script);
const ctx = new ScriptContext(dataKey); const ctx = new ScriptContext(dataKey);
const paramsStr = getParams(params); const paramsStr = getParams(params);
let ret; return await sql.doInTransaction(async () => execute(ctx, script, paramsStr));
}
await sql.doInTransaction(async () => { async function execute(ctx, script, paramsStr) {
ret = await (function() { return eval(`(${script})(${paramsStr})`); }.call(ctx)); return await (function() { return eval(`const api = this; (${script})(${paramsStr})`); }.call(ctx));
}); }
return ret; const timeouts = {};
const intervals = {};
function clearExistingJob(name) {
if (timeouts[name]) {
clearTimeout(timeouts[name]);
delete timeouts[name];
}
if (intervals[name]) {
clearInterval(intervals[name]);
delete intervals[name];
}
}
async function executeJob(script, params, manualTransactionHandling) {
const ctx = new ScriptContext();
const paramsStr = getParams(params);
if (manualTransactionHandling) {
return await execute(ctx, script, paramsStr);
}
else {
return await sql.doInTransaction(async () => execute(ctx, script, paramsStr));
}
}
async function setJob(opts) {
const { name, runEveryMs, initialRunAfterMs } = opts;
clearExistingJob(name);
const jobFunc = () => executeJob(opts.job, opts.params, opts.manualTransactionHandling);
if (runEveryMs && runEveryMs > 0) {
intervals[name] = setInterval(jobFunc, runEveryMs);
}
if (initialRunAfterMs && initialRunAfterMs > 0) {
timeouts[name] = setTimeout(jobFunc, initialRunAfterMs);
}
} }
function getParams(params) { function getParams(params) {
return params.map(p => JSON.stringify(p)).join(","); if (!params) {
return params;
}
return params.map(p => {
if (typeof p === "string" && p.startsWith("!@#Function: ")) {
return p.substr(13);
}
else {
return JSON.stringify(p);
}
}).join(",");
} }
module.exports = { module.exports = {
executeScript executeScript,
setJob
}; };

View File

@@ -1,20 +1,33 @@
const log = require('./log'); const log = require('./log');
const protected_session = require('./protected_session'); const protected_session = require('./protected_session');
const notes = require('./notes'); const notes = require('./notes');
const sql = require('./sql');
const utils = require('./utils');
const attributes = require('./attributes'); const attributes = require('./attributes');
const date_notes = require('./date_notes'); const date_notes = require('./date_notes');
const config = require('./config');
const Repository = require('./repository'); const Repository = require('./repository');
const axios = require('axios');
function ScriptContext(noteId, dataKey) { function ScriptContext(dataKey) {
this.dataKey = protected_session.getDataKey(dataKey); dataKey = protected_session.getDataKey(dataKey);
this.repository = new Repository(dataKey); const repository = new Repository(dataKey);
this.axios = axios;
this.utils = {
unescapeHtml: utils.unescapeHtml,
isoDateTimeStr: utils.dateStr
};
this.getInstanceName = () => config.General ? config.General.instanceName : null;
this.getNoteById = async function(noteId) { this.getNoteById = async function(noteId) {
return this.repository.getNote(noteId); return repository.getNote(noteId);
}; };
this.getNotesWithAttribute = async function (attrName, attrValue) { this.getNotesWithAttribute = async function (attrName, attrValue) {
return await attributes.getNotesWithAttribute(this.dataKey, attrName, attrValue); return await attributes.getNotesWithAttribute(dataKey, attrName, attrValue);
}; };
this.getNoteWithAttribute = async function (attrName, attrValue) { this.getNoteWithAttribute = async function (attrName, attrValue) {
@@ -23,46 +36,22 @@ function ScriptContext(noteId, dataKey) {
return notes.length > 0 ? notes[0] : null; return notes.length > 0 ? notes[0] : null;
}; };
this.createNote = async function (parentNoteId, name, jsonContent, extraOptions = {}) { this.createNote = async function(parentNoteId, title, content = "", extraOptions = {}) {
const note = { extraOptions.dataKey = dataKey;
title: name,
content: extraOptions.json ? JSON.stringify(jsonContent, null, '\t') : jsonContent,
target: 'into',
isProtected: extraOptions.isProtected !== undefined ? extraOptions.isProtected : false,
type: extraOptions.type,
mime: extraOptions.mime
};
if (extraOptions.json) { return await notes.createNote(parentNoteId, title, content, extraOptions);
note.type = "code";
note.mime = "application/json";
}
if (!note.type) {
note.type = "text";
note.mime = "text/html";
}
const noteId = (await notes.createNewNote(parentNoteId, note, this.dataKey)).noteId;
if (extraOptions.attributes) {
for (const attrName in extraOptions.attributes) {
await attributes.createAttribute(noteId, attrName, extraOptions.attributes[attrName]);
}
}
return noteId;
}; };
this.createAttribute = attributes.createAttribute; this.createAttribute = attributes.createAttribute;
this.updateEntity = this.repository.updateEntity; this.updateEntity = repository.updateEntity;
this.log = function(message) { this.log = message => log.info(`Script: ${message}`);
log.info(`Script: ${message}`);
};
this.getRootCalendarNoteId = date_notes.getRootCalendarNoteId;
this.getDateNoteId = date_notes.getDateNoteId; this.getDateNoteId = date_notes.getDateNoteId;
this.transaction = sql.doInTransaction;
} }
module.exports = ScriptContext; module.exports = ScriptContext;

View File

@@ -195,6 +195,7 @@ async function doInTransaction(func) {
await transactionPromise; await transactionPromise;
} }
let ret = null;
const error = new Error(); // to capture correct stack trace in case of exception const error = new Error(); // to capture correct stack trace in case of exception
transactionActive = true; transactionActive = true;
@@ -202,7 +203,7 @@ async function doInTransaction(func) {
try { try {
await beginTransaction(); await beginTransaction();
await func(); ret = await func();
await commit(); await commit();
@@ -223,6 +224,8 @@ async function doInTransaction(func) {
if (transactionActive) { if (transactionActive) {
await transactionPromise; await transactionPromise;
} }
return ret;
} }
async function isDbUpToDate() { async function isDbUpToDate() {

View File

@@ -204,6 +204,8 @@ async function pushEntity(sync, syncContext) {
if (sync.entityName === 'notes') { if (sync.entityName === 'notes') {
entity = await sql.getRow('SELECT * FROM notes WHERE noteId = ?', [sync.entityId]); entity = await sql.getRow('SELECT * FROM notes WHERE noteId = ?', [sync.entityId]);
serializeNoteContentBuffer(entity);
} }
else if (sync.entityName === 'note_tree') { else if (sync.entityName === 'note_tree') {
entity = await sql.getRow('SELECT * FROM note_tree WHERE noteTreeId = ?', [sync.entityId]); entity = await sql.getRow('SELECT * FROM note_tree WHERE noteTreeId = ?', [sync.entityId]);
@@ -258,6 +260,12 @@ async function pushEntity(sync, syncContext) {
await syncRequest(syncContext, 'PUT', '/api/sync/' + sync.entityName, payload); await syncRequest(syncContext, 'PUT', '/api/sync/' + sync.entityName, payload);
} }
function serializeNoteContentBuffer(note) {
if (note.type === 'file') {
note.content = note.content.toString("binary");
}
}
async function checkContentHash(syncContext) { async function checkContentHash(syncContext) {
const resp = await syncRequest(syncContext, 'GET', '/api/sync/check'); const resp = await syncRequest(syncContext, 'GET', '/api/sync/check');
@@ -350,5 +358,6 @@ sql.dbReady.then(() => {
}); });
module.exports = { module.exports = {
sync sync,
serializeNoteContentBuffer
}; };

View File

@@ -1,10 +1,17 @@
const sql = require('./sql'); const sql = require('./sql');
const log = require('./log'); const log = require('./log');
const eventLog = require('./event_log'); const eventLog = require('./event_log');
const notes = require('./notes');
const sync_table = require('./sync_table'); const sync_table = require('./sync_table');
function deserializeNoteContentBuffer(note) {
if (note.type === 'file') {
note.content = new Buffer(note.content, 'binary');
}
}
async function updateNote(entity, sourceId) { async function updateNote(entity, sourceId) {
deserializeNoteContentBuffer(entity);
const origNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [entity.noteId]); const origNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [entity.noteId]);
if (!origNote || origNote.dateModified <= entity.dateModified) { if (!origNote || origNote.dateModified <= entity.dateModified) {

View File

@@ -2,6 +2,7 @@
const crypto = require('crypto'); const crypto = require('crypto');
const randtoken = require('rand-token').generator({source: 'crypto'}); const randtoken = require('rand-token').generator({source: 'crypto'});
const unescape = require('unescape');
function newNoteId() { function newNoteId() {
return randomString(12); return randomString(12);
@@ -129,6 +130,10 @@ async function stopWatch(what, func) {
return ret; return ret;
} }
function unescapeHtml(str) {
return unescape(str);
}
module.exports = { module.exports = {
randomSecureToken, randomSecureToken,
randomString, randomString,
@@ -153,5 +158,6 @@ module.exports = {
getDateTimeForFile, getDateTimeForFile,
sanitizeSql, sanitizeSql,
assertArguments, assertArguments,
stopWatch stopWatch,
unescapeHtml
}; };

View File

@@ -56,6 +56,8 @@
<img src="images/icons/search.png" alt="Search in notes"/> <img src="images/icons/search.png" alt="Search in notes"/>
</a> </a>
</div> </div>
<input type="file" id="import-upload" style="display: none" />
</div> </div>
<div id="search-box" class="hide-toggle" style="grid-area: search; display: none; padding: 10px; margin-top: 10px;"> <div id="search-box" class="hide-toggle" style="grid-area: search; display: none; padding: 10px; margin-top: 10px;">
@@ -105,7 +107,7 @@
onclick="noteEditor.executeCurrentNote()">Execute <kbd>Ctrl+Enter</kbd></button> onclick="noteEditor.executeCurrentNote()">Execute <kbd>Ctrl+Enter</kbd></button>
<div class="dropdown" id="note-type"> <div class="dropdown" id="note-type">
<button id="dLabel" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm"> <button data-bind="disable: isDisabled()" id="dLabel" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm">
Type: <span data-bind="text: typeString()"></span> Type: <span data-bind="text: typeString()"></span>
<span class="caret"></span> <span class="caret"></span>
</button> </button>
@@ -130,6 +132,7 @@
<li><a onclick="noteHistory.showCurrentNoteHistory();"><kbd>Alt+H</kbd> History</a></li> <li><a onclick="noteHistory.showCurrentNoteHistory();"><kbd>Alt+H</kbd> History</a></li>
<li><a onclick="attributesDialog.showDialog();"><kbd>Alt+A</kbd> Attributes</a></li> <li><a onclick="attributesDialog.showDialog();"><kbd>Alt+A</kbd> Attributes</a></li>
<li><a onclick="noteSource.showDialog();"><kbd>Ctrl+U</kbd> HTML source</a></li> <li><a onclick="noteSource.showDialog();"><kbd>Ctrl+U</kbd> HTML source</a></li>
<li><a onclick="uploadAttachment();">Upload attachment</a></li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -141,6 +144,32 @@
<div id="note-detail-code"></div> <div id="note-detail-code"></div>
<div id="note-detail-render"></div> <div id="note-detail-render"></div>
<div id="note-detail-attachment">
<table id="attachment-table">
<tr>
<th>File name:</th>
<td id="attachment-filename"></td>
</tr>
<tr>
<th>File type:</th>
<td id="attachment-filetype"></td>
</tr>
<tr>
<th>File size:</th>
<td id="attachment-filesize"></td>
</tr>
<tr>
<td>
<button id="attachment-download" class="btn btn-primary" type="button">Download</button>
&nbsp;
<button id="attachment-open" class="btn btn-primary" type="button">Open</button>
</td>
</tr>
</table>
</div>
<input type="file" id="attachment-upload" style="display: none" />
</div> </div>
<div id="attribute-list"> <div id="attribute-list">
@@ -449,8 +478,6 @@
<link href="libraries/fancytree/skin-win8/ui.fancytree.css" rel="stylesheet"> <link href="libraries/fancytree/skin-win8/ui.fancytree.css" rel="stylesheet">
<script src="libraries/fancytree/jquery.fancytree-all.min.js"></script> <script src="libraries/fancytree/jquery.fancytree-all.min.js"></script>
<script src="libraries/ckeditor/ckeditor.js"></script>
<script src="libraries/jquery.hotkeys.js"></script> <script src="libraries/jquery.hotkeys.js"></script>
<script src="libraries/jquery.fancytree.hotkeys.js"></script> <script src="libraries/jquery.fancytree.hotkeys.js"></script>
@@ -458,15 +485,6 @@
<script src="libraries/knockout.min.js"></script> <script src="libraries/knockout.min.js"></script>
<script src="libraries/codemirror/codemirror.js"></script>
<link rel="stylesheet" href="libraries/codemirror/codemirror.css">
<script src="libraries/codemirror/addon/mode/loadmode.js"></script>
<script src="libraries/codemirror/addon/fold/xml-fold.js"></script>
<script src="libraries/codemirror/addon/edit/matchbrackets.js"></script>
<script src="libraries/codemirror/addon/edit/matchtags.js"></script>
<script src="libraries/codemirror/addon/search/match-highlighter.js"></script>
<script src="libraries/codemirror/mode/meta.js"></script>
<link href="stylesheets/style.css" rel="stylesheet"> <link href="stylesheets/style.css" rel="stylesheet">
<script src="javascripts/utils.js"></script> <script src="javascripts/utils.js"></script>
@@ -481,6 +499,7 @@
<script src="javascripts/drag_and_drop.js"></script> <script src="javascripts/drag_and_drop.js"></script>
<script src="javascripts/context_menu.js"></script> <script src="javascripts/context_menu.js"></script>
<script src="javascripts/search_tree.js"></script> <script src="javascripts/search_tree.js"></script>
<script src="javascripts/export.js"></script>
<!-- Note detail --> <!-- Note detail -->
<script src="javascripts/note_editor.js"></script> <script src="javascripts/note_editor.js"></script>