Compare commits

...

223 Commits

Author SHA1 Message Date
zadam
6cc0dd5a80 release 0.29.0-beta 2019-02-03 15:44:19 +01:00
zadam
afd5f4823f added Steel Blue theme to demo document 2019-02-03 15:39:27 +01:00
zadam
b0cf82c91b fix 2019-02-03 15:37:01 +01:00
zadam
6a67cdd5af appThemeClass is redundant 2019-02-03 15:35:37 +01:00
zadam
bad7b84993 error handling in custom request handler 2019-02-03 11:15:32 +01:00
zadam
d3ca6b5ae6 styling for scrollbar which looks ugly otherwise in dark themes 2019-02-03 10:21:28 +01:00
zadam
da5009f089 main border color CSS variable 2019-02-03 10:09:59 +01:00
zadam
c08524c977 fix CSS class of user theme 2019-02-03 00:12:57 +01:00
zadam
f89537037e small styling fixes 2019-02-02 23:51:00 +01:00
zadam
c153793766 added CSS variable for disabled button background 2019-02-02 23:26:39 +01:00
zadam
0aec5927d5 added missing labels customRequestHandler, customResourceProvider to autocomplete 2019-02-02 22:33:02 +01:00
zadam
8aea9a1801 added font family CSS variables to theming API 2019-02-02 20:45:38 +01:00
zadam
73247e3220 minor library updates 2019-02-02 19:21:30 +01:00
zadam
89344a6eda final fixes and refactorings for consistency checks 2019-02-02 12:41:20 +01:00
zadam
40d2e6ea83 refactoring consistency checks WIP 2019-02-02 11:26:27 +01:00
zadam
910cfe9a17 refactoring consistency checks WIP 2019-02-02 10:38:33 +01:00
zadam
e58a80fc00 consistency checks WIP 2019-02-02 09:26:57 +01:00
zadam
4a2319cb33 refactoring of consistency checks plus some autofixers 2019-02-01 22:48:51 +01:00
zadam
5619088c41 raise payload size limit to 500 MB #395 2019-01-29 21:19:08 +01:00
zadam
60271993eb Merge pull request #392 from jkurei/ios_favicon
Better icon for iOS' homescreen
2019-01-28 23:01:12 +01:00
jkurei
6695e8b011 Better icon for iOS' homescreen 2019-01-28 22:48:01 +01:00
zadam
707df18b93 added type and mime classes on body as well #383 2019-01-28 21:42:37 +01:00
zadam
90895f1288 added noteId to file view 2019-01-27 23:10:37 +01:00
zadam
ba1ca506af change in referencing CSS resources to allow easier relative linking 2019-01-27 22:34:41 +01:00
zadam
f90ed99a40 fix leaf node having angle bracket in dark & black themes, closes #387 2019-01-27 21:54:24 +01:00
zadam
67630b1a22 options now allow selecting user theme 2019-01-27 21:18:11 +01:00
zadam
2c1580ea65 appCss/appTheme are now loaded as external CSS files instead of inline styles 2019-01-27 17:01:37 +01:00
zadam
840a0b5f64 custom handler refactoring 2019-01-27 16:37:18 +01:00
zadam
b39f6ef7ad bug fixes for custom handlers 2019-01-27 15:47:40 +01:00
zadam
fb27088fcd smaller children overview font 2019-01-27 14:26:39 +01:00
azivner
76fbff68ba added readOnly attribute which puts text editor into readonly mode #371 2019-01-27 13:10:03 +01:00
azivner
54de4d236d custom HTTP handler which triggers associated script notes WIP, #356 2019-01-27 12:28:20 +01:00
azivner
e211dd65ad exit on detection of not supported node version, #324 2019-01-26 19:59:51 +01:00
azivner
a87f4d8653 search can be triggered from URL, closes #385 2019-01-25 22:18:34 +01:00
azivner
b59c175c2e add HTML header with UTF-8 meta encoding declaration to exported HTML files, fixes #384 2019-01-25 21:34:14 +01:00
azivner
580104c4c5 using trilium's confirm dialog, small refactoring 2019-01-24 22:18:31 +01:00
zadam
0fc3053b0a Merge pull request #377 from flurmbo/master
add confirm type change dialog when note not empty
2019-01-24 22:12:09 +01:00
Phil Marshall
929e0f69c2 add confirm type change dialog when note not empty 2019-01-23 15:15:24 -06:00
azivner
e70af1300a drag and drop moves multiple items only if CTRL is pressed, active note has now bold text for more differentiation from selected note 2019-01-23 21:13:04 +01:00
azivner
4d0e46021b Mac uses CMD+Left, CMD+Right for history navigation, closes #376 2019-01-23 20:15:33 +01:00
azivner
1a9a49b739 release 0.28.3 2019-01-22 23:01:32 +01:00
azivner
2ac560c56e text note should change its icon after having new note inserted into, fixes #361 2019-01-22 21:21:44 +01:00
azivner
c23387c0fb make search dialog more responsive in narrow sidebar, fixes #367 2019-01-22 20:54:00 +01:00
azivner
0ff250fe15 font sizes were not created for new documents - fix plus migration for existing ones 2019-01-22 20:34:45 +01:00
azivner
52b1e58b26 make tree selection more visually consistent 2019-01-22 20:23:54 +01:00
azivner
484715e440 fix icon in desktop linux build, fixes #372 2019-01-22 19:49:33 +01:00
azivner
9d42c3d802 release 0.28.2 2019-01-21 22:55:12 +01:00
azivner
c654172d33 auto fixer for "undeleted branch of deleted note" consistency check 2019-01-21 22:51:49 +01:00
azivner
e17b26c883 revert consistency checks refactoring for now 2019-01-21 22:46:27 +01:00
azivner
24d02d9cf5 fix in task manager script to not error out so much on deletion 2019-01-21 22:05:37 +01:00
azivner
7208a311ac check that note is not deleted before creating a branch 2019-01-21 21:55:40 +01:00
azivner
ad7355372b issue template 2019-01-21 19:55:07 +01:00
azivner
f18b5babad uncommented issue template 2019-01-20 16:47:49 +01:00
azivner
9831ec0ca9 added issue template 2019-01-20 16:46:56 +01:00
azivner
596544eca3 fix setNoteToParent API method, closes #360 2019-01-20 16:33:09 +01:00
azivner
1f853024ee more autofixers WIP 2019-01-19 09:57:51 +01:00
azivner
0308b13460 expose app info on the backend script api #345 2019-01-18 23:57:08 +01:00
azivner
06b8a82f70 refactoring of consistency checks + some auto fixers 2019-01-18 19:32:59 +01:00
azivner
91ca07929d before we clone the note we must make sure it's not deleted 2019-01-17 23:24:59 +01:00
azivner
afabaa5fdb workaround for hidden last line on mobile 2019-01-17 21:13:53 +01:00
azivner
a6fd3fa77c release 0.28.1-beta 2019-01-17 00:03:17 +01:00
azivner
58a2c08dcd release 0.28.1-beta 2019-01-16 23:58:10 +01:00
azivner
299bbff2f4 fix too large font on mobile 2019-01-16 23:57:09 +01:00
azivner
19d8947123 runOnNoteChange fires also on the frontend, closes #340 2019-01-16 22:52:32 +01:00
azivner
35edce7523 fix crazy clipboard operation on mac setup, closes #348 2019-01-16 22:03:30 +01:00
azivner
bc4cec69a5 use local dates to create day notes, closes #337 2019-01-15 23:46:01 +01:00
azivner
cce8c1b674 create top level note and collapse tree now work relative to hoisted note, closes #343 2019-01-15 20:30:54 +01:00
azivner
aa58788769 added basic DB size diagnostic log 2019-01-15 20:00:24 +01:00
azivner
6c62ab7a52 removing logging params for slow queries 2019-01-15 19:36:04 +01:00
azivner
d6ab638b30 fix file names in github release script 2019-01-15 00:10:47 +01:00
azivner
bd4db406de release 0.28.0-beta 2019-01-14 23:51:55 +01:00
azivner
e50f9cd0a3 fix device detection in setup 2019-01-14 23:50:45 +01:00
azivner
2b64cbce2c added dark theme (to have same set of themes as before) 2019-01-13 23:25:30 +01:00
azivner
2797c942ab changes to options dialog to allow configuring font size, closes #326 2019-01-13 22:03:06 +01:00
azivner
f1c3278874 store font size in options #326 2019-01-13 21:27:32 +01:00
azivner
424c22dcde split UI to 3 different font sizes #326 2019-01-13 21:16:51 +01:00
azivner
5c223dfd12 all fonts are relative so it's easier to scale them properly 2019-01-13 21:04:08 +01:00
azivner
6a3e7a5a8e generate css classes for each mime type, #328 2019-01-13 20:14:33 +01:00
azivner
f88cdac000 fixes for black theme for relation map and code notes 2019-01-13 20:03:28 +01:00
azivner
eeead90f32 improved theme support using CSS variables, #328 2019-01-13 18:57:46 +01:00
azivner
b607857409 fix weight tracker demo to use relations instead of hardcoded noteId, fixes #329 2019-01-13 12:21:17 +01:00
azivner
9268f88bc3 frontend scripts now have startNote, currentNote and targetNote as NoteShort entities which e.g. provides easy access to relations/labels 2019-01-13 12:16:05 +01:00
azivner
f7f0560a9f simplification of script bundles on backend 2019-01-13 11:56:50 +01:00
azivner
3d8905207e fixed export with non-ASCII characters in note title, fixes #285, #331 2019-01-13 10:22:17 +01:00
azivner
dbc312010b update fancytree to 2.30.2 2019-01-13 09:24:00 +01:00
azivner
b115a7cf19 delete note through its entity instead of manually with SQL, closes #303 2019-01-13 00:24:51 +01:00
azivner
348562352c fixes in ASAR build and zipping 2019-01-12 19:48:45 +01:00
azivner
70fd917e7c build desktop versions into ASAR, closes #271 2019-01-12 00:05:13 +01:00
zadam
d2b60764cd Merge pull request #289 from perissology/electron-builder
use electron-builder for dev install
2019-01-11 23:54:11 +01:00
azivner
581b1fdaa5 trigger runOnAttributeChange, runOnNoteChange on entity deletions as well 2019-01-11 23:43:22 +01:00
azivner
3c19a712c0 active protected state button has darker background and is disabled 2019-01-11 23:29:56 +01:00
azivner
cc27f16088 store iv directly in the respective columns 2019-01-11 23:04:51 +01:00
azivner
dffdb82288 after creating new note, it is focused and now also selected so renaming is super easy, #318 2019-01-10 22:46:08 +01:00
azivner
2b32addade release 0.27.4 2019-01-10 21:31:30 +01:00
azivner
0b251530fa open recent notes autocomplete by focus so it is then closed with blur, fixes #272 2019-01-10 21:04:06 +01:00
azivner
f5b933149a Merge remote-tracking branch 'origin/master' 2019-01-10 19:53:47 +01:00
azivner
48bbfb8bdb fix activating note by noteId when hoisting, fixes #320 2019-01-10 19:53:42 +01:00
zadam
889971c4d6 Merge pull request #312 from perissology/evernote-import
Fixes evernote import errors
2019-01-09 23:41:17 +01:00
azivner
0722494d41 fix saving JSON note with invalid JSON (previously in such a case content was not updated), fixes #307 2019-01-09 23:36:17 +01:00
azivner
4b977a3306 setup keyboard shortcuts on the setup page as well, closes #267 2019-01-09 22:08:24 +01:00
azivner
3ff3021acd shortcuts for mac should use cmd instead of ctrl, closes #290 2019-01-09 21:42:16 +01:00
azivner
99e56a9c42 make sure to save the search note before refreshing the tree 2019-01-09 19:54:32 +01:00
azivner
77279dfe16 fix anonymization 2019-01-09 19:49:02 +01:00
perissology
93f8050454 use resizedImage if image optimization fails 2019-01-09 06:29:49 -08:00
perissology
31cfede7a7 enex import: attempt to get correct mime from Buffer 2019-01-09 06:29:13 -08:00
azivner
c8ec86e537 allow refreshing saved note, closes #304 2019-01-08 23:32:03 +01:00
azivner
05aee884b6 fix saving search note content #304 2019-01-08 22:48:53 +01:00
azivner
012ba9e060 don't attempt to run protected notes outside of protected session, fixes #279 2019-01-08 21:21:49 +01:00
azivner
8e8fd88857 process only whitelisted mime types as an image, fixes #288 2019-01-08 20:45:34 +01:00
azivner
523ccdad6b reload note cache after import, closes #293 2019-01-08 20:19:41 +01:00
azivner
ded3f605be fix almost invisible buttons on options page, closes #297 2019-01-08 19:47:35 +01:00
perissology
62b44e3549 use electron-builder for npm install
This will build deps as required for electron as well as the os.

