Compare commits

..

72 Commits

Author SHA1 Message Date
azivner cf96baad48 release 0.27.0-beta 2018-12-30 22:38:11 +01:00
azivner 32220476aa better behavior on default loading detail / switching between tree and detail 2018-12-30 22:36:39 +01:00
azivner 86bc84a2ad better detection of float parameter in label filters, fixes #265 2018-12-30 22:09:14 +01:00
azivner de9e0c7929 lazy loading icon for both mobile and desktop 2018-12-30 21:13:35 +01:00
azivner 6963e662ef fix layout on chrome 2018-12-30 19:15:44 +01:00
azivner 7127822e8f placeholder for empty note content 2018-12-30 19:02:42 +01:00
azivner 0a35abf68f refresh note detail view after attribute save (from full dialog, not promoted notes), closes #261 2018-12-30 15:21:53 +01:00
azivner 3f8e8f8561 fix saving branch prefix, fixes #260 2018-12-29 10:35:44 +01:00
azivner e02eca87b0 dont toggle panes note on the first load 2018-12-29 10:19:28 +01:00
azivner 2f680c4326 tweaks, fixes, refactorings, creating new note from detail, not tree 2018-12-29 10:04:59 +01:00
azivner 0b4a44a403 layout tweaks etc. 2018-12-29 09:13:52 +01:00
azivner 89299f865c api method to detect navigation away from current page 2018-12-29 08:40:35 +01:00
azivner f6db9a50ab "switch to desktop version" button 2018-12-29 00:09:16 +01:00
azivner 78d9fac1e6 global actions context menu, logout 2018-12-28 23:57:11 +01:00
azivner 458ed1faff mobile detection 2018-12-28 23:47:06 +01:00
azivner 0657815de5 Merge branch 'master' into mobile 2018-12-28 22:31:51 +01:00
azivner a608832681 change to relation map note type doesn't trigger content change, related issue #258 2018-12-28 22:31:06 +01:00
azivner d2f72529b3 context menu with delete 2018-12-28 22:05:04 +01:00
azivner 28185af158 fix layout/scrolling issues in the tree 2018-12-28 20:36:18 +01:00
azivner 7aacd01ad7 separate css layout file for mobile and desktop 2018-12-28 19:47:02 +01:00
azivner 74cc34696e unifying expander icons in both desktop and mobile 2018-12-27 22:38:19 +01:00
azivner d4baac0bb5 mobile fixes 2018-12-27 22:36:11 +01:00
azivner 35bc1421f1 cannot edit note title when not in protected session 2018-12-27 20:22:33 +01:00
azivner d4db265fd9 non-modal protected session password form is now done separately as a kind of detail component. Also reworked branch prefix dialog input 2018-12-27 20:04:59 +01:00
azivner 1dad919de9 working protected session in mobile 2018-12-25 10:58:40 +01:00
azivner 246dfbdcb4 fix attribute loading for mobile 2018-12-24 23:08:43 +01:00
azivner f8d32d64f5 content scrolling 2018-12-24 22:39:17 +01:00
azivner 5dea271d6f basic responsive layout 2018-12-24 20:38:38 +01:00
azivner eeb62a6cf2 fix keyboard shortcut setup on mobile 2018-12-24 18:39:31 +01:00
azivner 35cf8026b0 activating note opens dialog with its detail 2018-12-24 12:49:27 +01:00
azivner e0028ab6f1 mobile frontend WIP, removing extra dependencies 2018-12-24 10:10:36 +01:00
azivner a32645cdeb fix scrolling to the top of detail after switching note 2018-12-24 09:47:00 +01:00
azivner 812f9f6fca initial work on mobile frontend 2018-12-23 22:28:57 +01:00
azivner f7a670ec24 responsive login page 2018-12-23 11:06:11 +01:00
azivner d0d24f0f4a unify ejs indentation to 4 spaces 2018-12-23 10:57:40 +01:00
azivner 042f9b7f2d fix positioning of protected session password dialog 2018-12-22 23:54:06 +01:00
azivner 2d260cdbed explicit node-abi required for 4.0.0 electron build 2018-12-22 23:10:03 +01:00
azivner 6a786cad83 updated API docs 2018-12-22 22:28:49 +01:00
azivner 67019b3d6c added attributes to frontend entity model 2018-12-22 22:16:32 +01:00
azivner 1db6e59077 tooltip related changes for scripting support 2018-12-22 20:57:09 +01:00
azivner 5e4770875e upgrade to bootstrap 4.2.1 2018-12-22 10:33:50 +01:00
azivner e9a77f3f16 image sync fixes and 4.0.0 electron 2018-12-22 09:54:09 +01:00
azivner cbec85f295 release 0.26.1 2018-12-20 19:54:09 +01:00
azivner 14bd5d301d fix broken image sync 2018-12-20 19:51:03 +01:00
azivner 321d0e8d64 explicit error handler for connection errors 2018-12-19 21:29:35 +01:00
azivner 84e1512031 added robots.txt to public root 2018-12-19 20:56:10 +01:00
azivner b036852b2d release 0.26.0-beta 2018-12-18 22:49:48 +01:00
azivner f693dc31e8 mention note hoisting in the readme 2018-12-18 20:50:03 +01:00
azivner af8f5b11b8 better error handling in request 2018-12-18 20:39:56 +01:00
azivner 8ca943f87b fix hoistedNoteId option init 2018-12-18 20:34:24 +01:00
azivner 35cc9da626 note about system proxy in the settings 2018-12-17 22:58:53 +01:00
azivner 44cc86a1a5 added support for explicit proxy 2018-12-17 22:54:54 +01:00
azivner 7c3bbfd45e using http service for sync setup as well, removed request(-promise) dependency 2018-12-17 22:12:26 +01:00
azivner a1f939e3a0 refactoring of network code into separate service 2018-12-17 21:34:02 +01:00
azivner 4ca7886090 uniform requests through electron's net and node http/https to support different proxy setups 2018-12-16 23:30:53 +01:00
azivner b942163748 sync with electron net API (for system proxy support) - working, but WIP 2018-12-16 21:19:12 +01:00
azivner 5b6d15acb3 note path might not always include root explicitly 2018-12-16 10:41:47 +01:00
azivner 841420360e quick unhoist link/button 2018-12-15 20:29:08 +01:00
azivner a680bb4612 unhoist note when activating note outside of the hoisted note subtree 2018-12-13 23:28:48 +01:00
azivner f763e13996 fix searching without hoisting 2018-12-13 21:43:13 +01:00
azivner e73e1262ae Merge branch 'stable' into hoist 2018-12-13 21:18:44 +01:00
azivner b774d56cf7 note titles in jump to note start from hoisted note instead of root 2018-12-13 21:18:35 +01:00
azivner bae55f2e8b release 0.25.2 2018-12-12 21:30:06 +01:00
azivner 6c16cdb011 recent notes and jump to note are filtered by hoisted note 2018-12-12 21:28:38 +01:00
azivner 86fcbb0354 keyboard shortcut for hoisting, hoisted note is always expanded and has arrow icon 2018-12-12 20:54:58 +01:00
azivner 17d030e800 continuing work on hoisting notes 2018-12-12 20:39:56 +01:00
azivner 6fbf28b30d hoisting notes WIP 2018-12-11 21:53:56 +01:00
azivner 8171b68b18 electron 4.0 beta 9, context menu padding 2018-12-11 19:50:31 +01:00
azivner d30e3a4052 bind standard shortcuts on both command and ctrl modifiers, #254 2018-12-10 20:44:50 +01:00
azivner 87aab7ac5b fix attributes leaking into imported notes, #241 2018-12-10 20:29:49 +01:00
azivner c46f5805b1 removed the note from readme about missing mac build 2018-12-09 21:50:29 +01:00
azivner dd6a29dfc0 no ia32 release upload 2018-12-09 21:34:08 +01:00
100 changed files with 6246 additions and 2200 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:10.14.0-alpine
FROM node:10.15.0-alpine
# Create app directory
WORKDIR /usr/src/app
+3 -3
View File
@@ -10,7 +10,7 @@ Trilium Notes is a hierarchical note taking application with focus on building l
* Notes can be arranged into arbitrarily deep tree. Single note can be placed into multiple places in the tree (see [cloning](https://github.com/zadam/trilium/wiki/Cloning-notes))
* Rich WYSIWYG note editing including e.g. tables and images with markdown [autoformat](https://github.com/zadam/trilium/wiki/Text-editor#autoformat)
* Support for editing [notes with source code](https://github.com/zadam/trilium/wiki/Code-notes), including syntax highlighting
* Fast and easy [navigation between notes](https://github.com/zadam/trilium/wiki/Note-navigation)
* Fast and easy [navigation between notes](https://github.com/zadam/trilium/wiki/Note-navigation), full text search and [note hoisting](https://github.com/zadam/trilium/wiki/Note-hoisting)
* Seamless [note versioning](https://github.com/zadam/trilium/wiki/Note-revisions)
* Note [attributes](https://github.com/zadam/trilium/wiki/Attributes) can be used for note organization, querying and advanced [scripting](https://github.com/zadam/trilium/wiki/Scripts)
* [Synchronization](https://github.com/zadam/trilium/wiki/Synchronization) with self-hosted sync server
@@ -23,9 +23,9 @@ Trilium Notes is a hierarchical note taking application with focus on building l
## Builds
Trilium is provided as either desktop application (Linux, Windows) or web application hosted on your server (Linux). Mac is planned but not available at the moment.
Trilium is provided as either desktop application (Linux, Windows, Mac) or web application hosted on your server (Linux).
* If you want to use Trilium on the desktop, download binary release for your platform (currently Linux and Windows are supported) from [latest release](https://github.com/zadam/trilium/releases/latest), unzip the package and run ```trilium``` executable.
* If you want to use Trilium on the desktop, download binary release for your platform from [latest release](https://github.com/zadam/trilium/releases/latest), unzip the package and run ```trilium``` executable.
* If you want to install Trilium on server, follow [this page](https://github.com/zadam/trilium/wiki/Server-installation).
* Currently only recent Chrome and Firefox are supported (tested) browsers.
+2 -2
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
PKG_DIR=dist/trilium-linux-x64-server
NODE_VERSION=10.14.1
NODE_VERSION=10.15.0
rm -r $PKG_DIR
mkdir $PKG_DIR
@@ -33,5 +33,5 @@ chmod 755 trilium.sh
cd ..
VERSION=`jq -r ".version" ../../package.json`
VERSION=`jq -r ".version" ../package.json`
7z a trilium-linux-x64-server-${VERSION}.7z trilium-linux-x64-server
-8
View File
@@ -43,7 +43,6 @@ git push origin $TAG
bin/build.sh
LINUX_X64_BUILD=trilium-linux-x64-$VERSION.7z
LINUX_IA32_BUILD=trilium-linux-ia32-$VERSION.7z
WINDOWS_X64_BUILD=trilium-windows-x64-$VERSION.7z
MAC_X64_BUILD=trilium-mac-x64-$VERSION.7z
SERVER_BUILD=trilium-linux-x64-server-$VERSION.7z
@@ -67,13 +66,6 @@ github-release upload \
--name "$LINUX_X64_BUILD" \
--file "dist/$LINUX_X64_BUILD"
echo "Uploading linux ia32 build"
github-release upload \
--tag $TAG \
--name "$LINUX_IA32_BUILD" \
--file "dist/$LINUX_IA32_BUILD"
echo "Uploading windows x64 build"
github-release upload \
@@ -0,0 +1,2 @@
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
VALUES ('hoistedNoteId', 'root', '2018-12-11T18:31:00.874Z', '2018-12-11T18:31:00.874Z', 0);
+1 -1
View File
@@ -3010,7 +3010,7 @@ the backend.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line234">line 234</a>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line236">line 236</a>
</li></ul></dd>
+766 -662
View File
File diff suppressed because it is too large Load Diff
@@ -133,6 +133,11 @@ class Attribute extends Entity {
this.dateModified = dateUtils.nowDate();
}
}
// cannot be static!
updatePojo(pojo) {
delete pojo.isOwned;
}
}
module.exports = Attribute;</code></pre>
+5
View File
@@ -87,6 +87,11 @@ class Branch extends Entity {
this.dateModified = dateUtils.nowDate();
}
}
// cannot be static!
updatePojo(pojo) {
delete pojo.origParentNoteId;
}
}
module.exports = Branch;</code></pre>
+45 -17
View File
@@ -32,6 +32,7 @@ const Entity = require('./entity');
const Attribute = require('./attribute');
const protectedSessionService = require('../services/protected_session');
const repository = require('../services/repository');
const sql = require('../services/sql');
const dateUtils = require('../services/date_utils');
const LABEL = 'label';
@@ -102,7 +103,9 @@ class Note extends Entity {
/** @returns {boolean} true if this note is JavaScript (code or attachment) */
isJavaScript() {
return (this.type === "code" || this.type === "file")
&amp;&amp; (this.mime.startsWith("application/javascript") || this.mime === "application/x-javascript");
&amp;&amp; (this.mime.startsWith("application/javascript")
|| this.mime === "application/x-javascript"
|| this.mime === "text/javascript");
}
/** @returns {boolean} true if this note is HTML */
@@ -394,6 +397,16 @@ class Note extends Entity {
*/
async getRelationValue(name) { return await this.getAttributeValue(RELATION, name); }
/**
* @param {string} name
* @returns {Promise&lt;Note>|null} target note of the relation or null (if target is empty or note was not found)
*/
async getRelationTarget(name) {
const relation = await this.getRelation(name);
return relation ? await repository.getNote(relation.value) : null;
}
/**
* Based on enabled, label is either set or removed.
*
@@ -451,24 +464,32 @@ class Note extends Entity {
async removeRelation(name, value) { return await this.removeAttribute(RELATION, name, value); }
/**
* @param {string} name
* @returns {Promise&lt;Note>|null} target note of the relation or null (if target is empty or note was not found)
* @return {Promise&lt;string[]>} return list of all descendant noteIds of this note. Returning just noteIds because number of notes can be huge. Includes also this note's noteId
*/
async getRelationTarget(name) {
const relation = await this.getRelation(name);
return relation ? await repository.getNote(relation.value) : null;
async getDescendantNoteIds() {
return await sql.getColumn(`
WITH RECURSIVE
tree(noteId) AS (
SELECT ?
UNION
SELECT branches.noteId FROM branches
JOIN tree ON branches.parentNoteId = tree.noteId
JOIN notes ON notes.noteId = branches.noteId
WHERE notes.isDeleted = 0
AND branches.isDeleted = 0
)
SELECT noteId FROM tree`, [this.noteId]);
}
/**
* Finds child notes with given attribute name and value. Only own attributes are considered, not inherited ones
* Finds descendant notes with given attribute name and value. Only own attributes are considered, not inherited ones
*
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @param {string} [value] - attribute value
* @returns {Promise&lt;Note[]>}
*/
async findChildNotesWithAttribute(type, name, value) {
async getDescendantNotesWithAttribute(type, name, value) {
const params = [this.noteId, name];
let valueCondition = "";
@@ -500,22 +521,22 @@ class Note extends Entity {
}
/**
* Finds notes with given label name and value. Only own labels are considered, not inherited ones
* Finds descendant notes with given label name and value. Only own labels are considered, not inherited ones
*
* @param {string} name - label name
* @param {string} [value] - label value
* @returns {Promise&lt;Note[]>}
*/
async findChildNotesWithLabel(name, value) { return await this.findChildNotesWithAttribute(LABEL, name, value); }
async getDescendantNotesWithLabel(name, value) { return await this.getDescendantNotesWithAttribute(LABEL, name, value); }
/**
* Finds notes with given relation name and value. Only own relations are considered, not inherited ones
* Finds descendant notes with given relation name and value. Only own relations are considered, not inherited ones
*
* @param {string} name - relation name
* @param {string} [value] - relation value
* @returns {Promise&lt;Note[]>}
*/
async findChildNotesWithRelation(name, value) { return await this.findChildNotesWithAttribute(RELATION, name, value); }
async getDescendantNotesWithRelation(name, value) { return await this.getDescendantNotesWithAttribute(RELATION, name, value); }
/**
* Returns note revisions of this note.
@@ -615,10 +636,6 @@ class Note extends Entity {
// we do this here because encryption needs the note ID for the IV
this.generateIdIfNecessary();
if (this.isProtected) {
protectedSessionService.encryptNote(this);
}
if (!this.isDeleted) {
this.isDeleted = false;
}
@@ -633,6 +650,17 @@ class Note extends Entity {
this.dateModified = dateUtils.nowDate();
}
}
// cannot be static!
updatePojo(pojo) {
if (pojo.isProtected) {
protectedSessionService.encryptNote(pojo);
}
delete pojo.jsonContent;
delete pojo.isContentAvailable;
delete pojo.__attributeCache;
}
}
module.exports = Note;</code></pre>
@@ -253,6 +253,8 @@ function BackendScriptApi(startNote, currentNote, originEntity) {
*/
this.transactional = sql.transactional;
this.sql = sql;
/**
* Trigger tree refresh in all connected clients. This is required when some tree change happens in
* the backend.
+152 -21
View File
@@ -81,7 +81,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line16">line 16</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line17">line 17</a>
</li></ul></dd>
@@ -221,7 +221,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line22">line 22</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line23">line 23</a>
</li></ul></dd>
@@ -334,7 +334,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line24">line 24</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line25">line 25</a>
</li></ul></dd>
@@ -444,7 +444,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line20">line 20</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line21">line 21</a>
</li></ul></dd>
@@ -573,7 +573,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line41">line 41</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line42">line 42</a>
</li></ul></dd>
@@ -726,7 +726,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line33">line 33</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line34">line 34</a>
</li></ul></dd>
@@ -879,7 +879,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line60">line 60</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line61">line 61</a>
</li></ul></dd>
@@ -1057,7 +1057,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line197">line 197</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line198">line 198</a>
</li></ul></dd>
@@ -1188,7 +1188,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line157">line 157</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line158">line 158</a>
</li></ul></dd>
@@ -1292,7 +1292,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line221">line 221</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line222">line 222</a>
</li></ul></dd>
@@ -1396,7 +1396,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line203">line 203</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line204">line 204</a>
</li></ul></dd>
@@ -1500,7 +1500,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line215">line 215</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line216">line 216</a>
</li></ul></dd>
@@ -1609,7 +1609,7 @@ if some action needs to happen on only one specific instance.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line150">line 150</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line151">line 151</a>
</li></ul></dd>
@@ -1808,7 +1808,7 @@ otherwise (by e.g. createNoteLink())
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line142">line 142</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line143">line 143</a>
</li></ul></dd>
@@ -1957,7 +1957,7 @@ otherwise (by e.g. createNoteLink())
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line209">line 209</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line210">line 210</a>
</li></ul></dd>
@@ -2088,7 +2088,7 @@ otherwise (by e.g. createNoteLink())
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line164">line 164</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line165">line 165</a>
</li></ul></dd>
@@ -2196,7 +2196,7 @@ otherwise (by e.g. createNoteLink())
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line188">line 188</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line189">line 189</a>
</li></ul></dd>
@@ -2373,7 +2373,7 @@ Internally this serializes the anonymous function into string and sends it to ba
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line110">line 110</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line111">line 111</a>
</li></ul></dd>
@@ -2526,7 +2526,138 @@ Internally this serializes the anonymous function into string and sends it to ba
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line227">line 227</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line228">line 228</a>
</li></ul></dd>
</dl>
<h4 class="name" id="setupElementTooltip"><span class="type-signature"></span>setupElementTooltip<span class="signature">($el)</span><span class="type-signature"></span></h4>
<h5>Parameters:</h5>
<table class="params">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th class="last">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="name"><code>$el</code></td>
<td class="type">
<span class="param-type">object</span>
</td>
<td class="description last">jquery object on which to setup the tooltip</td>
</tr>
</tbody>
</table>
<dl class="details">
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line234">line 234</a>
</li></ul></dd>
@@ -2661,7 +2792,7 @@ Internally this serializes the anonymous function into string and sends it to ba
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line180">line 180</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line181">line 181</a>
</li></ul></dd>
@@ -2796,7 +2927,7 @@ Internally this serializes the anonymous function into string and sends it to ba
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line172">line 172</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line173">line 173</a>
</li></ul></dd>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: entities/attribute.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: entities/attribute.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>class Attribute {
constructor(treeCache, row) {
this.treeCache = treeCache;
/** @param {string} attributeId */
this.attributeId = row.attributeId;
/** @param {string} noteId */
this.noteId = row.noteId;
/** @param {string} type */
this.type = row.type;
/** @param {string} name */
this.name = row.name;
/** @param {string} value */
this.value = row.value;
/** @param {int} position */
this.position = row.position;
/** @param {boolean} isInheritable */
this.isInheritable = row.isInheritable;
/** @param {boolean} isDeleted */
this.isDeleted = row.isDeleted;
/** @param {string} dateCreated */
this.dateCreated = row.dateCreated;
/** @param {string} dateModified */
this.dateModified = row.dateModified;
}
/** @returns {NoteShort} */
async getNote() {
return await this.treeCache.getNote(this.noteId);
}
get toString() {
return `Attribute(attributeId=${this.attributeId}, type=${this.type}, name=${this.name})`;
}
}</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="NoteFull.html">NoteFull</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3><a href="global.html">Global</a></h3>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>
+1 -1
View File
@@ -42,7 +42,7 @@ class Branch {
/** @param {string} */
this.prefix = row.prefix;
/** @param {boolean} */
this.isExpanded = row.isExpanded;
this.isExpanded = !!row.isExpanded;
}
/** @returns {NoteShort} */
+143 -1
View File
@@ -26,7 +26,14 @@
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
<pre class="prettyprint source linenums"><code>import server from '../services/server.js';
const LABEL = 'label';
const LABEL_DEFINITION = 'label-definition';
const RELATION = 'relation';
const RELATION_DEFINITION = 'relation-definition';
/**
* This note's representation is used in note tree and is kept in TreeCache.
* Its notable omission is the note content.
*/
@@ -99,6 +106,140 @@ class NoteShort {
return await this.treeCache.getNotes(this.getChildNoteIds());
}
/**
* @param {string} [name] - attribute name to filter
* @returns {Promise&lt;Attribute[]>}
*/
async getAttributes(name) {
if (!this.attributeCache) {
this.attributeCache = await server.get('notes/' + this.noteId + '/attributes');
}
if (name) {
return this.attributeCache.filter(attr => attr.name === name);
}
else {
return this.attributeCache;
}
}
/**
* @param {string} [name] - label name to filter
* @returns {Promise&lt;Attribute[]>} all note's labels (attributes with type label), including inherited ones
*/
async getLabels(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === LABEL);
}
/**
* @param {string} [name] - label name to filter
* @returns {Promise&lt;Attribute[]>} all note's label definitions, including inherited ones
*/
async getLabelDefinitions(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === LABEL_DEFINITION);
}
/**
* @param {string} [name] - relation name to filter
* @returns {Promise&lt;Attribute[]>} all note's relations (attributes with type relation), including inherited ones
*/
async getRelations(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === RELATION);
}
/**
* @param {string} [name] - relation name to filter
* @returns {Promise&lt;Attribute[]>} all note's relation definitions including inherited ones
*/
async getRelationDefinitions(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === RELATION_DEFINITION);
}
/**
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {Promise&lt;boolean>} true if note has an attribute with given type and name (including inherited)
*/
async hasAttribute(type, name) {
return !!await this.getAttribute(type, name);
}
/**
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {Promise&lt;Attribute>} attribute of given type and name. If there's more such attributes, first is returned. Returns null if there's no such attribute belonging to this note.
*/
async getAttribute(type, name) {
const attributes = await this.getAttributes();
return attributes.find(attr => attr.type === type &amp;&amp; attr.name === name);
}
/**
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {Promise&lt;string>} attribute value of given type and name or null if no such attribute exists.
*/
async getAttributeValue(type, name) {
const attr = await this.getAttribute(type, name);
return attr ? attr.value : null;
}
/**
* @param {string} name - label name
* @returns {Promise&lt;boolean>} true if label exists (including inherited)
*/
async hasLabel(name) { return await this.hasAttribute(LABEL, name); }
/**
* @param {string} name - relation name
* @returns {Promise&lt;boolean>} true if relation exists (including inherited)
*/
async hasRelation(name) { return await this.hasAttribute(RELATION, name); }
/**
* @param {string} name - label name
* @returns {Promise&lt;Attribute>} label if it exists, null otherwise
*/
async getLabel(name) { return await this.getAttribute(LABEL, name); }
/**
* @param {string} name - relation name
* @returns {Promise&lt;Attribute>} relation if it exists, null otherwise
*/
async getRelation(name) { return await this.getAttribute(RELATION, name); }
/**
* @param {string} name - label name
* @returns {Promise&lt;string>} label value if label exists, null otherwise
*/
async getLabelValue(name) { return await this.getAttributeValue(LABEL, name); }
/**
* @param {string} name - relation name
* @returns {Promise&lt;string>} relation value if relation exists, null otherwise
*/
async getRelationValue(name) { return await this.getAttributeValue(RELATION, name); }
/**
* @param {string} name
* @returns {Promise&lt;Note>|null} target note of the relation or null (if target is empty or note was not found)
*/
async getRelationTarget(name) {
const relation = await this.getRelation(name);
return relation ? await repository.getNote(relation.value) : null;
}
/**
* Clear note's attributes cache to force fresh reload for next attribute request.
* Cache is note instance scoped.
*/
invalidateAttributeCache() {
this.attributeCache = null;
}
get toString() {
return `Note(noteId=${this.noteId}, title=${this.title})`;
}
@@ -107,6 +248,7 @@ class NoteShort {
const dto = Object.assign({}, this);
delete dto.treeCache;
delete dto.archived;
delete dto.attributeCache;
return dto;
}
+1 -1
View File
@@ -303,7 +303,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line47">line 47</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line48">line 48</a>
</li></ul></dd>
@@ -34,6 +34,7 @@ import linkService from './link.js';
import treeCache from './tree_cache.js';
import noteDetailService from './note_detail.js';
import noteTypeService from './note_type.js';
import noteTooltipService from './note_tooltip.js';
/**
* This is the main frontend API interface for scripts. It's published in the local "api" object.
@@ -253,6 +254,12 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null) {
* @param {array} types - list of mime types to be used
*/
this.setCodeMimeTypes = noteTypeService.setCodeMimeTypes;
/**
* @method
* @param {object} $el - jquery object on which to setup the tooltip
*/
this.setupElementTooltip = noteTooltipService.setupElementTooltip
}
export default FrontendScriptApi;</code></pre>
+79 -130
View File
@@ -1,6 +1,6 @@
{
"name": "trilium",
"version": "0.25.0-beta",
"version": "0.26.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -399,9 +399,9 @@
"integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow=="
},
"@types/node": {
"version": "8.10.38",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.38.tgz",
"integrity": "sha512-EibsnbJerd0hBFaDjJStFrVbVBAtOy4dgL8zZFw0uOvPqzBAX59Ci8cgjg3+RgJIWhsB5A4c+pi+D4P9tQQh/A==",
"version": "8.10.39",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.39.tgz",
"integrity": "sha512-rE7fktr02J8ybFf6eysife+WF+L4sAHWzw09DgdCebEu+qDwMvv4zl6Bc+825ttGZP73kCKxa3dhJOoGJ8+5mA==",
"dev": true
},
"abab": {
@@ -671,9 +671,9 @@
"integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0="
},
"asar": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/asar/-/asar-0.14.5.tgz",
"integrity": "sha512-2Di/TnY1sridHFKMFgxBh0Wk0gVxSZN4qQhRhjJn3UywZAvP5MHI0RNVSkpzmJ+n6t0BC8w/+1257wtSgQ3Kdg==",
"version": "0.14.6",
"resolved": "https://registry.npmjs.org/asar/-/asar-0.14.6.tgz",
"integrity": "sha512-ZqybKcdO5At6y3ge2RHxVImc6Eltb2t3sxT7lk4T4zjZBSFUuIGCIZY6f41dCjlvJSizN5QPRr8YTgMhpgBjLg==",
"dev": true,
"requires": {
"chromium-pickle-js": "^0.2.0",
@@ -682,7 +682,7 @@
"glob": "^6.0.4",
"minimatch": "^3.0.3",
"mkdirp": "^0.5.0",
"mksnapshot": "^0.3.0",
"mksnapshot": "^0.3.4",
"tmp": "0.0.28"
},
"dependencies": {
@@ -969,7 +969,8 @@
"bluebird": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.2.tgz",
"integrity": "sha512-dhHTWMI7kMx5whMQntl7Vr9C6BvV10lFXDAasnqnrMYhXVCzzk6IO9Fo2L75jXHT07WrOngL1WDXOp+yYS91Yg=="
"integrity": "sha512-dhHTWMI7kMx5whMQntl7Vr9C6BvV10lFXDAasnqnrMYhXVCzzk6IO9Fo2L75jXHT07WrOngL1WDXOp+yYS91Yg==",
"dev": true
},
"bmp-js": {
"version": "0.1.0",
@@ -1819,9 +1820,9 @@
"integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI="
},
"debug": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz",
"integrity": "sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"requires": {
"ms": "^2.1.1"
},
@@ -2034,7 +2035,7 @@
},
"string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
"dev": true
}
@@ -2374,9 +2375,9 @@
"integrity": "sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ=="
},
"electron": {
"version": "4.0.0-beta.8",
"resolved": "https://registry.npmjs.org/electron/-/electron-4.0.0-beta.8.tgz",
"integrity": "sha512-zo0Tf3t1uary5O9Skdlo37axs+vvh9CPC+MkvZnnX3MA/3kWIc+I6jN+AL4bHUdxsMVtjbY8KnusjnCjKSb0sg==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-4.0.0.tgz",
"integrity": "sha512-3XPG/3IXlvnT1oe1K6zEushoD0SKbP8xwdrL10EWGe6k2iOV4hSHqJ8vWnR8yZ7VbSXmBRfomEFDNAo/q/cwKw==",
"dev": true,
"requires": {
"@types/node": "^8.0.24",
@@ -2526,10 +2527,20 @@
}
}
},
"electron-notarize": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/electron-notarize/-/electron-notarize-0.0.5.tgz",
"integrity": "sha512-YzrqZ6RDQ7Wt2RWlxzRoQUuxnTeXrfp7laH7XKcmQqrZ6GaAr50DMPvFMpqDKdrZSHSbcgZgB7ktIQbjvITmCQ==",
"dev": true,
"requires": {
"debug": "^4.1.0",
"fs-extra": "^7.0.0"
}
},
"electron-osx-sign": {
"version": "0.4.10",
"resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.4.10.tgz",
"integrity": "sha1-vk87ibKnWh3F8eckkIGrKSnKOiY=",
"version": "0.4.11",
"resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.4.11.tgz",
"integrity": "sha512-VVd40nrnVqymvFrY9ZkOYgHJOvexHHYTR3di/SN+mjJ0OWhR1I8BRVj3U+Yamw6hnkZZNKZp52rqL5EFAAPFkQ==",
"dev": true,
"requires": {
"bluebird": "^3.5.0",
@@ -2537,7 +2548,7 @@
"debug": "^2.6.8",
"isbinaryfile": "^3.0.2",
"minimist": "^1.2.0",
"plist": "^2.1.0"
"plist": "^3.0.1"
},
"dependencies": {
"debug": {
@@ -2552,69 +2563,50 @@
}
},
"electron-packager": {
"version": "12.2.0",
"resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-12.2.0.tgz",
"integrity": "sha512-T5W/FIK4VXhYIOWxkehmz6zXt2S/sA9JZ3AL+/jeKCicQY6QVQ0K8B7W801L+GPTwbgTPycHjO+iqEf1BhZ+Iw==",
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-13.0.1.tgz",
"integrity": "sha512-fXfldaZ1wihpPaMTSGMxvCeETJwVArlnMmKafVXLJbbZwS+WTjY4iL7ju9WMQ0LNGuiiIwSMCQFxt5iA087mqg==",
"dev": true,
"requires": {
"asar": "^0.14.0",
"debug": "^3.0.0",
"debug": "^4.0.1",
"electron-download": "^4.1.1",
"electron-osx-sign": "^0.4.1",
"electron-notarize": "^0.0.5",
"electron-osx-sign": "^0.4.11",
"extract-zip": "^1.0.3",
"fs-extra": "^5.0.0",
"fs-extra": "^7.0.0",
"galactus": "^0.2.1",
"get-package-info": "^1.0.0",
"nodeify": "^1.0.1",
"parse-author": "^2.0.0",
"pify": "^3.0.0",
"plist": "^2.0.0",
"pify": "^4.0.0",
"plist": "^3.0.0",
"rcedit": "^1.0.0",
"resolve": "^1.1.6",
"sanitize-filename": "^1.6.0",
"semver": "^5.3.0",
"yargs-parser": "^10.0.0"
"yargs-parser": "^11.0.0"
},
"dependencies": {
"camelcase": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
"integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz",
"integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==",
"dev": true
},
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"dev": true,
"requires": {
"ms": "^2.1.1"
}
},
"fs-extra": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz",
"integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.2",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
}
},
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
"pify": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
"dev": true
},
"yargs-parser": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz",
"integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==",
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz",
"integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==",
"dev": true,
"requires": {
"camelcase": "^4.1.0"
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
}
}
@@ -4305,7 +4297,7 @@
},
"pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
"dev": true
},
@@ -4339,9 +4331,9 @@
}
},
"get-port": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/get-port/-/get-port-4.0.0.tgz",
"integrity": "sha512-Yy3yNI2oShgbaWg4cmPhWjkZfktEvpKI09aDX4PZzNtlU9obuYrX7x2mumQsrNxlF+Ls7OtMQW/u+X4s896bOQ=="
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/get-port/-/get-port-4.1.0.tgz",
"integrity": "sha512-4/fqAYrzrzOiqDrdeZRKXGdTGgbkfTEumGlNQPeP6Jy8w0PzN9mzeNQ3XgHaTNie8pQ3hOUkrwlZt2Fzk5H9mA=="
},
"get-proxy": {
"version": "1.1.0",
@@ -5608,12 +5600,6 @@
"resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz",
"integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU="
},
"is-promise": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-1.0.1.tgz",
"integrity": "sha1-MVc3YcBX4zwukaq56W2gjO++duU=",
"dev": true
},
"is-proto-prop": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-proto-prop/-/is-proto-prop-1.0.1.tgz",
@@ -6542,19 +6528,19 @@
"dev": true
},
"mksnapshot": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/mksnapshot/-/mksnapshot-0.3.1.tgz",
"integrity": "sha1-JQHAVldDbXQs6Vik/5LHfkDdN+Y=",
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/mksnapshot/-/mksnapshot-0.3.4.tgz",
"integrity": "sha512-FgUTiWiY+35LgL95P/MDYrBuQO5o0s3MmaWKX6ZJWoX4vMOY9vPsAv763l1OSSelL9jPsBQ/wf4bzfqTLNPSFg==",
"dev": true,
"requires": {
"decompress-zip": "0.3.0",
"fs-extra": "0.26.7",
"request": "^2.79.0"
"request": "2.x"
},
"dependencies": {
"fs-extra": {
"version": "0.26.7",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.26.7.tgz",
"resolved": "http://registry.npmjs.org/fs-extra/-/fs-extra-0.26.7.tgz",
"integrity": "sha1-muH92UiXeY7at20JGM9C0MMYT6k=",
"dev": true,
"requires": {
@@ -6582,9 +6568,9 @@
"integrity": "sha1-mi3sg4Bvuy2XXyK+7IWcoms5OqE="
},
"moment": {
"version": "2.22.2",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz",
"integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y="
"version": "2.23.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.23.0.tgz",
"integrity": "sha512-3IE39bHVqFbWWaPOMHZF98Q9c3LDKGTmypMiTM2QygGXXElkFWIH7GxfmlwmY2vwa+wmNsoYZmG2iusf1ZjJoA=="
},
"mozjpeg": {
"version": "6.0.1",
@@ -7208,10 +7194,9 @@
"integrity": "sha1-ICtIAhoMTL3i34DeFaF0Q8i0OYA="
},
"node-abi": {
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.4.5.tgz",
"integrity": "sha512-aa/UC6Nr3+tqhHGRsAuw/edz7/q9nnetBrKWxj6rpTtm+0X9T1qU7lIEHMS3yN9JwAbRiKUbRRFy1PLz/y3aaA==",
"dev": true,
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.5.1.tgz",
"integrity": "sha512-oDbFc7vCFx0RWWCweTer3hFm1u+e60N5FtGnmRV6QqvgATGFH/XRR6vqWIeBVosCYCqt6YdIr2L0exLZuEdVcQ==",
"requires": {
"semver": "^5.4.1"
}
@@ -7286,16 +7271,6 @@
"resolved": "https://registry.npmjs.org/node-status-codes/-/node-status-codes-1.0.0.tgz",
"integrity": "sha1-WuVUHQJGRdMqWPzdyc7s6nrjrC8="
},
"nodeify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/nodeify/-/nodeify-1.0.1.tgz",
"integrity": "sha1-ZKtpp7268DzhB7TwM1yHwLnpGx0=",
"dev": true,
"requires": {
"is-promise": "~1.0.0",
"promise": "~1.3.0"
}
},
"nopt": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz",
@@ -8041,26 +8016,20 @@
"integrity": "sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q=="
},
"plist": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/plist/-/plist-2.1.0.tgz",
"integrity": "sha1-V8zbeggh3yGDEhejytVOPhRqECU=",
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/plist/-/plist-3.0.1.tgz",
"integrity": "sha512-GpgvHHocGRyQm74b6FWEZZVRroHKE1I0/BTjAmySaohK+cUn+hZpbqXkc3KWgW3gQYkqcQej35FohcT0FRlkRQ==",
"dev": true,
"requires": {
"base64-js": "1.2.0",
"xmlbuilder": "8.2.2",
"base64-js": "^1.2.3",
"xmlbuilder": "^9.0.7",
"xmldom": "0.1.x"
},
"dependencies": {
"base64-js": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.0.tgz",
"integrity": "sha1-o5mS1yNYSBGYK+XikLtqU9hnAPE=",
"dev": true
},
"xmlbuilder": {
"version": "8.2.2",
"resolved": "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz",
"integrity": "sha1-aSSGc0ELS6QuGmE2VR0pIjNap3M=",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz",
"integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==",
"dev": true
}
}
@@ -8490,15 +8459,6 @@
}
}
},
"promise": {
"version": "1.3.0",
"resolved": "http://registry.npmjs.org/promise/-/promise-1.3.0.tgz",
"integrity": "sha1-5cyaTIJ45GZP/twBx9qEhCsEAXU=",
"dev": true,
"requires": {
"is-promise": "~1"
}
},
"proto-list": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
@@ -8826,17 +8786,6 @@
}
}
},
"request-promise": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.2.tgz",
"integrity": "sha1-0epG1lSm7k+O5qT+oQGMIpEZBLQ=",
"requires": {
"bluebird": "^3.5.0",
"request-promise-core": "1.1.1",
"stealthy-require": "^1.1.0",
"tough-cookie": ">=2.3.3"
}
},
"request-promise-core": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz",
@@ -9229,9 +9178,9 @@
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
},
"simple-node-logger": {
"version": "0.93.40",
"resolved": "https://registry.npmjs.org/simple-node-logger/-/simple-node-logger-0.93.40.tgz",
"integrity": "sha512-ByWh6D6DgXteLICr5Bhca5CIDxuGt7xOysulWPIrBcwTT6ZCNF4SrwmtIDhC+cSNlJsz/fGytn7mH2Zqyh9euA==",
"version": "18.12.21",
"resolved": "https://registry.npmjs.org/simple-node-logger/-/simple-node-logger-18.12.21.tgz",
"integrity": "sha512-I2dA9JLiNrdTsmOI2bT0mIQNEKhTKbj9OM8tny3Il/5R6QdJHlWrv1NDdS+6UQHEnYV1413FnwgqTjzmV0ctCw==",
"requires": {
"lodash": "^4.17.10",
"moment": "^2.20.1"
+8 -9
View File
@@ -2,7 +2,7 @@
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
"version": "0.25.1-beta",
"version": "0.27.0-beta",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
@@ -26,7 +26,7 @@
"cls-hooked": "4.2.2",
"commonmark": "0.28.1",
"cookie-parser": "1.4.3",
"debug": "4.1.0",
"debug": "4.1.1",
"ejs": "2.6.1",
"electron-debug": "2.0.0",
"electron-dl": "1.12.0",
@@ -34,7 +34,7 @@
"express": "4.16.4",
"express-session": "1.15.6",
"fs-extra": "7.0.1",
"get-port": "4.0.0",
"get-port": "4.1.0",
"helmet": "3.15.0",
"html": "1.0.0",
"image-type": "3.0.0",
@@ -45,19 +45,18 @@
"ini": "1.3.5",
"jimp": "0.6.0",
"mime-types": "^2.1.21",
"moment": "2.22.2",
"moment": "2.23.0",
"multer": "1.4.1",
"node-abi": "2.5.1",
"open": "0.0.5",
"rand-token": "0.4.0",
"rcedit": "1.1.1",
"request": "2.88.0",
"request-promise": "4.2.2",
"rimraf": "2.6.2",
"sanitize-filename": "1.6.1",
"sax": "^1.2.4",
"serve-favicon": "2.5.0",
"session-file-store": "1.2.0",
"simple-node-logger": "0.93.40",
"simple-node-logger": "18.12.21",
"sqlite": "3.0.0",
"tar-stream": "1.6.2",
"turndown": "5.0.1",
@@ -67,9 +66,9 @@
},
"devDependencies": {
"devtron": "1.4.0",
"electron": "4.0.0-beta.8",
"electron": "4.0.0",
"electron-compile": "6.4.3",
"electron-packager": "12.2.0",
"electron-packager": "13.0.1",
"electron-rebuild": "1.8.2",
"lorem-ipsum": "1.0.6",
"tape": "4.9.1",
+10 -10
View File
@@ -369,6 +369,16 @@ class Note extends Entity {
*/
async getRelationValue(name) { return await this.getAttributeValue(RELATION, name); }
/**
* @param {string} name
* @returns {Promise<Note>|null} target note of the relation or null (if target is empty or note was not found)
*/
async getRelationTarget(name) {
const relation = await this.getRelation(name);
return relation ? await repository.getNote(relation.value) : null;
}
/**
* Based on enabled, label is either set or removed.
*
@@ -425,16 +435,6 @@ class Note extends Entity {
*/
async removeRelation(name, value) { return await this.removeAttribute(RELATION, name, value); }
/**
* @param {string} name
* @returns {Promise<Note>|null} target note of the relation or null (if target is empty or note was not found)
*/
async getRelationTarget(name) {
const relation = await this.getRelation(name);
return relation ? await repository.getNote(relation.value) : null;
}
/**
* @return {Promise<string[]>} return list of all descendant noteIds of this note. Returning just noteIds because number of notes can be huge. Includes also this note's noteId
*/
@@ -1,37 +1,41 @@
import addLinkDialog from '../dialogs/add_link.js';
import jumpToNoteDialog from '../dialogs/jump_to_note.js';
import attributesDialog from '../dialogs/attributes.js';
import noteRevisionsDialog from '../dialogs/note_revisions.js';
import noteSourceDialog from '../dialogs/note_source.js';
import recentChangesDialog from '../dialogs/recent_changes.js';
import optionsDialog from '../dialogs/options.js';
import sqlConsoleDialog from '../dialogs/sql_console.js';
import markdownImportDialog from '../dialogs/markdown_import.js';
import exportDialog from '../dialogs/export.js';
import addLinkDialog from './dialogs/add_link.js';
import jumpToNoteDialog from './dialogs/jump_to_note.js';
import attributesDialog from './dialogs/attributes.js';
import noteRevisionsDialog from './dialogs/note_revisions.js';
import noteSourceDialog from './dialogs/note_source.js';
import recentChangesDialog from './dialogs/recent_changes.js';
import optionsDialog from './dialogs/options.js';
import sqlConsoleDialog from './dialogs/sql_console.js';
import markdownImportDialog from './dialogs/markdown_import.js';
import exportDialog from './dialogs/export.js';
import cloning from './cloning.js';
import contextMenu from './tree_context_menu.js';
import dragAndDropSetup from './drag_and_drop.js';
import exportService from './export.js';
import link from './link.js';
import messagingService from './messaging.js';
import noteDetailService from './note_detail.js';
import noteType from './note_type.js';
import protected_session from './protected_session.js';
import searchNotesService from './search_notes.js';
import FrontendScriptApi from './frontend_script_api.js';
import ScriptContext from './script_context.js';
import sync from './sync.js';
import treeService from './tree.js';
import treeChanges from './branches.js';
import treeUtils from './tree_utils.js';
import utils from './utils.js';
import server from './server.js';
import entrypoints from './entrypoints.js';
import tooltip from './tooltip.js';
import bundle from "./bundle.js";
import treeCache from "./tree_cache.js";
import libraryLoader from "./library_loader.js";
import cloning from './services/cloning.js';
import contextMenu from './services/tree_context_menu.js';
import dragAndDropSetup from './services/drag_and_drop.js';
import exportService from './services/export.js';
import link from './services/link.js';
import messagingService from './services/messaging.js';
import noteDetailService from './services/note_detail.js';
import noteType from './services/note_type.js';
import protected_session from './services/protected_session.js';
import searchNotesService from './services/search_notes.js';
import FrontendScriptApi from './services/frontend_script_api.js';
import ScriptContext from './services/script_context.js';
import sync from './services/sync.js';
import treeService from './services/tree.js';
import treeChanges from './services/branches.js';
import treeUtils from './services/tree_utils.js';
import utils from './services/utils.js';
import server from './services/server.js';
import entrypoints from './services/entrypoints.js';
import noteTooltipService from './services/note_tooltip.js';
import bundle from "./services/bundle.js";
import treeCache from "./services/tree_cache.js";
import libraryLoader from "./services/library_loader.js";
import hoistedNoteService from './services/hoisted_note.js';
import noteTypeService from './services/note_type.js';
import linkService from './services/link.js';
import noteAutocompleteService from './services/note_autocomplete.js';
// required for CKEditor image upload plugin
window.glob.getCurrentNode = treeService.getCurrentNode;
@@ -84,6 +88,8 @@ $(document).on("click", "button[data-help-page]", e => {
$("#logout-button").toggle(!utils.isElectron());
$("#tree").on("click", ".unhoist-button", hoistedNoteService.unhoist);
if (utils.isElectron()) {
require('electron').ipcRenderer.on('create-day-sub-note', async function(event, parentNoteId) {
// this might occur when day note had to be created
@@ -117,6 +123,13 @@ if (utils.isElectron() && utils.isMac()) {
utils.bindShortcut('ctrl+a', () => exec('selectAll'));
utils.bindShortcut('ctrl+z', () => exec('undo'));
utils.bindShortcut('ctrl+y', () => exec('redo'));
utils.bindShortcut('meta+c', () => exec("copy"));
utils.bindShortcut('meta+v', () => exec('paste'));
utils.bindShortcut('meta+x', () => exec('cut'));
utils.bindShortcut('meta+a', () => exec('selectAll'));
utils.bindShortcut('meta+z', () => exec('undo'));
utils.bindShortcut('meta+y', () => exec('redo'));
}
$("#export-note-button").click(function () {
@@ -131,6 +144,12 @@ treeService.showTree();
entrypoints.registerEntrypoints();
tooltip.setupTooltip();
noteTooltipService.setupGlobalTooltip();
bundle.executeStartupBundles();
noteTypeService.init();
linkService.init();
noteAutocompleteService.init();
@@ -169,6 +169,8 @@ function AttributesModel() {
infoService.showMessage("Attributes have been saved.");
attributeService.refreshAttributes();
noteDetailService.reload();
};
function addLastEmptyRow() {
@@ -2,6 +2,7 @@ import treeService from '../services/tree.js';
import server from '../services/server.js';
import treeCache from "../services/tree_cache.js";
import treeUtils from "../services/tree_utils.js";
import infoService from "../services/info.js";
const $dialog = $("#branch-prefix-dialog");
const $form = $("#branch-prefix-form");
@@ -24,7 +25,7 @@ async function showDialog() {
const noteTitle = await treeUtils.getNoteTitle(currentNode.data.noteId);
$noteTitle.html(noteTitle);
$noteTitle.text(" - " + noteTitle);
}
async function savePrefix() {
@@ -35,6 +36,8 @@ async function savePrefix() {
await treeService.setPrefix(branchId, prefix);
$dialog.modal('hide');
infoService.showMessage("Branch prefix has been saved.");
}
$form.submit(() => {
@@ -0,0 +1,36 @@
class Attribute {
constructor(treeCache, row) {
this.treeCache = treeCache;
/** @param {string} attributeId */
this.attributeId = row.attributeId;
/** @param {string} noteId */
this.noteId = row.noteId;
/** @param {string} type */
this.type = row.type;
/** @param {string} name */
this.name = row.name;
/** @param {string} value */
this.value = row.value;
/** @param {int} position */
this.position = row.position;
/** @param {boolean} isInheritable */
this.isInheritable = row.isInheritable;
/** @param {boolean} isDeleted */
this.isDeleted = row.isDeleted;
/** @param {string} dateCreated */
this.dateCreated = row.dateCreated;
/** @param {string} dateModified */
this.dateModified = row.dateModified;
}
/** @returns {NoteShort} */
async getNote() {
return await this.treeCache.getNote(this.noteId);
}
get toString() {
return `Attribute(attributeId=${this.attributeId}, type=${this.type}, name=${this.name})`;
}
}
export default Attribute;
@@ -1,3 +1,10 @@
import server from '../services/server.js';
const LABEL = 'label';
const LABEL_DEFINITION = 'label-definition';
const RELATION = 'relation';
const RELATION_DEFINITION = 'relation-definition';
/**
* This note's representation is used in note tree and is kept in TreeCache.
* Its notable omission is the note content.
@@ -71,6 +78,140 @@ class NoteShort {
return await this.treeCache.getNotes(this.getChildNoteIds());
}
/**
* @param {string} [name] - attribute name to filter
* @returns {Promise<Attribute[]>}
*/
async getAttributes(name) {
if (!this.attributeCache) {
this.attributeCache = await server.get('notes/' + this.noteId + '/attributes');
}
if (name) {
return this.attributeCache.filter(attr => attr.name === name);
}
else {
return this.attributeCache;
}
}
/**
* @param {string} [name] - label name to filter
* @returns {Promise<Attribute[]>} all note's labels (attributes with type label), including inherited ones
*/
async getLabels(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === LABEL);
}
/**
* @param {string} [name] - label name to filter
* @returns {Promise<Attribute[]>} all note's label definitions, including inherited ones
*/
async getLabelDefinitions(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === LABEL_DEFINITION);
}
/**
* @param {string} [name] - relation name to filter
* @returns {Promise<Attribute[]>} all note's relations (attributes with type relation), including inherited ones
*/
async getRelations(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === RELATION);
}
/**
* @param {string} [name] - relation name to filter
* @returns {Promise<Attribute[]>} all note's relation definitions including inherited ones
*/
async getRelationDefinitions(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === RELATION_DEFINITION);
}
/**
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {Promise<boolean>} true if note has an attribute with given type and name (including inherited)
*/
async hasAttribute(type, name) {
return !!await this.getAttribute(type, name);
}
/**
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {Promise<Attribute>} attribute of given type and name. If there's more such attributes, first is returned. Returns null if there's no such attribute belonging to this note.
*/
async getAttribute(type, name) {
const attributes = await this.getAttributes();
return attributes.find(attr => attr.type === type && attr.name === name);
}
/**
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {Promise<string>} attribute value of given type and name or null if no such attribute exists.
*/
async getAttributeValue(type, name) {
const attr = await this.getAttribute(type, name);
return attr ? attr.value : null;
}
/**
* @param {string} name - label name
* @returns {Promise<boolean>} true if label exists (including inherited)
*/
async hasLabel(name) { return await this.hasAttribute(LABEL, name); }
/**
* @param {string} name - relation name
* @returns {Promise<boolean>} true if relation exists (including inherited)
*/
async hasRelation(name) { return await this.hasAttribute(RELATION, name); }
/**
* @param {string} name - label name
* @returns {Promise<Attribute>} label if it exists, null otherwise
*/
async getLabel(name) { return await this.getAttribute(LABEL, name); }
/**
* @param {string} name - relation name
* @returns {Promise<Attribute>} relation if it exists, null otherwise
*/
async getRelation(name) { return await this.getAttribute(RELATION, name); }
/**
* @param {string} name - label name
* @returns {Promise<string>} label value if label exists, null otherwise
*/
async getLabelValue(name) { return await this.getAttributeValue(LABEL, name); }
/**
* @param {string} name - relation name
* @returns {Promise<string>} relation value if relation exists, null otherwise
*/
async getRelationValue(name) { return await this.getAttributeValue(RELATION, name); }
/**
* @param {string} name
* @returns {Promise<Note>|null} target note of the relation or null (if target is empty or note was not found)
*/
async getRelationTarget(name) {
const relation = await this.getRelation(name);
return relation ? await repository.getNote(relation.value) : null;
}
/**
* Clear note's attributes cache to force fresh reload for next attribute request.
* Cache is note instance scoped.
*/
invalidateAttributeCache() {
this.attributeCache = null;
}
get toString() {
return `Note(noteId=${this.noteId}, title=${this.title})`;
}
@@ -79,6 +220,7 @@ class NoteShort {
const dto = Object.assign({}, this);
delete dto.treeCache;
delete dto.archived;
delete dto.attributeCache;
return dto;
}
+128
View File
@@ -0,0 +1,128 @@
import treeService from "./services/tree.js";
import noteDetailService from "./services/note_detail.js";
import dragAndDropSetup from "./services/drag_and_drop.js";
import treeCache from "./services/tree_cache.js";
import treeBuilder from "./services/tree_builder.js";
import contextMenuWidget from "./services/context_menu.js";
import ContextMenuItemsContainer from "./services/context_menu_items_container.js";
import treeChangesService from "./services/branches.js";
import utils from "./services/utils.js";
import treeUtils from "./services/tree_utils.js";
const $leftPane = $("#left-pane");
const $tree = $("#tree");
const $detail = $("#detail");
const $closeDetailButton = $("#close-detail-button");
function togglePanes() {
if (!$leftPane.is(":visible") || !$detail.is(":visible")) {
$detail.toggleClass("d-none");
$leftPane.toggleClass("d-none");
}
}
function showDetailPane() {
if (!$detail.is(":visible")) {
$detail.removeClass("d-none");
$leftPane.addClass("d-none");
}
}
$closeDetailButton.click(() => {
// no page is opened
document.location.hash = '-';
togglePanes();
});
async function showTree() {
const tree = await treeService.loadTree();
$tree.fancytree({
autoScroll: true,
extensions: ["dnd5", "clones"],
source: tree,
scrollParent: $tree,
minExpandLevel: 2, // root can't be collapsed
activate: (event, data) => {
const node = data.node;
const noteId = node.data.noteId;
treeService.clearSelectedNodes();
treeService.setCurrentNotePathToHash(node);
showDetailPane();
noteDetailService.switchToNote(noteId, true);
},
expand: (event, data) => treeService.setExpandedToServer(data.node.data.branchId, true),
collapse: (event, data) => treeService.setExpandedToServer(data.node.data.branchId, false),
init: (event, data) => treeService.treeInitialized(), // don't collapse to short form
dnd5: dragAndDropSetup,
lazyLoad: function(event, data) {
const noteId = data.node.data.noteId;
data.result = treeCache.getNote(noteId).then(note => treeBuilder.prepareBranch(note));
},
clones: {
highlightActiveClones: true
}
});
}
$("#note-menu-button").click(async e => {
const node = treeService.getCurrentNode();
const branch = await treeCache.getBranch(node.data.branchId);
const note = await treeCache.getNote(node.data.noteId);
const parentNote = await treeCache.getNote(branch.parentNoteId);
const isNotRoot = note.noteId !== 'root';
const itemsContainer = new ContextMenuItemsContainer([
{title: "Insert note after", cmd: "insertNoteAfter", uiIcon: "plus"},
{title: "Insert child note", cmd: "insertChildNote", uiIcon: "plus"},
{title: "Delete this note", cmd: "delete", uiIcon: "trash"}
]);
itemsContainer.enableItem("insertNoteAfter", isNotRoot && parentNote.type !== 'search');
itemsContainer.enableItem("insertChildNote", note.type !== 'search');
itemsContainer.enableItem("delete", isNotRoot && parentNote.type !== 'search');
contextMenuWidget.initContextMenu(e, itemsContainer, (event, cmd) => {
if (cmd === "insertNoteAfter") {
const parentNoteId = node.data.parentNoteId;
const isProtected = treeUtils.getParentProtectedStatus(node);
treeService.createNote(node, parentNoteId, 'after', isProtected);
}
else if (cmd === "insertChildNote") {
treeService.createNote(node, node.data.noteId, 'into');
}
else if (cmd === "delete") {
treeChangesService.deleteNodes([node]);
// move to the tree
togglePanes();
}
else {
throw new Error("Unrecognized command " + cmd);
}
});
});
$("#switch-to-desktop-button").click(() => {
utils.setCookie('trilium-device', 'desktop');
utils.reloadApp();
});
$("#log-out-button").click(() => {
$("#logout-form").submit();
});
// this is done so that startNotePath is not used
if (!document.location.hash) {
document.location.hash = '-';
}
showTree();
+15 -2
View File
@@ -13,13 +13,25 @@ const $savedIndicator = $("#saved-indicator");
let attributePromise;
async function refreshAttributes() {
function invalidateAttributes() {
attributePromise = null;
}
function reloadAttributes() {
attributePromise = server.get('notes/' + noteDetailService.getCurrentNoteId() + '/attributes');
}
async function refreshAttributes() {
reloadAttributes();
await showAttributes();
}
async function getAttributes() {
if (!attributePromise) {
reloadAttributes();
}
return await attributePromise;
}
@@ -286,5 +298,6 @@ async function promotedAttributeChanged(event) {
export default {
getAttributes,
showAttributes,
refreshAttributes
refreshAttributes,
invalidateAttributes
}
@@ -1,9 +1,11 @@
const $contextMenuContainer = $("#context-menu-container");
function initContextMenu(event, contextMenuItems, selectContextMenuItem) {
function initContextMenu(event, itemContainer, selectContextMenuItem) {
event.stopPropagation();
$contextMenuContainer.empty();
for (const item of contextMenuItems) {
for (const item of itemContainer.getItems()) {
if (item.title === '----') {
$contextMenuContainer.append($("<div>").addClass("dropdown-divider"));
} else {
@@ -0,0 +1,28 @@
class ContextMenuItemsContainer {
constructor(items) {
// clone the item array and the items themselves
this.items = items.map(item => Object.assign({}, item));
}
hideItem(cmd, hidden = true) {
if (hidden) {
this.items = this.items.filter(item => item.cmd !== cmd);
}
}
enableItem(cmd, enabled) {
const item = this.items.find(item => item.cmd === cmd);
if (!item) {
throw new Error(`Command ${cmd} has not been found!`);
}
item.enabled = enabled;
}
getItems() {
return this.items;
}
}
export default ContextMenuItemsContainer;
@@ -95,7 +95,7 @@ function registerEntrypoints() {
}
});
$(document).bind('keydown', 'ctrl+f', () => {
function openInPageSearch() {
if (utils.isElectron()) {
const $searchWindowWebview = $(".electron-in-page-search-window");
$searchWindowWebview.show();
@@ -113,7 +113,13 @@ function registerEntrypoints() {
return false;
}
});
}
utils.bindShortcut('ctrl+f', openInPageSearch);
if (utils.isMac()) {
utils.bindShortcut('meta+f', openInPageSearch);
}
// FIXME: do we really need these at this point?
utils.bindShortcut("ctrl+shift+up", () => {
@@ -6,6 +6,7 @@ import linkService from './link.js';
import treeCache from './tree_cache.js';
import noteDetailService from './note_detail.js';
import noteTypeService from './note_type.js';
import noteTooltipService from './note_tooltip.js';
/**
* This is the main frontend API interface for scripts. It's published in the local "api" object.
@@ -202,6 +203,19 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null) {
*/
this.getCurrentNoteContent = noteDetailService.getCurrentNoteContent;
/**
* This method checks whether user navigated away from the note from which the scripts has been started.
* This is necessary because script execution is async and by the time it is finished, the user might have
* already navigated away from this page - the end result would be that script might return data for the wrong
* note.
*
* @method
* @return {boolean} returns true if the original note is still loaded, false if user switched to another
*/
this.isNoteStillLoaded = () => {
return this.originEntity.noteId === noteDetailService.getCurrentNoteId();
};
/**
* @method
* @param {function} func - callback called on note change as user is typing (not necessarily tied to save event)
@@ -225,6 +239,12 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null) {
* @param {array} types - list of mime types to be used
*/
this.setCodeMimeTypes = noteTypeService.setCodeMimeTypes;
/**
* @method
* @param {object} $el - jquery object on which to setup the tooltip
*/
this.setupElementTooltip = noteTooltipService.setupElementTooltip
}
export default FrontendScriptApi;
@@ -0,0 +1,33 @@
import optionsInit from './options_init.js';
import server from "./server.js";
import tree from "./tree.js";
let hoistedNoteId;
optionsInit.optionsReady.then(options => {
hoistedNoteId = options['hoistedNoteId'];
});
async function getHoistedNoteId() {
await optionsInit.optionsReady;
return hoistedNoteId;
}
async function setHoistedNoteId(noteId) {
hoistedNoteId = noteId;
await server.put('options/hoistedNoteId/' + noteId);
await tree.reload();
}
async function unhoist() {
await setHoistedNoteId('root');
}
export default {
getHoistedNoteId,
setHoistedNoteId,
unhoist
}
+12 -9
View File
@@ -88,17 +88,19 @@ function addTextToEditor(text) {
});
}
ko.bindingHandlers.noteLink = {
init: async function(element, valueAccessor, allBindings, viewModel, bindingContext) {
const noteId = ko.unwrap(valueAccessor());
function init() {
ko.bindingHandlers.noteLink = {
init: async function(element, valueAccessor, allBindings, viewModel, bindingContext) {
const noteId = ko.unwrap(valueAccessor());
if (noteId) {
const link = await createNoteLink(noteId);
if (noteId) {
const link = await createNoteLink(noteId);
$(element).append(link);
$(element).append(link);
}
}
}
};
};
}
// when click on link popup, in case of internal link, just go the the referenced note instead of default behavior
// of opening the link in new window/tab
@@ -124,5 +126,6 @@ export default {
getNotePathFromUrl,
createNoteLink,
addLinkToEditor,
addTextToEditor
addTextToEditor,
init
};
@@ -101,40 +101,42 @@ function initNoteAutocomplete($el, options) {
return $el;
}
$.fn.getSelectedPath = function() {
if (!$(this).val().trim()) {
return "";
}
else {
return $(this).attr(SELECTED_PATH_KEY);
}
};
function init() {
$.fn.getSelectedPath = function () {
if (!$(this).val().trim()) {
return "";
} else {
return $(this).attr(SELECTED_PATH_KEY);
}
};
$.fn.setSelectedPath = function(path) {
path = path || "";
$.fn.setSelectedPath = function (path) {
path = path || "";
$(this).attr(SELECTED_PATH_KEY, path);
$(this).attr(SELECTED_PATH_KEY, path);
$(this)
.closest(".input-group")
.find(".go-to-selected-note-button")
.toggleClass("disabled", !path.trim())
.attr(SELECTED_PATH_KEY, path); // we also set attr here so tooltip can be displayed
};
$(this)
.closest(".input-group")
.find(".go-to-selected-note-button")
.toggleClass("disabled", !path.trim())
.attr(SELECTED_PATH_KEY, path); // we also set attr here so tooltip can be displayed
};
ko.bindingHandlers.noteAutocomplete = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
initNoteAutocomplete($(element));
ko.bindingHandlers.noteAutocomplete = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
initNoteAutocomplete($(element));
$(element).setSelectedPath(bindingContext.$data.selectedPath);
$(element).setSelectedPath(bindingContext.$data.selectedPath);
$(element).on('autocomplete:selected', function(event, suggestion, dataset) {
bindingContext.$data.selectedPath = $(element).val().trim() ? suggestion.path : '';
});
}
};
$(element).on('autocomplete:selected', function (event, suggestion, dataset) {
bindingContext.$data.selectedPath = $(element).val().trim() ? suggestion.path : '';
});
}
};
}
export default {
initNoteAutocomplete,
showRecentNotes
showRecentNotes,
init
}
+26 -9
View File
@@ -17,6 +17,7 @@ import noteDetailRender from './note_detail_render.js';
import noteDetailRelationMap from './note_detail_relation_map.js';
import bundleService from "./bundle.js";
import attributeService from "./attributes.js";
import utils from "./utils.js";
const $noteTitle = $("#note-title");
@@ -108,6 +109,10 @@ function onNoteChange(func) {
async function saveNote() {
const note = getCurrentNote();
if (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) {
return;
}
note.title = $noteTitle.val();
note.content = getCurrentNoteContent(note);
@@ -177,8 +182,14 @@ async function loadNoteDetail(noteId) {
// only now that we're in sync with tree active node we will switch currentNote
currentNote = loadedNote;
// needs to happend after loading the note itself because it references current noteId
attributeService.refreshAttributes();
if (utils.isDesktop()) {
// needs to happen after loading the note itself because it references current noteId
attributeService.refreshAttributes();
}
else {
// mobile usually doesn't need attributes so we just invalidate
attributeService.invalidateAttributes();
}
if (isNewNoteCreated) {
isNewNoteCreated = false;
@@ -197,8 +208,10 @@ async function loadNoteDetail(noteId) {
try {
$noteTitle.val(currentNote.title);
noteTypeService.setNoteType(currentNote.type);
noteTypeService.setNoteMime(currentNote.mime);
if (utils.isDesktop()) {
noteTypeService.setNoteType(currentNote.type);
noteTypeService.setNoteMime(currentNote.mime);
}
for (const componentType in components) {
if (componentType !== currentNote.type) {
@@ -214,6 +227,8 @@ async function loadNoteDetail(noteId) {
return;
}
$noteTitle.removeAttr("readonly"); // this can be set by protected session service
await getComponent(currentNote.type).show();
}
finally {
@@ -223,15 +238,17 @@ async function loadNoteDetail(noteId) {
treeService.setBranchBackgroundBasedOnProtectedStatus(noteId);
// after loading new note make sure editor is scrolled to the top
$noteDetailWrapper.scrollTop(0);
getComponent(currentNote.type).scrollToTop();
$scriptArea.empty();
if (utils.isDesktop()) {
$scriptArea.empty();
await bundleService.executeRelationBundles(getCurrentNote(), 'runOnNoteView');
await bundleService.executeRelationBundles(getCurrentNote(), 'runOnNoteView');
await attributeService.showAttributes();
await attributeService.showAttributes();
await showChildrenOverview();
await showChildrenOverview();
}
}
async function showChildrenOverview() {
@@ -3,6 +3,7 @@ import bundleService from "./bundle.js";
import infoService from "./info.js";
import server from "./server.js";
import noteDetailService from "./note_detail.js";
import utils from "./utils.js";
let codeEditor = null;
@@ -94,7 +95,7 @@ function onNoteChange(func) {
codeEditor.on('change', func);
}
$(document).bind('keydown', "ctrl+return", executeCurrentNote);
utils.bindShortcut("ctrl+return", executeCurrentNote);
$executeScriptButton.click(executeCurrentNote);
@@ -107,5 +108,6 @@ export default {
if (codeEditor) {
codeEditor.setValue('');
}
}
},
scrollToTop: () => $component.scrollTop(0)
}
@@ -53,5 +53,6 @@ export default {
getContent: () => null,
focus: () => null,
onNoteChange: () => null,
cleanup: () => null
cleanup: () => null,
scrollToTop: () => null
}
@@ -71,5 +71,6 @@ export default {
getContent: () => null,
focus: () => null,
onNoteChange: () => null,
cleanup: () => null
cleanup: () => null,
scrollToTop: () => $component.scrollTop(0)
}
@@ -9,6 +9,7 @@ import attributeAutocompleteService from "./attribute_autocomplete.js";
import promptDialog from "../dialogs/prompt.js";
import infoDialog from "../dialogs/info.js";
import confirmDialog from "../dialogs/confirm.js";
import ContextMenuItemsContainer from "./context_menu_items_container.js";
const $component = $("#note-detail-relation-map");
const $relationMapContainer = $("#relation-map-container");
@@ -80,7 +81,16 @@ const linkOverlays = [
function loadMapData() {
const currentNote = noteDetailService.getCurrentNote();
mapData = {
notes: []
notes: [],
// it is important to have this exact value here so that initial transform is same as this
// which will guarantee note won't be saved on first conversion to relation map note type
// this keeps the principle that note type change doesn't destroy note content unless user
// does some actual change
transform: {
x: 0,
y: 0,
scale: 1
}
};
if (currentNote.content) {
@@ -304,9 +314,9 @@ function connectionContextMenuHandler(connection, event) {
event.preventDefault();
event.stopPropagation();
const contextMenuItems = [ {title: "Remove relation", cmd: "remove", uiIcon: "trash"} ];
const contextMenuItemsContainer = new ContextMenuItemsContainer([ {title: "Remove relation", cmd: "remove", uiIcon: "trash"} ]);
contextMenuWidget.initContextMenu(event, contextMenuItems, async (event, cmd) => {
contextMenuWidget.initContextMenu(event, contextMenuItemsContainer, async (event, cmd) => {
if (cmd === 'remove') {
if (!await confirmDialog.confirm("Are you sure you want to remove the relation?")) {
return;
@@ -380,12 +390,12 @@ async function connectionCreatedHandler(info, originalEvent) {
}
$relationMapContainer.on("contextmenu", ".note-box", e => {
const contextMenuItems = [
const contextMenuItemsContainer = new ContextMenuItemsContainer([
{title: "Remove note", cmd: "remove", uiIcon: "trash"},
{title: "Edit title", cmd: "edit-title", uiIcon: "pencil"},
];
]);
contextMenuWidget.initContextMenu(e, contextMenuItems, noteContextMenuHandler);
contextMenuWidget.initContextMenu(e, contextMenuItemsContainer, noteContextMenuHandler);
return false;
});
@@ -566,5 +576,6 @@ export default {
getContent: () => JSON.stringify(mapData),
focus: () => null,
onNoteChange: () => null,
cleanup
cleanup,
scrollToTop: () => null
}
@@ -37,5 +37,6 @@ export default {
getContent: () => "",
focus: () => null,
onNoteChange: () => null,
cleanup: () => $noteDetailRenderContent.empty()
cleanup: () => $noteDetailRenderContent.empty(),
scrollToTop: () => $component.scrollTop(0)
}
@@ -30,5 +30,6 @@ export default {
show,
focus: () => null,
onNoteChange: () => null,
cleanup: () => null
cleanup: () => null,
scrollToTop: () => null
}
@@ -74,5 +74,6 @@ export default {
if (textEditor) {
textEditor.setData('');
}
}
},
scrollToTop: () => $component.scrollTop(0)
}
@@ -3,64 +3,72 @@ import treeUtils from "./tree_utils.js";
import linkService from "./link.js";
import server from "./server.js";
function setupTooltip() {
$(document).on("mouseenter", "a", async function() {
const $link = $(this);
if ($link.hasClass("no-tooltip-preview") || $link.hasClass("disabled")) {
return;
}
// this is to avoid showing tooltip from inside CKEditor link editor dialog
if ($link.closest(".ck-link-actions").length) {
return;
}
let notePath = linkService.getNotePathFromUrl($link.attr("href"));
if (!notePath) {
notePath = $link.attr("data-note-path");
}
if (!notePath) {
return;
}
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
const notePromise = noteDetailService.loadNote(noteId);
const attributePromise = server.get('notes/' + noteId + '/attributes');
const [note, attributes] = await Promise.all([notePromise, attributePromise]);
const html = await renderTooltip(note, attributes);
// we need to check if we're still hovering over the element
// since the operation to get tooltip content was async, it is possible that
// we now create tooltip which won't close because it won't receive mouseleave event
if ($(this).is(":hover")) {
$(this).tooltip({
delay: {"show": 300, "hide": 100},
container: 'body',
placement: 'auto',
trigger: 'manual',
boundary: 'window',
title: html,
html: true
});
$(this).tooltip('show');
}
});
$(document).on("mouseleave", "a", function() {
$(this).tooltip('dispose');
});
function setupGlobalTooltip() {
$(document).on("mouseenter", "a", mouseEnterHandler);
$(document).on("mouseleave", "a", mouseLeaveHandler);
// close any tooltip after click, this fixes the problem that sometimes tooltips remained on the screen
$(document).on("click", () => $('.tooltip').remove());
}
function setupElementTooltip($el) {
$el.on('mouseenter', mouseEnterHandler);
$el.on('mouseleave', mouseLeaveHandler);
}
async function mouseEnterHandler() {
const $link = $(this);
if ($link.hasClass("no-tooltip-preview") || $link.hasClass("disabled")) {
return;
}
// this is to avoid showing tooltip from inside CKEditor link editor dialog
if ($link.closest(".ck-link-actions").length) {
return;
}
let notePath = linkService.getNotePathFromUrl($link.attr("href"));
if (!notePath) {
notePath = $link.attr("data-note-path");
}
if (!notePath) {
return;
}
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
const notePromise = noteDetailService.loadNote(noteId);
const attributePromise = server.get('notes/' + noteId + '/attributes');
const [note, attributes] = await Promise.all([notePromise, attributePromise]);
const html = await renderTooltip(note, attributes);
// we need to check if we're still hovering over the element
// since the operation to get tooltip content was async, it is possible that
// we now create tooltip which won't close because it won't receive mouseleave event
if ($(this).is(":hover")) {
$(this).tooltip({
delay: {"show": 300, "hide": 100},
container: 'body',
placement: 'auto',
trigger: 'manual',
boundary: 'window',
title: html,
html: true
});
$(this).tooltip('show');
}
}
function mouseLeaveHandler() {
$(this).tooltip('dispose');
}
async function renderTooltip(note, attributes) {
let content = '';
const promoted = attributes.filter(attr =>
@@ -103,7 +111,9 @@ async function renderTooltip(note, attributes) {
}
if (note.type === 'text') {
content += note.content;
// surround with <div> for a case when note.content is pure text (e.g. "[protected]") which
// then fails the jquery non-empty text test
content += '<div>' + note.content + '</div>';
}
else if (note.type === 'code') {
content += $("<pre>")
@@ -125,5 +135,6 @@ async function renderTooltip(note, attributes) {
}
export default {
setupTooltip
setupGlobalTooltip,
setupElementTooltip
}
+8 -3
View File
@@ -44,7 +44,7 @@ const DEFAULT_MIME_TYPES = [
{ mime: 'text/x-yaml', title: 'YAML' }
];
const noteTypeModel = new NoteTypeModel();
let noteTypeModel;
function NoteTypeModel() {
const self = this;
@@ -153,7 +153,11 @@ function NoteTypeModel() {
}
}
ko.applyBindings(noteTypeModel, document.getElementById('note-type-wrapper'));
function init() {
noteTypeModel = new NoteTypeModel();
ko.applyBindings(noteTypeModel, document.getElementById('note-type-wrapper'));
}
export default {
getNoteType: () => noteTypeModel.type(),
@@ -168,5 +172,6 @@ export default {
getDefaultCodeMimeTypes: () => DEFAULT_MIME_TYPES.slice(),
getCodeMimeTypes: () => noteTypeModel.codeMimeTypes(),
setCodeMimeTypes: types => noteTypeModel.codeMimeTypes(types)
setCodeMimeTypes: types => noteTypeModel.codeMimeTypes(types),
init
};
@@ -6,13 +6,16 @@ import protectedSessionHolder from './protected_session_holder.js';
import infoService from "./info.js";
const $dialog = $("#protected-session-password-dialog");
const $passwordForm = $("#protected-session-password-form");
const $password = $("#protected-session-password");
const $component = $("#protected-session-password-component");
const $passwordForms = $(".protected-session-password-form");
const $passwordInputs = $(".protected-session-password");
const $passwordInModal = $("#protected-session-password-in-modal");
const $noteDetailWrapper = $("#note-detail-wrapper");
const $protectButton = $("#protect-button");
const $unprotectButton = $("#unprotect-button");
const $enterProtectedSessionButton = $("#enter-protected-session-button");
const $leaveProtectedSessionButton = $("#leave-protected-session-button");
const $noteTitle = $("#note-title");
let protectedSessionDeferred = null;
@@ -36,12 +39,19 @@ function ensureProtectedSession(requireProtectedSession, modal) {
// using deferred instead of promise because it allows resolving from outside
protectedSessionDeferred = dfd;
if (treeService.getCurrentNode().data.isProtected) {
$noteDetailWrapper.hide();
}
// user shouldn't be able to edit note title
$noteTitle.prop("readonly", true);
$dialog.toggleClass("modalless", !modal);
$dialog.modal();
if (modal) {
if (treeService.getCurrentNode().data.isProtected) {
$noteDetailWrapper.hide();
}
$dialog.modal();
}
else {
$component.show();
}
}
else {
dfd.resolve(false);
@@ -50,9 +60,8 @@ function ensureProtectedSession(requireProtectedSession, modal) {
return dfd.promise();
}
async function setupProtectedSession() {
const password = $password.val();
$password.val("");
async function setupProtectedSession(password) {
$passwordInputs.val("");
const response = await enterProtectedSessionOnServer(password);
@@ -72,7 +81,7 @@ async function setupProtectedSession() {
await noteDetailService.reload();
if (protectedSessionDeferred !== null) {
ensureDialogIsClosed($dialog, $password);
ensureDialogIsClosed();
$noteDetailWrapper.show();
@@ -93,7 +102,7 @@ function ensureDialogIsClosed() {
}
catch (e) {}
$password.val('');
$passwordInputs.val('');
}
async function enterProtectedSessionOnServer(password) {
@@ -116,7 +125,7 @@ async function protectNoteAndSendToServer() {
treeService.setProtected(note.noteId, note.isProtected);
noteDetailService.setNoteBackgroundIfProtected(note);console.log(note);
noteDetailService.setNoteBackgroundIfProtected(note);
}
async function unprotectNoteAndSendToServer() {
@@ -157,27 +166,18 @@ async function protectSubtree(noteId, protect) {
noteDetailService.reload();
}
$passwordForm.submit(() => {
setupProtectedSession();
$passwordForms.submit(function() { // needs to stay as function
const password = $(this).find(".protected-session-password").val();
setupProtectedSession(password);
return false;
});
// this doesn't work, event is not triggered :/
$dialog.on("show.bs.modal", e => function() {
if ($(this).hasClass("modalless")) {
// return "stolen" focus to tree
treeService.getCurrentNode().setFocus();
}
else {
$password.focus();
}
});
$protectButton.click(protectNoteAndSendToServer);
$unprotectButton.click(unprotectNoteAndSendToServer);
$dialog.on("shown.bs.modal", e => $password.focus());
$dialog.on("shown.bs.modal", e => $passwordInModal.focus());
export default {
ensureProtectedSession,
+46 -25
View File
@@ -14,6 +14,8 @@ import treeBuilder from "./tree_builder.js";
import treeKeyBindings from "./tree_keybindings.js";
import Branch from '../entities/branch.js';
import NoteShort from '../entities/note_short.js';
import hoistedNoteService from '../services/hoisted_note.js';
import confirmDialog from "../dialogs/confirm.js";
const $tree = $("#tree");
const $createTopLevelNoteButton = $("#create-top-level-note-button");
@@ -88,10 +90,11 @@ async function expandToNote(notePath, expandOpts) {
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
let parentNoteId = 'none';
let parentNoteId = null;
for (const childNoteId of runPath) {
const node = getNodesByNoteId(childNoteId).find(node => node.data.parentNoteId === parentNoteId);
// for first node (!parentNoteId) it doesn't matter which node is found
const node = getNodesByNoteId(childNoteId).find(node => !parentNoteId || node.data.parentNoteId === parentNoteId);
if (!node) {
console.error(`Can't find node for noteId=${childNoteId} with parentNoteId=${parentNoteId}`);
@@ -111,6 +114,17 @@ async function expandToNote(notePath, expandOpts) {
async function activateNote(notePath, newNote) {
utils.assertArguments(notePath);
const hoistedNoteId = await hoistedNoteService.getHoistedNoteId();
if (hoistedNoteId !== 'root' && !notePath.includes(hoistedNoteId)) {
if (!await confirmDialog.confirm("Requested note is outside of hoisted note subtree. Do you want to unhoist?")) {
return;
}
// unhoist so we can activate the note
await hoistedNoteService.unhoist();
}
if (glob.activeDialog) {
glob.activeDialog.modal('hide');
}
@@ -143,6 +157,8 @@ async function getRunPath(notePath) {
path.push('root');
}
const hoistedNoteId = await hoistedNoteService.getHoistedNoteId();
const effectivePath = [];
let childNoteId = null;
let i = 0;
@@ -195,13 +211,12 @@ async function getRunPath(notePath) {
}
}
if (parentNoteId === 'none') {
effectivePath.push(parentNoteId);
childNoteId = parentNoteId;
if (parentNoteId === hoistedNoteId) {
break;
}
else {
effectivePath.push(parentNoteId);
childNoteId = parentNoteId;
}
}
return effectivePath.reverse();
@@ -322,6 +337,10 @@ function clearSelectedNodes() {
}
async function treeInitialized() {
if (startNotePath === '-') {
return;
}
const noteId = treeUtils.getNoteIdFromNotePath(startNotePath);
if (!await treeCache.getNote(noteId)) {
@@ -329,6 +348,7 @@ async function treeInitialized() {
startNotePath = null;
}
// - is used in mobile to indicate that we don't want to activate any note after load
if (startNotePath) {
const node = await activateNote(startNotePath);
@@ -344,7 +364,7 @@ function initFancyTree(tree) {
$tree.fancytree({
autoScroll: true,
keyboard: false, // we takover keyboard handling in the hotkeys plugin
extensions: ["hotkeys", "filter", "dnd5", "clones"],
extensions: ["hotkeys", "dnd5", "clones"],
source: tree,
scrollParent: $tree,
minExpandLevel: 2, // root can't be collapsed
@@ -382,18 +402,6 @@ function initFancyTree(tree) {
hotkeys: {
keydown: treeKeyBindings
},
filter: {
autoApply: true, // Re-apply last filter if lazy data is loaded
autoExpand: true, // Expand all branches that contain matches while filtered
counter: false, // Show a badge with number of matching child nodes near parent icons
fuzzy: false, // Match single characters in order, e.g. 'fb' will match 'FooBar'
hideExpandedCounter: true, // Hide counter badge if parent is expanded
hideExpanders: false, // Hide expanders if all child nodes are hidden by filter
highlight: true, // Highlight matches by wrapping inside <mark> tags
leavesOnly: false, // Match end nodes only
nodata: true, // Display a 'no data' status node if result is empty
mode: "hide" // Grayout unmatched nodes (pass "hide" to remove unmatched node instead)
},
dnd5: dragAndDropSetup,
lazyLoad: function(event, data) {
const noteId = data.node.data.noteId;
@@ -402,12 +410,21 @@ function initFancyTree(tree) {
},
clones: {
highlightActiveClones: true
},
renderNode: async function (event, data) {
const node = data.node;
if (node.data.noteId !== 'root' && node.data.noteId === await hoistedNoteService.getHoistedNoteId()) {
const unhoistButton = $('<span>&nbsp; (<a class="unhoist-button">unhoist</a>)</span>');
$(node.span).append(unhoistButton);
}
}
});
$tree.on('contextmenu', '.fancytree-node', function(e) {
treeContextMenuService.getContextMenuItems(e).then(contextMenuItems => {
contextMenuWidget.initContextMenu(e, contextMenuItems, treeContextMenuService.selectContextMenuItem);
treeContextMenuService.getContextMenuItems(e).then(contextMenuItemContainer => {
contextMenuWidget.initContextMenu(e, contextMenuItemContainer, treeContextMenuService.selectContextMenuItem);
});
return false; // blocks default browser right click menu
@@ -545,7 +562,7 @@ async function createNote(node, parentNoteId, target, isProtected, saveSelection
branchId: branchEntity.branchId,
isProtected: isProtected,
extraClasses: await treeBuilder.getExtraClasses(noteEntity),
icon: treeBuilder.getIcon(noteEntity)
icon: await treeBuilder.getIcon(noteEntity)
};
if (target === 'after') {
@@ -640,7 +657,7 @@ utils.bindShortcut('ctrl+.', scrollToCurrentNote);
$(window).bind('hashchange', function() {
const notePath = getNotePathFromAddress();
if (getCurrentNotePath() !== notePath) {
if (notePath !== '-' && getCurrentNotePath() !== notePath) {
console.debug("Switching to " + notePath + " because of hash change");
activateNote(notePath);
@@ -669,8 +686,12 @@ export default {
setPrefix,
createNewTopLevelNote,
createNote,
createNoteInto,
getSelectedNodes,
clearSelectedNodes,
sortAlphabetically,
showTree
showTree,
loadTree,
treeInitialized,
setExpandedToServer
};
@@ -4,13 +4,26 @@ import Branch from "../entities/branch.js";
import server from "./server.js";
import treeCache from "./tree_cache.js";
import messagingService from "./messaging.js";
import hoistedNoteService from "./hoisted_note.js";
async function prepareTree(noteRows, branchRows, relations) {
utils.assertArguments(noteRows, branchRows, relations);
treeCache.load(noteRows, branchRows, relations);
return [ await prepareNode(await treeCache.getBranch('root')) ];
const hoistedNoteId = await hoistedNoteService.getHoistedNoteId();
let hoistedBranch;
if (hoistedNoteId === 'root') {
hoistedBranch = await treeCache.getBranch('root');
}
else {
const hoistedNote = await treeCache.getNote(hoistedNoteId);
hoistedBranch = (await hoistedNote.getBranches())[0];
}
return [ await prepareNode(hoistedBranch) ];
}
async function prepareBranch(note) {
@@ -22,10 +35,15 @@ async function prepareBranch(note) {
}
}
function getIcon(note) {
async function getIcon(note) {
const hoistedNoteId = await hoistedNoteService.getHoistedNoteId();
if (note.noteId === 'root') {
return "jam jam-chevrons-right";
}
else if (note.noteId === hoistedNoteId) {
return "jam jam-arrow-up";
}
else if (note.type === 'text') {
if (note.hasChildren()) {
return "jam jam-folder";
@@ -57,6 +75,7 @@ function getIcon(note) {
async function prepareNode(branch) {
const note = await branch.getNote();
const title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title;
const hoistedNoteId = await hoistedNoteService.getHoistedNoteId();
const node = {
noteId: note.noteId,
@@ -65,9 +84,9 @@ async function prepareNode(branch) {
isProtected: note.isProtected,
title: utils.escapeHtml(title),
extraClasses: await getExtraClasses(note),
icon: getIcon(note),
icon: await getIcon(note),
refKey: note.noteId,
expanded: note.type !== 'search' && branch.isExpanded
expanded: (note.type !== 'search' && branch.isExpanded) || hoistedNoteId === note.noteId
};
if (note.hasChildren() || note.type === 'search') {
@@ -135,10 +154,6 @@ async function getExtraClasses(note) {
const extraClasses = [];
if (note.noteId === 'root') {
extraClasses.push("tree-root");
}
if (note.isProtected) {
extraClasses.push("protected");
}
@@ -10,7 +10,8 @@ import exportDialog from '../dialogs/export.js';
import infoService from "./info.js";
import treeCache from "./tree_cache.js";
import syncService from "./sync.js";
import contextMenuService from "./context_menu.js";
import hoistedNoteService from './hoisted_note.js';
import ContextMenuItemsContainer from './context_menu_items_container.js';
const $tree = $("#tree");
@@ -79,10 +80,12 @@ function cut(nodes) {
}
const contextMenuItems = [
{title: "Insert note here <kbd>Ctrl+O</kbd>", cmd: "insertNoteHere", uiIcon: "plus"},
{title: "Insert note after <kbd>Ctrl+O</kbd>", cmd: "insertNoteAfter", uiIcon: "plus"},
{title: "Insert child note <kbd>Ctrl+P</kbd>", cmd: "insertChildNote", uiIcon: "plus"},
{title: "Delete", cmd: "delete", uiIcon: "trash"},
{title: "Delete <kbd>Delete</kbd>", cmd: "delete", uiIcon: "trash"},
{title: "----"},
{title: "Hoist note <kbd>Ctrl-H</kbd>", cmd: "hoist", uiIcon: "arrow-up"},
{title: "Unhoist note <kbd>Ctrl-H</kbd>", cmd: "unhoist", uiIcon: "arrow-up"},
{title: "Edit branch prefix <kbd>F2</kbd>", cmd: "editBranchPrefix", uiIcon: "pencil"},
{title: "----"},
{title: "Protect subtree", cmd: "protectSubtree", uiIcon: "shield-check"},
@@ -101,16 +104,6 @@ const contextMenuItems = [
{title: "Sort alphabetically <kbd>Alt+S</kbd>", cmd: "sortAlphabetically", uiIcon: "arrows-v"}
];
function enableItem(cmd, enabled) {
const item = contextMenuItems.find(item => item.cmd === cmd);
if (!item) {
throw new Error(`Command ${cmd} has not been found!`);
}
item.enabled = enabled;
}
async function getContextMenuItems(event) {
const node = $.ui.fancytree.getNode(event);
const branch = await treeCache.getBranch(node.data.branchId);
@@ -118,17 +111,24 @@ async function getContextMenuItems(event) {
const parentNote = await treeCache.getNote(branch.parentNoteId);
const isNotRoot = note.noteId !== 'root';
const itemsContainer = new ContextMenuItemsContainer(contextMenuItems);
// Modify menu entries depending on node status
enableItem("insertNoteHere", isNotRoot && parentNote.type !== 'search');
enableItem("insertChildNote", note.type !== 'search');
enableItem("delete", isNotRoot && parentNote.type !== 'search');
enableItem("copy", isNotRoot);
enableItem("cut", isNotRoot);
enableItem("pasteAfter", clipboardIds.length > 0 && isNotRoot && parentNote.type !== 'search');
enableItem("pasteInto", clipboardIds.length > 0 && note.type !== 'search');
enableItem("importIntoNote", note.type !== 'search');
enableItem("export", note.type !== 'search');
enableItem("editBranchPrefix", isNotRoot && parentNote.type !== 'search');
itemsContainer.enableItem("insertNoteAfter", isNotRoot && parentNote.type !== 'search');
itemsContainer.enableItem("insertChildNote", note.type !== 'search');
itemsContainer.enableItem("delete", isNotRoot && parentNote.type !== 'search');
itemsContainer.enableItem("copy", isNotRoot);
itemsContainer.enableItem("cut", isNotRoot);
itemsContainer.enableItem("pasteAfter", clipboardIds.length > 0 && isNotRoot && parentNote.type !== 'search');
itemsContainer.enableItem("pasteInto", clipboardIds.length > 0 && note.type !== 'search');
itemsContainer.enableItem("importIntoNote", note.type !== 'search');
itemsContainer.enableItem("export", note.type !== 'search');
itemsContainer.enableItem("editBranchPrefix", isNotRoot && parentNote.type !== 'search');
const hoistedNoteId = await hoistedNoteService.getHoistedNoteId();
itemsContainer.hideItem("hoist", note.noteId === hoistedNoteId);
itemsContainer.hideItem("unhoist", note.noteId !== hoistedNoteId || !isNotRoot);
// Activate node on right-click
node.setActive();
@@ -139,14 +139,14 @@ async function getContextMenuItems(event) {
node.setSelected(true);
treeService.clearSelectedNodes();
return contextMenuItems;
return itemsContainer;
}
function selectContextMenuItem(event, cmd) {
// context menu is always triggered on current node
const node = treeService.getCurrentNode();
if (cmd === "insertNoteHere") {
if (cmd === "insertNoteAfter") {
const parentNoteId = node.data.parentNoteId;
const isProtected = treeUtils.getParentProtectedStatus(node);
@@ -194,6 +194,12 @@ function selectContextMenuItem(event, cmd) {
else if (cmd === "sortAlphabetically") {
treeService.sortAlphabetically(node.data.noteId);
}
else if (cmd === "hoist") {
hoistedNoteService.setHoistedNoteId(node.data.noteId);
}
else if (cmd === "unhoist") {
hoistedNoteService.unhoist();
}
else {
messagingService.logError("Unknown command: " + cmd);
}
@@ -4,6 +4,7 @@ import treeChangesService from "./branches.js";
import contextMenuService from "./tree_context_menu.js";
import treeService from "./tree.js";
import editBranchPrefixDialog from "../dialogs/branch_prefix.js";
import hoistedNoteService from "./hoisted_note.js";
const keyBindings = {
"del": node => {
@@ -113,6 +114,18 @@ const keyBindings = {
node.getParent().setActive().then(treeService.clearSelectedNodes);
}
},
"ctrl+h": node => {
hoistedNoteService.getHoistedNoteId().then(hoistedNoteId => {
if (node.data.noteId === hoistedNoteId) {
hoistedNoteService.unhoist();
}
else {
hoistedNoteService.setHoistedNoteId(node.data.noteId);
}
});
return false;
},
// code below shouldn't be necessary normally, however there's some problem with interaction with context menu plugin
// after opening context menu, standard shortcuts don't work, but they are detected here
// so we essentially takeover the standard handling with our implementation.
+25 -5
View File
@@ -136,11 +136,28 @@ function randomString(len) {
}
function bindShortcut(keyboardShortcut, handler) {
$(document).bind('keydown', keyboardShortcut, e => {
handler();
if (isDesktop()) {
$(document).bind('keydown', keyboardShortcut, e => {
handler();
e.preventDefault();
});
e.preventDefault();
});
}
}
function isMobile() {
return window.device === "mobile";
}
function isDesktop() {
return window.device === "desktop";
}
function setCookie(name, value) {
const date = new Date(Date.now() + 10 * 365 * 24 * 60 * 60 * 1000);
const expires = "; expires=" + date.toUTCString();
document.cookie = name + "=" + (value || "") + expires + "; path=/";
}
export default {
@@ -166,5 +183,8 @@ export default {
download,
toObject,
randomString,
bindShortcut
bindShortcut,
isMobile,
isDesktop,
setCookie
};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
User-agent: *
Disallow: /
+78
View File
@@ -0,0 +1,78 @@
#container {
margin: 0 auto; /* center */
height: 100vh;
display: grid;
grid-template-areas: "header header"
"left-pane title"
"left-pane note-detail";
grid-template-rows: auto
auto
1fr;
justify-content: center;
grid-gap: 10px;
}
#search-box {
display: none;
padding: 10px;
margin-top: 10px;
}
#tree {
overflow: auto;
flex-grow: 1;
flex-shrink: 1;
flex-basis: 60%;
margin-top: 10px;
}
#left-pane {
grid-area: left-pane;
display: flex;
flex-direction: column;
}
#header {
grid-area: header;
background-color: #f8f8f8;
display: flex;
align-items: center;
padding: 4px;
}
#header button {
padding: 1px 5px 1px 5px;
font-size: small;
}
#global-buttons {
display: flex;
justify-content: space-around;
padding: 10px 0 10px 0;
margin: 0 10px 0 16px;
border: 1px solid #ccc;
}
#context-menu-container {
padding: 3px 0 0;
}
#context-menu-container .dropdown-item {
padding: 0 7px 0 10px;
}
.fancytree-loading span.fancytree-expander {
width: 16px;
height: 16px;
margin-left: 6px;
}
.fancytree-loading span.fancytree-expander:after {
width: 12px;
height: 12px;
margin-top: 2px;
border-width: 1px;
border-style: solid;
}
+105
View File
@@ -0,0 +1,105 @@
html, body {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
}
#container-row {
height: 100%;
}
#left-pane {
height: 100%;
display: flex;
flex-direction: column;
}
#global-buttons {
display: flex;
flex-shrink: 0;
justify-content: space-around;
padding: 10px 0 10px 0;
margin: 0 10px 0 16px;
}
#tree {
font-size: larger;
width: 100%;
overflow: auto;
}
#tree .action-button {
position: relative;
top: -5px;
margin-left: 10px;
}
#detail {
padding: 5px 20px 10px 0px;
height: 100%;
flex-direction: column;
}
#detail-content {
position: relative;
overflow: auto;
flex-direction: column;
height: 100%;
/* large left padding is necessary for ckeditor gutter in detail-only (smartphone) layout */
padding-left: 35px;
}
#note-title-row {
display: flex;
padding-left: 15px;
flex-shrink: 0;
}
.action-button {
background: none;
border: none;
cursor: pointer;
font-size: 1.5em;
padding-left: 0.5em;
padding-right: 0.5em;
}
.fancytree-custom-icon {
font-size: 2em;
}
.fancytree-title {
font-size: 1.5em;
margin-left: 0.6em !important;
}
.fancytree-node {
padding: 5px;
}
.fancytree-node .fancytree-expander:before {
font-size: 2em;
}
kbd {
display: none;
}
.dropdown-menu {
font-size: larger;
}
.fancytree-loading span.fancytree-expander {
width: 32px;
height: 32px;
margin-left: 4px;
}
.fancytree-loading span.fancytree-expander:after {
width: 20px;
height: 20px;
margin: 4px;
border-width: 2px;
border-style: solid;
}
+84 -73
View File
@@ -4,59 +4,50 @@ body {
position: fixed;
}
#container {
margin: 0 auto; /* center */
height: 100vh;
display: grid;
grid-template-areas: "header header"
"left-pane title"
"left-pane note-detail";
grid-template-rows: auto
auto
1fr;
justify-content: center;
grid-gap: 10px;
}
#header {
grid-area: header;
background-color: #f8f8f8;
display: flex;
align-items: center;
padding: 4px;
}
#header button {
padding: 1px 5px 1px 5px;
font-size: small;
}
#left-pane {
grid-area: left-pane;
display: flex;
flex-direction: column;
}
#global-buttons {
display: flex;
justify-content: space-around;
padding: 10px 0 10px 0;
margin: 0 10px 0 16px;
border: 1px solid #ccc;
}
#search-box {
display: none;
padding: 10px;
margin-top: 10px;
}
#title-container {
grid-area: title;
}
#note-title {
margin-left: 15px;
font-size: x-large;
border: 0;
width: 5em;
flex-grow: 100;
}
.fancytree-node:not(.fancytree-loading) .fancytree-expander {
background: none;
width: auto;
height: auto;
}
.fancytree-node:not(.fancytree-loading) .fancytree-expander:before {
font-family: 'jam-icons' !important;
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
padding-left: 5px;
padding-right: 5px;
content: "\e9bc";
}
/* this is done to preserve correct indentation. Better solution would be preferable */
.fancytree-node:not(.fancytree-folder) .fancytree-expander:before {
color: white;
}
.fancytree-node.fancytree-expanded .fancytree-expander:before {
content: "\e9ba";
}
#note-title[readonly] {
background: inherit;
}
.tdialog {
display: none;
}
@@ -185,14 +176,6 @@ div.ui-tooltip {
width: auto;
}
#tree {
overflow: auto;
flex-grow: 1;
flex-shrink: 1;
flex-basis: 60%;
margin-top: 10px;
}
#search-results {
padding: 0 5px 5px 15px;
flex-basis: 40%;
@@ -531,14 +514,6 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th
padding: 20px;
}
.context-menu-container {
font-size: small;
}
#context-menu-container .dropdown-item {
padding: 0 7px 0 10px;
}
/* if modal height overflows, then only modal body scrolls */
.modal-body {
max-height: calc(100vh - 200px);
@@ -619,14 +594,6 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th
cursor: pointer;
}
.modalless {
top: 15%;
left: 40%;
bottom: auto;
right: auto;
margin-left: -300px;
}
.multiplicity {
font-size: 1.3em;
}
@@ -698,4 +665,48 @@ div[data-notify="container"] {
#export-form .form-check-label {
padding: 2px;
}
.unhoist-button {
text-decoration: underline !important;
color: blue !important;
cursor: pointer !important;
}
#protected-session-password-component {
max-width: 450px;
margin: auto;
padding-top: 50px;
}
.ck-editor__is-empty.ck-content.ck-editor__editable::before {
content: 'You can start writing note here ...';
position: absolute;
display: block;
margin: var(--ck-spacing-large) 0;
color: #aaa;
}
.fancytree-loading span.fancytree-expander {
background-image: none !important;
display: inline-block;
}
.fancytree-loading span.fancytree-expander:after {
content: " ";
display: block;
border-radius: 50%;
border-color: #000 transparent #000 transparent;
animation: lds-dual-ring 1.2s linear infinite;
}
@keyframes lds-dual-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
+11 -1
View File
@@ -3,6 +3,8 @@
const noteCacheService = require('../../services/note_cache');
const repository = require('../../services/repository');
const log = require('../../services/log');
const utils = require('../../services/utils');
const optionService = require('../../services/options');
async function getAutocomplete(req) {
const query = req.query.query;
@@ -16,7 +18,7 @@ async function getAutocomplete(req) {
results = await getRecentNotes(currentNoteId);
}
else {
results = noteCacheService.findNotes(query);
results = await noteCacheService.findNotes(query);
}
const msTaken = Date.now() - timestampStarted;
@@ -29,6 +31,13 @@ async function getAutocomplete(req) {
}
async function getRecentNotes(currentNoteId) {
let extraCondition = '';
const hoistedNoteId = await optionService.getOption('hoistedNoteId');
if (hoistedNoteId !== 'root') {
extraCondition = `AND recent_notes.notePath LIKE '%${utils.sanitizeSql(hoistedNoteId)}%'`;
}
const recentNotes = await repository.getEntities(`
SELECT
recent_notes.*
@@ -39,6 +48,7 @@ async function getRecentNotes(currentNoteId) {
recent_notes.isDeleted = 0
AND branches.isDeleted = 0
AND branches.noteId != ?
${extraCondition}
ORDER BY
dateCreated DESC
LIMIT 200`, [currentNoteId]);
+1 -1
View File
@@ -29,7 +29,7 @@ async function loginSync(req) {
const syncVersion = req.body.syncVersion;
if (syncVersion !== appInfo.syncVersion) {
return [400, { message: 'Non-matching sync versions, local is version ' + appInfo.syncVersion }];
return [400, { message: `Non-matching sync versions, local is version ${appInfo.syncVersion}, remote is ${syncVersion}` }];
}
const documentSecret = await options.getOption('documentSecret');
+1 -1
View File
@@ -6,7 +6,7 @@ const log = require('../../services/log');
// options allowed to be updated directly in options dialog
const ALLOWED_OPTIONS = ['protectedSessionTimeout', 'noteRevisionSnapshotTimeInterval',
'zoomFactor', 'theme', 'syncServerHost', 'syncServerTimeout', 'syncProxy', 'leftPaneMinWidth', 'leftPaneWidthPercent'];
'zoomFactor', 'theme', 'syncServerHost', 'syncServerTimeout', 'syncProxy', 'leftPaneMinWidth', 'leftPaneWidthPercent', 'hoistedNoteId'];
async function getOptions() {
return await optionService.getOptionsMap(ALLOWED_OPTIONS);
+1 -1
View File
@@ -20,7 +20,7 @@ async function searchNotes(req) {
let searchTextResults = null;
if (searchText.trim().length > 0) {
searchTextResults = noteCacheService.findNotes(searchText);
searchTextResults = await noteCacheService.findNotes(searchText);
let fullTextNoteIds = await getFullTextResults(searchText);
+8 -2
View File
@@ -38,6 +38,7 @@ async function getNotes(noteIds) {
async function getRelations(noteIds) {
// we need to fetch both parentNoteId and noteId matches because we can have loaded child
// of which only some of the parents has been loaded.
// also now with note hoisting, it is possible to have the note displayed without its parent chain being loaded
const relations = await sql.getManyRows(`SELECT branchId, noteId AS 'childNoteId', parentNoteId, notePosition FROM branches WHERE isDeleted = 0
AND (parentNoteId IN (???) OR noteId IN (???))`, noteIds);
@@ -50,18 +51,23 @@ async function getRelations(noteIds) {
}
async function getTree() {
const hoistedNoteId = await optionService.getOption('hoistedNoteId');
// we fetch all branches of notes, even if that particular branch isn't visible
// this allows us to e.g. detect and properly display clones
const branches = await sql.getRows(`
WITH RECURSIVE
tree(branchId, noteId, isExpanded) AS (
SELECT branchId, noteId, isExpanded FROM branches WHERE branchId = 'root'
SELECT branchId, noteId, isExpanded FROM branches WHERE noteId = ?
UNION ALL
SELECT branches.branchId, branches.noteId, branches.isExpanded FROM branches
JOIN tree ON branches.parentNoteId = tree.noteId
WHERE tree.isExpanded = 1 AND branches.isDeleted = 0
)
SELECT branches.* FROM tree JOIN branches USING(noteId) WHERE branches.isDeleted = 0 ORDER BY branches.notePosition`);
SELECT branches.* FROM tree JOIN branches USING(noteId) WHERE branches.isDeleted = 0 ORDER BY branches.notePosition`, [hoistedNoteId]);
// we also want root branch in there because all the paths start with root
branches.push(await sql.getRow(`SELECT * FROM branches WHERE branchId = 'root'`));
const noteIds = Array.from(new Set(branches.map(b => b.noteId)));
+3 -1
View File
@@ -9,7 +9,9 @@ const optionService = require('../services/options');
async function index(req, res) {
const options = await optionService.getOptionsMap();
res.render('index', {
const view = req.cookies['trilium-device'] === 'mobile' ? 'mobile' : 'desktop';
res.render(view, {
theme: options.theme,
leftPaneMinWidth: parseInt(options.leftPaneMinWidth),
leftPaneWidthPercent: parseInt(options.leftPaneWidthPercent),
+3 -3
View File
@@ -1,6 +1,6 @@
const indexRoute = require('./index');
const loginRoute = require('./login');
const setupRoute = require('./setup');
const loginRoute = require('./login');
const indexRoute = require('./index');
const multer = require('multer')();
// API routes
@@ -151,7 +151,7 @@ function register(app) {
apiRoute(GET, '/api/recent-changes', recentChangesApiRoute.getRecentChanges);
apiRoute(GET, '/api/options', optionsApiRoute.getOptions);
apiRoute(PUT, '/api/options/:name/:value', optionsApiRoute.updateOption);
apiRoute(PUT, '/api/options/:name/:value*', optionsApiRoute.updateOption);
apiRoute(PUT, '/api/options', optionsApiRoute.updateOptions);
apiRoute(POST, '/api/password/change', passwordApiRoute.changePassword);
+1 -1
View File
@@ -3,7 +3,7 @@
const build = require('./build');
const packageJson = require('../../package');
const APP_DB_VERSION = 120;
const APP_DB_VERSION = 121;
const SYNC_VERSION = 2;
module.exports = {
+1 -1
View File
@@ -1 +1 @@
module.exports = { buildDate:"2018-12-09T21:22:33+01:00", buildRevision: "492adc29092cbc6ff7a79ef4fec542e6da1769a8" };
module.exports = { buildDate:"2018-12-30T22:38:11+01:00", buildRevision: "32220476aa6795bab036b7dd9057ea3357d7dd51" };
+8 -2
View File
@@ -23,9 +23,15 @@ module.exports = function(attributeFilters) {
whereParams.push(filter.value);
}
else if ([">", ">=", "<", "<="].includes(filter.operator)) {
const floatParam = parseFloat(filter.value);
let floatParam;
if (isNaN(floatParam)) {
// from https://stackoverflow.com/questions/12643009/regular-expression-for-floating-point-numbers
if (/^[+-]?([0-9]*[.])?[0-9]+$/.test(filter.value)) {
floatParam = parseFloat(filter.value);
}
if (floatParam === undefined || isNaN(floatParam)) {
// if the value can't be parsed as float then we assume that string comparison should be used instead of numeric
where += `attribute${i}.value ${filter.operator} ?`;
whereParams.push(filter.value);
}
+19
View File
@@ -0,0 +1,19 @@
const optionService = require('./options');
const sqlInit = require('./sql_init');
const eventService = require('./events');
let hoistedNoteId = 'root';
eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entity}) => {
if (entityName === 'options' && entity.name === 'hoistedNoteId') {
hoistedNoteId = entity.value;
}
});
sqlInit.dbReady.then(async () => {
hoistedNoteId = await optionService.getOption('hoistedNoteId');
});
module.exports = {
getHoistedNoteId: () => hoistedNoteId
};
+5 -1
View File
@@ -54,7 +54,11 @@ async function importEnex(file, parentNote) {
function extractContent(enNote) {
// [] thing is workaround for https://github.com/Leonidas-from-XIV/node-xml2js/issues/484
let content = xmlBuilder.buildObject([enNote]);
content = content.substr(3, content.length - 7).trim();
const endOfFirstTagIndex = content.indexOf('>');
// strip the <0> and </0> tags
content = content.substr(endOfFirstTagIndex + 1, content.length - endOfFirstTagIndex - 5).trim();
// workaround for https://github.com/ckeditor/ckeditor5-list/issues/116
content = content.replace(/<li>\s+<div>/g, "<li>");
+24 -11
View File
@@ -4,6 +4,7 @@ const eventService = require('./events');
const repository = require('./repository');
const protectedSessionService = require('./protected_session');
const utils = require('./utils');
const hoistedNoteService = require('./hoisted_note');
let loaded = false;
let noteTitles = {};
@@ -63,7 +64,7 @@ function highlightResults(results, allTokens) {
}
}
function findNotes(query) {
async function findNotes(query) {
if (!noteTitles || !query.length) {
return [];
}
@@ -72,7 +73,7 @@ function findNotes(query) {
// filtering '/' because it's used as separator
const allTokens = query.trim().toLowerCase().split(" ").filter(token => token !== '/');
const tokens = allTokens.slice();
const results = [];
let results = [];
let noteIds = Object.keys(noteTitles);
@@ -120,6 +121,10 @@ function findNotes(query) {
}
}
if (hoistedNoteService.getHoistedNoteId() !== 'root') {
results = results.filter(res => res.pathArray.includes(hoistedNoteService.getHoistedNoteId()));
}
// sort results by depth of the note. This is based on the assumption that more important results
// are closer to the note root.
results.sort((a, b) => {
@@ -214,21 +219,25 @@ function getNoteTitle(noteId, parentNoteId) {
function getNoteTitleArrayForPath(path) {
const titles = [];
if (path[0] === 'root') {
if (path.length === 1) {
return [ getNoteTitle('root') ];
}
else {
path = path.slice(1);
}
if (path[0] === hoistedNoteService.getHoistedNoteId() && path.length === 1) {
return [ getNoteTitle(hoistedNoteService.getHoistedNoteId()) ];
}
let parentNoteId = 'root';
let hoistedNotePassed = false;
for (const noteId of path) {
const title = getNoteTitle(noteId, parentNoteId);
// start collecting path segment titles only after hoisted note
if (hoistedNotePassed) {
const title = getNoteTitle(noteId, parentNoteId);
titles.push(title);
}
if (noteId === hoistedNoteService.getHoistedNoteId()) {
hoistedNotePassed = true;
}
titles.push(title);
parentNoteId = noteId;
}
@@ -246,6 +255,10 @@ function getSomePath(noteId, path) {
path.push(noteId);
path.reverse();
if (!path.includes(hoistedNoteService.getHoistedNoteId())) {
return false;
}
return path;
}
+1
View File
@@ -31,6 +31,7 @@ async function initSyncedOptions(username, password) {
async function initNotSyncedOptions(initialized, startNotePath = 'root', syncServerHost = '', syncProxy = '') {
await optionService.createOption('startNotePath', startNotePath, false);
await optionService.createOption('hoistedNoteId', 'root', false);
await optionService.createOption('lastDailyBackupDate', dateUtils.nowDate(), false);
await optionService.createOption('lastWeeklyBackupDate', dateUtils.nowDate(), false);
await optionService.createOption('lastMonthlyBackupDate', dateUtils.nowDate(), false);
+4 -3
View File
@@ -94,9 +94,10 @@ async function updateEntity(entity) {
const primaryKey = entity[primaryKeyName];
if (entity.isChanged && (entityName !== 'options' || entity.isSynced)) {
await syncTableService.addEntitySync(entityName, primaryKey);
if (entity.isChanged) {
if (entityName !== 'options' || entity.isSynced) {
await syncTableService.addEntitySync(entityName, primaryKey);
}
if (!cls.isEntityEventsDisabled()) {
const eventPayload = {
+120
View File
@@ -0,0 +1,120 @@
"use strict";
const utils = require('./utils');
const log = require('./log');
const url = require('url');
// this service provides abstraction over node's HTTP/HTTPS and electron net.client APIs
// this allows to support system proxy
function exec(opts) {
const client = getClient(opts);
const parsedTargetUrl = url.parse(opts.url);
return new Promise(async (resolve, reject) => {
try {
const headers = {
Cookie: (opts.cookieJar && opts.cookieJar.header) || "",
'Content-Type': 'application/json'
};
if (opts.auth) {
const token = new Buffer(opts.auth.user + ":" + opts.auth.pass).toString('base64');
headers['Authorization'] = `Basic ${token}`;
}
let host = parsedTargetUrl.hostname;
let protocol = parsedTargetUrl.protocol;
let port = parsedTargetUrl.port;
let path = parsedTargetUrl.path;
if (opts.proxy) {
// see https://stackoverflow.com/questions/3862813/how-can-i-use-an-http-proxy-with-node-js-http-client
const parsedProxyUrl = url.parse(opts.proxy);
protocol = parsedProxyUrl.protocol;
host = parsedProxyUrl.hostname;
port = parsedProxyUrl.port;
path = opts.url;
headers['Host'] = parsedTargetUrl.host; // host also includes port
}
const request = client.request({
method: opts.method,
// url is used by electron net module
url: opts.url,
// 4 fields below are used by http and https node modules
protocol,
host,
port,
path,
timeout: opts.timeout,
headers
});
request.on('error', err => reject(generateError(opts, err)));
request.on('response', response => {
if (![200, 201, 204].includes(response.statusCode)) {
reject(generateError(opts, response.statusCode + ' ' + response.statusMessage));
}
if (opts.cookieJar && response.headers['set-cookie']) {
opts.cookieJar.header = response.headers['set-cookie'];
}
let responseStr = '';
response.on('data', chunk => responseStr += chunk);
response.on('end', () => {
try {
const jsonObj = responseStr.trim() ? JSON.parse(responseStr) : null;
resolve(jsonObj);
}
catch (e) {
log.error("Failed to deserialize sync response: " + responseStr);
reject(generateError(opts, e.message));
}
});
});
request.end(opts.body ? JSON.stringify(opts.body) : undefined);
}
catch (e) {
reject(generateError(opts, e.message));
}
})
}
function getClient(opts) {
// it's not clear how to explicitly configure proxy (as opposed to system proxy)
// so in that case we always use node's modules
if (utils.isElectron() && !opts.proxy) {
return require('electron').net;
}
else {
// in case there's explicit proxy then we need to use protocol of the proxy since we're actually
// connecting to the proxy server and not to the end-target server
const {protocol} = url.parse(opts.proxy || opts.url);
if (protocol === 'http:' || protocol === 'https:') {
return require(protocol.substr(0, protocol.length - 1));
}
else {
throw new Error(`Unrecognized protocol "${protocol}"`);
}
}
}
function generateError(opts, message) {
return new Error(`Request to ${opts.method} ${opts.url} failed, error: ${message}`);
}
module.exports = {
exec
};
+12 -25
View File
@@ -1,10 +1,10 @@
const rp = require('request-promise');
const syncService = require('./sync');
const log = require('./log');
const sqlInit = require('./sql_init');
const repository = require('./repository');
const optionService = require('./options');
const syncOptions = require('./sync_options');
const request = require('./request');
async function hasSyncServerSchemaAndSeed() {
const response = await requestToSyncServer('GET', '/api/setup/status');
@@ -37,23 +37,13 @@ async function sendSeedToSyncServer() {
}
async function requestToSyncServer(method, path, body = null) {
const rpOpts = {
uri: await syncOptions.getSyncServerHost() + path,
method: method,
json: true
};
if (body) {
rpOpts.body = body;
}
const syncProxy = await syncOptions.getSyncProxy();
if (syncProxy) {
rpOpts.proxy = syncProxy;
}
return await rp(rpOpts);
return await request.exec({
method,
url: await syncOptions.getSyncServerHost() + path,
body,
proxy: await syncOptions.getSyncProxy(),
timeout: await syncOptions.getSyncTimeout()
});
}
async function setupSyncFromSyncServer(syncServerHost, syncProxy, username, password) {
@@ -68,19 +58,16 @@ async function setupSyncFromSyncServer(syncServerHost, syncProxy, username, pass
log.info("Getting document options from sync server.");
// response is expected to contain documentId and documentSecret options
const options = await rp.get({
uri: syncServerHost + '/api/setup/sync-seed',
const options = await request.exec({
method: 'get',
url: syncServerHost + '/api/setup/sync-seed',
auth: {
'user': username,
'pass': password
},
json: true
proxy: syncProxy
});
if (syncProxy) {
options.proxy = syncProxy;
}
await sqlInit.createDatabaseForSync(options, syncServerHost, syncProxy);
triggerSync();
+21 -31
View File
@@ -1,7 +1,7 @@
"use strict";
const url = require('url');
const log = require('./log');
const rp = require('request-promise');
const sql = require('./sql');
const sqlInit = require('./sql_init');
const optionService = require('./options');
@@ -14,6 +14,7 @@ const appInfo = require('./app_info');
const syncOptions = require('./sync_options');
const syncMutexService = require('./sync_mutex');
const cls = require('./cls');
const request = require('./request');
let proxyToggle = true;
@@ -49,7 +50,7 @@ async function sync() {
catch (e) {
proxyToggle = !proxyToggle;
if (e.message.indexOf('ECONNREFUSED') !== -1) {
if (e.message && e.message.indexOf('ECONNREFUSED') !== -1) {
log.info("No connection to sync server.");
return {
@@ -84,7 +85,7 @@ async function doLogin() {
const documentSecret = await optionService.getOption('documentSecret');
const hash = utils.hmac(documentSecret, timestamp);
const syncContext = { cookieJar: rp.jar() };
const syncContext = { cookieJar: {} };
const resp = await syncRequest(syncContext, 'POST', '/api/login/sync', {
timestamp: timestamp,
@@ -111,6 +112,10 @@ async function pullSync(syncContext) {
const resp = await syncRequest(syncContext, 'GET', changesUri);
stats.outstandingPulls = resp.maxSyncId - lastSyncedPull;
if (stats.outstandingPulls < 0) {
stats.outstandingPulls = 0;
}
const rows = resp.syncs;
if (rows.length === 0) {
@@ -212,30 +217,15 @@ async function checkContentHash(syncContext) {
await contentHashService.checkContentHashes(resp.hashes);
}
async function syncRequest(syncContext, method, uri, body) {
const fullUri = await syncOptions.getSyncServerHost() + uri;
try {
const options = {
method: method,
uri: fullUri,
jar: syncContext.cookieJar,
json: true,
body: body,
timeout: await syncOptions.getSyncTimeout()
};
const syncProxy = await syncOptions.getSyncProxy();
if (syncProxy && proxyToggle) {
options.proxy = syncProxy;
}
return await rp(options);
}
catch (e) {
throw new Error(`Request to ${method} ${fullUri} failed, error: ${e.message}`);
}
async function syncRequest(syncContext, method, requestPath, body) {
return await request.exec({
method,
url: await syncOptions.getSyncServerHost() + requestPath,
cookieJar: syncContext.cookieJar,
timeout: await syncOptions.getSyncTimeout(),
body,
proxy: proxyToggle ? await syncOptions.getSyncProxy() : null
});
}
const primaryKeys = {
@@ -262,12 +252,12 @@ async function getEntityRow(entityName, entityId) {
const entity = await sql.getRow(`SELECT * FROM ${entityName} WHERE ${primaryKey} = ?`, [entityId]);
if (entityName === 'notes' && entity.type === 'file') {
if (entityName === 'notes'
&& entity.content !== null
&& (entity.type === 'file' || entity.type === 'image')) {
entity.content = entity.content.toString("binary");
}
else if (entityName === 'images') {
entity.data = entity.data.toString('base64');
}
return entity;
}
+8 -2
View File
@@ -16,7 +16,13 @@ async function get(name) {
module.exports = {
getSyncServerHost: async () => await get('syncServerHost'),
isSyncSetup: async () => !!await get('syncServerHost'),
getSyncTimeout: async () => await get('syncServerTimeout'),
isSyncSetup: async () => {
const syncServerHost = await get('syncServerHost');
// special value "disabled" is here to support use case where document is configured with sync server
// and we need to override it with config from config.ini
return !!syncServerHost && syncServerHost !== 'disabled';
},
getSyncTimeout: async () => parseInt(await get('syncServerTimeout')),
getSyncProxy: async () => await get('syncProxy')
};
+1 -1
View File
@@ -39,7 +39,7 @@ async function updateEntity(sync, entity, sourceId) {
}
function deserializeNoteContentBuffer(note) {
if (note.type === 'file') {
if (note.content !== null && (note.type === 'file' || note.type === 'image')) {
note.content = new Buffer(note.content, 'binary');
}
}
+255
View File
@@ -0,0 +1,255 @@
<!DOCTYPE html>
<html lang="en" class="theme-<%= theme %>">
<head>
<meta charset="utf-8">
<title>Trilium Notes</title>
</head>
<body class="desktop">
<div id="container" style="display: none; grid-template-columns: minmax(<%= leftPaneMinWidth %>px, <%= leftPaneWidthPercent %>fr) <%= rightPaneWidthPercent %>fr">
<div id="header" class="hide-toggle">
<div id="history-navigation" style="display: none;">
<a id="history-back-button" title="Go to previous note." class="icon-action jam jam-arrow-square-left"></a>
&nbsp; &nbsp;
<a id="history-forward-button" title="Go to next note." class="icon-action jam jam-arrow-square-right"></a>
</div>
<div style="flex-grow: 100; display: flex;">
<button class="btn btn-sm" id="jump-to-note-dialog-button" title="CTRL+J">
<span class="jam jam-direction"></span>
Jump to note
</button>
<button class="btn btn-sm" id="recent-changes-button">
<span class="jam jam-history"></span>
Recent changes
</button>
<button class="btn btn-sm" id="enter-protected-session-button" title="Enter protected session to be able to find and view protected notes">
<span class="jam jam-door"></span>
Enter protected session
</button>
<button class="btn btn-sm" id="leave-protected-session-button" title="Leave protected session so that protected notes are not accessible any more." style="display: none;">
<span class="jam jam-log-out"></span>
Leave protected session
</button>
</div>
<div id="plugin-buttons">
</div>
<div>
<button class="btn btn-sm" id="sync-now-button" title="Trigger sync">
<span class="jam jam-refresh"></span>
Sync (<span id="outstanding-syncs-count">0</span>)
</button>
<button class="btn btn-sm" id="options-button">
<span class="jam jam-settings-alt"></span> Options</button>
<form action="logout" id="logout-button" method="POST" style="display: inline;">
<button type="submit" class="btn btn-sm">
<span class="jam jam-log-out"></span>
Logout
</button>
</form>
</div>
</div>
<div id="left-pane" class="hide-toggle">
<div id="global-buttons">
<a id="create-top-level-note-button" title="Create new top level note" class="icon-action jam jam-plus-circle"></a>
<a id="collapse-tree-button" title="Collapse note tree. Shortcut ALT+C" class="icon-action jam jam-layers"></a>
<a id="scroll-to-current-note-button" title="Scroll to current note. Shortcut CTRL+." class="icon-action jam jam-download"></a>
<a id="toggle-search-button" title="Search in notes. Shortcut CTRL+S" class="icon-action jam jam-search"></a>
</div>
<input type="file" id="import-upload" style="display: none" />
<div id="search-box">
<div style="display: flex; align-items: center;">
<input name="search-text" placeholder="Search text, labels" style="flex-grow: 100; margin-left: 5px; margin-right: 5px;" autocomplete="off">
<button id="do-search-button" class="btn btn-sm icon-button jam jam-search" title="Search (enter)"></button>
&nbsp;
<button id="save-search-button" class="btn btn-sm icon-button jam jam-save" title="Save search"></button>
&nbsp;
<button id="close-search-button" class="btn btn-sm icon-button jam jam-close" title="Close search"></button>
</div>
</div>
<div id="search-results">
<strong>Search results:</strong>
<ul id="search-results-inner"></ul>
</div>
<div id="tree"></div>
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container"></div>
</div>
<div id="title-container">
<div style="display: flex; align-items: center;">
<div class="dropdown hide-toggle">
<button id="note-path-list-button" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle">
<span id="note-path-count">1 path</span>
<span class="caret"></span>
</button>
<ul id="note-path-list" class="dropdown-menu" aria-labelledby="note-path-list-button">
</ul>
</div>
<input autocomplete="off" value="" id="note-title" tabindex="1">
<div class="hide-toggle" style="display: flex; align-items: center;">
<span id="note-id-display" title="Note ID"></span>
<button class="btn btn-sm icon-button jam jam-play"
style="display: none; margin-right: 10px;"
title="Render"
id="render-button"></button>
<button class="btn btn-sm icon-button jam jam-play"
style="display: none; margin-right: 10px;"
title="Execute (Ctrl+Enter)"
id="execute-script-button"></button>
<div class="btn-group btn-group-xs">
<button type="button"
class="btn btn-sm icon-button jam jam-shield-check"
id="protect-button"
title="Protected note can be viewed and edited only after entering password">
</button>
<button type="button"
class="btn btn-sm icon-button jam jam-shield-close"
id="unprotect-button"
title="Not protected note can be viewed without entering password">
</button>
</div>
&nbsp; &nbsp;
<div id="note-type-wrapper" style="display: flex;">
<div class="dropdown" id="note-type">
<button data-bind="disable: isDisabled()" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle">
Type: <span data-bind="text: typeString()"></span>
<span class="caret"></span>
</button>
<div id="note-type-dropdown" class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" data-bind="click: selectText, css: { selected: type() == 'text' }"><span class="check">&check;</span> <strong>Text</strong></a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" data-bind="click: selectRelationMap, css: { selected: type() == 'relation-map' && mime() == '' }"><span class="check">&check;</span> <strong>Relation Map</strong></a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" data-bind="click: selectRender, css: { selected: type() == 'render' && mime() == '' }"><span class="check">&check;</span> <strong>Render HTML note</strong></a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" data-bind="click: selectCode, css: { selected: type() == 'code' && mime() == '' }"><span class="check">&check;</span> <strong>Code</strong></a>
<!-- ko foreach: codeMimeTypes -->
<a class="dropdown-item" data-bind="click: $parent.selectCodeMime, css: { selected: $parent.type() == 'code' && $parent.mime() == mime }"><span class="check">&check;</span> <span data-bind="text: title"></span></a>
<!-- /ko -->
</div>
</div>
<div class="dropdown" id="note-actions">
<button type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle">
Note actions
<span class="caret"></span>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" id="show-note-revisions-button" data-bind="css: { disabled: type() == 'file' || type() == 'image' }">Revisions</a>
<a class="dropdown-item show-attributes-button"><kbd>Alt+A</kbd> Attributes</a>
<a class="dropdown-item" id="show-source-button" data-bind="css: { disabled: type() != 'text' && type() != 'code' && type() != 'relation-map' && type() != 'search' }">Note source</a>
<a class="dropdown-item" id="upload-file-button">Upload file</a>
<a class="dropdown-item" id="export-note-button" data-bind="css: { disabled: type() != 'text' }">Export note</a>
</div>
</div>
</div>
</div>
</div>
</div>
<% include details/detail.ejs %>
<% include dialogs/add_link.ejs %>
<% include dialogs/attributes.ejs %>
<% include dialogs/branch_prefix.ejs %>
<% include dialogs/event_log.ejs %>
<% include dialogs/export.ejs %>
<% include dialogs/jump_to_note.ejs %>
<% include dialogs/markdown_import.ejs %>
<% include dialogs/note_revisions.ejs %>
<% include dialogs/note_source.ejs %>
<% include dialogs/options.ejs %>
<% include dialogs/protected_session_password.ejs %>
<% include dialogs/recent_changes.ejs %>
<% include dialogs/sql_console.ejs %>
<% include dialogs/info.ejs %>
<% include dialogs/prompt.ejs %>
<% include dialogs/confirm.ejs %>
</div>
<webview class="electron-in-page-search-window" nodeintegration disablewebsecurity src="libraries/electron-in-page-search/search-window.html"></webview>
<script type="text/javascript">
window.baseApiUrl = 'api/';
window.device = "desktop";
window.glob = {
activeDialog: null,
sourceId: '<%= sourceId %>',
maxSyncIdAtLoad: <%= maxSyncIdAtLoad %>,
instanceName: '<%= instanceName %>'
};
</script>
<!-- Required for correct loading of scripts in Electron -->
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
<script src="libraries/jquery.min.js"></script>
<link href="libraries/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<script src="libraries/bootstrap/js/bootstrap.bundle.min.js"></script>
<script src="libraries/bootstrap-notify.min.js"></script>
<!-- Include Fancytree skin and library -->
<link href="libraries/fancytree/skin-win8/ui.fancytree.css" rel="stylesheet">
<script src="libraries/fancytree/jquery.fancytree-all-deps.min.js"></script>
<script src="libraries/jquery.hotkeys.js"></script>
<script src="libraries/jquery.fancytree.hotkeys.js"></script>
<script src="libraries/knockout.min.js"></script>
<script src="libraries/autocomplete.jquery.min.js"></script>
<link href="stylesheets/style.css" rel="stylesheet">
<link href="stylesheets/desktop.css" rel="stylesheet">
<script src="javascripts/desktop.js" crossorigin type="module"></script>
<link rel="stylesheet" type="text/css" href="libraries/jam/css/jam.min.css">
<script type="text/javascript">
// we hide container initally because otherwise it is rendered first without CSS and then flickers into
// final form which is pretty ugly.
$("#container").show();
</script>
<style type="text/css">
<%= appCss %>
</style>
</body>
</html>
+2
View File
@@ -21,6 +21,8 @@
<% include relation_map.ejs %>
<% include protected_session_password.ejs %>
<div id="children-overview"></div>
</div>
@@ -0,0 +1,10 @@
<div id="protected-session-password-component" class="note-detail-component">
<form class="protected-session-password-form">
<div class="form-group">
<label for="protected-session-password-in-detail">Showing protected note requires entering your password:</label>
<input id="protected-session-password-in-detail" class="form-control protected-session-password" type="password">
</div>
<button class="btn btn-primary">Start protected session <kbd>enter</kbd></button>
</form>
</div>
+42 -42
View File
@@ -1,53 +1,53 @@
<div id="add-link-dialog" class="modal mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" style="max-width: 1000px" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title mr-auto">Add note link</h5>
<div class="modal-dialog modal-lg" style="max-width: 1000px" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title mr-auto">Add note link</h5>
<button type="button" class="help-button" title="Help on links" data-help-page="Links">?</button>
<button type="button" class="help-button" title="Help on links" data-help-page="Links">?</button>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0 !important;">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form id="add-link-form">
<div class="modal-body">
<div id="add-link-type-div" class="radio">
<label title="Add HTML link to the selected note at cursor in current note">
<input type="radio" name="add-link-type" value="html"/>
add normal HTML link</label>
<label title="Add selected note as a child of current note">
<input type="radio" name="add-link-type" value="selected-to-current"/>
add selected note to current note</label>
<label title="Add current note as a child of the selected note">
<input type="radio" name="add-link-type" value="current-to-selected"/>
add current note to selected note</label>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0 !important;">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form id="add-link-form">
<div class="modal-body">
<div id="add-link-type-div" class="radio">
<label title="Add HTML link to the selected note at cursor in current note">
<input type="radio" name="add-link-type" value="html"/>
add normal HTML link</label>
<div class="form-group">
<label for="note-autocomplete">Note</label>
<label title="Add selected note as a child of current note">
<input type="radio" name="add-link-type" value="selected-to-current"/>
add selected note to current note</label>
<div class="input-group">
<input id="note-autocomplete" class="form-control" placeholder="search for note by its name">
</div>
</div>
<label title="Add current note as a child of the selected note">
<input type="radio" name="add-link-type" value="current-to-selected"/>
add current note to selected note</label>
</div>
<div class="form-group" id="add-link-title-form-group">
<label for="link-title">Link title</label>
<input id="link-title" class="form-control" style="width: 100%;">
</div>
<div class="form-group">
<label for="note-autocomplete">Note</label>
<div class="form-group" id="add-link-prefix-form-group" title="Cloned note will be shown in note tree with given prefix">
<label for="clone-prefix">Prefix (optional)</label>
<input id="clone-prefix" class="form-control" style="width: 100%;">
</div>
<div class="input-group">
<input id="note-autocomplete" class="form-control" placeholder="search for note by its name">
</div>
</div>
<div class="form-group" id="add-link-title-form-group">
<label for="link-title">Link title</label>
<input id="link-title" class="form-control" style="width: 100%;">
</div>
<div class="form-group" id="add-link-prefix-form-group" title="Cloned note will be shown in note tree with given prefix">
<label for="clone-prefix">Prefix (optional)</label>
<input id="clone-prefix" class="form-control" style="width: 100%;">
</div>
</div>
<div class="modal-footer" style="display: flex; justify-content: space-between;">
<button type="submit" class="btn btn-primary">Add note link <kbd>enter</kbd></button>
</div>
</form>
</div>
<div class="modal-footer" style="display: flex; justify-content: space-between;">
<button type="submit" class="btn btn-primary">Add note link <kbd>enter</kbd></button>
</div>
</form>
</div>
</div>
</div>
+117 -117
View File
@@ -1,149 +1,149 @@
<div id="attributes-dialog" class="modal fade mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" style="max-width: 1000px" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title mr-auto">Note attributes</h5>
<div class="modal-dialog modal-lg" style="max-width: 1000px" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title mr-auto">Note attributes</h5>
<button class="help-button" type="button" data-help-page="Attributes" title="Help on Attributes">?</button>
<button class="help-button" type="button" data-help-page="Attributes" title="Help on Attributes">?</button>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0;">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form data-bind="submit: save">
<div class="modal-body">
<div style="height: 97%; overflow: auto">
<table id="owned-attributes-table" class="table">
<thead>
<tr>
<th>Type</th>
<th>Name</th>
<th>Value</th>
<th>Inheritable</th>
</tr>
</thead>
<tbody data-bind="foreach: ownedAttributes">
<tr data-bind="if: !isDeleted">
<td>
<input type="hidden" name="position" data-bind="value: position"/>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0;">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form data-bind="submit: save">
<div class="modal-body">
<div style="height: 97%; overflow: auto">
<table id="owned-attributes-table" class="table">
<thead>
<tr>
<th>Type</th>
<th>Name</th>
<th>Value</th>
<th>Inheritable</th>
</tr>
</thead>
<tbody data-bind="foreach: ownedAttributes">
<tr data-bind="if: !isDeleted">
<td>
<input type="hidden" name="position" data-bind="value: position"/>
<select class="form-control attribute-type-select" style="width: auto;"
data-bind="options: $parent.availableTypes, optionsText: 'text', optionsValue: 'value', value: type, event: { change: $parent.typeChanged }"></select>
</td>
<td>
<!-- Change to valueUpdate: blur is necessary because jQuery UI autocomplete hijacks change event -->
<input type="text" class="attribute-name form-control"
data-bind="value: name, valueUpdate: 'blur', event: { blur: $parent.attributeChanged }"/>
<div style="color: red" data-bind="if: $parent.isEmptyName($index())">Attribute name
can't be empty.
</div>
</td>
<td>
<input type="text" class="label-value form-control"
data-bind="visible: type == 'label', value: labelValue, valueUpdate: 'blur', event: { blur: $parent.attributeChanged }"
style="width: 300px"/>
<select class="form-control attribute-type-select" style="width: auto;"
data-bind="options: $parent.availableTypes, optionsText: 'text', optionsValue: 'value', value: type, event: { change: $parent.typeChanged }"></select>
</td>
<td>
<!-- Change to valueUpdate: blur is necessary because jQuery UI autocomplete hijacks change event -->
<input type="text" class="attribute-name form-control"
data-bind="value: name, valueUpdate: 'blur', event: { blur: $parent.attributeChanged }"/>
<div style="color: red" data-bind="if: $parent.isEmptyName($index())">Attribute name
can't be empty.
</div>
</td>
<td>
<input type="text" class="label-value form-control"
data-bind="visible: type == 'label', value: labelValue, valueUpdate: 'blur', event: { blur: $parent.attributeChanged }"
style="width: 300px"/>
<div class="relation-value input-group" data-bind="visible: type == 'relation'"
style="width: 300px;">
<input class="form-control relation-target-note-id"
placeholder="search for note by its name"
data-bind="noteAutocomplete, value: relationValue, valueUpdate: 'blur', event: { blur: $parent.attributeChanged }">
<div class="relation-value input-group" data-bind="visible: type == 'relation'"
style="width: 300px;">
<input class="form-control relation-target-note-id"
placeholder="search for note by its name"
data-bind="noteAutocomplete, value: relationValue, valueUpdate: 'blur', event: { blur: $parent.attributeChanged }">
<div style="color: red" data-bind="if: $parent.isEmptyRelationTarget($index())">Relation target note
can't be empty.
</div>
</div>
<div style="color: red" data-bind="if: $parent.isEmptyRelationTarget($index())">Relation target note
can't be empty.
</div>
</div>
<div data-bind="visible: type == 'label-definition'">
<select data-bind="options: $parent.availableLabelTypes, optionsText: 'text', optionsValue: 'value', value: labelDefinition.labelType"></select>
<div data-bind="visible: type == 'label-definition'">
<select data-bind="options: $parent.availableLabelTypes, optionsText: 'text', optionsValue: 'value', value: labelDefinition.labelType"></select>
<select data-bind="options: $parent.multiplicityTypes, optionsText: 'text', optionsValue: 'value', value: labelDefinition.multiplicityType"></select>
<select data-bind="options: $parent.multiplicityTypes, optionsText: 'text', optionsValue: 'value', value: labelDefinition.multiplicityType"></select>
<label>
<input type="checkbox" value="true"
data-bind="checked: labelDefinition.isPromoted"/>
Promoted
</label>
<label>
<input type="checkbox" value="true"
data-bind="checked: labelDefinition.isPromoted"/>
Promoted
</label>
<div data-bind="visible: labelDefinition.labelType === 'number'"
title="Precision of floating point numbers - 0 means effectively integer, 2 allows entering e.g. 1.23">
Number precision: <input type="number" min="0" max="9" data-bind="value: labelDefinition.numberPrecision" style="width: 50px;"/>
</div>
</div>
<div data-bind="visible: labelDefinition.labelType === 'number'"
title="Precision of floating point numbers - 0 means effectively integer, 2 allows entering e.g. 1.23">
Number precision: <input type="number" min="0" max="9" data-bind="value: labelDefinition.numberPrecision" style="width: 50px;"/>
</div>
</div>
<div data-bind="visible: type == 'relation-definition'">
<select data-bind="options: $parent.multiplicityTypes, optionsText: 'text', optionsValue: 'value', value: relationDefinition.multiplicityType"></select>
<div data-bind="visible: type == 'relation-definition'">
<select data-bind="options: $parent.multiplicityTypes, optionsText: 'text', optionsValue: 'value', value: relationDefinition.multiplicityType"></select>
<label>
<input type="checkbox" value="true"
data-bind="checked: relationDefinition.isPromoted"/>
Promoted
</label>
<br/>
<label>
Inverse relation:
<label>
<input type="checkbox" value="true"
data-bind="checked: relationDefinition.isPromoted"/>
Promoted
</label>
<br/>
<label>
Inverse relation:
<input type="text" value="true" class="attribute-name" data-bind="value: relationDefinition.inverseRelation"/>
</label>
</div>
</td>
<td title="Inheritable relations are automatically inherited to the child notes">
<input type="checkbox" value="1" data-bind="checked: isInheritable"/>
<input type="text" value="true" class="attribute-name" data-bind="value: relationDefinition.inverseRelation"/>
</label>
</div>
</td>
<td title="Inheritable relations are automatically inherited to the child notes">
<input type="checkbox" value="1" data-bind="checked: isInheritable"/>
&nbsp;
&nbsp;
<span title="Delete" style="padding: 13px; cursor: pointer;" class="jam jam-trash"
data-bind="click: $parent.deleteAttribute"></span>
</td>
</tr>
</tbody>
</table>
<span title="Delete" style="padding: 13px; cursor: pointer;" class="jam jam-trash"
data-bind="click: $parent.deleteAttribute"></span>
</td>
</tr>
</tbody>
</table>
<div data-bind="if: inheritedAttributes().length > 0">
<h4>Inherited attributes</h4>
<div data-bind="if: inheritedAttributes().length > 0">
<h4>Inherited attributes</h4>
<table class="table">
<thead>
<tr>
<th>Type</th>
<th>Name</th>
<th>Value</th>
<th>Owning note</th>
</tr>
</thead>
<tbody data-bind="foreach: inheritedAttributes">
<tr>
<td data-bind="text: type"></td>
<td data-bind="text: name"></td>
<td>
<table class="table">
<thead>
<tr>
<th>Type</th>
<th>Name</th>
<th>Value</th>
<th>Owning note</th>
</tr>
</thead>
<tbody data-bind="foreach: inheritedAttributes">
<tr>
<td data-bind="text: type"></td>
<td data-bind="text: name"></td>
<td>
<span data-bind="if: type == 'label'">
<span data-bind="text: value"></span>
</span>
<span data-bind="if: type == 'relation'">
<span data-bind="if: type == 'relation'">
<span data-bind="noteLink: value"></span>
</span>
<span data-bind="if: type == 'label-definition'">
<span data-bind="if: type == 'label-definition'">
<span data-bind="text: value.labelType"></span>
<span data-bind="text: value.multiplicityType"></span>
promoted: <span data-bind="text: value.isPromoted"></span>
</span>
<span data-bind="if: type == 'relation-definition'">
<span data-bind="if: type == 'relation-definition'">
<span data-bind="text: value.multiplicityType"></span>
promoted: <span data-bind="text: value.isPromoted"></span>
</span>
</td>
<td data-bind="noteLink: noteId"></td>
</tr>
</tbody>
</table>
</div>
</div>
</td>
<td data-bind="noteLink: noteId"></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary btn-large" style="width: 200px;" id="save-attributes-button" type="submit">
Save changes <kbd>enter</kbd></button>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-primary btn-large" style="width: 200px;" id="save-attributes-button" type="submit">
Save changes <kbd>enter</kbd></button>
</div>
</form>
</div>
</div>
</div>
+28 -19
View File
@@ -1,24 +1,33 @@
<div id="branch-prefix-dialog" class="modal fade mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title mr-auto">Edit branch prefix</h5>
<div class="modal-dialog modal-lg" role="document">
<form id="branch-prefix-form">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title mr-auto">Edit branch prefix</h5>
<button class="help-button" type="button" data-help-page="Tree-concepts#prefix" title="Help on Tree prefix">?</button>
<button class="help-button" type="button" data-help-page="Tree-concepts#prefix" title="Help on Tree prefix">?</button>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0;">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="branch-prefix-input">Prefix: </label> &nbsp;
<input id="branch-prefix-input" style="width: 20em;"> - <span id="branch-prefix-note-title"></span>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary btn-sm">Save</button>
</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0;">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="branch-prefix-input">Prefix: </label> &nbsp;
<div class="input-group">
<input id="branch-prefix-input" class="form-control">
<div class="input-group-append">
<div id="branch-prefix-note-title" class="input-group-text"></div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary btn-sm">Save</button>
</div>
</div>
</form>
</div>
</div>
</div>
+12 -12
View File
@@ -1,15 +1,15 @@
<div id="event-log-dialog" class="modal fade mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Event log</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<ul id="event-log-list"></ul>
</div>
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Event log</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<ul id="event-log-list"></ul>
</div>
</div>
</div>
</div>
</div>
+19 -19
View File
@@ -1,23 +1,23 @@
<div id="jump-to-note-dialog" class="modal mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Jump to note</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="jump-to-note-autocomplete">Note</label>
<div class="input-group">
<input id="jump-to-note-autocomplete" class="form-control" placeholder="search for note by its name">
</div>
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Jump to note</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="jump-to-note-autocomplete">Note</label>
<div class="input-group">
<input id="jump-to-note-autocomplete" class="form-control" placeholder="search for note by its name">
</div>
</div>
</div>
<div class="modal-footer">
<button id="show-in-full-text-button" class="btn btn-sm">Search in full text <kbd>ctrl+enter</kbd></button>
</div>
</div>
</div>
<div class="modal-footer">
<button id="show-in-full-text-button" class="btn btn-sm">Search in full text <kbd>ctrl+enter</kbd></button>
</div>
</div>
</div>
</div>
+16 -16
View File
@@ -1,20 +1,20 @@
<div id="markdown-import-dialog" class="modal fade mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Markdown import</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>Because of browser sandbox it's not possible to directly read clipboard from JavaScript. Please paste the Markdown to import to textarea below and click on Import button</p>
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Markdown import</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>Because of browser sandbox it's not possible to directly read clipboard from JavaScript. Please paste the Markdown to import to textarea below and click on Import button</p>
<textarea id="markdown-import-textarea" style="height: 340px; width: 100%"></textarea>
</div>
<div class="modal-footer">
<button id="markdown-import-button" class="btn btn-primary">Import <kbd>CTRL+Enter</kbd></button>
</div>
<textarea id="markdown-import-textarea" style="height: 340px; width: 100%"></textarea>
</div>
<div class="modal-footer">
<button id="markdown-import-button" class="btn btn-primary">Import <kbd>CTRL+Enter</kbd></button>
</div>
</div>
</div>
</div>
</div>
+19 -19
View File
@@ -1,27 +1,27 @@
<div id="note-revisions-dialog" class="modal fade mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title mr-auto">Note revisions</h5>
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title mr-auto">Note revisions</h5>
<button class="help-button" type="button" data-help-page="Note-revisions" title="Help on Note revisions">?</button>
<button class="help-button" type="button" data-help-page="Note-revisions" title="Help on Note revisions">?</button>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0 !important;">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" style="display: flex;">
<select id="note-revision-list" size="25" style="width: 150px; height: 630px;">
</select>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0 !important;">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" style="display: flex;">
<select id="note-revision-list" size="25" style="width: 150px; height: 630px;">
</select>
<div id="note-revision-content-wrapper" style="flex-grow: 1; margin-left: 20px;">
<div style="display: flex">
<h3 id="note-revision-title" style="margin: 3px; flex-grow: 100;"></h3>
</div>
<div id="note-revision-content-wrapper" style="flex-grow: 1; margin-left: 20px;">
<div style="display: flex">
<h3 id="note-revision-title" style="margin: 3px; flex-grow: 100;"></h3>
</div>
<div id="note-revision-content" style="height: 600px; width: 600px; overflow: auto;"></div>
<div id="note-revision-content" style="height: 600px; width: 600px; overflow: auto;"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
+12 -12
View File
@@ -1,15 +1,15 @@
<div id="note-source-dialog" class="modal fade mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Note source</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<textarea id="note-source" readonly="readonly" style="min-height: 500px;"></textarea>
</div>
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Note source</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<textarea id="note-source" readonly="readonly" style="min-height: 500px;"></textarea>
</div>
</div>
</div>
</div>
</div>
+218 -216
View File
@@ -1,224 +1,226 @@
<div id="options-dialog" class="modal fade mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" style="min-width: 1000px;" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Options</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div>
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" href="#appearance">Appearance</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#change-password">Change password</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#protected-session-timeout">Protected session</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#note-revision-snapshot-time-interval">Note revisions</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#sync-setup">Sync</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#advanced">Advanced</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#about">About Trilium</a>
</li>
</ul>
<br/>
<div class="tab-content">
<div id="appearance" class="tab-pane active">
<p><strong>Settings on this options tab are saved automatically after each change.</strong></p>
<form>
<div class="form-group">
<label for="theme-select">Theme</label>
<select class="form-control" id="theme-select">
<option value="white">White</option>
<option value="dark">Dark</option>
<option value="black">Black</option>
</select>
</div>
<div class="form-group">
<label for="zoom-factor-select">Zoom factor (desktop build only)</label>
<input type="number" class="form-control" id="zoom-factor-select" min="0.3" max="2.0" step="0.1"/>
</div>
<p>Zooming can be controlled with CTRL-+ and CTRL-= shortcuts as well.</p>
<h3>Left pane sizing</h3>
<div class="form-group">
<label for="left-pane-min-width">Left pane minimum width (in pixels)</label>
<input type="number" class="form-control" id="left-pane-min-width" min="100" max="2000" step="1"/>
</div>
<div class="form-group">
<label for="left-pane-min-width">Left pane width percent of window size</label>
<input type="number" class="form-control" id="left-pane-width-percent" min="0" max="99" step="1"/>
</div>
<p>Left pane width is calculated from the percent of window size, if this is smaller than minimum width, then minimum width is used. If you want to have fixed width left pane, set minimum width to the desired width and set percent to 0.</p>
</form>
<div class="modal-dialog modal-lg" style="min-width: 1000px;" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Options</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div>
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" href="#appearance">Appearance</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#change-password">Change password</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#protected-session-timeout">Protected session</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#note-revision-snapshot-time-interval">Note revisions</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#sync-setup">Sync</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#advanced">Advanced</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#about">About Trilium</a>
</li>
</ul>
<br/>
<div class="tab-content">
<div id="appearance" class="tab-pane active">
<p><strong>Settings on this options tab are saved automatically after each change.</strong></p>
<div id="change-password" class="tab-pane">
<form id="change-password-form">
<div class="form-group">
<label for="old-password">Old password</label>
<input class="form-control" id="old-password" type="password">
<form>
<div class="form-group">
<label for="theme-select">Theme</label>
<select class="form-control" id="theme-select">
<option value="white">White</option>
<option value="dark">Dark</option>
<option value="black">Black</option>
</select>
</div>
<div class="form-group">
<label for="zoom-factor-select">Zoom factor (desktop build only)</label>
<input type="number" class="form-control" id="zoom-factor-select" min="0.3" max="2.0" step="0.1"/>
</div>
<p>Zooming can be controlled with CTRL-+ and CTRL-= shortcuts as well.</p>
<h3>Left pane sizing</h3>
<div class="form-group">
<label for="left-pane-min-width">Left pane minimum width (in pixels)</label>
<input type="number" class="form-control" id="left-pane-min-width" min="100" max="2000" step="1"/>
</div>
<div class="form-group">
<label for="left-pane-min-width">Left pane width percent of window size</label>
<input type="number" class="form-control" id="left-pane-width-percent" min="0" max="99" step="1"/>
</div>
<p>Left pane width is calculated from the percent of window size, if this is smaller than minimum width, then minimum width is used. If you want to have fixed width left pane, set minimum width to the desired width and set percent to 0.</p>
</form>
</div>
<div id="change-password" class="tab-pane">
<form id="change-password-form">
<div class="form-group">
<label for="old-password">Old password</label>
<input class="form-control" id="old-password" type="password">
</div>
<div class="form-group">
<label for="new-password1">New password</label>
<input class="form-control" id="new-password1" type="password">
</div>
<div class="form-group">
<label for="new-password2">New password once more</label>
<input class="form-control" id="new-password2" type="password">
</div>
<button class="btn btn-primary">Change password</button>
</form>
</div>
<div id="protected-session-timeout" class="tab-pane">
<p>Protected session timeout is a time period after which the protected session is wiped out from
browser's memory. This is measured from the last interaction with protected notes.</p>
<form id="protected-session-timeout-form">
<div class="form-group">
<label for="protected-session-timeout-in-seconds">Protected session timeout (in seconds)</label>
<input class="form-control" id="protected-session-timeout-in-seconds" type="number">
</div>
<div style="display: flex; justify-content: space-between;">
<button class="btn btn-primary">Save</button>
<button class="btn btn-default" type="button" data-help-page="Protected-notes">Help</button>
</div>
</form>
</div>
<div id="note-revision-snapshot-time-interval" class="tab-pane">
<p>Note revision snapshot time interval is time in seconds after which new note revision will be created for the note.</p>
<form id="note-revision-snapshot-time-interval-form">
<div class="form-group">
<label for="note-revision-snapshot-time-interval-in-seconds">Note revision snapshot time interval (in seconds)</label>
<input class="form-control" id="note-revision-snapshot-time-interval-in-seconds" type="number">
</div>
<div style="display: flex; justify-content: space-between;">
<button class="btn btn-primary">Save</button>
<button class="btn btn-default" type="button" data-help-page="Note-revisions">Help</button>
</div>
</form>
</div>
<div id="sync-setup" class="tab-pane">
<h4 style="margin-top: 0px;">Sync configuration</h4>
<form id="sync-setup-form">
<div class="form-group">
<label for="sync-server-host">Server instance address</label>
<input class="form-control" id="sync-server-host" placeholder="https://<host>:<port>">
</div>
<div class="form-group">
<label for="sync-server-timeout">Sync timeout (milliseconds)</label>
<input class="form-control" id="sync-server-timeout" min="1" max="10000000" type="number">
</div>
<div class="form-group">
<label for="sync-proxy">Sync proxy server (optional)</label>
<input class="form-control" id="sync-proxy" placeholder="https://<host>:<port>">
<p><strong>Note:</strong> If you leave proxy setting blank, system proxy will be used (applies to desktop/electron build only)</p>
</div>
<div style="display: flex; justify-content: space-between;">
<button class="btn btn-primary">Save</button>
<button class="btn btn-default" type="button" data-help-page="Synchronization">Help</button>
</div>
</form>
<br/>
<h4>Sync test</h4>
<p>This will test connection and handshake to the sync server. If sync server isn't initialized, this will set it up to sync with local document.</p>
<button id="test-sync-button" class="btn btn-default">Test sync</button>
</div>
<div id="advanced" class="tab-pane">
<h4 style="margin-top: 0px;">Sync</h4>
<button id="force-full-sync-button" class="btn btn-default">Force full sync</button>
<br/>
<br/>
<button id="fill-sync-rows-button" class="btn btn-default">Fill sync rows</button>
<br/>
<br/>
<h4>Debugging</h4>
<button id="anonymize-button" class="btn btn-default">Save anonymized database</button><br/><br/>
<p>This action will create a new copy of the database and anonymise it (remove all note content and leave only structure and metadata)
for sharing online for debugging purposes without fear of leaking your personal data.</p>
<h4>Vacuum database</h4>
<p>This will rebuild database which will typically result in smaller database file. No data will be actually changed.</p>
<button id="vacuum-database-button" class="btn btn-default">Vacuum database</button>
</div>
<div id="about" class="tab-pane">
<table class="table">
<tr>
<th>App version:</th>
<td id="app-version"></td>
</tr>
<tr>
<th>DB version:</th>
<td id="db-version"></td>
</tr>
<tr>
<th>Sync version:</th>
<td id="sync-version"></td>
</tr>
<tr>
<th>Build date:</th>
<td id="build-date"></td>
</tr>
<tr>
<th>Build revision:</th>
<td><a href="" target="_blank" id="build-revision"></a></td>
</tr>
</table>
</div>
</div>
</div>
<div class="form-group">
<label for="new-password1">New password</label>
<input class="form-control" id="new-password1" type="password">
</div>
<div class="form-group">
<label for="new-password2">New password once more</label>
<input class="form-control" id="new-password2" type="password">
</div>
<button class="btn btn-primary">Change password</button>
</form>
</div>
<div id="protected-session-timeout" class="tab-pane">
<p>Protected session timeout is a time period after which the protected session is wiped out from
browser's memory. This is measured from the last interaction with protected notes.</p>
<form id="protected-session-timeout-form">
<div class="form-group">
<label for="protected-session-timeout-in-seconds">Protected session timeout (in seconds)</label>
<input class="form-control" id="protected-session-timeout-in-seconds" type="number">
</div>
<div style="display: flex; justify-content: space-between;">
<button class="btn btn-primary">Save</button>
<button class="btn btn-default" type="button" data-help-page="Protected-notes">Help</button>
</div>
</form>
</div>
<div id="note-revision-snapshot-time-interval" class="tab-pane">
<p>Note revision snapshot time interval is time in seconds after which new note revision will be created for the note.</p>
<form id="note-revision-snapshot-time-interval-form">
<div class="form-group">
<label for="note-revision-snapshot-time-interval-in-seconds">Note revision snapshot time interval (in seconds)</label>
<input class="form-control" id="note-revision-snapshot-time-interval-in-seconds" type="number">
</div>
<div style="display: flex; justify-content: space-between;">
<button class="btn btn-primary">Save</button>
<button class="btn btn-default" type="button" data-help-page="Note-revisions">Help</button>
</div>
</form>
</div>
<div id="sync-setup" class="tab-pane">
<h4 style="margin-top: 0px;">Sync configuration</h4>
<form id="sync-setup-form">
<div class="form-group">
<label for="sync-server-host">Server instance address</label>
<input class="form-control" id="sync-server-host" placeholder="https://<host>:<port>">
</div>
<div class="form-group">
<label for="sync-server-timeout">Sync timeout (milliseconds)</label>
<input class="form-control" id="sync-server-timeout" min="1" max="10000000" type="number">
</div>
<div class="form-group">
<label for="sync-proxy">Sync proxy server (optional)</label>
<input class="form-control" id="sync-proxy" placeholder="https://<host>:<port>">
</div>
<div style="display: flex; justify-content: space-between;">
<button class="btn btn-primary">Save</button>
<button class="btn btn-default" type="button" data-help-page="Synchronization">Help</button>
</div>
</form>
<br/>
<h4>Sync test</h4>
<p>This will test connection and handshake to the sync server. If sync server isn't initialized, this will set it up to sync with local document.</p>
<button id="test-sync-button" class="btn btn-default">Test sync</button>
</div>
<div id="advanced" class="tab-pane">
<h4 style="margin-top: 0px;">Sync</h4>
<button id="force-full-sync-button" class="btn btn-default">Force full sync</button>
<br/>
<br/>
<button id="fill-sync-rows-button" class="btn btn-default">Fill sync rows</button>
<br/>
<br/>
<h4>Debugging</h4>
<button id="anonymize-button" class="btn btn-default">Save anonymized database</button><br/><br/>
<p>This action will create a new copy of the database and anonymise it (remove all note content and leave only structure and metadata)
for sharing online for debugging purposes without fear of leaking your personal data.</p>
<h4>Vacuum database</h4>
<p>This will rebuild database which will typically result in smaller database file. No data will be actually changed.</p>
<button id="vacuum-database-button" class="btn btn-default">Vacuum database</button>
</div>
<div id="about" class="tab-pane">
<table class="table">
<tr>
<th>App version:</th>
<td id="app-version"></td>
</tr>
<tr>
<th>DB version:</th>
<td id="db-version"></td>
</tr>
<tr>
<th>Sync version:</th>
<td id="sync-version"></td>
</tr>
<tr>
<th>Build date:</th>
<td id="build-date"></td>
</tr>
<tr>
<th>Build revision:</th>
<td><a href="" target="_blank" id="build-revision"></a></td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -1,26 +1,26 @@
<div id="protected-session-password-dialog" class="modal mx-auto" data-backdrop="false" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title mr-auto">Protected session</h5>
<div class="modal-dialog modal-md" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title mr-auto">Protected session</h5>
<button class="help-button" type="button" data-help-page="Protected-notes" title="Help on Protected notes">?</button>
<button class="help-button" type="button" data-help-page="Protected-notes" title="Help on Protected notes">?</button>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0;">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form id="protected-session-password-form">
<div class="modal-body">
<div class="form-group">
<label for="protected-session-password">To proceed with requested action you need to start protected session by entering password:</label>
<input id="protected-session-password" class="form-control" type="password">
</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0;">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form class="protected-session-password-form">
<div class="modal-body">
<div class="form-group">
<label for="protected-session-password-in-modal">To proceed with requested action you need to start protected session by entering password:</label>
<input id="protected-session-password-in-modal" class="form-control protected-session-password" type="password">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary">Start protected session <kbd>enter</kbd></button>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-primary">Start protected session <kbd>enter</kbd></button>
</div>
</form>
</div>
</div>
</div>
+1
View File
@@ -1,3 +1,4 @@
<div id="recent-changes-dialog" class="modal fade mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
+19 -19
View File
@@ -1,24 +1,24 @@
<div id="sql-console-dialog" class="modal fade mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" style="max-width: 1000px;" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">SQL console</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div id="sql-console-query"></div>
<div class="modal-dialog modal-lg" style="max-width: 1000px;" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">SQL console</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div id="sql-console-query"></div>
<div style="text-align: center">
<button class="btn btn-danger" id="sql-console-execute">Execute <kbd>CTRL+ENTER</kbd></button>
<div style="text-align: center">
<button class="btn btn-danger" id="sql-console-execute">Execute <kbd>CTRL+ENTER</kbd></button>
</div>
<table id="sql-console-results" class="table table-striped" style="overflow: scroll; width: 100%;">
<thead></thead>
<tbody></tbody>
</table>
</div>
</div>
<table id="sql-console-results" class="table table-striped" style="overflow: scroll; width: 100%;">
<thead></thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
-253
View File
@@ -1,253 +0,0 @@
<!DOCTYPE html>
<html lang="en" class="theme-<%= theme %>">
<head>
<meta charset="utf-8">
<title>Trilium Notes</title>
</head>
<body>
<div id="container" style="display:none; grid-template-columns: minmax(<%= leftPaneMinWidth %>px, <%= leftPaneWidthPercent %>fr) <%= rightPaneWidthPercent %>fr">
<div id="header" class="hide-toggle">
<div id="history-navigation" style="display: none;">
<a id="history-back-button" title="Go to previous note." class="icon-action jam jam-arrow-square-left"></a>
&nbsp; &nbsp;
<a id="history-forward-button" title="Go to next note." class="icon-action jam jam-arrow-square-right"></a>
</div>
<div style="flex-grow: 100; display: flex;">
<button class="btn btn-sm" id="jump-to-note-dialog-button" title="CTRL+J">
<span class="jam jam-direction"></span>
Jump to note
</button>
<button class="btn btn-sm" id="recent-changes-button">
<span class="jam jam-history"></span>
Recent changes
</button>
<button class="btn btn-sm" id="enter-protected-session-button" title="Enter protected session to be able to find and view protected notes">
<span class="jam jam-door"></span>
Enter protected session
</button>
<button class="btn btn-sm" id="leave-protected-session-button" title="Leave protected session so that protected notes are not accessible any more." style="display: none;">
<span class="jam jam-log-out"></span>
Leave protected session
</button>
</div>
<div id="plugin-buttons">
</div>
<div>
<button class="btn btn-sm" id="sync-now-button" title="Trigger sync">
<span class="jam jam-refresh"></span>
Sync (<span id="outstanding-syncs-count">0</span>)
</button>
<button class="btn btn-sm" id="options-button">
<span class="jam jam-settings-alt"></span> Options</button>
<form action="logout" id="logout-button" method="POST" style="display: inline;">
<button type="submit" class="btn btn-sm">
<span class="jam jam-log-out"></span>
Logout
</button>
</form>
</div>
</div>
<div id="left-pane" class="hide-toggle">
<div id="global-buttons">
<a id="create-top-level-note-button" title="Create new top level note" class="icon-action jam jam-plus-circle"></a>
<a id="collapse-tree-button" title="Collapse note tree. Shortcut ALT+C" class="icon-action jam jam-align-justify"></a>
<a id="scroll-to-current-note-button" title="Scroll to current note. Shortcut CTRL+." class="icon-action jam jam-download"></a>
<a id="toggle-search-button" title="Search in notes. Shortcut CTRL+S" class="icon-action jam jam-search"></a>
</div>
<input type="file" id="import-upload" style="display: none" />
<div id="search-box">
<div style="display: flex; align-items: center;">
<input name="search-text" placeholder="Search text, labels" style="flex-grow: 100; margin-left: 5px; margin-right: 5px;" autocomplete="off">
<button id="do-search-button" class="btn btn-sm icon-button jam jam-search" title="Search (enter)"></button>
&nbsp;
<button id="save-search-button" class="btn btn-sm icon-button jam jam-save" title="Save search"></button>
&nbsp;
<button id="close-search-button" class="btn btn-sm icon-button jam jam-close" title="Close search"></button>
</div>
</div>
<div id="search-results">
<strong>Search results:</strong>
<ul id="search-results-inner"></ul>
</div>
<div id="tree"></div>
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container"></div>
</div>
<div id="title-container">
<div style="display: flex; align-items: center;">
<div class="dropdown hide-toggle">
<button id="note-path-list-button" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle">
<span id="note-path-count">1 path</span>
<span class="caret"></span>
</button>
<ul id="note-path-list" class="dropdown-menu" aria-labelledby="note-path-list-button">
</ul>
</div>
<input autocomplete="off" value="" id="note-title" style="margin-left: 15px; font-size: x-large; border: 0; width: 5em; flex-grow: 100;" tabindex="1">
<div class="hide-toggle" style="display: flex; align-items: center;">
<span id="note-id-display" title="Note ID"></span>
<button class="btn btn-sm icon-button jam jam-play"
style="display: none; margin-right: 10px;"
title="Render"
id="render-button"></button>
<button class="btn btn-sm icon-button jam jam-play"
style="display: none; margin-right: 10px;"
title="Execute (Ctrl+Enter)"
id="execute-script-button"></button>
<div class="btn-group btn-group-xs">
<button type="button"
class="btn btn-sm icon-button jam jam-shield-check"
id="protect-button"
title="Protected note can be viewed and edited only after entering password">
</button>
<button type="button"
class="btn btn-sm icon-button jam jam-shield-close"
id="unprotect-button"
title="Not protected note can be viewed without entering password">
</button>
</div>
&nbsp; &nbsp;
<div id="note-type-wrapper" style="display: flex;">
<div class="dropdown" id="note-type">
<button data-bind="disable: isDisabled()" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle">
Type: <span data-bind="text: typeString()"></span>
<span class="caret"></span>
</button>
<div id="note-type-dropdown" class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" data-bind="click: selectText, css: { selected: type() == 'text' }"><span class="check">&check;</span> <strong>Text</strong></a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" data-bind="click: selectRelationMap, css: { selected: type() == 'relation-map' && mime() == '' }"><span class="check">&check;</span> <strong>Relation Map</strong></a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" data-bind="click: selectRender, css: { selected: type() == 'render' && mime() == '' }"><span class="check">&check;</span> <strong>Render HTML note</strong></a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" data-bind="click: selectCode, css: { selected: type() == 'code' && mime() == '' }"><span class="check">&check;</span> <strong>Code</strong></a>
<!-- ko foreach: codeMimeTypes -->
<a class="dropdown-item" data-bind="click: $parent.selectCodeMime, css: { selected: $parent.type() == 'code' && $parent.mime() == mime }"><span class="check">&check;</span> <span data-bind="text: title"></span></a>
<!-- /ko -->
</div>
</div>
<div class="dropdown" id="note-actions">
<button type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle">
Note actions
<span class="caret"></span>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" id="show-note-revisions-button" data-bind="css: { disabled: type() == 'file' || type() == 'image' }">Revisions</a>
<a class="dropdown-item show-attributes-button"><kbd>Alt+A</kbd> Attributes</a>
<a class="dropdown-item" id="show-source-button" data-bind="css: { disabled: type() != 'text' && type() != 'code' && type() != 'relation-map' && type() != 'search' }">Note source</a>
<a class="dropdown-item" id="upload-file-button">Upload file</a>
<a class="dropdown-item" id="export-note-button" data-bind="css: { disabled: type() != 'text' }">Export note</a>
</div>
</div>
</div>
</div>
</div>
</div>
<% include details/detail.ejs %>
<% include dialogs/add_link.ejs %>
<% include dialogs/attributes.ejs %>
<% include dialogs/branch_prefix.ejs %>
<% include dialogs/event_log.ejs %>
<% include dialogs/export.ejs %>
<% include dialogs/jump_to_note.ejs %>
<% include dialogs/markdown_import.ejs %>
<% include dialogs/note_revisions.ejs %>
<% include dialogs/note_source.ejs %>
<% include dialogs/options.ejs %>
<% include dialogs/protected_session_password.ejs %>
<% include dialogs/recent_changes.ejs %>
<% include dialogs/sql_console.ejs %>
<% include dialogs/info.ejs %>
<% include dialogs/prompt.ejs %>
<% include dialogs/confirm.ejs %>
</div>
<webview class="electron-in-page-search-window" nodeintegration disablewebsecurity src="libraries/electron-in-page-search/search-window.html"></webview>
<script type="text/javascript">
window.baseApiUrl = 'api/';
window.glob = {
activeDialog: null,
sourceId: '<%= sourceId %>',
maxSyncIdAtLoad: <%= maxSyncIdAtLoad %>,
instanceName: '<%= instanceName %>'
};
</script>
<!-- Required for correct loading of scripts in Electron -->
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
<script src="libraries/jquery.min.js"></script>
<link href="libraries/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<script src="libraries/bootstrap/js/bootstrap.bundle.min.js"></script>
<script src="libraries/bootstrap-notify.min.js"></script>
<!-- Include Fancytree skin and library -->
<link href="libraries/fancytree/skin-win8/ui.fancytree.css" rel="stylesheet">
<script src="libraries/fancytree/jquery.fancytree-all-deps.min.js"></script>
<script src="libraries/jquery.hotkeys.js"></script>
<script src="libraries/jquery.fancytree.hotkeys.js"></script>
<script src="libraries/knockout.min.js"></script>
<script src="libraries/autocomplete.jquery.min.js"></script>
<link href="stylesheets/style.css" rel="stylesheet">
<script src="javascripts/services/bootstrap.js" crossorigin type="module"></script>
<link rel="stylesheet" type="text/css" href="libraries/jam/css/jam.min.css">
<script type="text/javascript">
// we hide container initally because otherwise it is rendered first without CSS and then flickers into
// final form which is pretty ugly.
$("#container").show();
</script>
<style type="text/css">
<%= appCss %>
</style>
</body>
</html>
+66 -39
View File
@@ -1,48 +1,75 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Login</title>
</head>
<body>
<div style="width: 400px; margin: auto; padding-top: 30px;">
<h1>Trilium login</h1>
</head>
<body>
<div class="container">
<div class="col-md-5 offset-md-3" style="padding-top: 25px;">
<h1>Trilium login</h1>
<% if (failedAuth) { %>
<div class="alert alert-warning">
Username and / or password are incorrect. Please try again.
</div>
<% } %>
<% if (failedAuth) { %>
<div class="alert alert-warning">
Username and / or password are incorrect. Please try again.
</div>
<% } %>
<form action="login" method="POST">
<div class="form-group">
<label for="username">Username</label>
<div class="controls">
<input id="username" name="username" placeholder="" class="form-control" type="text">
</div>
</div>
<div class="form-group">
<label for="password">Password</label>
<div class="controls">
<input id="password" name="password" placeholder="" class="form-control" type="password">
</div>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input id="remember-me" name="remember_me" value="1" type="checkbox"> Remember me
</label>
</div>
</div>
<div class="form-group">
<button class="btn btn-success">Login</button>
</div>
</form>
<form action="login" method="POST">
<div class="form-group">
<label for="username">Username</label>
<div class="controls">
<input id="username" name="username" placeholder="" class="form-control" type="text">
</div>
</div>
<div class="form-group">
<label for="password">Password</label>
<div class="controls">
<input id="password" name="password" placeholder="" class="form-control" type="password">
</div>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input id="remember-me" name="remember_me" value="1" type="checkbox"> Remember me
</label>
</div>
</div>
<div class="form-group">
<button class="btn btn-success">Login</button>
</div>
</form>
</div>
</div>
<!-- Required for correct loading of scripts in Electron -->
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
<script>
// Required for correct loading of scripts in Electron
if (typeof module === 'object') {window.module = module; module = undefined;}
<link href="/libraries/bootstrap/css/bootstrap.min.css" rel="stylesheet">
</body>
let device;
if (window.location.search === '?desktop') {
device = "desktop";
}
else if (window.location.search === '?mobile') {
device = "mobile";
}
else {
// mobile device detection based on https://stackoverflow.com/a/24600597/944162
device = /Mobi/.test(navigator.userAgent) ? "mobile" : "desktop";
}
setCookie("trilium-device", device);
function setCookie(name, value) {
const date = new Date(Date.now() + 10 * 365 * 24 * 60 * 60 * 1000);
const expires = "; expires=" + date.toUTCString();
document.cookie = name + "=" + (value || "") + expires + "; path=/";
}
</script>
<link href="libraries/bootstrap/css/bootstrap.min.css" rel="stylesheet">
</body>
</html>
+107
View File
@@ -0,0 +1,107 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Trilium Notes</title>
</head>
<body class="mobile">
<div class="row" id="container-row" style="display: none;">
<div id="left-pane" class="d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-5 col-md-4 col-lg-4 col-xl-4">
<div id="global-buttons">
<a id="create-top-level-note-button" title="Create new top level note" class="icon-action jam jam-plus-circle"></a>
<a id="collapse-tree-button" title="Collapse note tree. Shortcut ALT+C" class="icon-action jam jam-layers"></a>
<a id="scroll-to-current-note-button" title="Scroll to current note. Shortcut CTRL+." class="icon-action jam jam-download"></a>
<div class="dropdown">
<a id="global-actions-button" title="Global actions" class="icon-action jam jam-cogs dropdown-toggle" data-toggle="dropdown"></a>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" id="switch-to-desktop-button"><span class="jam jam-computer"></span> Switch to desktop version</a>
<a class="dropdown-item" id="log-out-button"><span class="jam jam-log-out"></span> Logout</a>
</div>
</div>
</div>
<div id="tree"></div>
</div>
<div id="detail" class="d-none d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-7 col-md-8 col-lg-8">
<div id="note-title-row">
<button type="button" id="note-menu-button" class="action-button jam jam-align-justify"></button>
<input autocomplete="off" value="" id="note-title" class="form-control" tabindex="1">
<button type="button" id="close-detail-button" class="action-button d-sm-none d-md-none d-lg-none d-xl-none" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div style="position: relative; height: 100%;"> <!-- This div here is for saved indicator to have suitable parent -->
<span id="saved-indicator" title="All changes have been saved" class="jam jam-check"></span>
<div id="detail-content">
<div id="note-detail-text" class="note-detail-component" tabindex="10000"></div>
<div id="note-detail-code" class="note-detail-component"></div>
<% include details/search.ejs %>
<% include details/render.ejs %>
<% include details/file.ejs %>
<% include details/image.ejs %>
<% include details/relation_map.ejs %>
<% include details/protected_session_password.ejs %>
</div>
</div>
</div>
<% include dialogs/protected_session_password.ejs %>
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container"></div>
<form action="logout" id="logout-form" method="POST" style="display: none;"></form>
</div>
<script type="text/javascript">
window.baseApiUrl = 'api/';
window.device = "mobile";
window.glob = {
activeDialog: null,
sourceId: '<%= sourceId %>',
maxSyncIdAtLoad: <%= maxSyncIdAtLoad %>,
instanceName: '<%= instanceName %>'
};
</script>
<script src="libraries/jquery.min.js"></script>
<link href="libraries/fancytree/skin-win8/ui.fancytree.css" rel="stylesheet">
<script src="libraries/fancytree/jquery.fancytree-all-deps.min.js"></script>
<link href="libraries/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<script src="libraries/bootstrap/js/bootstrap.bundle.min.js"></script>
<script src="javascripts/mobile.js" crossorigin type="module"></script>
<link href="stylesheets/style.css" rel="stylesheet">
<link href="stylesheets/mobile.css" rel="stylesheet">
<script src="libraries/bootstrap-notify.min.js"></script>
<link rel="stylesheet" type="text/css" href="libraries/jam/css/jam.min.css">
<script type="text/javascript">
// we hide container initally because otherwise it is rendered first without CSS and then flickers into
// final form which is pretty ugly.
$("#container-row").show();
</script>
</body>
</html>
+100 -98
View File
@@ -1,134 +1,136 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Setup</title>
<meta charset="utf-8">
<title>Setup</title>
</head>
<body>
<div id="setup-dialog" style="width: 700px; margin: auto; padding-top: 50px; display:none; font-size: larger;">
<h1>Trilium Notes setup</h1>
<h1>Trilium Notes setup</h1>
<div class="alert alert-warning" id="alert" style="display: none;">
</div>
<div id="setup-type" data-bind="visible: step() == 'setup-type'" style="margin-top: 20px;">
<div class="radio" style="margin-bottom: 15px;">
<label><input type="radio" name="setup-type" value="new-document" data-bind="checked: setupNewDocument">
I'm a new user and I want to create new Trilium document for my notes</label>
</div>
<div class="radio" data-bind="if: instanceType == 'server'" style="margin-bottom: 15px;">
<label><input type="radio" name="setup-type" value="sync-from-desktop" data-bind="checked: setupSyncFromDesktop">
I have desktop instance already and I want to setup sync with it</label>
</div>
<div class="radio" data-bind="if: instanceType == 'desktop'" style="margin-bottom: 15px;">
<label><input type="radio" name="setup-type" value="sync-from-server" data-bind="checked: setupSyncFromServer">
I have server instance up and I want to setup sync with it</label>
<div class="alert alert-warning" id="alert" style="display: none;">
</div>
<button type="button" data-bind="disable: !setupTypeSelected(), click: selectSetupType" class="btn btn-primary">Next</button>
</div>
<div id="setup-type" data-bind="visible: step() == 'setup-type'" style="margin-top: 20px;">
<div class="radio" style="margin-bottom: 15px;">
<label><input type="radio" name="setup-type" value="new-document" data-bind="checked: setupNewDocument">
I'm a new user and I want to create new Trilium document for my notes</label>
</div>
<div class="radio" data-bind="if: instanceType == 'server'" style="margin-bottom: 15px;">
<label><input type="radio" name="setup-type" value="sync-from-desktop" data-bind="checked: setupSyncFromDesktop">
I have desktop instance already and I want to setup sync with it</label>
</div>
<div class="radio" data-bind="if: instanceType == 'desktop'" style="margin-bottom: 15px;">
<label><input type="radio" name="setup-type" value="sync-from-server" data-bind="checked: setupSyncFromServer">
I have server instance up and I want to setup sync with it</label>
</div>
<div data-bind="visible: step() == 'new-document'">
<h2>New document</h2>
<p>You're almost done with the setup. The last thing is to choose username and password using which you'll login to the application.
This password is also used for generating encryption key which encrypts protected notes.</p>
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" data-bind="value: username" placeholder="Choose alphanumeric username">
</div>
<div class="form-group">
<label for="password1">Password</label>
<input type="password" class="form-control" data-bind="value: password1" placeholder="Password">
</div>
<div class="form-group">
<label for="password2">Repeat password</label>
<input type="password" class="form-control" data-bind="value: password2" placeholder="Password">
<button type="button" data-bind="disable: !setupTypeSelected(), click: selectSetupType" class="btn btn-primary">Next</button>
</div>
<button type="button" data-bind="click: back" class="btn btn-default">Back</button>
<div data-bind="visible: step() == 'new-document'">
<h2>New document</h2>
&nbsp;
<p>You're almost done with the setup. The last thing is to choose username and password using which you'll login to the application.
This password is also used for generating encryption key which encrypts protected notes.</p>
<button type="button" data-bind="click: finish" class="btn btn-primary">Finish setup</button>
</div>
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" data-bind="value: username" placeholder="Choose alphanumeric username">
</div>
<div class="form-group">
<label for="password1">Password</label>
<input type="password" class="form-control" data-bind="value: password1" placeholder="Password">
</div>
<div class="form-group">
<label for="password2">Repeat password</label>
<input type="password" class="form-control" data-bind="value: password2" placeholder="Password">
</div>
<div data-bind="visible: step() == 'sync-from-desktop'">
<h2>Sync from Desktop</h2>
<button type="button" data-bind="click: back" class="btn btn-default">Back</button>
<p>This setup needs to be initiated from the desktop instance:</p>
&nbsp;
<ol>
<li>please open your desktop instance of Trilium Notes</li>
<li>click on Options button in the top right</li>
<li>click on Sync tab</li>
<li>configure server instance address to the: <span id="current-host"></span> and click save.</li>
<li>click on "Test sync" button</li>
<li>once you've done all this, click <a href="/">here</a></li>
</ol>
<button type="button" data-bind="click: back" class="btn btn-default">Back</button>
</div>
<div data-bind="visible: step() == 'sync-from-server'">
<h2>Sync from Server</h2>
<p>Please enter Trilium server address and credentials below. This will download the whole Trilium document from server and setup sync to it. Depending on the document size and your connection speed, this may take a while.</p>
<div class="form-group">
<label for="sync-server-host">Trilium server address</label>
<input type="text" id="syncServerHost" class="form-control" data-bind="value: syncServerHost" placeholder="https://<hostname>:<port>">
</div>
<div class="form-group">
<label for="sync-proxy">Proxy server (optional)</label>
<input type="text" id="sync-proxy" class="form-control" data-bind="value: syncProxy" placeholder="https://<hostname>:<port>">
</div>
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" class="form-control" data-bind="value: username" placeholder="Username">
</div>
<div class="form-group">
<label for="password1">Password</label>
<input type="password" id="password1" class="form-control" data-bind="value: password1" placeholder="Password">
<button type="button" data-bind="click: finish" class="btn btn-primary">Finish setup</button>
</div>
<button type="button" data-bind="click: back" class="btn btn-default">Back</button>
<div data-bind="visible: step() == 'sync-from-desktop'">
<h2>Sync from Desktop</h2>
&nbsp;
<p>This setup needs to be initiated from the desktop instance:</p>
<button type="button" data-bind="click: finish" class="btn btn-primary">Finish setup</button>
</div>
<ol>
<li>please open your desktop instance of Trilium Notes</li>
<li>click on Options button in the top right</li>
<li>click on Sync tab</li>
<li>configure server instance address to the: <span id="current-host"></span> and click save.</li>
<li>click on "Test sync" button</li>
<li>once you've done all this, click <a href="/">here</a></li>
</ol>
<div data-bind="visible: step() == 'sync-in-progress'">
<h2>Sync in progress</h2>
<div class="alert alert-success">Sync has been correctly set up. It will take some time for the initial sync to finish. Once it's done, you'll be redirected to the login page.</div>
<div data-bind="if: instanceType == 'desktop'">
Outstanding sync items: <strong id="outstanding-syncs">N/A</strong>
<button type="button" data-bind="click: back" class="btn btn-default">Back</button>
</div>
<div data-bind="visible: step() == 'sync-from-server'">
<h2>Sync from Server</h2>
<p>Please enter Trilium server address and credentials below. This will download the whole Trilium document from server and setup sync to it. Depending on the document size and your connection speed, this may take a while.</p>
<div class="form-group">
<label for="sync-server-host">Trilium server address</label>
<input type="text" id="syncServerHost" class="form-control" data-bind="value: syncServerHost" placeholder="https://<hostname>:<port>">
</div>
<div class="form-group">
<label for="sync-proxy">Proxy server (optional)</label>
<input type="text" id="sync-proxy" class="form-control" data-bind="value: syncProxy" placeholder="https://<hostname>:<port>">
<p><strong>Note:</strong> If you leave proxy setting blank, system proxy will be used (applies to desktop/electron build only)</p>
</div>
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" class="form-control" data-bind="value: username" placeholder="Username">
</div>
<div class="form-group">
<label for="password1">Password</label>
<input type="password" id="password1" class="form-control" data-bind="value: password1" placeholder="Password">
</div>
<button type="button" data-bind="click: back" class="btn btn-default">Back</button>
&nbsp;
<button type="button" data-bind="click: finish" class="btn btn-primary">Finish setup</button>
</div>
<div data-bind="visible: step() == 'sync-in-progress'">
<h2>Sync in progress</h2>
<div class="alert alert-success">Sync has been correctly set up. It will take some time for the initial sync to finish. Once it's done, you'll be redirected to the login page.</div>
<div data-bind="if: instanceType == 'desktop'">
Outstanding sync items: <strong id="outstanding-syncs">N/A</strong>
</div>
</div>
</div>
</div>
<script type="text/javascript">
const baseApiUrl = 'api/';
const glob = {
sourceId: ''
};
const syncInProgress = <%= syncInProgress ? 'true' : 'false' %>;
const baseApiUrl = 'api/';
const glob = {
sourceId: ''
};
const syncInProgress = <%= syncInProgress ? 'true' : 'false' %>;
</script>
<!-- Required for correct loading of scripts in Electron -->
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
<script src="/libraries/jquery.min.js"></script>
<script src="libraries/jquery.min.js"></script>
<link href="/libraries/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<script src="/libraries/bootstrap/js/bootstrap.bundle.min.js"></script>
<link href="libraries/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<script src="libraries/bootstrap/js/bootstrap.bundle.min.js"></script>
<script src="/libraries/knockout.min.js"></script>
<script src="libraries/knockout.min.js"></script>
<script src="/javascripts/setup.js" crossorigin type="module"></script>
<script src="javascripts/setup.js" crossorigin type="module"></script>
</body>
</html>