This fixes the missing binding error for sqlite3 when trying to run the
electron app after a fresh npm i
2019-01-08 06:50:48 -08:00
azivner
030d12a465 stretch sync login token validity to 5 minutes #277 2019-01-07 23:29:56 +01:00
azivner
4d15628840 Merge remote-tracking branch 'origin/master' 2019-01-07 23:17:45 +01:00
azivner
81b849898c stretch body to full window width, fixes #276 2019-01-07 23:17:12 +01:00
zadam
3824486b85 Merge pull request #275 from Lee303/Dockerfile-dependency-fix
Update Dockerfile
2019-01-07 22:49:27 +01:00
Lee Spottiswood
081ab00a0a Update Dockerfile 2019-01-07 21:21:23 +00:00
zadam
04f6af5c9a Merge pull request #270 from svenefftinge/master
Make contributions easier
2019-01-07 21:46:02 +01:00
Sven Efftinge
4dc1f1f6eb Added contribute section and gitpod config 2019-01-07 12:52:02 +00:00
azivner
3930a02123 tree now uses standard font size which effectively makes it a bit larger 2019-01-06 20:59:19 +01:00
azivner
3112de105e fancytree selection/hover colors are shades of gray, border is rounded 2019-01-06 18:58:12 +01:00
azivner
3b8d7b8fba release 0.27.3 2019-01-05 22:48:11 +01:00
azivner
9fca7f09a5 link to mobile frontend 2019-01-05 22:45:18 +01:00
azivner
fd39d6b3a9 using btn-secondary instead of btn-default since that doesn't exist in BS4 2019-01-05 21:51:27 +01:00
azivner
a103886ea5 responsive setup page 2019-01-05 21:49:40 +01:00
azivner
373408e401 fix Branch reference to parent note id after parent change 2019-01-05 19:25:22 +01:00
azivner
db44c1d8e6 border color tweaks 2019-01-05 11:49:17 +01:00
azivner
95a34c9e2d small tweaks of icon alignment 2019-01-05 11:41:09 +01:00
azivner
6ce401f260 release 0.27.2-beta 2019-01-04 23:33:32 +01:00
azivner
5d74dcd256 package-lock 2019-01-04 23:33:30 +01:00
azivner
5a9fc1697b electron 4.0.1 2019-01-04 23:32:14 +01:00
azivner
927415838c using enhanceTitle event instead of renderNode per https://github.com/mar10/fancytree/issues/927 2019-01-04 20:18:07 +01:00
azivner
d72fcefdc7 fix sync of deleted notes 2019-01-04 20:10:16 +01:00
azivner
0be173a8f7 lower lastSyncedPull if server max sync ID is lower 2019-01-04 18:58:46 +01:00
azivner
c3913a8735 updating the note cache after sync 2019-01-03 23:27:10 +01:00
azivner
e2dfe1b6de fixed setup page issues which caused wrong choice to be used in the wizard 2019-01-03 22:32:36 +01:00
azivner
fec3e47eb8 serialize binary note content into base64, incremented sync version 2019-01-03 22:13:58 +01:00
azivner
d72efd2450 removed unnecessary logging of data dir which is now logged as part of app info 2019-01-03 21:46:32 +01:00
azivner
ef1c840aa7 tiny fixes 2019-01-02 22:36:06 +01:00
azivner
1581464d8c fix layout - promoted attributes should never grow beyond its content 2019-01-02 20:21:34 +01:00
azivner
9de29584a4 added data directory to options -> about 2019-01-02 20:08:41 +01:00
azivner
9e2e6fb50c fixed glitch where "unhoist" button disappears after renaming the hoisted note 2019-01-02 19:26:01 +01:00
azivner
c85979b66b it should not be possible to add note after hoisted note 2019-01-02 18:59:08 +01:00
azivner
ecdc5865a6 release 0.27.1-beta 2019-01-01 20:54:23 +01:00
azivner
1771ddb787 responsive promoted attributes view and attribute overview to make sure note content is always visible. Fixes #262 2019-01-01 20:53:18 +01:00
azivner
3ab657fe46 created concept of "detail loaded listeners" which allow scripts to execute some action after the note detail has been loaded 2019-01-01 19:32:34 +01:00
azivner
8785dae753 force reload fancytree node when child is not found 2019-01-01 18:27:36 +01:00
azivner
2f1c5b29d4 update eslint 2019-01-01 18:06:52 +01:00
azivner
7135349a10 allow deleting protected notes outside of protected session 2019-01-01 17:04:21 +01:00
azivner
66c639d5e3 frontend API addition to protect current note 2019-01-01 15:39:13 +01:00
azivner
6704b755d8 attribute overview shows only owned attributes 2019-01-01 15:29:58 +01:00
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
170 changed files with 45098 additions and 25403 deletions

7
.gitpod.yml Normal file
View File

@@ -0,0 +1,7 @@
tasks:
- before: nvm install 10 && nvm use 10
init: npm install
command: npm run start
ports:
- port: 8080
onOpen: open-preview

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
@@ -17,6 +17,7 @@ RUN set -x \
libtool \
make \
nasm \
libpng-dev \
&& npm install --production \
&& apk del .build-dependencies

View File

@@ -1,7 +1,7 @@
# Trilium Notes
[![Join the chat at https://gitter.im/trilium-notes/Lobby](https://badges.gitter.im/trilium-notes/Lobby.svg)](https://gitter.im/trilium-notes/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
Trilium Notes is a hierarchical note taking application with focus on building large personal knowledge bases. See [screenshots](https://github.com/zadam/trilium/wiki/Screenshot-tour) for quick overview:
Trilium Notes is a hierarchical note taking application with focus on building large personal knowledge bases. See [screenshots](https://github.com/zadam/trilium/wiki/Screenshot-tour) for quick overview:
![](https://raw.githubusercontent.com/wiki/zadam/trilium/images/screenshot.png)
@@ -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
@@ -18,14 +18,15 @@ Trilium Notes is a hierarchical note taking application with focus on building l
* [Relation maps](https://github.com/zadam/trilium/wiki/Relation-map) for visualizing notes and their relations
* [Scripting](https://github.com/zadam/trilium/wiki/Scripts) - see [Advanced showcases](https://github.com/zadam/trilium/wiki/Advanced-showcases)
* Scales well in both usability and performance upwards of 100 000 notes
* Touch optimized [mobile frontend](https://github.com/zadam/trilium/wiki/Mobile-frontend) for smartphones and tablets
* [Night theme](https://github.com/zadam/trilium/wiki/Themes)
* [Evernote](https://github.com/zadam/trilium/wiki/Evernote-import) and [Markdown import & export](https://github.com/zadam/trilium/wiki/Markdown)
## 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.
@@ -33,4 +34,16 @@ Trilium is provided as either desktop application (Linux, Windows) or web applic
[See wiki for complete list of documentation pages.](https://github.com/zadam/trilium/wiki/)
You can also read [Patterns of personal knowledge base](https://github.com/zadam/trilium/wiki/Patterns-of-personal-knowledge-base) to get some inspiration on how you might use Trilium.
You can also read [Patterns of personal knowledge base](https://github.com/zadam/trilium/wiki/Patterns-of-personal-knowledge-base) to get some inspiration on how you might use Trilium.
## Contribute
Use a browser based dev environment
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/zadam/trilium)
Or clone locally and run
```
npm install
npm run start
```

View File

@@ -3,22 +3,22 @@
BUILD_DIR=./dist/trilium-linux-x64
rm -rf $BUILD_DIR
# we build x64 as second so that we keep X64 binaries in node_modules for local development and server build
echo "Rebuilding binaries for linux-x64"
./node_modules/.bin/electron-rebuild --arch=x64
rm -r node_modules/sqlite3/lib/binding/*
./node_modules/.bin/electron-packager . --out=dist --executable-name=trilium --platform=linux --arch=x64 --overwrite
cp -r bin/deps/linux-x64/sqlite/* node_modules/sqlite3/lib/binding/
./node_modules/.bin/electron-packager . --asar --out=dist --executable-name=trilium --platform=linux --arch=x64 --overwrite
mv "./dist/Trilium Notes-linux-x64" $BUILD_DIR
rm -r "$BUILD_DIR/resources/app/node_modules/sqlite3/lib/binding/*"
cp src/public/images/app-icons/png/128x128.png $BUILD_DIR/icon.png
cp -r bin/deps/linux-x64/sqlite/electron* "$BUILD_DIR/resources/app/node_modules/sqlite3/lib/binding/"
rm -r $BUILD_DIR/resources/app/bin/deps
# removing software WebGL binaries because they are pretty huge and not necessary
rm -r $BUILD_DIR/swiftshader
echo "Packaging linux x64 electron distribution..."
VERSION=`jq -r ".version" package.json`
7z a $BUILD_DIR-${VERSION}.7z $BUILD_DIR
cd dist
tar cJf trilium-linux-x64-${VERSION}.tar.xz trilium-linux-x64

View File

@@ -3,25 +3,30 @@
BUILD_DIR=./dist/trilium-mac-x64
rm -rf $BUILD_DIR
./node_modules/.bin/electron-packager . --out=dist --executable-name=trilium --platform=darwin --arch=x64 --overwrite --icon=src/public/images/app-icons/mac/icon.icns
echo "Copying required mac binaries"
rm -r node_modules/sqlite3/lib/binding/*
rm -r node_modules/mozjpeg/vendor/*
rm -r node_modules/pngquant-bin/vendor/*
rm -r node_modules/giflossy/vendor/*
cp -r bin/deps/mac-x64/sqlite/* node_modules/sqlite3/lib/binding/
cp bin/deps/mac-x64/image/cjpeg node_modules/mozjpeg/vendor/
cp bin/deps/mac-x64/image/pngquant node_modules/pngquant-bin/vendor/
cp bin/deps/mac-x64/image/gifsicle node_modules/giflossy/vendor/
./node_modules/.bin/electron-packager . --asar --out=dist --executable-name=trilium --platform=darwin --arch=x64 --overwrite --icon=src/public/images/app-icons/mac/icon.icns
# Mac build has by default useless directory level
mv "./dist/Trilium Notes-darwin-x64" $BUILD_DIR
echo "Copying required mac binaries"
./reset-local.sh
MAC_RES_DIR=$BUILD_DIR/Trilium\ Notes.app/Contents/Resources/app
rm -r "$MAC_RES_DIR/node_modules/sqlite3/lib/binding/*"
cp -r bin/deps/mac-x64/sqlite/* "$MAC_RES_DIR/node_modules/sqlite3/lib/binding/"
cp bin/deps/mac-x64/image/cjpeg "$MAC_RES_DIR/node_modules/mozjpeg/vendor/"
cp bin/deps/mac-x64/image/pngquant "$MAC_RES_DIR/node_modules/pngquant-bin/vendor/"
cp bin/deps/mac-x64/image/gifsicle "$MAC_RES_DIR/node_modules/giflossy/vendor/"
rm -r "$MAC_RES_DIR/bin/deps"
echo "Packaging mac x64 electron distribution..."
echo "Zipping mac x64 electron distribution..."
VERSION=`jq -r ".version" package.json`
7z a $BUILD_DIR-${VERSION}.7z $BUILD_DIR
cd dist
rm trilium-mac-x64-${VERSION}.zip
zip -r9 --symlinks trilium-mac-x64-${VERSION}.zip trilium-mac-x64

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,6 @@ chmod 755 trilium.sh
cd ..
VERSION=`jq -r ".version" ../../package.json`
7z a trilium-linux-x64-server-${VERSION}.7z trilium-linux-x64-server
VERSION=`jq -r ".version" ../package.json`
tar cJf trilium-linux-x64-server-${VERSION}.tar.xz trilium-linux-x64-server

View File

@@ -3,23 +3,30 @@
BUILD_DIR=./dist/trilium-windows-x64
rm -rf $BUILD_DIR
./node_modules/.bin/electron-packager . --out=dist --executable-name=trilium --platform=win32 --arch=x64 --overwrite --icon=src/public/images/app-icons/win/icon.ico
echo "Copying required windows binaries"
rm -r node_modules/sqlite3/lib/binding/*
rm -r node_modules/mozjpeg/vendor/*
rm -r node_modules/pngquant-bin/vendor/*
rm -r node_modules/giflossy/vendor/*
cp -r bin/deps/win-x64/sqlite/* node_modules/sqlite3/lib/binding/
cp bin/deps/win-x64/image/cjpeg.exe node_modules/mozjpeg/vendor/
cp bin/deps/win-x64/image/pngquant.exe node_modules/pngquant-bin/vendor/
cp bin/deps/win-x64/image/gifsicle.exe node_modules/giflossy/vendor/
./node_modules/.bin/electron-packager . --asar --out=dist --executable-name=trilium --platform=win32 --arch=x64 --overwrite --icon=src/public/images/app-icons/win/icon.ico
mv "./dist/Trilium Notes-win32-x64" $BUILD_DIR
echo "Copying required windows binaries"
WIN_RES_DIR=$BUILD_DIR/resources/app
cp -r bin/deps/win-x64/sqlite/* $WIN_RES_DIR/node_modules/sqlite3/lib/binding/
cp bin/deps/win-x64/image/cjpeg.exe $WIN_RES_DIR/node_modules/mozjpeg/vendor/
cp bin/deps/win-x64/image/pngquant.exe $WIN_RES_DIR/node_modules/pngquant-bin/vendor/
cp bin/deps/win-x64/image/gifsicle.exe $WIN_RES_DIR/node_modules/giflossy/vendor/
rm -r $WIN_RES_DIR/bin/deps
# removing software WebGL binaries because they are pretty huge and not necessary
rm -r $BUILD_DIR/swiftshader
echo "Packaging windows x64 electron distribution..."
./reset-local.sh
echo "Zipping windows x64 electron distribution..."
VERSION=`jq -r ".version" package.json`
7z a $BUILD_DIR-${VERSION}.7z $BUILD_DIR
cd dist
zip -r9 trilium-windows-x64-${VERSION}.zip trilium-windows-x64

View File

@@ -42,11 +42,10 @@ 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
LINUX_X64_BUILD=trilium-linux-x64-$VERSION.tar.xz
WINDOWS_X64_BUILD=trilium-windows-x64-$VERSION.zip
MAC_X64_BUILD=trilium-mac-x64-$VERSION.zip
SERVER_BUILD=trilium-linux-x64-server-$VERSION.tar.xz
echo "Creating release in GitHub"
@@ -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 \

3
bin/reset-local.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env bash
./node_modules/.bin/electron-rebuild --arch=x64

Binary file not shown.

View File

@@ -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);

View File

@@ -0,0 +1,62 @@
const sql = require('../../src/services/sql');
function prependIv(cipherText, ivText) {
const arr = ivText.split("").map(c => parseInt(c) || 0);
const iv = Buffer.from(arr);
const payload = Buffer.from(cipherText, 'base64');
const complete = Buffer.concat([iv, payload]);
return complete.toString('base64');
}
async function updateEncryptedDataKey() {
const encryptedDataKey = await sql.getValue("SELECT value FROM options WHERE name = 'encryptedDataKey'");
const encryptedDataKeyIv = await sql.getValue("SELECT value FROM options WHERE name = 'encryptedDataKeyIv'");
const newEncryptedDataKey = prependIv(encryptedDataKey, encryptedDataKeyIv);
await sql.execute("UPDATE options SET value = ? WHERE name = 'encryptedDataKey'", [newEncryptedDataKey]);
await sql.execute("DELETE FROM options WHERE name = 'encryptedDataKeyIv'");
await sql.execute("DELETE FROM sync WHERE entityName = 'options' AND entityId = 'encryptedDataKeyIv'");
}
async function updateNotes() {
const protectedNotes = await sql.getRows("SELECT noteId, title, content FROM notes WHERE isProtected = 1");
for (const note of protectedNotes) {
if (note.title !== null) {
note.title = prependIv(note.title, "0" + note.noteId);
}
if (note.content !== null) {
note.content = prependIv(note.content, "1" + note.noteId);
}
await sql.execute("UPDATE notes SET title = ?, content = ? WHERE noteId = ?", [note.title, note.content, note.noteId]);
}
}
async function updateNoteRevisions() {
const protectedNoteRevisions = await sql.getRows("SELECT noteRevisionId, title, content FROM note_revisions WHERE isProtected = 1");
for (const noteRevision of protectedNoteRevisions) {
if (noteRevision.title !== null) {
noteRevision.title = prependIv(noteRevision.title, "0" + noteRevision.noteRevisionId);
}
if (noteRevision.content !== null) {
noteRevision.content = prependIv(noteRevision.content, "1" + noteRevision.noteRevisionId);
}
await sql.execute("UPDATE note_revisions SET title = ?, content = ? WHERE noteRevisionId = ?", [noteRevision.title, noteRevision.content, noteRevision.noteRevisionId]);
}
}
module.exports = async () => {
await updateEncryptedDataKey();
await updateNotes();
await updateNoteRevisions();
};

View File

@@ -0,0 +1,8 @@
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
VALUES ('mainFontSize', '100', '2019-01-13T18:31:00.874Z', '2019-01-13T18:31:00.874Z', 0);
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
VALUES ('treeFontSize', '100', '2019-01-13T18:31:00.874Z', '2019-01-13T18:31:00.874Z', 0);
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
VALUES ('detailFontSize', '110', '2019-01-13T18:31:00.874Z', '2019-01-13T18:31:00.874Z', 0);

View File

@@ -0,0 +1,11 @@
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
SELECT 'mainFontSize', '100', '2019-01-13T18:31:00.874Z', '2019-01-13T18:31:00.874Z', 0
WHERE NOT EXISTS (SELECT 1 FROM options WHERE name = 'mainFontSize');
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
SELECT 'treeFontSize', '100', '2019-01-13T18:31:00.874Z', '2019-01-13T18:31:00.874Z', 0
WHERE NOT EXISTS (SELECT 1 FROM options WHERE name = 'treeFontSize');
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
SELECT 'detailFontSize', '110', '2019-01-13T18:31:00.874Z', '2019-01-13T18:31:00.874Z', 0
WHERE NOT EXISTS (SELECT 1 FROM options WHERE name = 'detailFontSize');

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>

File diff suppressed because it is too large Load Diff

View File

@@ -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>

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>

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>

View File

@@ -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.

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

View File

@@ -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>

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} */

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;
}

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>

View File

@@ -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>

View File

@@ -81,7 +81,7 @@ app.on('ready', async () => {
const dateNoteService = require('./src/services/date_notes');
const dateUtils = require('./src/services/date_utils');
const parentNote = await dateNoteService.getDateNote(dateUtils.nowDate());
const parentNote = await dateNoteService.getDateNote(dateUtils.nowLocalDate());
// window may be hidden / not in focus
mainWindow.focus();

5
issue_template.md Normal file
View File

@@ -0,0 +1,5 @@
For bug reports, please mention **version of the application** and include **log files** from following location:
* `/home/[user]/.local/share/trilium-data/log` for Linux
* `C:\Users\[user]\AppData\Roaming\trilium-data\log` for Windows Vista and up
* `/Users/[user]/Library/Application Support/trilium-data/log` for Mac OS

2509
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
"version": "0.25.1-beta",
"version": "0.29.0-beta",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
@@ -17,7 +17,8 @@
"start-electron": "electron . --disable-gpu",
"build-backend-docs": "jsdoc -c jsdoc-conf.json -d ./docs/backend_api src/entities/*.js src/services/backend_script_api.js",
"build-frontend-docs": "jsdoc -c jsdoc-conf.json -d ./docs/frontend_api src/public/javascripts/entities/*.js src/public/javascripts/services/frontend_script_api.js",
"build-docs": "npm run build-backend-docs && npm run build-frontend-docs"
"build-docs": "npm run build-backend-docs && npm run build-frontend-docs",
"postinstall": "electron-builder install-app-deps"
},
"dependencies": {
"async-mutex": "0.1.3",
@@ -26,54 +27,56 @@
"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",
"electron-debug": "2.1.0",
"electron-dl": "1.13.0",
"electron-in-page-search": "1.3.2",
"express": "4.16.4",
"express-session": "1.15.6",
"file-type": "10.7.1",
"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",
"imagemin": "6.0.0",
"imagemin": "6.1.0",
"imagemin-giflossy": "5.1.10",
"imagemin-mozjpeg": "8.0.0",
"imagemin-pngquant": "6.0.0",
"imagemin-pngquant": "7.0.0",
"ini": "1.3.5",
"jimp": "0.6.0",
"mime-types": "^2.1.21",
"moment": "2.22.2",
"moment": "2.24.0",
"multer": "1.4.1",
"node-abi": "2.6.0",
"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",
"rimraf": "2.6.3",
"sanitize-filename": "1.6.1",
"sax": "^1.2.4",
"semver": "^5.6.0",
"serve-favicon": "2.5.0",
"session-file-store": "1.2.0",
"simple-node-logger": "0.93.40",
"sqlite": "3.0.0",
"simple-node-logger": "18.12.21",
"sqlite": "3.0.1",
"tar-stream": "1.6.2",
"turndown": "5.0.1",
"turndown": "5.0.3",
"unescape": "1.0.1",
"ws": "6.1.2",
"ws": "6.1.3",
"xml2js": "0.4.19"
},
"devDependencies": {
"devtron": "1.4.0",
"electron": "4.0.0-beta.8",
"electron-compile": "6.4.3",
"electron-packager": "12.2.0",
"electron": "4.0.3",
"electron-builder": "20.38.5",
"electron-compile": "6.4.4",
"electron-packager": "13.0.1",
"electron-rebuild": "1.8.2",
"lorem-ipsum": "1.0.6",
"tape": "4.9.1",
"xo": "0.23.0"
"tape": "4.9.2",
"xo": "0.24.0"
},
"xo": {
"envs": [

View File

@@ -39,7 +39,7 @@ app.use((req, res, next) => {
});
});
app.use(bodyParser.json({limit: '50mb'}));
app.use(bodyParser.json({limit: '500mb'}));
app.use(bodyParser.urlencoded({extended: false}));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
@@ -63,6 +63,8 @@ app.use(favicon(__dirname + '/public/images/app-icons/win/icon.ico'));
require('./routes/routes').register(app);
require('./routes/custom').register(app);
// catch 404 and forward to error handler
app.use((req, res, next) => {
const err = new Error('Router not found for request ' + req.url);

View File

@@ -15,7 +15,8 @@ const ENTITY_NAME_TO_ENTITY = {
"note_revisions": NoteRevision,
"recent_notes": RecentNote,
"options": Option,
"api_tokens": ApiToken
"api_tokens": ApiToken,
"links": Link
};
function getEntityFromEntityName(entityName) {

View File

@@ -47,7 +47,18 @@ class Note extends Entity {
if (this.isProtected && this.noteId) {
this.isContentAvailable = protectedSessionService.isProtectedSessionAvailable();
protectedSessionService.decryptNote(this);
if (this.isContentAvailable) {
protectedSessionService.decryptNote(this);
}
else {
// saving ciphertexts in case we do want to update protected note outside of protected session
// (which is allowed)
this.titleCipherText = this.title;
this.contentCipherText = this.content;
this.title = "[protected]";
this.content = "";
}
}
this.setContent(this.content);
@@ -56,6 +67,9 @@ class Note extends Entity {
setContent(content) {
this.content = content;
// if parsing below is not successful then there's no jsonContent as opposed to still having the old unupdated ones
delete this.jsonContent;
try {
this.jsonContent = JSON.parse(this.content);
}
@@ -369,6 +383,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 +449,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
*/
@@ -626,12 +640,21 @@ class Note extends Entity {
// cannot be static!
updatePojo(pojo) {
if (pojo.isProtected) {
protectedSessionService.encryptNote(pojo);
if (this.isContentAvailable) {
protectedSessionService.encryptNote(pojo);
}
else {
// updating protected note outside of protected session means we will keep original ciphertexts
pojo.title = pojo.titleCipherText;
pojo.content = pojo.contentCipherText;
}
}
delete pojo.jsonContent;
delete pojo.isContentAvailable;
delete pojo.__attributeCache;
delete pojo.titleCipherText;
delete pojo.contentCipherText;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -1,37 +1,43 @@
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';
import macInit from './services/mac_init.js';
import cssLoader from './services/css_loader.js';
// required for CKEditor image upload plugin
window.glob.getCurrentNode = treeService.getCurrentNode;
@@ -74,6 +80,10 @@ window.onerror = function (msg, url, lineNo, columnNo, error) {
return false;
};
for (const appCssNoteId of window.appCssNoteIds) {
cssLoader.requireCss(`/api/notes/download/${appCssNoteId}`);
}
const wikiBaseUrl = "https://github.com/zadam/trilium/wiki/";
$(document).on("click", "button[data-help-page]", e => {
@@ -84,6 +94,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
@@ -104,21 +116,6 @@ if (utils.isElectron()) {
});
}
function exec(cmd) {
document.execCommand(cmd);
return false;
}
if (utils.isElectron() && utils.isMac()) {
utils.bindShortcut('ctrl+c', () => exec("copy"));
utils.bindShortcut('ctrl+v', () => exec('paste'));
utils.bindShortcut('ctrl+x', () => exec('cut'));
utils.bindShortcut('ctrl+a', () => exec('selectAll'));
utils.bindShortcut('ctrl+z', () => exec('undo'));
utils.bindShortcut('ctrl+y', () => exec('redo'));
}
$("#export-note-button").click(function () {
if ($(this).hasClass("disabled")) {
return;
@@ -127,10 +124,20 @@ $("#export-note-button").click(function () {
exportDialog.showDialog('single');
});
macInit.init();
searchNotesService.init(); // should be in front of treeService since that one manipulates address bar hash
treeService.showTree();
entrypoints.registerEntrypoints();
tooltip.setupTooltip();
noteTooltipService.setupGlobalTooltip();
bundle.executeStartupBundles();
noteTypeService.init();
linkService.init();
noteAutocompleteService.init();

View File

@@ -97,7 +97,7 @@ function AttributesModel() {
await showAttributes(attributes);
// attribute might not be rendered immediatelly so could not focus
setTimeout(() => $(".attribute-type-select:last").focus(), 100);
setTimeout(() => $(".attribute-type-select:last").focus(), 1000);
};
this.deleteAttribute = function(data, event) {
@@ -169,6 +169,8 @@ function AttributesModel() {
infoService.showMessage("Attributes have been saved.");
attributeService.refreshAttributes();
noteDetailService.reload();
};
function addLastEmptyRow() {

View File

@@ -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(() => {

View File

@@ -6,8 +6,6 @@ const $dialog = $("#jump-to-note-dialog");
const $autoComplete = $("#jump-to-note-autocomplete");
const $showInFullTextButton = $("#show-in-full-text-button");
$dialog.on("shown.bs.modal", e => $autoComplete.focus());
async function showDialog() {
glob.activeDialog = $dialog;

View File

@@ -5,6 +5,7 @@ import server from '../services/server.js';
import infoService from "../services/info.js";
import zoomService from "../services/zoom.js";
import utils from "../services/utils.js";
import cssLoader from "../services/css_loader.js";
const $dialog = $("#options-dialog");
@@ -44,10 +45,28 @@ addTabHandler((function() {
const $zoomFactorSelect = $("#zoom-factor-select");
const $leftPaneMinWidth = $("#left-pane-min-width");
const $leftPaneWidthPercent = $("#left-pane-width-percent");
const $html = $("html");
const $mainFontSize = $("#main-font-size");
const $treeFontSize = $("#tree-font-size");
const $detailFontSize = $("#detail-font-size");
const $body = $("body");
const $container = $("#container");
function optionsLoaded(options) {
async function optionsLoaded(options) {
const themes = [
{ val: 'white', title: 'White' },
{ val: 'dark', title: 'Dark' },
{ val: 'black', title: 'Black' }
].concat(await server.get('options/user-themes'));
$themeSelect.empty();
for (const theme of themes) {
$themeSelect.append($("<option>")
.attr("value", theme.val)
.attr("data-note-id", theme.noteId)
.html(theme.title));
}
$themeSelect.val(options.theme);
if (utils.isElectron()) {
@@ -59,21 +78,35 @@ addTabHandler((function() {
$leftPaneMinWidth.val(options.leftPaneMinWidth);
$leftPaneWidthPercent.val(options.leftPaneWidthPercent);
$mainFontSize.val(options.mainFontSize);
$treeFontSize.val(options.treeFontSize);
$detailFontSize.val(options.detailFontSize);
}
$themeSelect.change(function() {
const newTheme = $(this).val();
$html.attr("class", "theme-" + newTheme);
for (const clazz of Array.from($body[0].classList)) { // create copy to safely iterate over while removing classes
if (clazz.startsWith("theme-")) {
$body.removeClass(clazz);
}
}
const noteId = $(this).find(":selected").attr("data-note-id");
if (noteId) {
// make sure the CSS is loaded
// if the CSS has been loaded and then updated then the changes won't take effect though
cssLoader.requireCss(`/api/notes/download/${noteId}`);
}
$body.addClass("theme-" + newTheme);
server.put('options/theme/' + newTheme);
});
$zoomFactorSelect.change(function() {
const newZoomFactor = $(this).val();
zoomService.setZoomFactorAndSave(newZoomFactor);
});
$zoomFactorSelect.change(function() { zoomService.setZoomFactorAndSave($(this).val()); });
function resizeLeftPanel() {
const leftPanePercent = parseInt($leftPaneWidthPercent.val());
@@ -83,20 +116,42 @@ addTabHandler((function() {
$container.css("grid-template-columns", `minmax(${leftPaneMinWidth}px, ${leftPanePercent}fr) ${rightPanePercent}fr`);
}
$leftPaneMinWidth.change(function() {
const newMinWidth = $(this).val();
$leftPaneMinWidth.change(async function() {
await server.put('options/leftPaneMinWidth/' + $(this).val());
resizeLeftPanel();
server.put('options/leftPaneMinWidth/' + newMinWidth);
});
$leftPaneWidthPercent.change(function() {
const newWidthPercent = $(this).val();
$leftPaneWidthPercent.change(async function() {
await server.put('options/leftPaneWidthPercent/' + $(this).val());
resizeLeftPanel();
});
server.put('options/leftPaneWidthPercent/' + newWidthPercent);
function applyFontSizes() {
console.log($mainFontSize.val() + "% !important");
$body.get(0).style.setProperty("--main-font-size", $mainFontSize.val() + "%");
$body.get(0).style.setProperty("--tree-font-size", $treeFontSize.val() + "%");
$body.get(0).style.setProperty("--detail-font-size", $detailFontSize.val() + "%");
}
$mainFontSize.change(async function() {
await server.put('options/mainFontSize/' + $(this).val());
applyFontSizes();
});
$treeFontSize.change(async function() {
await server.put('options/treeFontSize/' + $(this).val());
applyFontSizes();
});
$detailFontSize.change(async function() {
await server.put('options/detailFontSize/' + $(this).val());
applyFontSizes();
});
return {
@@ -198,15 +253,17 @@ addTabHandler((async function () {
const $syncVersion = $("#sync-version");
const $buildDate = $("#build-date");
const $buildRevision = $("#build-revision");
const $dataDirectory = $("#data-directory");
const appInfo = await server.get('app-info');
$appVersion.html(appInfo.appVersion);
$dbVersion.html(appInfo.dbVersion);
$syncVersion.html(appInfo.syncVersion);
$buildDate.html(appInfo.buildDate);
$buildRevision.html(appInfo.buildRevision);
$appVersion.text(appInfo.appVersion);
$dbVersion.text(appInfo.dbVersion);
$syncVersion.text(appInfo.syncVersion);
$buildDate.text(appInfo.buildDate);
$buildRevision.text(appInfo.buildRevision);
$buildRevision.attr('href', 'https://github.com/zadam/trilium/commit/' + appInfo.buildRevision);
$dataDirectory.text(appInfo.dataDirectory);
return {};
})());

View File

@@ -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;

View File

@@ -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;
}

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();

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;
}
@@ -73,8 +85,11 @@ async function showAttributes() {
$promotedAttributesContainer.empty().append($tbody);
}
else if (note.type !== 'relation-map') {
if (attributes.length > 0) {
for (const attribute of attributes) {
// display only "own" notes
const ownedAttributes = attributes.filter(attr => attr.noteId === note.noteId);
if (ownedAttributes.length > 0) {
for (const attribute of ownedAttributes) {
if (attribute.type === 'label') {
$attributeListInner.append(utils.formatLabel(attribute) + " ");
}
@@ -120,7 +135,9 @@ async function createPromotedAttributeRow(definitionAttr, valueAttr) {
const $inputCell = $("<td>").append($("<div>").addClass("input-group").append($input));
const $actionCell = $("<td>");
const $multiplicityCell = $("<td>").addClass("multiplicity");
const $multiplicityCell = $("<td>")
.addClass("multiplicity")
.attr("nowrap", true);
$tr
.append($labelCell)
@@ -143,7 +160,7 @@ async function createPromotedAttributeRow(definitionAttr, valueAttr) {
$input.autocomplete({
appendTo: document.querySelector('body'),
hint: false,
autoselect: true,
autoselect: false,
openOnFocus: true,
minLength: 0,
tabAutocomplete: false
@@ -286,5 +303,6 @@ async function promotedAttributeChanged(event) {
export default {
getAttributes,
showAttributes,
refreshAttributes
refreshAttributes,
invalidateAttributes
}

View File

@@ -9,7 +9,7 @@ async function getAndExecuteBundle(noteId, originEntity = null) {
}
async function executeBundle(bundle, originEntity) {
const apiContext = ScriptContext(bundle.note, bundle.allNotes, originEntity);
const apiContext = await ScriptContext(bundle.noteId, bundle.allNoteIds, originEntity);
try {
return await (function () {
@@ -30,9 +30,13 @@ async function executeStartupBundles() {
}
async function executeRelationBundles(note, relationName) {
const bundlesToRun = await server.get("script/relation/" + note.noteId + "/" + relationName);
note.bundleCache = note.bundleCache || {};
for (const bundle of bundlesToRun) {
if (!note.bundleCache[relationName]) {
note.bundleCache[relationName] = await server.get("script/relation/" + note.noteId + "/" + relationName);
}
for (const bundle of note.bundleCache[relationName]) {
await executeBundle(bundle, note);
}
}

View File

@@ -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 {

View File

@@ -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;

View File

@@ -0,0 +1,13 @@
async function requireCss(url) {
const css = Array
.from(document.querySelectorAll('link'))
.map(scr => scr.href);
if (!css.includes(url)) {
$('head').append($('<link rel="stylesheet" type="text/css" />').attr('href', url));
}
}
export default {
requireCss
}

View File

@@ -9,6 +9,14 @@ const dragAndDropSetup = {
return false;
}
if (!data.originalEvent.ctrlKey) {
// keep existing selection only if CTRL key is pressed
for (const selectedNode of treeService.getSelectedNodes()) {
selectedNode.setSelected(false);
selectedNode.renderTitle();
}
}
node.setSelected(true);
// this is for dragging notes into relation map

View File

@@ -61,8 +61,15 @@ function registerEntrypoints() {
$("#history-back-button").click(window.history.back);
$("#history-forward-button").click(window.history.forward);
utils.bindShortcut('alt+left', window.history.back);
utils.bindShortcut('alt+right', window.history.forward);
if (utils.isMac()) {
// Mac has a different history navigation shortcuts - https://github.com/zadam/trilium/issues/376
utils.bindShortcut('meta+left', window.history.back);
utils.bindShortcut('meta+right', window.history.forward);
}
else {
utils.bindShortcut('alt+left', window.history.back);
utils.bindShortcut('alt+right', window.history.forward);
}
}
utils.bindShortcut('alt+m', e => {
@@ -87,7 +94,7 @@ function registerEntrypoints() {
utils.bindShortcut('ctrl+r', utils.reloadApp);
$(document).bind('keydown', 'ctrl+shift+i', () => {
utils.bindShortcut('ctrl+shift+i', () => {
if (utils.isElectron()) {
require('electron').remote.getCurrentWindow().toggleDevTools();
@@ -95,7 +102,7 @@ function registerEntrypoints() {
}
});
$(document).bind('keydown', 'ctrl+f', () => {
function openInPageSearch() {
if (utils.isElectron()) {
const $searchWindowWebview = $(".electron-in-page-search-window");
$searchWindowWebview.show();
@@ -113,7 +120,9 @@ function registerEntrypoints() {
return false;
}
});
}
utils.bindShortcut('ctrl+f', openInPageSearch);
// FIXME: do we really need these at this point?
utils.bindShortcut("ctrl+shift+up", () => {
@@ -133,8 +142,8 @@ function registerEntrypoints() {
});
if (utils.isElectron()) {
$(document).bind('keydown', 'ctrl+-', zoomService.decreaseZoomFactor);
$(document).bind('keydown', 'ctrl+=', zoomService.increaseZoomFactor);
utils.bindShortcut('ctrl+-', zoomService.decreaseZoomFactor);
utils.bindShortcut('ctrl+=', zoomService.increaseZoomFactor);
}
$("#note-title").bind('keydown', 'return', () => $("#note-detail-text").focus());

View File

@@ -6,6 +6,8 @@ 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';
import protectedSessionService from'./protected_session.js';
/**
* This is the main frontend API interface for scripts. It's published in the local "api" object.
@@ -41,7 +43,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null) {
this.activateNewNote = async notePath => {
await treeService.reload();
await treeService.activateNote(notePath, true);
await treeService.activateNote(notePath, noteDetailService.focusAndSelectTitle);
};
/**
@@ -202,6 +204,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 +240,17 @@ 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;
/**
* @method
*/
this.protectCurrentNote = protectedSessionService.protectNoteAndSendToServer;
}
export default FrontendScriptApi;

View File

@@ -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
}

View File

@@ -1,3 +1,5 @@
import cssLoader from './css_loader.js';
const CKEDITOR = {"js": ["libraries/ckeditor/ckeditor.js"]};
const CODE_MIRROR = {
@@ -34,7 +36,7 @@ const RELATION_MAP = {
async function requireLibrary(library) {
if (library.css) {
library.css.map(cssUrl => requireCss(cssUrl));
library.css.map(cssUrl => cssLoader.requireCss(cssUrl));
}
if (library.js) {
@@ -59,16 +61,6 @@ async function requireScript(url) {
await loadedScriptPromises[url];
}
async function requireCss(url) {
const css = Array
.from(document.querySelectorAll('link'))
.map(scr => scr.href);
if (!css.includes(url)) {
$('head').append($('<link rel="stylesheet" type="text/css" />').attr('href', url));
}
}
export default {
requireLibrary,
CKEDITOR,

View File

@@ -88,23 +88,26 @@ 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
$(document).on('click', "a[data-action='note']", goToLink);
$(document).on('click', 'div.popover-content a, div.ui-tooltip-content a', goToLink);
$(document).on('dblclick', '#note-detail-text a', goToLink);
$(document).on('click', '#note-detail-text.ck-read-only a', goToLink);
$(document).on('click', 'span.ck-button__label', e => {
// this is a link preview dialog from CKEditor link editing
// for some reason clicked element is span
@@ -124,5 +127,6 @@ export default {
getNotePathFromUrl,
createNoteLink,
addLinkToEditor,
addTextToEditor
addTextToEditor,
init
};

View File

@@ -0,0 +1,25 @@
/**
* Mac specific initialization
*/
import utils from "./utils.js";
function init() {
if (utils.isElectron() && utils.isMac()) {
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'));
}
}
function exec(cmd) {
document.execCommand(cmd);
return false;
}
export default {
init
}

View File

@@ -28,7 +28,7 @@ function clearText($el) {
function showRecentNotes($el) {
$el.setSelectedPath("");
$el.autocomplete("val", "");
$el.autocomplete("open");
$el.focus();
}
function initNoteAutocomplete($el, options) {
@@ -61,7 +61,13 @@ function initNoteAutocomplete($el, options) {
$clearTextButton.click(() => clearText($el));
$showRecentNotesButton.click(() => showRecentNotes($el));
$showRecentNotesButton.click(e => {
showRecentNotes($el);
// this will cause the click not give focus to the "show recent notes" button
// this is important because otherwise input will lose focus immediatelly and not show the results
return false;
});
$goToSelectedNoteButton.click(() => {
if ($el.hasClass("disabled")) {
@@ -101,40 +107,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
}

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");
@@ -29,6 +30,7 @@ const $noteIdDisplay = $("#note-id-display");
const $childrenOverview = $("#children-overview");
const $scriptArea = $("#note-detail-script-area");
const $savedIndicator = $("#saved-indicator");
const $body = $("body");
let currentNote = null;
@@ -36,6 +38,8 @@ let noteChangeDisabled = false;
let isNoteChanged = false;
let detailLoadedListeners = [];
const components = {
'code': noteDetailCode,
'text': noteDetailText,
@@ -108,6 +112,10 @@ function onNoteChange(func) {
async function saveNote() {
const note = getCurrentNote();
if (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) {
return;
}
note.title = $noteTitle.val();
note.content = getCurrentNoteContent(note);
@@ -124,6 +132,9 @@ async function saveNote() {
}
$savedIndicator.fadeIn();
// run async
bundleService.executeRelationBundles(getCurrentNote(), 'runOnNoteChange');
}
async function saveNoteIfChanged() {
@@ -135,17 +146,21 @@ async function saveNoteIfChanged() {
$savedIndicator.fadeIn();
}
function setNoteBackgroundIfProtected(note) {
$noteDetailWrapper.toggleClass("protected", note.isProtected);
$protectButton.toggleClass("active", note.isProtected);
$unprotectButton.toggleClass("active", !note.isProtected);
$unprotectButton.prop("disabled", !protectedSessionHolder.isProtectedSessionAvailable());
}
function updateNoteView() {
$noteDetailWrapper.toggleClass("protected", currentNote.isProtected);
$protectButton.toggleClass("active", currentNote.isProtected);
$protectButton.prop("disabled", currentNote.isProtected);
$unprotectButton.toggleClass("active", !currentNote.isProtected);
$unprotectButton.prop("disabled", !currentNote.isProtected || !protectedSessionHolder.isProtectedSessionAvailable());
let isNewNoteCreated = false;
for (const clazz of Array.from($body[0].classList)) { // create copy to safely iterate over while removing classes
if (clazz.startsWith("type-") || clazz.startsWith("mime-")) {
$body.removeClass(clazz);
}
}
function newNoteCreated() {
isNewNoteCreated = true;
$body.addClass(utils.getNoteTypeClass(currentNote.type));
$body.addClass(utils.getMimeTypeClass(currentNote.mime));
}
async function handleProtectedSession() {
@@ -177,18 +192,18 @@ 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 (isNewNoteCreated) {
isNewNoteCreated = false;
$noteTitle.focus().select();
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();
}
$noteIdDisplay.html(noteId);
setNoteBackgroundIfProtected(currentNote);
updateNoteView();
$noteDetailWrapper.show();
@@ -197,8 +212,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 +231,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 +242,19 @@ 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();
fireDetailLoaded();
$scriptArea.empty();
await bundleService.executeRelationBundles(getCurrentNote(), 'runOnNoteView');
await attributeService.showAttributes();
if (utils.isDesktop()) {
await attributeService.showAttributes();
await showChildrenOverview();
await showChildrenOverview();
}
}
async function showChildrenOverview() {
@@ -257,7 +280,7 @@ async function showChildrenOverview() {
text: await treeUtils.getNoteTitle(childBranch.noteId, childBranch.parentNoteId)
}).attr('data-action', 'note').attr('data-note-path', notePath + '/' + childBranch.noteId);
const childEl = $('<div class="child-overview">').html(link);
const childEl = $('<div class="child-overview-item">').html(link);
$childrenOverview.append(childEl);
}
@@ -274,6 +297,34 @@ function focusOnTitle() {
$noteTitle.focus();
}
function focusAndSelectTitle() {
$noteTitle.focus().select();
}
/**
* Since detail loading may take some time and user might just browse through the notes using UP-DOWN keys,
* we intentionally decouple activation of the note in the tree and full load of the note so just avaiting on
* fancytree's activate() won't wait for the full load.
*
* This causes an issue where in some cases you want to do some action after detail is loaded. For this reason
* we provide the listeners here which will be triggered after the detail is loaded and if the loaded note
* is the one registered in the listener.
*/
function addDetailLoadedListener(noteId, callback) {
detailLoadedListeners.push({ noteId, callback });
}
function fireDetailLoaded() {
for (const {noteId, callback} of detailLoadedListeners) {
if (noteId === currentNote.noteId) {
callback();
}
}
// all the listeners are one time only
detailLoadedListeners = [];
}
messagingService.subscribeToSyncMessages(syncData => {
if (syncData.some(sync => sync.entityName === 'notes' && sync.entityId === getCurrentNoteId())) {
infoService.showMessage('Reloading note because of background changes');
@@ -303,16 +354,17 @@ setInterval(saveNoteIfChanged, 3000);
export default {
reload,
switchToNote,
setNoteBackgroundIfProtected,
updateNoteView,
loadNote,
getCurrentNote,
getCurrentNoteType,
getCurrentNoteId,
newNoteCreated,
focusOnTitle,
focusAndSelectTitle,
saveNote,
saveNoteIfChanged,
noteChanged,
getCurrentNoteContent,
onNoteChange
onNoteChange,
addDetailLoadedListener
};

View File

@@ -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)
}

View File

@@ -5,6 +5,7 @@ import noteDetailService from "./note_detail.js";
const $component = $('#note-detail-file');
const $fileNoteId = $("#file-note-id");
const $fileName = $("#file-filename");
const $fileType = $("#file-filetype");
const $fileSize = $("#file-filesize");
@@ -21,6 +22,7 @@ async function show() {
$component.show();
$fileNoteId.text(currentNote.noteId);
$fileName.text(attributeMap.originalFileName || "?");
$fileSize.text((attributeMap.fileSize || "?") + " bytes");
$fileType.text(currentNote.mime);
@@ -53,5 +55,6 @@ export default {
getContent: () => null,
focus: () => null,
onNoteChange: () => null,
cleanup: () => null
cleanup: () => null,
scrollToTop: () => null
}

View File

@@ -71,5 +71,6 @@ export default {
getContent: () => null,
focus: () => null,
onNoteChange: () => null,
cleanup: () => null
cleanup: () => null,
scrollToTop: () => $component.scrollTop(0)
}

View File

@@ -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
}

View File

@@ -37,5 +37,6 @@ export default {
getContent: () => "",
focus: () => null,
onNoteChange: () => null,
cleanup: () => $noteDetailRenderContent.empty()
cleanup: () => $noteDetailRenderContent.empty(),
scrollToTop: () => $component.scrollTop(0)
}

View File

@@ -1,10 +1,13 @@
import noteDetailService from "./note_detail.js";
import treeService from "./tree.js";
import infoService from './info.js';
const $searchString = $("#search-string");
const $component = $('#note-detail-search');
const $refreshButton = $('#note-detail-search-refresh-results-button');
function getContent() {
JSON.stringify({
return JSON.stringify({
searchString: $searchString.val()
});
}
@@ -25,10 +28,19 @@ function show() {
$searchString.on('input', noteDetailService.noteChanged);
}
$refreshButton.click(async () => {
await noteDetailService.saveNoteIfChanged();
treeService.reload();
infoService.showMessage('Tree has been refreshed.');
});
export default {
getContent,
show,
focus: () => null,
onNoteChange: () => null,
cleanup: () => null
cleanup: () => null,
scrollToTop: () => null
}

View File

@@ -1,6 +1,7 @@
import libraryLoader from "./library_loader.js";
import noteDetailService from './note_detail.js';
import treeService from './tree.js';
import attributeService from "./attributes.js";
const $component = $('#note-detail-text');
@@ -19,6 +20,8 @@ async function show() {
}
}
textEditor.isReadOnly = await isReadOnly();
textEditor.setData(noteDetailService.getCurrentNote().content);
$component.show();
@@ -36,6 +39,12 @@ function getContent() {
return content;
}
async function isReadOnly() {
const attributes = await attributeService.getAttributes();
return attributes.some(attr => attr.type === 'label' && attr.name === 'readOnly');
}
function focus() {
$component.focus();
}
@@ -74,5 +83,6 @@ export default {
if (textEditor) {
textEditor.setData('');
}
}
},
scrollToTop: () => $component.scrollTop(0)
}

View File

@@ -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
}

View File

@@ -2,6 +2,7 @@ import treeService from './tree.js';
import noteDetailService from './note_detail.js';
import server from './server.js';
import infoService from "./info.js";
import confirmDialog from "../dialogs/confirm.js";
const $executeScriptButton = $("#execute-script-button");
const $toggleEditButton = $('#toggle-edit-button');
@@ -44,7 +45,7 @@ const DEFAULT_MIME_TYPES = [
{ mime: 'text/x-yaml', title: 'YAML' }
];
const noteTypeModel = new NoteTypeModel();
let noteTypeModel;
function NoteTypeModel() {
const self = this;
@@ -110,35 +111,63 @@ function NoteTypeModel() {
self.updateExecuteScriptButtonVisibility();
}
this.selectText = function() {
function confirmChangeIfContent() {
if (!noteDetailService.getCurrentNoteContent()) {
return true;
}
return confirmDialog.confirm("It is not recommended to change note type when note content is not empty. Do you want to continue anyway?");
}
this.selectText = async function() {
if (!await confirmChangeIfContent()) {
return;
}
self.type('text');
self.mime('');
self.mime('text/html');
save();
};
this.selectRender = function() {
this.selectRender = async function() {
if (!await confirmChangeIfContent()) {
return;
}
self.type('render');
self.mime('text/html');
save();
};
this.selectRelationMap = function() {
this.selectRelationMap = async function() {
if (!await confirmChangeIfContent()) {
return;
}
self.type('relation-map');
self.mime('application/json');
save();
};
this.selectCode = function() {
this.selectCode = async function() {
if (!await confirmChangeIfContent()) {
return;
}
self.type('code');
self.mime('');
self.mime('text/plain');
save();
};
this.selectCodeMime = function(el) {
this.selectCodeMime = async function(el) {
if (!await confirmChangeIfContent()) {
return;
}
self.type('code');
self.mime(el.mime);
@@ -153,7 +182,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 +201,6 @@ export default {
getDefaultCodeMimeTypes: () => DEFAULT_MIME_TYPES.slice(),
getCodeMimeTypes: () => noteTypeModel.codeMimeTypes(),
setCodeMimeTypes: types => noteTypeModel.codeMimeTypes(types)
setCodeMimeTypes: types => noteTypeModel.codeMimeTypes(types),
init
};

View File

@@ -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.updateNoteView();
}
async function unprotectNoteAndSendToServer() {
@@ -143,7 +152,7 @@ async function unprotectNoteAndSendToServer() {
treeService.setProtected(currentNote.noteId, currentNote.isProtected);
noteDetailService.setNoteBackgroundIfProtected(currentNote);
noteDetailService.updateNoteView();
}
async function protectSubtree(noteId, protect) {
@@ -157,32 +166,24 @@ 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,
protectSubtree,
ensureDialogIsClosed,
enterProtectedSession,
leaveProtectedSession
leaveProtectedSession,
protectNoteAndSendToServer
};

View File

@@ -1,9 +1,13 @@
import FrontendScriptApi from './frontend_script_api.js';
import utils from './utils.js';
import treeCache from './tree_cache.js';
function ScriptContext(startNote, allNotes, originEntity = null) {
async function ScriptContext(startNoteId, allNoteIds, originEntity = null) {
const modules = {};
const startNote = await treeCache.getNote(startNoteId);
const allNotes = await treeCache.getNotes(allNoteIds);
return {
modules: modules,
notes: utils.toObject(allNotes, note => [note.noteId, note]),

View File

@@ -76,6 +76,15 @@ async function saveSearch() {
await treeService.activateNote(noteId);
}
function init() {
const hashValue = treeService.getHashValueFromAddress();
if (hashValue.startsWith("search=")) {
showSearch();
doSearch(hashValue.substr(7));
}
}
$searchInput.keyup(e => {
const searchText = $searchInput.val();
@@ -87,7 +96,7 @@ $searchInput.keyup(e => {
if (e && e.which === $.ui.keyCode.ENTER) {
doSearch();
}
}).focus();
});
$doSearchButton.click(() => doSearch()); // keep long form because of argument
$resetSearchButton.click(resetSearch);
@@ -100,5 +109,6 @@ export default {
toggleSearch,
resetSearch,
showSearch,
doSearch
doSearch,
init
};

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");
@@ -81,6 +83,10 @@ async function setNodeTitleWithPrefix(node) {
node.setTitle(utils.escapeHtml(title));
}
function getNode(childNoteId, parentNoteId) {
return getNodesByNoteId(childNoteId).find(node => !parentNoteId || node.data.parentNoteId === parentNoteId);
}
async function expandToNote(notePath, expandOpts) {
utils.assertArguments(notePath);
@@ -88,37 +94,72 @@ async function expandToNote(notePath, expandOpts) {
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
let parentNoteId = 'none';
const hoistedNoteId = await hoistedNoteService.getHoistedNoteId();
let hoistedNoteFound = false;
let parentNoteId = null;
for (const childNoteId of runPath) {
const node = getNodesByNoteId(childNoteId).find(node => node.data.parentNoteId === parentNoteId);
if (!node) {
console.error(`Can't find node for noteId=${childNoteId} with parentNoteId=${parentNoteId}`);
if (childNoteId === hoistedNoteId) {
hoistedNoteFound = true;
}
if (childNoteId === noteId) {
return node;
}
else {
await node.setExpanded(true, expandOpts);
// we expand only after hoisted note since before then nodes are not actually present in the tree
if (hoistedNoteFound) {
// for first node (!parentNoteId) it doesn't matter which node is found
let node = getNode(childNoteId, parentNoteId);
if (!node && parentNoteId) {
const parents = getNodesByNoteId(parentNoteId);
for (const parent of parents) {
// force load parents. This is useful when fancytree doesn't contain recently created notes yet.
await parent.load(true);
}
node = getNode(childNoteId, parentNoteId);
}
if (!node) {
console.error(`Can't find node for noteId=${childNoteId} with parentNoteId=${parentNoteId}`);
}
if (childNoteId === noteId) {
return node;
} else {
await node.setExpanded(true, expandOpts);
}
}
parentNoteId = childNoteId;
}
}
async function activateNote(notePath, newNote) {
async function activateNote(notePath, noteLoadedListener) {
utils.assertArguments(notePath);
// notePath argument can contain only noteId which is not good when hoisted since
// then we need to check the whole note path
const runNotePath = await getRunPath(notePath);
const hoistedNoteId = await hoistedNoteService.getHoistedNoteId();
if (hoistedNoteId !== 'root' && !runNotePath.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');
}
const node = await expandToNote(notePath);
if (newNote) {
noteDetailService.newNoteCreated();
if (noteLoadedListener) {
noteDetailService.addDetailLoadedListener(node.data.noteId, noteLoadedListener);
}
// we use noFocus because when we reload the tree because of background changes
@@ -143,6 +184,8 @@ async function getRunPath(notePath) {
path.push('root');
}
const hoistedNoteId = await hoistedNoteService.getHoistedNoteId();
const effectivePath = [];
let childNoteId = null;
let i = 0;
@@ -195,13 +238,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 +364,11 @@ function clearSelectedNodes() {
}
async function treeInitialized() {
// - is used in mobile to indicate that we don't want to activate any note after load
if (startNotePath === '-') {
return;
}
const noteId = treeUtils.getNoteIdFromNotePath(startNotePath);
if (!await treeCache.getNote(noteId)) {
@@ -344,7 +391,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 +429,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 +437,35 @@ function initFancyTree(tree) {
},
clones: {
highlightActiveClones: true
},
enhanceTitle: async function (event, data) {
const node = data.node;
const $span = $(node.span);
if (node.data.noteId !== 'root'
&& node.data.noteId === await hoistedNoteService.getHoistedNoteId()
&& $span.find('.unhoist-button').length === 0) {
const unhoistButton = $('<span>&nbsp; (<a class="unhoist-button">unhoist</a>)</span>');
$span.append(unhoistButton);
}
},
// this is done to automatically lazy load all expanded search notes after tree load
loadChildren: function(event, data) {
data.node.visit(function(subNode){
// Load all lazy/unloaded child nodes
// (which will trigger `loadChildren` recursively)
if( subNode.isUndefined() && subNode.isExpanded() ) {
subNode.load();
}
});
}
});
$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
@@ -425,24 +483,30 @@ async function reload() {
await getTree().reload(notes);
}
function getNotePathFromAddress() {
return document.location.hash.substr(1); // strip initial #
function isNotePathInAddress() {
return getHashValueFromAddress().startsWith("root");
}
function getHashValueFromAddress() {
return document.location.hash ? document.location.hash.substr(1) : ""; // strip initial #
}
async function loadTree() {
const resp = await server.get('tree');
startNotePath = resp.startNotePath;
if (document.location.hash) {
startNotePath = getNotePathFromAddress();
if (isNotePathInAddress()) {
startNotePath = getHashValueFromAddress();
}
return await treeBuilder.prepareTree(resp.notes, resp.branches, resp.relations);
}
function collapseTree(node = null) {
async function collapseTree(node = null) {
if (!node) {
node = $tree.fancytree("getRootNode");
const hoistedNoteId = await hoistedNoteService.getHoistedNoteId();
node = getNodesByNoteId(hoistedNoteId)[0];
}
node.setExpanded(false);
@@ -483,9 +547,11 @@ async function setNoteTitle(noteId, title) {
}
async function createNewTopLevelNote() {
const rootNode = getNodesByNoteId('root')[0];
const hoistedNoteId = await hoistedNoteService.getHoistedNoteId();
await createNote(rootNode, "root", "into", false);
const rootNode = getNodesByNoteId(hoistedNoteId)[0];
await createNote(rootNode, hoistedNoteId, "into", false);
}
async function createNote(node, parentNoteId, target, isProtected, saveSelection = false) {
@@ -530,7 +596,7 @@ async function createNote(node, parentNoteId, target, isProtected, saveSelection
await noteDetailService.saveNoteIfChanged();
noteDetailService.newNoteCreated();
noteDetailService.addDetailLoadedListener(note.noteId, noteDetailService.focusAndSelectTitle);
const noteEntity = new NoteShort(treeCache, note);
const branchEntity = new Branch(treeCache, branch);
@@ -545,7 +611,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') {
@@ -561,7 +627,10 @@ async function createNote(node, parentNoteId, target, isProtected, saveSelection
await node.getLastChild().setActive(true);
const parentNoteEntity = await treeCache.getNote(node.data.noteId);
node.folder = true;
node.icon = await treeBuilder.getIcon(parentNoteEntity); // icon might change into folder
node.renderTitle();
}
else {
@@ -617,11 +686,15 @@ messagingService.subscribeToSyncMessages(syncData => {
}
});
utils.bindShortcut('ctrl+o', () => {
utils.bindShortcut('ctrl+o', async () => {
const node = getCurrentNode();
const parentNoteId = node.data.parentNoteId;
const isProtected = treeUtils.getParentProtectedStatus(node);
if (node.data.noteId === 'root' || node.data.noteId === await hoistedNoteService.getHoistedNoteId()) {
return;
}
createNote(node, parentNoteId, 'after', isProtected, true);
});
@@ -638,12 +711,14 @@ utils.bindShortcut('ctrl+p', createNoteInto);
utils.bindShortcut('ctrl+.', scrollToCurrentNote);
$(window).bind('hashchange', function() {
const notePath = getNotePathFromAddress();
if (isNotePathInAddress()) {
const notePath = getHashValueFromAddress();
if (getCurrentNotePath() !== notePath) {
console.debug("Switching to " + notePath + " because of hash change");
if (notePath !== '-' && getCurrentNotePath() !== notePath) {
console.debug("Switching to " + notePath + " because of hash change");
activateNote(notePath);
activateNote(notePath);
}
}
});
@@ -669,8 +744,13 @@ export default {
setPrefix,
createNewTopLevelNote,
createNote,
createNoteInto,
getSelectedNodes,
clearSelectedNodes,
sortAlphabetically,
showTree
showTree,
loadTree,
treeInitialized,
setExpandedToServer,
getHashValueFromAddress
};

View File

@@ -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: 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");
}
@@ -151,7 +166,11 @@ async function getExtraClasses(note) {
extraClasses.push(note.cssClass);
}
extraClasses.push(note.type);
extraClasses.push(utils.getNoteTypeClass(note.type));
if (note.mime) { // some notes should not have mime type (e.g. render)
extraClasses.push(utils.getMimeTypeClass(note.mime));
}
return extraClasses.join(" ");
}

View File

@@ -158,7 +158,11 @@ class TreeCache {
return;
}
treeCache.childParentToBranch[childNoteId + '-' + newParentNoteId] = treeCache.childParentToBranch[childNoteId + '-' + oldParentNoteId];
const branchId = treeCache.childParentToBranch[childNoteId + '-' + oldParentNoteId];
const branch = await this.getBranch(branchId);
branch.parentNoteId = newParentNoteId;
treeCache.childParentToBranch[childNoteId + '-' + newParentNoteId] = branchId;
delete treeCache.childParentToBranch[childNoteId + '-' + oldParentNoteId]; // this is correct because we know that oldParentId isn't same as newParentId
// remove old associations

View File

@@ -10,9 +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";
const $tree = $("#tree");
import hoistedNoteService from './hoisted_note.js';
import ContextMenuItemsContainer from './context_menu_items_container.js';
let clipboardIds = [];
let clipboardMode = null;
@@ -79,10 +78,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,34 +102,30 @@ 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);
const note = await treeCache.getNote(node.data.noteId);
const parentNote = await treeCache.getNote(branch.parentNoteId);
const isNotRoot = note.noteId !== 'root';
const isHoisted = note.noteId === await hoistedNoteService.getHoistedNoteId();
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 && !isHoisted && 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');
itemsContainer.hideItem("hoist", isHoisted);
itemsContainer.hideItem("unhoist", !isHoisted || !isNotRoot);
// Activate node on right-click
node.setActive();
@@ -139,14 +136,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 +191,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);
}

View File

@@ -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.

View File

@@ -23,6 +23,7 @@ function formatTimeWithSeconds(date) {
return padNum(date.getHours()) + ":" + padNum(date.getMinutes()) + ":" + padNum(date.getSeconds());
}
// this is producing local time!
function formatDate(date) {
// return padNum(date.getDate()) + ". " + padNum(date.getMonth() + 1) + ". " + date.getFullYear();
// instead of european format we'll just use ISO as that's pretty unambiguous
@@ -30,6 +31,7 @@ function formatDate(date) {
return formatDateISO(date);
}
// this is producing local time!
function formatDateISO(date) {
return date.getFullYear() + "-" + padNum(date.getMonth() + 1) + "-" + padNum(date.getDate());
}
@@ -136,11 +138,53 @@ function randomString(len) {
}
function bindShortcut(keyboardShortcut, handler) {
$(document).bind('keydown', keyboardShortcut, e => {
handler();
if (isDesktop()) {
if (isMac()) {
// use CMD (meta) instead of CTRL for all shortcuts
keyboardShortcut = keyboardShortcut.replace("ctrl", "meta");
}
e.preventDefault();
});
$(document).bind('keydown', keyboardShortcut, e => {
console.log(e);
handler();
e.preventDefault();
});
}
}
function isMobile() {
return window.device === "mobile"
// window.device is not available in setup
|| (!window.device && /Mobi/.test(navigator.userAgent));
}
function isDesktop() {
return window.device === "desktop"
// window.device is not available in setup
|| (!window.device && !/Mobi/.test(navigator.userAgent));
}
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=/";
}
function getNoteTypeClass(type) {
return "type-" + type;
}
function getMimeTypeClass(mime) {
const semicolonIdx = mime.indexOf(';');
if (semicolonIdx !== -1) {
// stripping everything following the semicolon
mime = mime.substr(0, semicolonIdx);
}
return 'mime-' + mime.toLowerCase().replace(/[\W_]+/g,"-");
}
export default {
@@ -166,5 +210,10 @@ export default {
download,
toObject,
randomString,
bindShortcut
bindShortcut,
isMobile,
isDesktop,
setCookie,
getNoteTypeClass,
getMimeTypeClass
};

View File

@@ -1,4 +1,7 @@
import utils from "./services/utils.js";
import macInit from './services/mac_init.js';
macInit.init();
function SetupModel() {
if (syncInProgress) {
@@ -25,26 +28,20 @@ function SetupModel() {
this.instanceType = utils.isElectron() ? "desktop" : "server";
this.setupTypeSelected = this.getSetupType = () =>
this.setupNewDocument()
|| this.setupSyncFromDesktop()
|| this.setupSyncFromServer();
this.setupTypeSelected = () => !!this.setupType();
this.selectSetupType = () => {
this.step(this.getSetupType());
this.setupType(this.getSetupType());
this.step(this.setupType());
};
this.back = () => {
this.step("setup-type");
this.setupNewDocument(false);
this.setupSyncFromServer(false);
this.setupSyncFromDesktop(false);
this.setupType("");
};
this.finish = async () => {
if (this.setupNewDocument()) {
if (this.setupType() === 'new-document') {
const username = this.username();
const password1 = this.password1();
const password2 = this.password2();
@@ -72,7 +69,7 @@ function SetupModel() {
window.location.replace("/");
});
}
else if (this.setupSyncFromServer()) {
else if (this.setupType() === 'sync-from-server') {
const syncServerHost = this.syncServerHost();
const syncProxy = this.syncProxy();
const username = this.username();

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

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -9,12 +9,12 @@
*
* This section is automatically generated from the `skin-common.less` template.
*
* Copyright (c) 2008-2018, Martin Wendt (http://wwWendt.de)
* Copyright (c) 2008-2019, Martin Wendt (http://wwWendt.de)
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.30.0
* @date 2018-09-02T15:42:49Z
* @version 2.30.2
* @date 2019-01-13T08:17:01Z
******************************************************************************/
/*------------------------------------------------------------------------------
* Helpers
@@ -336,7 +336,8 @@ span.fancytree-icon {
.fancytree-loading span.fancytree-expander,
.fancytree-loading span.fancytree-expander:hover,
.fancytree-statusnode-loading span.fancytree-icon,
.fancytree-statusnode-loading span.fancytree-icon:hover {
.fancytree-statusnode-loading span.fancytree-icon:hover,
span.fancytree-icon.fancytree-icon-loading {
background-image: url("../skin-win8/loading.gif");
background-position: 0px 0px;
}
@@ -479,6 +480,8 @@ ul.fancytree-container.fancytree-rtl.fancytree-no-connector > li {
* 'table' extension
*----------------------------------------------------------------------------*/
table.fancytree-ext-table {
font-family: tahoma, arial, helvetica;
font-size: 10pt;
border-collapse: collapse;
/* ext-ariagrid */
}
@@ -536,6 +539,9 @@ table.fancytree-ext-columnview span.fancytree-node {
display: inline-block;
}
table.fancytree-ext-columnview span.fancytree-node.fancytree-expanded {
background-color: #e0e0e0;
}
table.fancytree-ext-columnview span.fancytree-node.fancytree-active {
background-color: #CBE8F6;
}
table.fancytree-ext-columnview .fancytree-has-children span.fancytree-cv-right {

File diff suppressed because one or more lines are too long

2
src/public/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

@@ -0,0 +1,107 @@
body {
font-size: var(--main-font-size);
}
#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;
font-family: var(--tree-font-family);
font-size: var(--tree-font-size);
}
#left-pane {
grid-area: left-pane;
display: flex;
flex-direction: column;
}
#header {
grid-area: header;
background-color: var(--header-background-color);
display: flex;
align-items: center;
padding: 4px;
}
#header button {
padding: 1px 5px 1px 5px;
font-size: smaller;
margin-bottom: 2px;
margin-top: 2px;
margin-right: 8px;
}
#history-navigation {
margin: 0 15px 0 5px;
position: relative;
top: 2px;
}
#global-buttons {
display: flex;
justify-content: space-around;
padding: 10px 0 10px 0;
margin: 0 20px 0 10px;
border: 1px solid var(--main-border-color);
border-radius: 7px;
}
#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;
}
#note-detail-wrapper {
font-size: var(--detail-font-size);
}
::-webkit-scrollbar {
width: 12px;
}
::-webkit-scrollbar-thumb {
border-radius: 3px;
border: 1px solid var(--main-border-color);
}

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 {
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;
/* for some reason detail overflows a little bit so we subtract few pixels */
height: calc(100% - 25px);
/* 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;
}

View File

@@ -12,12 +12,13 @@
.note-box {
padding: 16px;
position: absolute !important;
background-color: var(--accented-background-color);
color: var(--main-text-color);
z-index: 4;
border: 1px solid #666;
box-shadow: 2px 2px 19px #999;
border-radius: 8px;
opacity: 0.8;
background-color: white;
font-size: 11px;
width: auto;
height: auto;
@@ -28,7 +29,7 @@
}
.note-box:hover {
background-color: #ddd;
background-color: var(--more-accented-background-color);
}
.note-box .title {
@@ -37,11 +38,12 @@
}
.connection-label.jtk-hover, .jtk-source-hover, .jtk-target-hover {
background-color: #ddd;
background-color: var(--more-accented-background-color);
}
.connection-label {
background-color: white;
background-color: var(--accented-background-color);
color: var(--main-text-color);
opacity: 0.8;
padding: 0.3em;
border-radius: 0.5em;
@@ -64,10 +66,6 @@
box-shadow: 0 0 6px black;
}
.statemachine-demo .jtk-endpoint {
z-index: 3;
}
.dragHover {
border: 2px solid orange;
}

View File

@@ -1,62 +1,182 @@
:root {
--main-font-family: inherit;
--main-font-size: normal;
--tree-font-family: inherit;
--tree-font-size: normal;
--detail-font-family: inherit;
--detail-font-size: normal;
--detail-text-font-family: inherit;
--main-background-color: white;
--main-text-color: black;
--main-border-color: #ccc;
--accented-background-color: #eee;
--more-accented-background-color: #ccc;
--header-background-color: #f8f8f8;
--button-background-color: #eee;
--button-disabled-background-color: #ccc;
--button-border-color: #ddd;
--button-text-color: black;
--button-border-radius: 5px;
--muted-text-color: #444;
--input-text-color: black;
--input-background-color: white;
--modal-background-color: white;
--hover-item-text-color: black;
--hover-item-background-color: #eee;
--active-item-text-color: black;
--active-item-background-color: #ccc;
--menu-text-color: black;
--menu-background-color: white;
}
body.theme-black {
--main-background-color: black;
--main-text-color: white;
--main-border-color: #ddd;
--accented-background-color: #222;
--more-accented-background-color: #444;
--header-background-color: black;
--button-background-color: #333;
--button-border-color: #444;
--button-text-color: white;
--button-border-radius: 5px;
--muted-text-color: #ccc;
--input-text-color: white;
--input-background-color: black;
--modal-background-color: #222;
--hover-item-text-color: black;
--hover-item-background-color: #aaa;
--active-item-text-color: black;
--active-item-background-color: #ccc;
--menu-text-color: white;
--menu-background-color: #222;
}
body.theme-black .CodeMirror {
filter: invert(100%) hue-rotate(180deg);
}
body.theme-dark {
--main-background-color: #333;
--main-text-color: white;
--main-border-color: #ddd;
--accented-background-color: #555;
--more-accented-background-color: #777;
--header-background-color: #333;
--button-background-color: #555;
--button-border-color: #444;
--button-text-color: white;
--button-border-radius: 5px;
--muted-text-color: #ccc;
--input-text-color: white;
--input-background-color: #333;
--modal-background-color: #555;
--hover-item-text-color: black;
--hover-item-background-color: #aaa;
--active-item-text-color: black;
--active-item-background-color: #ccc;
--menu-text-color: white;
--menu-background-color: #222;
}
body.theme-dark .CodeMirror {
filter: invert(90%) hue-rotate(180deg);
}
html {
/* this fixes FF filter vs. position fixed bug: https://github.com/zadam/trilium/issues/233 */
height: 100%;
}
body {
/* Fix for CKEditor block gutter icon "stretching" body and causing scrollbar to appear after pressing enter
on the last line of the editor. */
position: fixed;
width: 100%;
background-color: var(--main-background-color);
color: var(--main-text-color);
font-family: var(--main-font-family);
}
#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;
input, select {
color: var(--input-text-color) !important;
background: var(--input-background-color) !important;
}
#header {
grid-area: header;
background-color: #f8f8f8;
display: flex;
align-items: center;
padding: 4px;
.input-group-text {
background-color: var(--accented-background-color) !important;
color: var(--muted-text-color) !important;
}
#header button {
padding: 1px 5px 1px 5px;
font-size: small;
button.close {
color: var(--main-text-color);
}
#left-pane {
grid-area: left-pane;
display: flex;
flex-direction: column;
.modal-content {
background-color: var(--modal-background-color) !important;
}
#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;
.nav-link.active {
background-color: var(--more-accented-background-color) !important;
color: var(--main-text-color) !important;
}
#title-container {
grid-area: title;
}
#note-title {
margin-left: 15px;
margin-right: 10px;
font-size: 150%;
border: 0;
width: 5em;
flex-grow: 100;
}
ul.fancytree-container {
/* override specific size from fancytree.css */
font-family: inherit !important;
font-size: inherit !important;
}
.fancytree-title {
margin-left: 7px !important;
}
.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: var(--main-background-color); /* setting to background color makes this invisible */
}
.fancytree-node.fancytree-expanded .fancytree-expander:before {
content: "\e9ba";
}
#note-title[readonly] {
background: inherit;
}
.tdialog {
display: none;
}
@@ -69,6 +189,7 @@ body {
padding-top: 10px;
display: flex;
flex-direction: column;
font-family: var(--detail-font-family);
}
#note-detail-component-wrapper {
@@ -79,6 +200,7 @@ body {
height: 100%;
display: flex;
flex-direction: column;
min-height: 200px;
}
.note-detail-component {
@@ -86,8 +208,6 @@ body {
display: none;
}
#note-detail-text { font-size: 1.1em; }
#note-detail-text h1 { font-size: 2.0em; }
#note-detail-text h2 { font-size: 1.8em; }
#note-detail-text h3 { font-size: 1.6em; }
@@ -101,6 +221,11 @@ body {
/* This is because with empty content height of editor is 0 and it's impossible to click into it */
min-height: 200px;
overflow: auto;
font-family: var(--detail-text-font-family);
}
#note-detail-text p:first-child, #note-detail-text::before {
margin-top: 0;
}
/** we disable shield background when in distraction free mode because I couldn't get it to stay static
@@ -114,12 +239,18 @@ body {
ul.fancytree-container {
outline: none !important;
background-color: inherit !important;
}
.fancytree-custom-icon {
font-size: 1.3em;
}
span.fancytree-title {
color: inherit !important;
background: inherit !important;
}
span.fancytree-node.protected > span.fancytree-custom-icon {
filter: drop-shadow(2px 2px 2px black);
}
@@ -137,10 +268,35 @@ span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-tit
padding-left: 5px;
}
/* By default not focused active tree item is not easily visible, this makes it more visible */
span.fancytree-active.fancytree-focused .fancytree-title {
color: var(--active-item-text-color) !important;
background-color: var(--active-item-background-color) !important;
border-color: var(--main-border-color) !important;
border-radius: 3px;
font-weight: bold;
}
span.fancytree-active:not(.fancytree-focused) .fancytree-title {
background-color: #ddd !important;
border-color: #555 !important;
color: var(--hover-item-text-color) !important;
background-color: var(--hover-item-background-color) !important;
border-color: var(--main-border-color) !important;
border-radius: 3px;
font-weight: bold;
}
span.fancytree-selected:not(.fancytree-active) .fancytree-title {
color: var(--hover-item-text-color) !important;
background-color: var(--hover-item-background-color) !important;
border-color: var(--main-border-color) !important;
border-radius: 3px;
font-style: italic;
}
span.fancytree-node:not(.fancytree-active):hover span.fancytree-title {
color: var(--hover-item-text-color) !important;
background-color: var(--hover-item-background-color) !important;
border-color: var(--main-border-color) !important;
border-radius: 3px;
}
.ui-autocomplete {
@@ -163,17 +319,6 @@ span.fancytree-active:not(.fancytree-focused) .fancytree-title {
color: #337ab7 !important;
}
#header-title {
padding: 5px 20px 5px 10px;
font-size: large;
font-weight: bold;
}
#header .btn-sm {
margin-bottom: 2px;
margin-right: 8px;
}
div.ui-tooltip {
max-width: 600px;
max-height: 600px;
@@ -185,14 +330,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%;
@@ -201,13 +338,17 @@ div.ui-tooltip {
margin-top: 10px;
display: none;
overflow: auto;
border-bottom: 2px solid #ddd;
border-bottom: 2px solid var(--main-border-color);
}
#search-results ul {
padding: 5px 5px 5px 15px;
}
#search-text {
border: 1px solid var(--main-border-color);
}
/*
* .electron-in-page-search-window is a class specified to default
* <webview> element for search window.
@@ -247,7 +388,7 @@ div.ui-tooltip {
/* Allow to use <kbd> elements inside the title to define shortcut hints. */
.ui-menu kbd, button kbd {
color: black;
color: var(--muted-text-color);
border: none;
background-color: transparent;
box-shadow: none;
@@ -268,8 +409,14 @@ div.ui-tooltip {
display: none;
}
.dropdown-menu {
color: var(--menu-text-color) !important;
background-color: var(--menu-background-color) !important;
}
.dropdown-menu a:hover:not(.disabled) {
background-color: #eee !important;
color: var(--hover-item-text-color) !important;
background-color: var(--hover-item-background-color) !important;
cursor: pointer;
}
@@ -279,7 +426,7 @@ div.ui-tooltip {
.dropdown-menu kbd
{
color: black;
color: var(--muted-text-color);
border: none;
background-color: transparent;
box-shadow: none;
@@ -312,6 +459,11 @@ div.ui-tooltip {
min-height: 200px;
}
.CodeMirror-gutters {
background-color: inherit !important;
border-right: none;
}
#note-id-display {
position: absolute;
right: 10px;
@@ -328,8 +480,16 @@ div.ui-tooltip {
.cm-matchhighlight {background-color: #eeeeee}
#attribute-list {
overflow: auto;
/* limiting the size since actual note content is more important */
max-height: 30%;
flex-shrink: 0;
flex-basis: 2em;
}
#label-list, #relation-list, #attribute-list {
color: #777777;
color: var(--muted-text-color);
padding: 5px;
display: none;
}
@@ -341,7 +501,7 @@ div.ui-tooltip {
#file-table th, #file-table td {
padding: 10px;
font-size: large;
font-size: larger;
}
#children-overview {
@@ -355,11 +515,10 @@ div.ui-tooltip {
overflow: auto;
}
.child-overview {
.child-overview-item {
font-weight: bold;
font-size: large;
padding: 10px;
background: #f4f4f4;
background: var(--accented-background-color);
width: 150px;
height: 90px;
line-height: 2em;
@@ -372,8 +531,8 @@ div.ui-tooltip {
word-wrap: break-word;
}
.child-overview a {
color: #444;
.child-overview-item a {
color: var(--muted-text-color);
}
#sql-console-query {
@@ -387,18 +546,18 @@ div.ui-tooltip {
height: 150px;
}
#history-navigation {
margin: 0 20px 0 5px;
display: flex;
.btn {
border-radius: var(--button-border-radius);
}
.btn:not(.btn-primary):not(.btn-danger) {
border-color: #bbb;
background-color: #eee;
.btn:not(.btn-primary):not(.btn-secondary):not(.btn-danger) {
border-color: var(--button-border-color);
background-color: var(--button-background-color);
color: var(--button-text-color);
}
.btn.active:not(.btn-primary) {
background-color: #ccc;
background-color: var(--button-disabled-background-color) !important;
}
#note-path-list a.current {
@@ -419,40 +578,18 @@ button.icon-button {
width: 15em;
}
/* Themes */
html {
/* this fixes FF filter vs. position fixed bug: https://github.com/zadam/trilium/issues/233 */
height: 100%;
}
html.theme-black, html.theme-black img, html.theme-black video {
filter: invert(100%) hue-rotate(180deg);
}
html.theme-black body {
background: black;
}
html.theme-dark {
filter: invert(90%) hue-rotate(180deg);
}
html.theme-dark img, html.theme-dark video {
filter: invert(100%) hue-rotate(180deg);
}
html.theme-dark body {
background: #191819;
}
.ck.ck-block-toolbar-button {
transform: translateX(10px);
}
#note-detail-promoted-attributes {
max-width: 70%;
margin: auto;
/* setting the display to block since "table" doesn't support scrolling */
display: block;
flex-basis: content;
flex-shrink: 1;
flex-grow: 0;
overflow: auto;
}
#note-detail-promoted-attributes td, #note-detail-promoted-attributes th {
@@ -467,8 +604,12 @@ html.theme-dark body {
max-width: 100%;
}
pre:not(.CodeMirror-line) {
color: var(--main-text-color) !important;
}
#file-preview-content {
background-color: #f6f6f6;
background-color: var(--accented-background-color);
padding: 15px;
max-width: 600px;
max-height: 300px;
@@ -510,7 +651,7 @@ html.theme-dark body {
.go-to-selected-note-button.disabled, .go-to-selected-note-button.disabled:hover {
cursor: inherit;
color: #ccc !important;
color: var(--button-disabled-background-color) !important;
}
.note-autocomplete-input {
@@ -531,14 +672,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);
@@ -551,13 +684,14 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th
}
.tooltip-inner {
background-color: #fbfbfb !important;
background-color: var(--accented-background-color) !important;
max-width: 400px;
/* height needs to stay small because tooltip has problem when it can't fit to either top or bottom of the cursor */
max-height: 300px;
overflow: hidden;
color: black;
border: 1px solid #aaa;
color: var(--main-text-color);
border: 1px solid var(--main-border-color);
border-radius: 5px;
text-align: left;
}
@@ -585,8 +719,8 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th
.algolia-autocomplete .aa-dropdown-menu {
width: 100%;
background-color: #fff;
border: 1px solid #999;
background-color: var(--main-background-color);
border: 1px solid var(--main-border-color);
border-top: none;
z-index: 2000 !important;
max-height: 500px;
@@ -607,7 +741,8 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th
}
.algolia-autocomplete .aa-dropdown-menu .aa-suggestion.aa-cursor {
background-color: #B2D7FF;
color: var(--hover-item-text-color);
background-color: var(--hover-item-background-color);
}
.help-button {
@@ -619,14 +754,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;
}
@@ -680,9 +807,9 @@ div[data-notify="container"] {
#saved-indicator {
position: absolute;
right: 10px;
top: 11px;
font-size: x-large;
color: #777;
top: -7px;
font-size: 150%;
color: var(--main-text-color);
z-index: 100;
}
@@ -698,4 +825,57 @@ 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: var(--muted-text-color);
}
.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);
}
}
.ck-content .image > figcaption {
color: var(--main-text-color);
background-color: var(--accented-background-color);
}
#options-dialog input[type=number] {
text-align: right;
}

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]);

View File

@@ -3,6 +3,7 @@
const noteService = require('../../services/notes');
const protectedSessionService = require('../../services/protected_session');
const repository = require('../../services/repository');
const utils = require('../../services/utils');
async function uploadFile(req) {
const parentNoteId = req.params.parentNoteId;
@@ -33,8 +34,7 @@ async function uploadFile(req) {
};
}
async function downloadFile(req, res) {
const noteId = req.params.noteId;
async function downloadNoteFile(noteId, res) {
const note = await repository.getNote(noteId);
if (!note) {
@@ -42,20 +42,27 @@ async function downloadFile(req, res) {
}
if (note.isProtected && !protectedSessionService.isProtectedSessionAvailable()) {
res.status(401).send("Protected session not available");
return;
return res.status(401).send("Protected session not available");
}
const originalFileName = await note.getLabel('originalFileName');
const fileName = originalFileName ? originalFileName.value : note.title;
res.setHeader('Content-Disposition', 'file; filename="' + fileName + '"');
res.setHeader('Content-Disposition', utils.getContentDisposition(fileName));
res.setHeader('Content-Type', note.mime);
res.send(note.content);
}
async function downloadFile(req, res) {
const noteId = req.params.noteId;
return await downloadNoteFile(noteId, res);
}
module.exports = {
uploadFile,
downloadFile
downloadFile,
downloadNoteFile
};

View File

@@ -7,6 +7,7 @@ const tarImportService = require('../../services/import/tar');
const singleImportService = require('../../services/import/single');
const cls = require('../../services/cls');
const path = require('path');
const noteCacheService = require('../../services/note_cache');
async function importToBranch(req) {
const parentNoteId = req.params.parentNoteId;
@@ -28,24 +29,32 @@ async function importToBranch(req) {
// and may produce unintended consequences
cls.disableEntityEvents();
let note; // typically root of the import - client can show it after finishing the import
if (extension === '.tar') {
return await tarImportService.importTar(file.buffer, parentNote);
note = await tarImportService.importTar(file.buffer, parentNote);
}
else if (extension === '.opml') {
return await opmlImportService.importOpml(file.buffer, parentNote);
note = await opmlImportService.importOpml(file.buffer, parentNote);
}
else if (extension === '.md') {
return await singleImportService.importMarkdown(file, parentNote);
note = await singleImportService.importMarkdown(file, parentNote);
}
else if (extension === '.html' || extension === '.htm') {
return await singleImportService.importHtml(file, parentNote);
note = await singleImportService.importHtml(file, parentNote);
}
else if (extension === '.enex') {
return await enexImportService.importEnex(file, parentNote);
note = await enexImportService.importEnex(file, parentNote);
}
else {
return [400, `Unrecognized extension ${extension}, must be .tar or .opml`];
}
// import has deactivated note events so note cache is not updated
// instead we force it to reload (can be async)
noteCacheService.load();
return note;
}
module.exports = {

View File

@@ -10,6 +10,7 @@ const appInfo = require('../../services/app_info');
const eventService = require('../../services/events');
const cls = require('../../services/cls');
const sqlInit = require('../../services/sql_init');
const sql = require('../../services/sql');
async function loginSync(req) {
if (!await sqlInit.schemaExists()) {
@@ -22,14 +23,15 @@ async function loginSync(req) {
const now = new Date();
if (Math.abs(timestamp.getTime() - now.getTime()) > 5000) {
// login token is valid for 5 minutes
if (Math.abs(timestamp.getTime() - now.getTime()) > 5 * 60 * 1000) {
return [400, { message: 'Auth request time is out of sync' }];
}
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');
@@ -44,7 +46,8 @@ async function loginSync(req) {
req.session.loggedIn = true;
return {
sourceId: sourceIdService.getCurrentSourceId()
sourceId: sourceIdService.getCurrentSourceId(),
maxSyncId: await sql.getValue("SELECT MAX(id) FROM sync")
};
}

View File

@@ -1,12 +1,12 @@
"use strict";
const sql = require('../../services/sql');
const optionService = require('../../services/options');
const log = require('../../services/log');
const attributes = require('../../services/attributes');
// 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', 'mainFontSize', 'treeFontSize', 'detailFontSize'];
async function getOptions() {
return await optionService.getOptionsMap(ALLOWED_OPTIONS);
@@ -42,8 +42,31 @@ async function update(name, value) {
return true;
}
async function getUserThemes() {
const notes = await attributes.getNotesWithLabel('appTheme');
const ret = [];
for (const note of notes) {
let value = await note.getLabelValue('appTheme');
if (!value) {
value = note.title.toLowerCase().replace(/[^a-z0-9]/gi, '-');
}
ret.push({
val: value,
title: note.title,
noteId: note.noteId
});
}
return ret;
}
module.exports = {
getOptions,
updateOption,
updateOptions
updateOptions,
getUserThemes
};

View File

@@ -19,7 +19,7 @@ async function exec(req) {
async function run(req) {
const note = await repository.getNote(req.params.noteId);
const result = await scriptService.executeNote(note, note);
const result = await scriptService.executeNote(note, { originEntity: note });
return { executionResult: result };
}
@@ -30,7 +30,7 @@ async function getStartupBundles() {
const bundles = [];
for (const note of notes) {
const bundle = await scriptService.getScriptBundle(note);
const bundle = await scriptService.getScriptBundleForFrontend(note);
if (bundle) {
bundles.push(bundle);
@@ -53,14 +53,26 @@ async function getRelationBundles(req) {
const bundles = [];
for (const noteId of uniqueNoteIds) {
bundles.push(await scriptService.getScriptBundleForNoteId(noteId));
const note = await repository.getNote(noteId);
if (!note.isJavaScript() || note.getScriptEnv() !== 'frontend') {
continue;
}
const bundle = await scriptService.getScriptBundleForFrontend(note);
if (bundle) {
bundles.push(bundle);
}
}
return bundles;
}
async function getBundle(req) {
return await scriptService.getScriptBundleForNoteId(req.params.noteId);
const note = await repository.getNote(req.params.noteId);
return await scriptService.getScriptBundleForFrontend(note);
}
module.exports = {

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);

Some files were not shown because too many files have changed in this diff Show More