Compare commits

..

72 Commits

Author SHA1 Message Date
zadam
df11b076bc release 0.42.1 2020-05-06 23:24:13 +02:00
MeIchthys
54ecd2ee75 Prevent td text from overlapping th text (#1002)
This makes all of the Note Info sections more consistent with each other. It prevents overlapping of text when the window is displayed in a small-width environment.
2020-05-06 23:12:28 +02:00
zadam
2369bcf9fc fixes for image download 2020-05-06 23:11:34 +02:00
zadam
5d8808a2ad fix renaming existing attributes + added new label autoReadOnlyDisabled to control automatic setting to readOnly mode 2020-05-06 21:41:14 +02:00
zadam
62b993f06f fix closing tab by mouse 2020-05-06 21:24:51 +02:00
zadam
8aa5608085 fix "Render note" and "Execute script" buttons + refactoring around data-trigger-command handling 2020-05-06 00:00:14 +02:00
zadam
b452d7e5c5 Merge remote-tracking branch 'origin/stable' 2020-05-06 00:00:01 +02:00
zadam
9b9d6d86d0 remove debugging console.log 2020-05-05 22:51:53 +02:00
zadam
7f2755d4a0 refresh button state change on note update 2020-05-05 22:18:09 +02:00
zadam
3b268cc8eb fix selecting note title after creation, closes #997 2020-05-05 21:42:18 +02:00
zadam
6dfe335707 added global menu item to open new empty window + some refactoring 2020-05-05 19:30:03 +02:00
zadam
c7125d2b50 createNoteAfter should have saveSelection: false 2020-05-05 19:09:01 +02:00
zadam
e8a33a5ee7 fix note title not updating when changing the title 2020-05-05 18:56:12 +02:00
zadam
deb0b24c4c release 0.42.0-beta 2020-05-04 21:59:14 +02:00
zadam
cafcb67a8a remove dangling autocompletes after closing the tab 2020-05-04 21:58:40 +02:00
zadam
4c6e9480e4 revert #980 because of performance issues 2020-05-04 21:49:03 +02:00
zadam
109bead1c7 removed unnecessary async/awaits 2020-05-04 10:19:11 +02:00
zadam
ae1220b970 remove debug 2020-05-04 10:04:50 +02:00
zadam
b89a2df462 fix image being redownloaded from localhost 2020-05-04 10:03:54 +02:00
zadam
8b5536ee3a note title widget and protected session entering fixes 2020-05-03 22:49:20 +02:00
zadam
647790885d downgrade sqlite3 library which has issues with electron build 2020-05-03 21:27:24 +02:00
zadam
227c3e4dcc fixes for offline downloading of images 2020-05-03 14:33:59 +02:00
zadam
4eb2407c73 fix folder icon for hidden included images 2020-05-03 13:59:49 +02:00
zadam
43e12fbea2 small fixes for collapse/expand 2020-05-03 13:52:12 +02:00
zadam
2a3091f788 reimplemented expand/collapse differently for better performance 2020-05-03 13:15:08 +02:00
zadam
742df25bc2 collapse/expand only folder notes 2020-05-03 09:49:56 +02:00
zadam
9be1d1f697 add note attribute cache to speed up tree loading 2020-05-02 18:19:41 +02:00
zadam
ed52f93bbb tree settings popup fixes 2020-05-02 13:52:02 +02:00
zadam
3466a19397 protection against recursive expansion of search notes 2020-05-02 12:16:48 +02:00
zadam
fe53e2351c basic implementation of note tree's config 2020-05-02 00:28:40 +02:00
zadam
5c35b870eb added titles with full date time including timezone offset also to note revisions dialog/widget 2020-05-01 00:01:53 +02:00
zadam
90d091aedb make note tree initial load non-lazy 2020-04-30 23:58:34 +02:00
zadam
0a05a40186 fix expand subtree's conflict with auto-lazy loading 2020-04-30 23:09:25 +02:00
zadam
7482ed063b Merge remote-tracking branch 'origin/stable' 2020-04-29 23:27:39 +02:00
zadam
70e343f2fc fix showing deleted notes in "recent changes" dialog, closes #994 2020-04-29 23:25:34 +02:00
zadam
358f3a7291 implement "expand subtree" contet menu, closes #993 2020-04-29 23:13:05 +02:00
zadam
6161b1c193 Merge remote-tracking branch 'origin/stable' 2020-04-29 22:27:46 +02:00
zadam
94b57dadd7 removed link context menu on JS-only links 2020-04-29 22:27:22 +02:00
zadam
81fbefb9cd added scripts for running trilium as "portable" 2020-04-29 21:55:57 +02:00
zadam
6d6695e3a9 ckeditor 19 2020-04-29 17:29:32 +02:00
zadam
4c308ad68f moved protected note switch to "note actions", added shadow to protected note title 2020-04-29 00:01:07 +02:00
zadam
989a003d2f borderless title bar buttons 2020-04-28 21:55:54 +02:00
zadam
ccdb41841e release 0.41.6 2020-04-27 23:46:48 +02:00
zadam
0a94622413 fix drag and drop in the tree, closes #984 2020-04-27 23:39:10 +02:00
zadam
5769587305 experimental hiding of images if they are included in the parent note 2020-04-27 23:27:45 +02:00
zadam
ffbfccb701 extra window now works in browsers too 2020-04-27 22:29:39 +02:00
zadam
56ce23fc36 fix collapsing of note revisions 2020-04-27 22:13:32 +02:00
zadam
cba7e5a59f fix collapsing of note revisions 2020-04-27 21:44:25 +02:00
zadam
86cf8f3202 fix download/export 2020-04-27 20:58:31 +02:00
zadam
907cdd8fcb fixes for extra window 2020-04-26 23:11:52 +02:00
zadam
7ea53d468e use local dates in the recent changes 2020-04-26 14:39:13 +02:00
zadam
586d6b4557 in web version use local client time instead of server time for recording dateModified etc. 2020-04-26 14:26:57 +02:00
zadam
8f68ff1932 tweaks for the code preview 2020-04-26 12:06:54 +02:00
zadam
a1ea2c9115 read only code notes WIP 2020-04-26 11:38:30 +02:00
zadam
e8ce81a133 organize widgets a bit 2020-04-26 09:40:02 +02:00
zadam
aff12950f0 Merge remote-tracking branch 'origin/master' into m42
# Conflicts:
#	src/public/app/services/app_context.js
2020-04-25 23:53:19 +02:00
zadam
75c58cbf79 refactored layouts for extra window 2020-04-25 23:52:13 +02:00
zadam
87a1e98fa2 default search should look also into attribute names and values, #980 2020-04-25 22:10:56 +02:00
zadam
d1eacbb574 more robust entering protected session and the following protection of a note 2020-04-25 17:15:57 +02:00
zadam
71d248cd87 touch protected session during note update 2020-04-25 11:09:07 +02:00
zadam
ac608b9334 small text changes 2020-04-24 21:21:22 +02:00
zadam
32020d78b5 prototype for new app window 2020-04-23 23:08:15 +02:00
zadam
ff853c7d0a implement lazy loading of tabs which speeds up especially initial startup with many tabs 2020-04-22 23:09:35 +02:00
zadam
8526cb2315 added collapsible widgets to the docs 2020-04-21 23:14:55 +02:00
zadam
dc89f72e75 fix display of text notes in note revisions 2020-04-21 22:59:37 +02:00
zadam
657ff16267 fix build webpack 2020-04-20 23:14:50 +02:00
zadam
ed759f5585 release 0.41.5 2020-04-20 22:40:02 +02:00
zadam
a86177bb59 release 0.41.5 2020-04-20 22:39:23 +02:00
zadam
9f1b3cc892 note paths widget has context menu too 2020-04-20 22:38:37 +02:00
zadam
8473f72ec8 fix support of multiple languages for spellchecking + list all available languages, closes #974 2020-04-20 22:26:31 +02:00
zadam
666d202a3a deps updates 2020-04-19 09:42:10 +02:00
zadam
988fae50cb fix help links and displayed shortcuts, closes #971 2020-04-18 11:14:09 +02:00
115 changed files with 2171 additions and 1080 deletions

2
.idea/dataSources.xml generated
View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="document.db" uuid="a2c75661-f9e2-478f-a69f-6a9409e69997">
<data-source source="LOCAL" name="SQLite - document.db" uuid="d0fd879f-1e1d-4d5c-9c21-0e5cf9ab2976">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<dataSource name="document.db">
<database-model serializer="dbm" dbms="SQLITE" family-id="SQLITE" format-version="4.17">
<database-model serializer="dbm" dbms="SQLITE" family-id="SQLITE" format-version="4.18">
<root id="1">
<ServerVersion>3.25.1</ServerVersion>
<ServerVersion>3.16.1</ServerVersion>
</root>
<schema id="2" parent="1" name="main">
<Current>1</Current>
@@ -57,6 +57,7 @@
<index id="24" parent="6" name="sqlite_autoindex_api_tokens_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>apiTokenId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="25" parent="6">
@@ -130,17 +131,21 @@
<index id="38" parent="7" name="sqlite_autoindex_attributes_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>attributeId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="39" parent="7" name="IDX_attributes_noteId_index">
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="40" parent="7" name="IDX_attributes_name_value">
<ColNames>name
value</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="41" parent="7" name="IDX_attributes_value_index">
<ColNames>value</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="42" parent="7">
<ColNames>attributeId</ColNames>
@@ -207,14 +212,17 @@ value</ColNames>
<index id="54" parent="8" name="sqlite_autoindex_branches_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>branchId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="55" parent="8" name="IDX_branches_noteId_parentNoteId">
<ColNames>noteId
parentNoteId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="56" parent="8" name="IDX_branches_parentNoteId">
<ColNames>parentNoteId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="57" parent="8">
<ColNames>branchId</ColNames>
@@ -245,6 +253,7 @@ parentNoteId</ColNames>
<index id="62" parent="9" name="sqlite_autoindex_note_contents_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="63" parent="9">
@@ -275,6 +284,7 @@ parentNoteId</ColNames>
<index id="68" parent="10" name="sqlite_autoindex_note_revision_contents_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteRevisionId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="69" parent="10">
@@ -359,22 +369,28 @@ parentNoteId</ColNames>
<index id="84" parent="11" name="sqlite_autoindex_note_revisions_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteRevisionId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="85" parent="11" name="IDX_note_revisions_noteId">
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="86" parent="11" name="IDX_note_revisions_utcDateLastEdited">
<ColNames>utcDateLastEdited</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="87" parent="11" name="IDX_note_revisions_utcDateCreated">
<ColNames>utcDateCreated</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="88" parent="11" name="IDX_note_revisions_dateLastEdited">
<ColNames>dateLastEdited</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="89" parent="11" name="IDX_note_revisions_dateCreated">
<ColNames>dateCreated</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="90" parent="11">
<ColNames>noteRevisionId</ColNames>
@@ -461,28 +477,36 @@ parentNoteId</ColNames>
<index id="105" parent="12" name="sqlite_autoindex_notes_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="106" parent="12" name="IDX_notes_title">
<ColNames>title</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="107" parent="12" name="IDX_notes_type">
<ColNames>type</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="108" parent="12" name="IDX_notes_isDeleted">
<ColNames>isDeleted</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="109" parent="12" name="IDX_notes_dateCreated">
<ColNames>dateCreated</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="110" parent="12" name="IDX_notes_dateModified">
<ColNames>dateModified</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="111" parent="12" name="IDX_notes_utcDateCreated">
<ColNames>utcDateCreated</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="112" parent="12" name="IDX_notes_utcDateModified">
<ColNames>utcDateModified</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="113" parent="12">
<ColNames>noteId</ColNames>
@@ -523,6 +547,7 @@ parentNoteId</ColNames>
<index id="120" parent="13" name="sqlite_autoindex_options_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>name</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="121" parent="13">
@@ -558,6 +583,7 @@ parentNoteId</ColNames>
<index id="127" parent="14" name="sqlite_autoindex_recent_notes_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="128" parent="14">
@@ -578,10 +604,12 @@ parentNoteId</ColNames>
<index id="131" parent="15" name="sqlite_autoindex_source_ids_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>sourceId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="132" parent="15" name="IDX_source_ids_utcDateCreated">
<ColNames>utcDateCreated</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="133" parent="15">
<ColNames>sourceId</ColNames>
@@ -602,7 +630,7 @@ parentNoteId</ColNames>
</column>
<column id="137" parent="16" name="rootpage">
<Position>4</Position>
<DataType>int|0s</DataType>
<DataType>integer|0s</DataType>
</column>
<column id="138" parent="16" name="sql">
<Position>5</Position>
@@ -649,10 +677,12 @@ parentNoteId</ColNames>
<index id="147" parent="18" name="IDX_sync_entityName_entityId">
<ColNames>entityName
entityId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="148" parent="18" name="IDX_sync_utcSyncDate">
<ColNames>utcSyncDate</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="149" parent="18">
<ColNames>id</ColNames>

View File

@@ -1,6 +1,7 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="JSUnfilteredForInLoop" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />

1
.idea/vcs.xml generated
View File

@@ -2,5 +2,6 @@
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
</component>
</project>

View File

@@ -8,7 +8,7 @@ Trilium Notes is a hierarchical note taking application with focus on building l
## Features
* 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)
* Rich WYSIWYG note editing including e.g. tables and images with markdown [autoformat](https://github.com/zadam/trilium/wiki/Text-notes#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), 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)

View File

@@ -29,6 +29,9 @@ cp images/app-icons/png/128x128.png $BUILD_DIR/icon.png
# removing software WebGL binaries because they are pretty huge and not necessary
rm -r $BUILD_DIR/swiftshader
cp bin/tpl/portable-trilium.sh $BUILD_DIR/
chmod 755 $BUILD_DIR/portable-trilium.sh
echo "Packaging linux x64 electron distribution..."
VERSION=`jq -r ".version" package.json`

View File

@@ -31,6 +31,8 @@ mv "./dist/Trilium Notes-win32-x64" $BUILD_DIR
# removing software WebGL binaries because they are pretty huge and not necessary
rm -r $BUILD_DIR/swiftshader
cp bin/tpl/portable-trilium.bat $BUILD_DIR/
echo "Zipping windows x64 electron distribution..."
VERSION=`jq -r ".version" package.json`

View File

@@ -5,6 +5,8 @@ if [[ $# -eq 0 ]] ; then
exit 1
fi
npm run webpack
DIR=$1
rm -rf $DIR
@@ -25,7 +27,7 @@ cp -r electron.js $DIR/
cp webpack-* $DIR/
# run in subshell (so we return to original dir)
(cd $DIR && npm install --only=prod && npm run webpack)
(cd $DIR && npm install --only=prod)
find $DIR/libraries -name "*.map" -type f -delete

View File

@@ -0,0 +1,4 @@
SET DIR=%~dp0
SET TRILIUM_DATA_DIR=%DIR%\trilium-data
cd %DIR%
start trilium.exe

7
bin/tpl/portable-trilium.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env sh
DIR=`dirname "$0"`
export TRILIUM_DATA_DIR="$DIR/trilium-data"
"$DIR/trilium"

View File

@@ -879,7 +879,7 @@
</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="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3><a href="global.html">Global</a></h3>
<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="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3>Global</h3><ul><li><a href="global.html#decorateWidget">decorateWidget</a></li><li><a href="global.html#doRenderBody">doRenderBody</a></li><li><a href="global.html#widgetCollapsedStateChangedEvent">widgetCollapsedStateChangedEvent</a></li></ul>
</nav>
<br class="clear">

View File

@@ -5483,7 +5483,7 @@ Typical use case is when new note has been created, we should wait until it is s
</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="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3><a href="global.html">Global</a></h3>
<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="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3>Global</h3><ul><li><a href="global.html#decorateWidget">decorateWidget</a></li><li><a href="global.html#doRenderBody">doRenderBody</a></li><li><a href="global.html#widgetCollapsedStateChangedEvent">widgetCollapsedStateChangedEvent</a></li></ul>
</nav>
<br class="clear">

View File

@@ -507,7 +507,7 @@
</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="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3><a href="global.html">Global</a></h3>
<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="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3>Global</h3><ul><li><a href="global.html#decorateWidget">decorateWidget</a></li><li><a href="global.html#doRenderBody">doRenderBody</a></li><li><a href="global.html#widgetCollapsedStateChangedEvent">widgetCollapsedStateChangedEvent</a></li></ul>
</nav>
<br class="clear">

View File

@@ -6813,7 +6813,7 @@ Cache is note instance scoped.
</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="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3><a href="global.html">Global</a></h3>
<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="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3>Global</h3><ul><li><a href="global.html#decorateWidget">decorateWidget</a></li><li><a href="global.html#doRenderBody">doRenderBody</a></li><li><a href="global.html#widgetCollapsedStateChangedEvent">widgetCollapsedStateChangedEvent</a></li></ul>
</nav>
<br class="clear">

View File

@@ -79,7 +79,7 @@ export default Attribute;</code></pre>
</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="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3><a href="global.html">Global</a></h3>
<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="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3>Global</h3><ul><li><a href="global.html#decorateWidget">decorateWidget</a></li><li><a href="global.html#doRenderBody">doRenderBody</a></li><li><a href="global.html#widgetCollapsedStateChangedEvent">widgetCollapsedStateChangedEvent</a></li></ul>
</nav>
<br class="clear">

View File

@@ -81,7 +81,7 @@ export default Branch;</code></pre>
</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="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3><a href="global.html">Global</a></h3>
<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="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3>Global</h3><ul><li><a href="global.html#decorateWidget">decorateWidget</a></li><li><a href="global.html#doRenderBody">doRenderBody</a></li><li><a href="global.html#widgetCollapsedStateChangedEvent">widgetCollapsedStateChangedEvent</a></li></ul>
</nav>
<br class="clear">

View File

@@ -61,7 +61,7 @@ export default NoteComplement;</code></pre>
</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="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3><a href="global.html">Global</a></h3>
<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="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3>Global</h3><ul><li><a href="global.html#decorateWidget">decorateWidget</a></li><li><a href="global.html#doRenderBody">doRenderBody</a></li><li><a href="global.html#widgetCollapsedStateChangedEvent">widgetCollapsedStateChangedEvent</a></li></ul>
</nav>
<br class="clear">

View File

@@ -493,7 +493,7 @@ export default NoteShort;</code></pre>
</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="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3><a href="global.html">Global</a></h3>
<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="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3>Global</h3><ul><li><a href="global.html#decorateWidget">decorateWidget</a></li><li><a href="global.html#doRenderBody">doRenderBody</a></li><li><a href="global.html#widgetCollapsedStateChangedEvent">widgetCollapsedStateChangedEvent</a></li></ul>
</nav>
<br class="clear">

View File

@@ -95,6 +95,275 @@
<h3 class="subsection-title">Methods</h3>
<h4 class="name" id="decorateWidget"><span class="type-signature"></span>decorateWidget<span class="signature">()</span><span class="type-signature"></span></h4>
<div class="description">
for overriding
</div>
<dl class="details">
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="widgets_collapsible_widget.js.html">widgets/collapsible_widget.js</a>, <a href="widgets_collapsible_widget.js.html#line93">line 93</a>
</li></ul></dd>
</dl>
<h4 class="name" id="doRenderBody"><span class="type-signature">(async) </span>doRenderBody<span class="signature">()</span><span class="type-signature"></span></h4>
<div class="description">
for overriding
</div>
<dl class="details">
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="widgets_collapsible_widget.js.html">widgets/collapsible_widget.js</a>, <a href="widgets_collapsible_widget.js.html#line96">line 96</a>
</li></ul></dd>
</dl>
<h4 class="name" id="widgetCollapsedStateChangedEvent"><span class="type-signature"></span>widgetCollapsedStateChangedEvent<span class="signature">()</span><span class="type-signature"></span></h4>
<div class="description">
This event is used to synchronize collapsed state of all the tab-cached widgets since they are all rendered
separately but should behave uniformly for the user.
</div>
<dl class="details">
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="widgets_collapsible_widget.js.html">widgets/collapsible_widget.js</a>, <a href="widgets_collapsible_widget.js.html#line86">line 86</a>
</li></ul></dd>
</dl>
@@ -333,7 +602,7 @@
</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="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3><a href="global.html">Global</a></h3>
<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="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3>Global</h3><ul><li><a href="global.html#decorateWidget">decorateWidget</a></li><li><a href="global.html#doRenderBody">doRenderBody</a></li><li><a href="global.html#widgetCollapsedStateChangedEvent">widgetCollapsedStateChangedEvent</a></li></ul>
</nav>
<br class="clear">

View File

@@ -50,7 +50,7 @@
</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="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3><a href="global.html">Global</a></h3>
<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="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3>Global</h3><ul><li><a href="global.html#decorateWidget">decorateWidget</a></li><li><a href="global.html#doRenderBody">doRenderBody</a></li><li><a href="global.html#widgetCollapsedStateChangedEvent">widgetCollapsedStateChangedEvent</a></li></ul>
</nav>
<br class="clear">

View File

@@ -443,7 +443,7 @@ export default FrontendScriptApi;</code></pre>
</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="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3><a href="global.html">Global</a></h3>
<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="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3>Global</h3><ul><li><a href="global.html#decorateWidget">decorateWidget</a></li><li><a href="global.html#doRenderBody">doRenderBody</a></li><li><a href="global.html#widgetCollapsedStateChangedEvent">widgetCollapsedStateChangedEvent</a></li></ul>
</nav>
<br class="clear">

View File

@@ -0,0 +1,151 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: widgets/collapsible_widget.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: widgets/collapsible_widget.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>import TabAwareWidget from "./tab_aware_widget.js";
import options from "../services/options.js";
const WIDGET_TPL = `
&lt;div class="card widget">
&lt;div class="card-header">
&lt;div>
&lt;button class="btn btn-sm widget-title" data-toggle="collapse" data-target="#[to be set]">
Collapsible Group Item
&lt;/button>
&lt;a class="widget-help external no-arrow bx bx-info-circle">&lt;/a>
&lt;/div>
&lt;div class="widget-header-actions">&lt;/div>
&lt;/div>
&lt;div id="[to be set]" class="collapse body-wrapper" style="transition: none; ">
&lt;div class="card-body">&lt;/div>
&lt;/div>
&lt;/div>`;
export default class CollapsibleWidget extends TabAwareWidget {
get widgetTitle() { return "Untitled widget"; }
get headerActions() { return []; }
get help() { return {}; }
doRender() {
this.$widget = $(WIDGET_TPL);
this.$widget.find('[data-target]').attr('data-target', "#" + this.componentId);
this.$bodyWrapper = this.$widget.find('.body-wrapper');
this.$bodyWrapper.attr('id', this.componentId); // for toggle to work we need id
this.widgetName = this.constructor.name;
if (!options.is(this.widgetName + 'Collapsed')) {
this.$bodyWrapper.collapse("show");
}
// using immediate variants of the event so that the previous collapse is not caught
this.$bodyWrapper.on('hide.bs.collapse', () => this.saveCollapsed(true));
this.$bodyWrapper.on('show.bs.collapse', () => this.saveCollapsed(false));
this.$body = this.$bodyWrapper.find('.card-body');
this.$title = this.$widget.find('.widget-title');
this.$title.text(this.widgetTitle);
this.$help = this.$widget.find('.widget-help');
if (this.help.title) {
this.$help.attr("title", this.help.title);
this.$help.attr("href", this.help.url || "javascript:");
if (!this.help.url) {
this.$help.addClass('no-link');
}
}
else {
this.$help.hide();
}
this.$headerActions = this.$widget.find('.widget-header-actions');
this.$headerActions.append(...this.headerActions);
this.initialized = this.doRenderBody();
this.decorateWidget();
return this.$widget;
}
saveCollapsed(collapse) {
options.save(this.widgetName + 'Collapsed', collapse.toString());
this.triggerEvent(`widgetCollapsedStateChanged`, {widgetName: this.widgetName, collapse});
}
/**
* This event is used to synchronize collapsed state of all the tab-cached widgets since they are all rendered
* separately but should behave uniformly for the user.
*/
widgetCollapsedStateChangedEvent({widgetName, collapse}) {
if (widgetName === this.widgetName) {
this.$bodyWrapper.toggleClass('show', !collapse);
}
}
/** for overriding */
decorateWidget() {}
/** for overriding */
async doRenderBody() {}
isExpanded() {
return this.$bodyWrapper.hasClass("show");
}
}</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="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3>Global</h3><ul><li><a href="global.html#decorateWidget">decorateWidget</a></li><li><a href="global.html#doRenderBody">doRenderBody</a></li><li><a href="global.html#widgetCollapsedStateChangedEvent">widgetCollapsedStateChangedEvent</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.4</a>
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -2986,7 +2986,7 @@ var uniqueId = $.fn.extend( {
self = this,
wasExpanded = this.isExpanded();
_assert(this.isLazy(), "load() requires a lazy node");
//_assert(this.isLazy(), "load() requires a lazy node");
// _assert( forceReload || this.isUndefined(), "Pass forceReload=true to re-load a lazy node" );
if (!forceReload && !this.isUndefined()) {
return _getResolvedPromise(this);

818
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.41.4-beta",
"version": "0.42.1",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
@@ -16,7 +16,7 @@
"start-server": "TRILIUM_ENV=dev node ./src/www",
"start-electron": "TRILIUM_ENV=dev electron .",
"build-backend-docs": "./node_modules/.bin/jsdoc -c jsdoc-conf.json -d ./docs/backend_api src/entities/*.js src/services/backend_script_api.js",
"build-frontend-docs": "./node_modules/.bin/jsdoc -c jsdoc-conf.json -d ./docs/frontend_api src/public/app/entities/*.js src/public/app/services/frontend_script_api.js",
"build-frontend-docs": "./node_modules/.bin/jsdoc -c jsdoc-conf.json -d ./docs/frontend_api src/public/app/entities/*.js src/public/app/services/frontend_script_api.js src/public/app/widgets/collapsible_widget.js",
"build-docs": "npm run build-backend-docs && npm run build-frontend-docs",
"webpack": "npx webpack -c webpack-desktop.config.js && npx webpack -c webpack-mobile.config.js && npx webpack -c webpack-setup.config.js"
},
@@ -28,16 +28,16 @@
"commonmark": "0.29.1",
"cookie-parser": "1.4.5",
"csurf": "1.11.0",
"dayjs": "1.8.24",
"dayjs": "1.8.26",
"debug": "4.1.1",
"ejs": "3.0.2",
"ejs": "3.1.2",
"electron-debug": "3.0.1",
"electron-dl": "3.0.0",
"electron-find": "1.0.6",
"electron-window-state": "5.0.3",
"express": "4.17.1",
"express-session": "1.17.0",
"file-type": "14.1.4",
"express-session": "1.17.1",
"file-type": "14.3.0",
"fs-extra": "9.0.0",
"helmet": "3.22.0",
"html": "1.0.0",
@@ -51,10 +51,10 @@
"imagemin-pngquant": "8.0.0",
"ini": "1.3.5",
"is-svg": "4.2.1",
"jimp": "0.10.2",
"mime-types": "2.1.26",
"jimp": "0.10.3",
"mime-types": "2.1.27",
"multer": "1.4.2",
"node-abi": "2.15.0",
"node-abi": "2.16.0",
"open": "7.0.3",
"portscanner": "2.2.0",
"rand-token": "1.0.1",
@@ -73,18 +73,18 @@
"turndown": "6.0.0",
"turndown-plugin-gfm": "1.0.2",
"unescape": "1.0.1",
"ws": "7.2.3",
"ws": "7.2.5",
"yauzl": "^2.10.0",
"yazl": "^2.5.1"
},
"devDependencies": {
"electron": "9.0.0-beta.16",
"electron-builder": "22.4.1",
"electron": "9.0.0-beta.22",
"electron-builder": "22.6.0",
"electron-packager": "14.2.1",
"electron-rebuild": "1.10.1",
"jsdoc": "3.6.4",
"lorem-ipsum": "2.0.3",
"webpack": "5.0.0-beta.14",
"webpack": "5.0.0-beta.16",
"webpack-cli": "4.0.0-beta.8"
},
"optionalDependencies": {

View File

@@ -5,8 +5,9 @@ import bundleService from "./services/bundle.js";
import noteAutocompleteService from './services/note_autocomplete.js';
import macInit from './services/mac_init.js';
import contextMenu from "./services/context_menu.js";
import DesktopLayout from "./widgets/desktop_layout.js";
import DesktopMainWindowLayout from "./layouts/desktop_main_window_layout.js";
import glob from "./services/glob.js";
import DesktopExtraWindowLayout from "./layouts/desktop_extra_window_layout.js";
glob.setupGlobs();
@@ -23,9 +24,11 @@ $('[data-toggle="tooltip"]').tooltip({
macInit.init();
bundleService.getWidgetBundlesByParent().then(widgetBundles => {
const desktopLayout = new DesktopLayout(widgetBundles);
const layout = window.glob.isMainWindow
? new DesktopMainWindowLayout(widgetBundles)
: new DesktopExtraWindowLayout(widgetBundles);
appContext.setLayout(desktopLayout);
appContext.setLayout(layout);
appContext.start();
});
@@ -82,7 +85,7 @@ if (utils.isElectron()) {
});
}
if (params.linkURL.length !== 0 && params.mediaType === 'none') {
if (!["", "javascript:", "about:blank#blocked"].includes(params.linkURL) && params.mediaType === 'none') {
items.push({
title: `Copy link`,
uiIcon: "copy",

View File

@@ -44,7 +44,8 @@ async function loadNoteRevisions(noteId, noteRevId) {
for (const item of revisionItems) {
$list.append($('<a class="dropdown-item" tabindex="0">')
.text(item.dateLastEdited.substr(0, 16) + ` (${item.contentLength} bytes)`)
.attr('data-note-revision-id', item.noteRevisionId));
.attr('data-note-revision-id', item.noteRevisionId))
.attr('title', 'This revision was last edited on ' + item.dateLastEdited);
}
$listDropdown.dropdown('show');

View File

@@ -1,4 +1,4 @@
import optionsService from "../../services/options.js";
import utils from "../../services/utils.js";
import server from "../../services/server.js";
import toastService from "../../services/toast.js";
@@ -20,7 +20,9 @@ const TPL = `
<input type="text" class="form-control" id="spell-check-language-code" placeholder="for example &quot;en-US&quot;, &quot;de-AT&quot;">
</div>
<p>Multiple languages can be separated by comman. Changes to the spell check options will take effect after application restart.</p>
<p>Multiple languages can be separated by comma, e.g. <code>en-US, de-DE, cs</code>. Changes to the spell check options will take effect after application restart.</p>
<p><strong>Available language codes: </strong> <span id="available-language-codes"></span></p>
</div>
<div>
@@ -95,6 +97,14 @@ export default class ProtectedSessionOptions {
return false;
});
this.$availableLanguageCodes = $("#available-language-codes");
if (utils.isElectron()) {
const {webContents} = utils.dynamicRequire('electron').remote.getCurrentWindow();
this.$availableLanguageCodes.text(webContents.session.availableSpellCheckerLanguages.join(', '));
}
this.$eraseNotesAfterTimeInSeconds = $("#erase-notes-after-time-in-seconds");
this.$eraseNotesAfterTimeInSeconds.on('change', () => {

View File

@@ -32,10 +32,10 @@ export async function showDialog(ancestorNoteId) {
for (const [dateDay, dayChanges] of groupedByDate) {
const $changesList = $('<ul>');
const dayEl = $('<div>').append($('<b>').html(utils.formatDate(dateDay))).append($changesList);
const dayEl = $('<div>').append($('<b>').text(dateDay)).append($changesList);
for (const change of dayChanges) {
const formattedTime = utils.formatTime(utils.parseDate(change.date));
const formattedTime = change.date.substr(11, 5);
let $noteLink;
@@ -82,7 +82,12 @@ export async function showDialog(ancestorNoteId) {
}
$changesList.append($('<li>')
.append(formattedTime + ' - ')
.append(
$("<span>")
.text(formattedTime)
.attr("title", change.date)
)
.append(' - ')
.append($noteLink));
}
@@ -92,23 +97,9 @@ export async function showDialog(ancestorNoteId) {
function groupByDate(result) {
const groupedByDate = new Map();
const dayCache = {};
for (const row of result) {
let dateDay = utils.parseDate(row.date);
dateDay.setHours(0);
dateDay.setMinutes(0);
dateDay.setSeconds(0);
dateDay.setMilliseconds(0);
// this stupidity is to make sure that we always use the same day object because Map uses only
// reference equality
if (dayCache[dateDay]) {
dateDay = dayCache[dateDay];
}
else {
dayCache[dateDay] = dateDay;
}
const dateDay = row.date.substr(0, 10);
if (!groupedByDate.has(dateDay)) {
groupedByDate.set(dateDay, []);

View File

@@ -1,5 +1,6 @@
import server from '../services/server.js';
import Attribute from './attribute.js';
import noteAttributeCache from "../services/note_attribute_cache.js";
const LABEL = 'label';
const LABEL_DEFINITION = 'label-definition';
@@ -7,6 +8,8 @@ const RELATION = 'relation';
const RELATION_DEFINITION = 'relation-definition';
/**
* FIXME: since there's no "full note" anymore we can rename this to Note
*
* This note's representation is used in note tree and is kept in TreeCache.
*/
class NoteShort {
@@ -156,9 +159,9 @@ class NoteShort {
getOwnedAttributes(type, name) {
const attrs = this.attributes
.map(attributeId => this.treeCache.attributes[attributeId])
.filter(attr => !!attr);
.filter(Boolean); // filter out nulls;
return this.__filterAttrs(attrs, type, name)
return this.__filterAttrs(attrs, type, name);
}
/**
@@ -167,43 +170,45 @@ class NoteShort {
* @returns {Attribute[]} all note's attributes, including inherited ones
*/
getAttributes(type, name) {
const ownedAttributes = this.getOwnedAttributes();
if (!(this.noteId in noteAttributeCache)) {
const ownedAttributes = this.getOwnedAttributes();
const attrArrs = [
ownedAttributes
];
const attrArrs = [
ownedAttributes
];
for (const templateAttr of ownedAttributes.filter(oa => oa.type === 'relation' && oa.name === 'template')) {
const templateNote = this.treeCache.getNoteFromCache(templateAttr.value);
for (const templateAttr of ownedAttributes.filter(oa => oa.type === 'relation' && oa.name === 'template')) {
const templateNote = this.treeCache.notes[templateAttr.value];
if (templateNote) {
attrArrs.push(templateNote.getAttributes());
}
}
if (this.noteId !== 'root') {
for (const parentNote of this.getParentNotes()) {
// these virtual parent-child relationships are also loaded into frontend tree cache
if (parentNote.type !== 'search') {
attrArrs.push(parentNote.getInheritableAttributes());
if (templateNote) {
attrArrs.push(templateNote.getAttributes());
}
}
if (this.noteId !== 'root') {
for (const parentNote of this.getParentNotes()) {
// these virtual parent-child relationships are also loaded into frontend tree cache
if (parentNote.type !== 'search') {
attrArrs.push(parentNote.getInheritableAttributes());
}
}
}
noteAttributeCache.attributes[this.noteId] = attrArrs.flat();
}
const attributes = attrArrs.flat();
return this.__filterAttrs(attributes, type, name);
return this.__filterAttrs(noteAttributeCache.attributes[this.noteId], type, name);
}
__filterAttrs(attributes, type, name) {
if (type && name) {
if (!type && !name) {
return attributes;
} else if (type && name) {
return attributes.filter(attr => attr.type === type && attr.name === name);
} else if (type) {
return attributes.filter(attr => attr.type === type);
} else if (name) {
return attributes.filter(attr => attr.name === name);
} else {
return attributes;
}
}

View File

@@ -0,0 +1,46 @@
import FlexContainer from "../widgets/flex_container.js";
import GlobalMenuWidget from "../widgets/global_menu.js";
import TabRowWidget from "../widgets/tab_row.js";
import TitleBarButtonsWidget from "../widgets/title_bar_buttons.js";
import NoteTreeWidget from "../widgets/note_tree.js";
import TabCachingWidget from "../widgets/tab_caching_widget.js";
import NoteTitleWidget from "../widgets/note_title.js";
import RunScriptButtonsWidget from "../widgets/run_script_buttons.js";
import NoteTypeWidget from "../widgets/note_type.js";
import NoteActionsWidget from "../widgets/note_actions.js";
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
import NoteDetailWidget from "../widgets/note_detail.js";
export default class DesktopExtraWindowLayout {
constructor(customWidgets) {
this.customWidgets = customWidgets;
}
getRootWidget(appContext) {
appContext.mainTreeWidget = new NoteTreeWidget();
return new FlexContainer('column')
.setParent(appContext)
.id('root-widget')
.css('height', '100vh')
.child(new FlexContainer('row')
.child(new GlobalMenuWidget())
.child(new TabRowWidget())
.child(new TitleBarButtonsWidget()))
.child(new FlexContainer('row')
.collapsible()
.child(new FlexContainer('column').id('center-pane').css('flex-grow', '1')
.child(new FlexContainer('row').class('title-row')
.cssBlock('.title-row > * { margin: 5px; }')
.child(new NoteTitleWidget())
.child(new RunScriptButtonsWidget().hideInZenMode())
.child(new NoteTypeWidget().hideInZenMode())
.child(new NoteActionsWidget().hideInZenMode())
)
.child(new TabCachingWidget(() => new PromotedAttributesWidget()))
.child(new TabCachingWidget(() => new NoteDetailWidget()))
.child(...this.customWidgets.get('center-pane'))
)
);
}
}

View File

@@ -1,30 +1,29 @@
import FlexContainer from "./flex_container.js";
import GlobalMenuWidget from "./global_menu.js";
import TabRowWidget from "./tab_row.js";
import TitleBarButtonsWidget from "./title_bar_buttons.js";
import StandardTopWidget from "./standard_top_widget.js";
import SidePaneContainer from "./side_pane_container.js";
import GlobalButtonsWidget from "./global_buttons.js";
import SearchBoxWidget from "./search_box.js";
import SearchResultsWidget from "./search_results.js";
import NoteTreeWidget from "./note_tree.js";
import TabCachingWidget from "./tab_caching_widget.js";
import NotePathsWidget from "./note_paths.js";
import NoteTitleWidget from "./note_title.js";
import RunScriptButtonsWidget from "./run_script_buttons.js";
import ProtectedNoteSwitchWidget from "./protected_note_switch.js";
import NoteTypeWidget from "./note_type.js";
import NoteActionsWidget from "./note_actions.js";
import PromotedAttributesWidget from "./promoted_attributes.js";
import NoteDetailWidget from "./note_detail.js";
import NoteInfoWidget from "./note_info.js";
import CalendarWidget from "./calendar.js";
import AttributesWidget from "./attributes.js";
import LinkMapWidget from "./link_map.js";
import NoteRevisionsWidget from "./note_revisions.js";
import SimilarNotesWidget from "./similar_notes.js";
import WhatLinksHereWidget from "./what_links_here.js";
import SidePaneToggles from "./side_pane_toggles.js";
import FlexContainer from "../widgets/flex_container.js";
import GlobalMenuWidget from "../widgets/global_menu.js";
import TabRowWidget from "../widgets/tab_row.js";
import TitleBarButtonsWidget from "../widgets/title_bar_buttons.js";
import StandardTopWidget from "../widgets/standard_top_widget.js";
import SidePaneContainer from "../widgets/side_pane_container.js";
import GlobalButtonsWidget from "../widgets/global_buttons.js";
import SearchBoxWidget from "../widgets/search_box.js";
import SearchResultsWidget from "../widgets/search_results.js";
import NoteTreeWidget from "../widgets/note_tree.js";
import TabCachingWidget from "../widgets/tab_caching_widget.js";
import NotePathsWidget from "../widgets/note_paths.js";
import NoteTitleWidget from "../widgets/note_title.js";
import RunScriptButtonsWidget from "../widgets/run_script_buttons.js";
import NoteTypeWidget from "../widgets/note_type.js";
import NoteActionsWidget from "../widgets/note_actions.js";
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
import NoteDetailWidget from "../widgets/note_detail.js";
import NoteInfoWidget from "../widgets/collapsible_widgets/note_info.js";
import CalendarWidget from "../widgets/collapsible_widgets/calendar.js";
import AttributesWidget from "../widgets/collapsible_widgets/attributes.js";
import LinkMapWidget from "../widgets/collapsible_widgets/link_map.js";
import NoteRevisionsWidget from "../widgets/collapsible_widgets/note_revisions.js";
import SimilarNotesWidget from "../widgets/collapsible_widgets/similar_notes.js";
import WhatLinksHereWidget from "../widgets/collapsible_widgets/what_links_here.js";
import SidePaneToggles from "../widgets/side_pane_toggles.js";
import appContext from "../services/app_context.js";
const RIGHT_PANE_CSS = `
@@ -98,13 +97,13 @@ const RIGHT_PANE_CSS = `
}
</style>`;
export default class DesktopLayout {
export default class DesktopMainWindowLayout {
constructor(customWidgets) {
this.customWidgets = customWidgets;
}
getRootWidget(appContext) {
appContext.mainTreeWidget = new NoteTreeWidget();
appContext.mainTreeWidget = new NoteTreeWidget("main");
return new FlexContainer('column')
.setParent(appContext)
@@ -132,7 +131,6 @@ export default class DesktopLayout {
.cssBlock('.title-row > * { margin: 5px; }')
.child(new NoteTitleWidget())
.child(new RunScriptButtonsWidget().hideInZenMode())
.child(new ProtectedNoteSwitchWidget().hideInZenMode())
.child(new NoteTypeWidget().hideInZenMode())
.child(new NoteActionsWidget().hideInZenMode())
)

View File

@@ -1,11 +1,11 @@
import FlexContainer from "./flex_container.js";
import NoteTitleWidget from "./note_title.js";
import NoteDetailWidget from "./note_detail.js";
import NoteTreeWidget from "./note_tree.js";
import MobileGlobalButtonsWidget from "./mobile_global_buttons.js";
import CloseDetailButtonWidget from "./close_detail_button.js";
import MobileDetailMenuWidget from "./mobile_detail_menu.js";
import ScreenContainer from "./screen_container.js";
import FlexContainer from "../widgets/flex_container.js";
import NoteTitleWidget from "../widgets/note_title.js";
import NoteDetailWidget from "../widgets/note_detail.js";
import NoteTreeWidget from "../widgets/note_tree.js";
import MobileGlobalButtonsWidget from "../widgets/mobile_widgets/mobile_global_buttons.js";
import CloseDetailButtonWidget from "../widgets/mobile_widgets/close_detail_button.js";
import MobileDetailMenuWidget from "../widgets/mobile_widgets/mobile_detail_menu.js";
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
const MOBILE_CSS = `
<style>
@@ -73,7 +73,7 @@ export default class MobileLayout {
.child(new ScreenContainer("tree", 'column')
.class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-5 col-md-4 col-lg-4 col-xl-4")
.child(new MobileGlobalButtonsWidget())
.child(new NoteTreeWidget().cssBlock(FANCYTREE_CSS)))
.child(new NoteTreeWidget("main").cssBlock(FANCYTREE_CSS)))
.child(new ScreenContainer("detail", "column")
.class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-7 col-md-8 col-lg-8")
.child(new FlexContainer('row')

View File

@@ -1,5 +1,5 @@
import appContext from "./services/app_context.js";
import MobileLayout from "./widgets/mobile_layout.js";
import MobileLayout from "./layouts/mobile_layout.js";
import glob from "./services/glob.js";
glob.setupGlobs();

View File

@@ -9,10 +9,16 @@ import TabManager from "./tab_manager.js";
import treeService from "./tree.js";
import Component from "../widgets/component.js";
import keyboardActionsService from "./keyboard_actions.js";
import MobileScreenSwitcherExecutor from "../widgets/mobile_screen_switcher.js";
import MobileScreenSwitcherExecutor from "../widgets/mobile_widgets/mobile_screen_switcher.js";
import MainTreeExecutors from "./main_tree_executors.js";
class AppContext extends Component {
constructor(isMainWindow) {
super();
this.isMainWindow = isMainWindow;
}
setLayout(layout) {
this.layout = layout;
}
@@ -39,10 +45,12 @@ class AppContext extends Component {
$("body").append($renderedWidget);
$renderedWidget.on('click', "[data-trigger-command]", e => {
const commandName = $(e.target).attr('data-trigger-command');
$renderedWidget.on('click', "[data-trigger-command]", function() {
const commandName = $(this).attr('data-trigger-command');
const $component = $(this).closest(".component");
const component = $component.prop("component");
this.triggerCommand(commandName);
component.triggerCommand(commandName, {$el: $(this)});
});
this.tabManager = new TabManager();
@@ -86,6 +94,8 @@ class AppContext extends Component {
}
}
// this might hint at error but sometimes this is used by components which are at different places
// in the component tree to communicate with each other
console.debug(`Unhandled command ${name}, converting to event.`);
return this.triggerEvent(name, data);
@@ -94,15 +104,9 @@ class AppContext extends Component {
getComponentByEl(el) {
return $(el).closest(".component").prop('component');
}
async protectedSessionStartedEvent() {
await treeCache.loadInitialTree();
this.triggerEvent('treeCacheReloaded');
}
}
const appContext = new AppContext();
const appContext = new AppContext(window.glob.isMainWindow);
// we should save all outstanding changes before the page/app is closed
$(window).on('beforeunload', () => {

View File

@@ -7,6 +7,7 @@ import Component from "../widgets/component.js";
import toastService from "./toast.js";
import noteCreateService from "./note_create.js";
import ws from "./ws.js";
import bundleService from "./bundle.js";
export default class Entrypoints extends Component {
constructor() {
@@ -182,4 +183,40 @@ export default class Entrypoints extends Component {
}
createTopLevelNoteCommand() { noteCreateService.createNewTopLevelNote(); }
async openInWindowCommand({notePath}) {
if (utils.isElectron()) {
const {ipcRenderer} = utils.dynamicRequire('electron');
ipcRenderer.send('create-extra-window', {notePath});
}
else {
const url = window.location.protocol + '//' + window.location.host + window.location.pathname + '?extra=1#' + notePath;
window.open(url, '', 'width=1000,height=800');
}
}
async openNewWindowCommand() {
this.openInWindowCommand({notePath: ''});
}
async runActiveNoteCommand() {
const note = appContext.tabManager.getActiveTabNote();
// ctrl+enter is also used elsewhere so make sure we're running only when appropriate
if (!note || note.type !== 'code') {
return;
}
if (note.mime.endsWith("env=frontend")) {
await bundleService.getAndExecuteBundle(note.noteId);
}
if (note.mime.endsWith("env=backend")) {
await server.post('script/run/' + note.noteId);
}
toastService.showMessage("Note executed");
}
}

View File

@@ -93,8 +93,8 @@ function updateDisplayedShortcuts($container) {
}
});
$container.find('button[data-command],a.icon-action[data-command],.kb-in-title').each(async (i, el) => {
const actionName = $(el).attr('data-command');
$container.find('[data-trigger-command]').each(async (i, el) => {
const actionName = $(el).attr('data-trigger-command');
const action = await getAction(actionName, true);
if (action) {

View File

@@ -113,12 +113,16 @@ function newTabContextMenu(e) {
x: e.pageX,
y: e.pageY,
items: [
{title: "Open note in new tab", command: "openNoteInNewTab", uiIcon: "arrow-up-right"}
{title: "Open note in new tab", command: "openNoteInNewTab", uiIcon: "arrow-up-right"},
{title: "Open note in new window", command: "openNoteInNewWindow", uiIcon: "window-open"}
],
selectMenuItemHandler: ({command}) => {
if (command === 'openNoteInNewTab') {
appContext.tabManager.openTabWithNote(notePath);
}
else if (command === 'openNoteInNewWindow') {
appContext.openInNewWindow(notePath);
}
}
});
}
@@ -162,6 +166,7 @@ $(document).on('contextmenu', 'a.ck-link-actions__preview', newTabContextMenu);
$(document).on('contextmenu', '.note-detail-text a', newTabContextMenu);
$(document).on('contextmenu', "a[data-action='note']", newTabContextMenu);
$(document).on('contextmenu', ".note-detail-render a", newTabContextMenu);
$(document).on('contextmenu', ".note-paths-widget a", newTabContextMenu);
export default {
getNotePathFromUrl,

View File

@@ -101,6 +101,15 @@ export default class LoadResults {
this.options.includes(name);
}
/**
* @return {boolean} true if there are changes which could affect the attributes (including inherited ones)
*/
hasAttributeRelatedChanges() {
return Object.keys(this.noteIdToSourceId).length === 0
&& this.branches.length === 0
&& this.attributes.length === 0;
}
isEmpty() {
return Object.keys(this.noteIdToSourceId).length === 0
&& this.branches.length === 0

View File

@@ -38,12 +38,6 @@ export default class MainTreeExecutors extends Component {
isProtected: activeNote.isProtected,
saveSelection: false
});
await ws.waitForMaxKnownSyncId();
appContext.tabManager.getActiveTabContext().setNote(note.noteId);
appContext.triggerCommand('focusAndSelectTitle');
}
async createNoteAfterCommand() {
@@ -55,17 +49,11 @@ export default class MainTreeExecutors extends Component {
return;
}
const {note} = await noteCreateService.createNote(parentNoteId, {
await noteCreateService.createNote(parentNoteId, {
target: 'after',
targetBranchId: node.data.branchId,
isProtected: isProtected,
saveSelection: true
saveSelection: false
});
await ws.waitForMaxKnownSyncId();
appContext.tabManager.getActiveTabContext().setNote(note.noteId);
appContext.triggerCommand('focusAndSelectTitle');
}
}

View File

@@ -0,0 +1,20 @@
/**
* Purpose of this class is to cache list of attributes for notes.
*
* Cache invalidation granularity is global - whenever a write operation is detected to notes, branches or attributes
* we invalidate the whole cache. That's OK, since the purpose for this is to speed up batch read-only operations, such
* as loading the tree which uses attributes heavily.
*/
class NoteAttributeCache {
constructor() {
this.attributes = {};
}
invalidate() {
this.attributes = {};
}
}
const noteAttributeCache = new NoteAttributeCache();
export default noteAttributeCache;

View File

@@ -48,8 +48,12 @@ async function createNote(parentNoteId, options = {}) {
}
if (options.activate) {
await ws.waitForMaxKnownSyncId();
const activeTabContext = appContext.tabManager.getActiveTabContext();
activeTabContext.setNote(note.noteId);
await activeTabContext.setNote(note.noteId);
appContext.triggerCommand('focusAndSelectTitle');
}
return {note, branch};

View File

@@ -1,13 +1,10 @@
import treeService from './tree.js';
import utils from './utils.js';
import server from './server.js';
import protectedSessionHolder from './protected_session_holder.js';
import toastService from "./toast.js";
import ws from "./ws.js";
import appContext from "./app_context.js";
const $enterProtectedSessionButton = $("#enter-protected-session-button");
const $leaveProtectedSessionButton = $("#leave-protected-session-button");
import treeCache from "./tree_cache.js";
let protectedSessionDeferred = null;
@@ -34,6 +31,15 @@ function enterProtectedSession() {
return dfd.promise();
}
async function reloadData() {
const allNoteIds = Object.keys(treeCache.notes);
await treeCache.loadInitialTree();
// make sure that all notes used in the application are loaded, including the ones not shown in the tree
await treeCache.reloadNotes(allNoteIds, true);
}
async function setupProtectedSession(password) {
const response = await enterProtectedSessionOnServer(password);
@@ -45,6 +51,10 @@ async function setupProtectedSession(password) {
protectedSessionHolder.setProtectedSessionId(response.protectedSessionId);
protectedSessionHolder.touchProtectedSession();
await reloadData();
await appContext.triggerEvent('treeCacheReloaded');
appContext.triggerEvent('protectedSessionStarted');
if (protectedSessionDeferred !== null) {
@@ -54,9 +64,6 @@ async function setupProtectedSession(password) {
protectedSessionDeferred = null;
}
$enterProtectedSessionButton.hide();
$leaveProtectedSessionButton.show();
toastService.showMessage("Protected session has been started.");
}

View File

@@ -39,9 +39,16 @@ function touchProtectedSession() {
}
}
function touchProtectedSessionIfNecessary(note) {
if (note && note.isProtected && isProtectedSessionAvailable()) {
touchProtectedSession();
}
}
export default {
setProtectedSessionId,
resetProtectedSession,
isProtectedSessionAvailable,
touchProtectedSession
touchProtectedSession,
touchProtectedSessionIfNecessary
};

View File

@@ -8,6 +8,7 @@ function getHeaders(headers) {
// also avoiding using underscores instead of dashes since nginx filters them out by default
const allHeaders = {
'trilium-source-id': glob.sourceId,
'trilium-local-now-datetime': utils.localNowDateTime(),
'x-csrf-token': glob.csrfToken
};

View File

@@ -58,6 +58,7 @@ class TabContext extends Component {
this.autoBookDisabled = false;
this.textPreviewDisabled = false;
this.codePreviewDisabled = false;
setTimeout(async () => {
// we include the note into recent list only if the user stayed on the note at least 5 seconds
@@ -69,10 +70,7 @@ class TabContext extends Component {
}
}, 5000);
if (this.note.isProtected && protectedSessionHolder.isProtectedSessionAvailable()) {
// FIXME: there are probably more places where this should be done
protectedSessionHolder.touchProtectedSession();
}
protectedSessionHolder.touchProtectedSessionIfNecessary(this.note);
if (triggerSwitchEvent) {
this.triggerEvent('tabNoteSwitched', {

View File

@@ -6,6 +6,7 @@ import treeCache from "./tree_cache.js";
import treeService from "./tree.js";
import utils from "./utils.js";
import TabContext from "./tab_context.js";
import appContext from "./app_context.js";
export default class TabManager extends Component {
constructor() {
@@ -14,6 +15,10 @@ export default class TabManager extends Component {
this.activeTabId = null;
this.tabsUpdate = new SpacedUpdate(async () => {
if (!appContext.isMainWindow) {
return;
}
const openTabs = this.tabContexts
.map(tc => tc.getTabState())
.filter(t => !!t);
@@ -30,7 +35,9 @@ export default class TabManager extends Component {
}
async loadTabs() {
const openTabs = options.getJson('openTabs') || [];
const tabsToOpen = appContext.isMainWindow
? (options.getJson('openTabs') || [])
: [];
// if there's notePath in the URL, make sure it's open and active
// (useful, among others, for opening clipped notes from clipper)
@@ -39,17 +46,17 @@ export default class TabManager extends Component {
const noteId = treeService.getNoteIdFromNotePath(notePath);
if (noteId && await treeCache.noteExists(noteId)) {
for (const tab of openTabs) {
for (const tab of tabsToOpen) {
tab.active = false;
}
const foundTab = openTabs.find(tab => noteId === treeService.getNoteIdFromNotePath(tab.notePath));
const foundTab = tabsToOpen.find(tab => noteId === treeService.getNoteIdFromNotePath(tab.notePath));
if (foundTab) {
foundTab.active = true;
}
else {
openTabs.push({
tabsToOpen.push({
notePath: notePath,
active: true
});
@@ -59,7 +66,7 @@ export default class TabManager extends Component {
let filteredTabs = [];
for (const openTab of openTabs) {
for (const openTab of tabsToOpen) {
const noteId = treeService.getNoteIdFromNotePath(openTab.notePath);
if (await treeCache.noteExists(noteId)) {
@@ -75,7 +82,7 @@ export default class TabManager extends Component {
if (filteredTabs.length === 0) {
filteredTabs.push({
notePath: 'root',
notePath: this.isMainWindow ? 'root' : '',
active: true
});
}
@@ -189,7 +196,9 @@ export default class TabManager extends Component {
async openTabWithNote(notePath, activate, tabId = null) {
const tabContext = await this.openEmptyTab(tabId);
await tabContext.setNote(notePath, !activate); // if activate is false then send normal noteSwitched event
if (notePath) {
await tabContext.setNote(notePath, !activate); // if activate is false then send normal noteSwitched event
}
if (activate) {
this.activateTab(tabContext.tabId, false);
@@ -258,6 +267,9 @@ export default class TabManager extends Component {
this.children = this.children.filter(tc => tc.tabId !== tabId);
// remove dangling autocompletes after closing the tab
$(".algolia-autocomplete").remove();
this.triggerEvent('tabRemoved', {tabId});
this.tabsUpdate.scheduleUpdate();
@@ -315,6 +327,14 @@ export default class TabManager extends Component {
}
}
moveTabToNewWindowCommand({tabId}) {
const notePath = this.getTabContextById(tabId).notePath;
this.removeTab(tabId);
this.triggerCommand('openInWindow', {notePath});
}
async hoistedNoteChangedEvent({hoistedNoteId}) {
if (hoistedNoteId === 'root') {
return;

View File

@@ -1,174 +0,0 @@
import utils from "./utils.js";
import treeCache from "./tree_cache.js";
import ws from "./ws.js";
import hoistedNoteService from "./hoisted_note.js";
async function prepareRootNode() {
await treeCache.initializedPromise;
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
let hoistedBranch;
if (hoistedNoteId === 'root') {
hoistedBranch = treeCache.getBranch('root');
}
else {
const hoistedNote = await treeCache.getNote(hoistedNoteId);
hoistedBranch = (await hoistedNote.getBranches())[0];
}
return await prepareNode(hoistedBranch);
}
async function prepareBranch(note) {
if (note.type === 'search') {
return await prepareSearchBranch(note);
}
else {
return await prepareRealBranch(note);
}
}
const NOTE_TYPE_ICONS = {
"file": "bx bx-file",
"image": "bx bx-image",
"code": "bx bx-code",
"render": "bx bx-extension",
"search": "bx bx-file-find",
"relation-map": "bx bx-map-alt",
"book": "bx bx-book"
};
function getIconClass(note) {
const labels = note.getLabels('iconClass');
return labels.map(l => l.value).join(' ');
}
function getIcon(note) {
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
const iconClass = getIconClass(note);
if (iconClass) {
return iconClass;
}
else if (note.noteId === 'root') {
return "bx bx-chevrons-right";
}
else if (note.noteId === hoistedNoteId) {
return "bx bxs-arrow-from-bottom";
}
else if (note.type === 'text') {
if (note.hasChildren()) {
return "bx bx-folder";
}
else {
return "bx bx-note";
}
}
else {
return NOTE_TYPE_ICONS[note.type];
}
}
async function prepareNode(branch) {
const note = await branch.getNote();
if (!note) {
throw new Error(`Branch has no note ` + branch.noteId);
}
const title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title;
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
const node = {
noteId: note.noteId,
parentNoteId: branch.parentNoteId,
branchId: branch.branchId,
isProtected: note.isProtected,
noteType: note.type,
title: utils.escapeHtml(title),
extraClasses: getExtraClasses(note),
icon: getIcon(note),
refKey: note.noteId,
expanded: branch.isExpanded || hoistedNoteId === note.noteId,
lazy: true,
key: utils.randomString(12) // this should prevent some "duplicate key" errors
};
if (note.hasChildren() || note.type === 'search') {
node.folder = true;
}
return node;
}
async function prepareRealBranch(parentNote) {
utils.assertArguments(parentNote);
const childBranches = await parentNote.getChildBranches();
if (!childBranches) {
ws.logError(`No children for ${parentNote}. This shouldn't happen.`);
return;
}
const noteList = [];
for (const branch of childBranches) {
const node = await prepareNode(branch);
noteList.push(node);
}
return noteList;
}
async function prepareSearchBranch(note) {
await treeCache.reloadNotes([note.noteId]);
const newNote = await treeCache.getNote(note.noteId);
return await prepareRealBranch(newNote);
}
function getExtraClasses(note) {
utils.assertArguments(note);
const extraClasses = [];
if (note.isProtected) {
extraClasses.push("protected");
}
if (note.getParentNoteIds().length > 1) {
extraClasses.push("multiple-parents");
}
const cssClass = note.getCssClass();
if (cssClass) {
extraClasses.push(cssClass);
}
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));
}
if (note.hasLabel('archived')) {
extraClasses.push("archived");
}
return extraClasses.join(" ");
}
export default {
prepareRootNode,
prepareBranch,
getExtraClasses,
getIcon
}

View File

@@ -20,6 +20,8 @@ class TreeCache {
async loadInitialTree() {
const resp = await server.get('tree');
await this.loadParents(resp, false);
// clear the cache only directly before adding new content which is important for e.g. switching to protected session
/** @type {Object.<string, NoteShort>} */
@@ -34,22 +36,22 @@ class TreeCache {
/** @type {Object.<string, Promise<NoteComplement>>} */
this.noteComplementPromises = {};
await this.loadParents(resp);
this.addResp(resp);
}
async loadParents(resp) {
async loadParents(resp, additiveLoad) {
const noteIds = new Set(resp.notes.map(note => note.noteId));
const missingNoteIds = [];
const existingNotes = additiveLoad ? this.notes : {};
for (const branch of resp.branches) {
if (!(branch.parentNoteId in this.notes) && !noteIds.has(branch.parentNoteId) && branch.parentNoteId !== 'none') {
if (!(branch.parentNoteId in existingNotes) && !noteIds.has(branch.parentNoteId) && branch.parentNoteId !== 'none') {
missingNoteIds.push(branch.parentNoteId);
}
}
for (const attr of resp.attributes) {
if (attr.type === 'relation' && attr.name === 'template' && !(attr.value in this.notes) && !noteIds.has(attr.value)) {
if (attr.type === 'relation' && attr.name === 'template' && !(attr.value in existingNotes) && !noteIds.has(attr.value)) {
missingNoteIds.push(attr.value);
}
}
@@ -61,7 +63,7 @@ class TreeCache {
resp.branches = resp.branches.concat(newResp.branches);
resp.attributes = resp.attributes.concat(newResp.attributes);
await this.loadParents(resp);
await this.loadParents(resp, additiveLoad);
}
}
@@ -154,7 +156,7 @@ class TreeCache {
const resp = await server.post('tree/load', { noteIds });
await this.loadParents(resp);
await this.loadParents(resp, true);
this.addResp(resp);
for (const note of resp.notes) {
@@ -231,7 +233,7 @@ class TreeCache {
/** @return {Promise<NoteShort>} */
async getNote(noteId, silentNotFoundError = false) {
if (noteId === 'none') {
console.log(`No 'none' note.`);
console.trace(`No 'none' note.`);
return null;
}
else if (!noteId) {
@@ -246,10 +248,10 @@ class TreeCache {
return this.notes[noteId];
}
getBranches(branchIds) {
getBranches(branchIds, silentNotFoundError = false) {
return branchIds
.map(branchId => this.getBranch(branchId))
.filter(b => b !== null);
.map(branchId => this.getBranch(branchId, silentNotFoundError))
.filter(b => !!b);
}
/** @return {Branch} */

View File

@@ -40,9 +40,9 @@ class TreeContextMenu {
async getMenuItems() {
const note = await treeCache.getNote(this.node.data.noteId);
const branch = treeCache.getBranch(this.node.data.branchId);
const parentNote = await treeCache.getNote(branch.parentNoteId);
const isNotRoot = note.noteId !== 'root';
const isHoisted = note.noteId === hoistedNoteService.getHoistedNoteId();
const parentNote = isNotRoot ? await treeCache.getNote(branch.parentNoteId) : null;
// some actions don't support multi-note so they are disabled when notes are selected
// the only exception is when the only selected note is the one that was right-clicked, then
@@ -56,7 +56,8 @@ class TreeContextMenu {
const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNotSearch;
return [
{ title: 'Open in new tab', command: "openInTab", uiIcon: "empty", enabled: noSelectedNotes },
{ title: 'Open in a new tab <kbd>Ctrl+Click</kbd>', command: "openInTab", uiIcon: "empty", enabled: noSelectedNotes },
{ title: 'Open in a new window', command: "openInWindow", uiIcon: "window-open", enabled: noSelectedNotes },
{ title: 'Insert note after <kbd data-command="createNoteAfter"></kbd>', command: "insertNoteAfter", uiIcon: "plus",
items: insertNoteAfterEnabled ? this.getNoteTypeItems("insertNoteAfter") : null,
enabled: insertNoteAfterEnabled && noSelectedNotes },
@@ -73,7 +74,8 @@ class TreeContextMenu {
{ title: 'Edit branch prefix <kbd data-command="editBranchPrefix"></kbd>', command: "editBranchPrefix", uiIcon: "empty",
enabled: isNotRoot && parentNotSearch && noSelectedNotes},
{ title: "Advanced", uiIcon: "empty", enabled: true, items: [
{ title: 'Collapse subtree <kbd data-command="collapseSubtree"></kbd>', command: "collapseSubtree", uiIcon: "align-justify", enabled: noSelectedNotes },
{ title: 'Expand subtree <kbd data-command="expandSubtree"></kbd>', command: "expandSubtree", uiIcon: "expand", enabled: noSelectedNotes },
{ title: 'Collapse subtree <kbd data-command="collapseSubtree"></kbd>', command: "collapseSubtree", uiIcon: "collapse", enabled: noSelectedNotes },
{ title: "Force note sync", command: "forceNoteSync", uiIcon: "refresh", enabled: noSelectedNotes },
{ title: 'Sort alphabetically <kbd data-command="sortChildNotes"></kbd>', command: "sortChildNotes", uiIcon: "empty", enabled: noSelectedNotes && notSearch },
{ title: 'Recent changes in subtree', command: "recentChangesInSubtree", uiIcon: "history", enabled: noSelectedNotes }
@@ -129,7 +131,7 @@ class TreeContextMenu {
});
}
else {
this.treeWidget.triggerCommand(command, {node: this.node});
this.treeWidget.triggerCommand(command, {node: this.node, notePath: notePath});
}
}
}

View File

@@ -40,6 +40,10 @@ function formatDateTime(date) {
return formatDate(date) + " " + formatTime(date);
}
function localNowDateTime() {
return dayjs().format('YYYY-MM-DD HH:mm:ss.SSSZZ')
}
function now() {
return formatTimeWithSeconds(new Date());
}
@@ -99,7 +103,7 @@ function download(url) {
url += '?' + Date.now(); // don't use cache
if (isElectron()) {
const remote = utils.dynamicRequire('electron').remote;
const remote = dynamicRequire('electron').remote;
remote.getCurrentWebContents().downloadURL(url);
}
@@ -239,7 +243,7 @@ function focusSavedElement() {
$lastFocusedElement = null;
}
function openDialog($dialog) {
async function openDialog($dialog) {
closeActiveDialog();
glob.activeDialog = $dialog;
@@ -253,6 +257,9 @@ function openDialog($dialog) {
focusSavedElement();
}
});
const keyboardActionsService = (await import("./keyboard_actions.js")).default;
keyboardActionsService.updateDisplayedShortcuts($dialog);
}
function isHtmlEmpty(html) {
@@ -270,7 +277,7 @@ function isHtmlEmpty(html) {
async function clearBrowserCache() {
if (isElectron()) {
const win = utils.dynamicRequire('electron').remote.getCurrentWindow();
const win = dynamicRequire('electron').remote.getCurrentWindow();
await win.webContents.session.clearCache();
}
}
@@ -318,6 +325,7 @@ export default {
formatDate,
formatDateISO,
formatDateTime,
localNowDateTime,
now,
isElectron,
isMac,

View File

@@ -6,6 +6,7 @@ import Branch from "../entities/branch.js";
import Attribute from "../entities/attribute.js";
import options from "./options.js";
import treeCache from "./tree_cache.js";
import noteAttributeCache from "./note_attribute_cache.js";
const $outstandingSyncsCount = $("#outstanding-syncs-count");
@@ -359,6 +360,10 @@ async function processSyncRows(syncRows) {
});
if (!loadResults.isEmpty()) {
if (loadResults.hasAttributeRelatedChanges()) {
noteAttributeCache.invalidate();
}
const appContext = (await import("./app_context.js")).default;
await appContext.triggerEvent('entitiesReloaded', {loadResults});
}

View File

@@ -57,7 +57,10 @@ class BasicWidget extends Component {
for (const key in this.attrs) {
if (key === 'style') {
if (this.attrs[key]) {
$widget.attr(key, $widget.attr('style') + ';' + this.attrs[key]);
let style = $widget.attr('style');
style = style ? `${style}; ${this.attrs[key]}` : this.attrs[key];
$widget.attr(key, style);
}
}
else {

View File

@@ -34,7 +34,8 @@ export default class CollapsibleWidget extends TabAwareWidget {
this.$bodyWrapper = this.$widget.find('.body-wrapper');
this.$bodyWrapper.attr('id', this.componentId); // for toggle to work we need id
this.widgetName = this.constructor.name;
// not using constructor name because of webpack mangling class names ...
this.widgetName = this.widgetTitle.replace(/[^[a-zA-Z0-9]/g, "_");
if (!options.is(this.widgetName + 'Collapsed')) {
this.$bodyWrapper.collapse("show");

View File

@@ -1,7 +1,7 @@
import utils from "../services/utils.js";
import linkService from "../services/link.js";
import ws from "../services/ws.js";
import CollapsibleWidget from "./collapsible_widget.js";
import utils from "../../services/utils.js";
import linkService from "../../services/link.js";
import ws from "../../services/ws.js";
import CollapsibleWidget from "../collapsible_widget.js";
export default class AttributesWidget extends CollapsibleWidget {
get widgetTitle() { return "Attributes"; }
@@ -16,7 +16,7 @@ export default class AttributesWidget extends CollapsibleWidget {
get headerActions() {
const $showFullButton = $("<a>").append("show dialog").addClass('widget-header-action');
$showFullButton.on('click', async () => {
const attributesDialog = await import("../dialogs/attributes.js");
const attributesDialog = await import("../../dialogs/attributes.js");
attributesDialog.showDialog();
});

View File

@@ -1,9 +1,9 @@
import CollapsibleWidget from "./collapsible_widget.js";
import libraryLoader from "../services/library_loader.js";
import utils from "../services/utils.js";
import dateNoteService from "../services/date_notes.js";
import server from "../services/server.js";
import appContext from "../services/app_context.js";
import CollapsibleWidget from "../collapsible_widget.js";
import libraryLoader from "../../services/library_loader.js";
import utils from "../../services/utils.js";
import dateNoteService from "../../services/date_notes.js";
import server from "../../services/server.js";
import appContext from "../../services/app_context.js";
const TPL = `
<div class="calendar-widget">

View File

@@ -1,7 +1,7 @@
import CollapsibleWidget from "./collapsible_widget.js";
import linkService from "../services/link.js";
import server from "../services/server.js";
import treeCache from "../services/tree_cache.js";
import CollapsibleWidget from "../collapsible_widget.js";
import linkService from "../../services/link.js";
import server from "../../services/server.js";
import treeCache from "../../services/tree_cache.js";
export default class EditedNotesWidget extends CollapsibleWidget {
get widgetTitle() { return "Edited notes on this day"; }

View File

@@ -1,4 +1,4 @@
import CollapsibleWidget from "./collapsible_widget.js";
import CollapsibleWidget from "../collapsible_widget.js";
let linkMapContainerIdCtr = 1;
@@ -21,7 +21,7 @@ export default class LinkMapWidget extends CollapsibleWidget {
get headerActions() {
const $showFullButton = $("<a>").append("show full").addClass('widget-header-action');
$showFullButton.on('click', async () => {
const linkMapDialog = await import("../dialogs/link_map.js");
const linkMapDialog = await import("../../dialogs/link_map.js");
linkMapDialog.showDialog();
});
@@ -66,7 +66,7 @@ export default class LinkMapWidget extends CollapsibleWidget {
const $linkMapContainer = this.$body.find('.link-map-container');
$linkMapContainer.attr("id", "link-map-container-" + linkMapContainerIdCtr++);
const LinkMapServiceClass = (await import('../services/link_map.js')).default;
const LinkMapServiceClass = (await import('../../services/link_map.js')).default;
this.linkMapService = new LinkMapServiceClass(note, $linkMapContainer, {
maxDepth: 1,

View File

@@ -1,4 +1,4 @@
import CollapsibleWidget from "./collapsible_widget.js";
import CollapsibleWidget from "../collapsible_widget.js";
const TPL = `
<table class="note-info-widget-table">
@@ -21,15 +21,15 @@ const TPL = `
</style>
<tr>
<th nowrap>Note ID:</th>
<th>Note ID:</th>
<td nowrap colspan="3" class="note-info-note-id"></td>
</tr>
<tr>
<th nowrap>Created:</th>
<th>Created:</th>
<td nowrap colspan="3" style="overflow: hidden; text-overflow: ellipsis;" class="note-info-date-created"></td>
</tr>
<tr>
<th nowrap>Modified:</th>
<th>Modified:</th>
<td nowrap colspan="3" style="overflow: hidden; text-overflow: ellipsis;" class="note-info-date-modified"></td>
</tr>
<tr>
@@ -79,4 +79,4 @@ export default class NoteInfoWidget extends CollapsibleWidget {
this.refresh();
}
}
}
}

View File

@@ -1,5 +1,5 @@
import server from "../services/server.js";
import CollapsibleWidget from "./collapsible_widget.js";
import server from "../../services/server.js";
import CollapsibleWidget from "../collapsible_widget.js";
const TPL = `
<ul class="note-revision-list" style="max-height: 150px; overflow: auto;">
@@ -19,7 +19,7 @@ class NoteRevisionsWidget extends CollapsibleWidget {
get headerActions() {
const $showFullButton = $("<a>").append("show dialog").addClass('widget-header-action');
$showFullButton.on('click', async () => {
const attributesDialog = await import("../dialogs/note_revisions.js");
const attributesDialog = await import("../../dialogs/note_revisions.js");
attributesDialog.showCurrentNoteRevisions(this.noteId);
});
@@ -59,6 +59,7 @@ class NoteRevisionsWidget extends CollapsibleWidget {
'data-action': 'note-revision',
'data-note-path': note.noteId,
'data-note-revision-id': item.noteRevisionId,
title: 'This revision was last edited on ' + item.dateLastEdited,
href: 'javascript:'
}).text(item.dateLastEdited.substr(0, 16)));

View File

@@ -1,7 +1,7 @@
import CollapsibleWidget from "./collapsible_widget.js";
import linkService from "../services/link.js";
import server from "../services/server.js";
import treeCache from "../services/tree_cache.js";
import CollapsibleWidget from "../collapsible_widget.js";
import linkService from "../../services/link.js";
import server from "../../services/server.js";
import treeCache from "../../services/tree_cache.js";
export default class SimilarNotesWidget extends CollapsibleWidget {
get widgetTitle() { return "Similar notes"; }

View File

@@ -1,5 +1,5 @@
import CollapsibleWidget from "./collapsible_widget.js";
import linkService from "../services/link.js";
import CollapsibleWidget from "../collapsible_widget.js";
import linkService from "../../services/link.js";
export default class WhatLinksHereWidget extends CollapsibleWidget {
get widgetTitle() { return "What links here"; }
@@ -13,7 +13,7 @@ export default class WhatLinksHereWidget extends CollapsibleWidget {
get headerActions() {
const $showFullButton = $("<a>").append("show link map").addClass('widget-header-action');
$showFullButton.on('click', async () => {
const linkMapDialog = await import("../dialogs/link_map.js");
const linkMapDialog = await import("../../dialogs/link_map.js");
linkMapDialog.showDialog();
});

View File

@@ -15,7 +15,7 @@ import Mutex from "../services/mutex.js";
*/
export default class Component {
constructor() {
this.componentId = `comp-${this.constructor.name}-` + utils.randomString(6);
this.componentId = `comp-` + utils.randomString(8);
/** @type Component[] */
this.children = [];
this.initialized = Promise.resolve();

View File

@@ -18,17 +18,14 @@ const WIDGET_TPL = `
<a data-trigger-command="collapseTree"
title="Collapse note tree"
data-command="collapseTree"
class="icon-action bx bx-layer-minus"></a>
<a data-trigger-command="scrollToActiveNote"
title="Scroll to active note"
data-command="scrollToActiveNote"
title="Scroll to active note"
class="icon-action bx bx-crosshair"></a>
<a data-trigger-command="searchNotes"
title="Search in notes"
data-command="searchNotes"
class="icon-action bx bx-search"></a>
</div>
`;

View File

@@ -1,5 +1,4 @@
import BasicWidget from "./basic_widget.js";
import keyboardActionService from "../services/keyboard_actions.js";
import utils from "../services/utils.js";
import syncService from "../services/sync.js";
@@ -14,7 +13,7 @@ const TPL = `
.global-menu button {
margin-right: 10px;
height: 33px;
border-bottom: none;
border: none;
}
.global-menu .dropdown-menu {
@@ -39,6 +38,12 @@ const TPL = `
Sync (<span id="outstanding-syncs-count">0</span>)
</a>
<a class="dropdown-item" data-trigger-command="openNewWindow">
<span class="bx bx-window-open"></span>
Open new window
<kbd data-command="openNewWindow"></kbd>
</a>
<a class="dropdown-item open-dev-tools-button" data-trigger-command="openDevTools">
<span class="bx bx-terminal"></span>
Open Dev Tools

View File

@@ -1,4 +1,4 @@
import BasicWidget from "./basic_widget.js";
import BasicWidget from "../basic_widget.js";
const TPL = `
<button type="button" class="action-button d-sm-none d-md-none d-lg-none d-xl-none" aria-label="Close">

View File

@@ -1,8 +1,8 @@
import BasicWidget from "./basic_widget.js";
import appContext from "../services/app_context.js";
import contextMenu from "../services/context_menu.js";
import noteCreateService from "../services/note_create.js";
import branchService from "../services/branches.js";
import BasicWidget from "../basic_widget.js";
import appContext from "../../services/app_context.js";
import contextMenu from "../../services/context_menu.js";
import noteCreateService from "../../services/note_create.js";
import branchService from "../../services/branches.js";
const TPL = `<button type="button" class="action-button bx bx-menu"></button>`;

View File

@@ -1,4 +1,4 @@
import BasicWidget from "./basic_widget.js";
import BasicWidget from "../basic_widget.js";
const WIDGET_TPL = `
<div id="global-buttons">

View File

@@ -1,4 +1,4 @@
import Component from "./component.js";
import Component from "../component.js";
export default class MobileScreenSwitcherExecutor extends Component {
setActiveScreenCommand({screen}) {

View File

@@ -1,4 +1,4 @@
import FlexContainer from "./flex_container.js";
import FlexContainer from "../flex_container.js";
export default class ScreenContainer extends FlexContainer {
constructor(screenName, direction) {

View File

@@ -1,4 +1,5 @@
import TabAwareWidget from "./tab_aware_widget.js";
import protectedSessionService from "../services/protected_session.js";
const TPL = `
<div class="dropdown note-actions">
@@ -12,6 +13,49 @@ const TPL = `
background-color: transparent !important;
pointer-events: none; /* makes it unclickable */
}
/* The switch - the box around the slider */
.switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
float: right;
}
/* The slider */
.slider {
border-radius: 24px;
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--more-accented-background-color);
transition: .4s;
}
.slider:before {
border-radius: 50%;
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: var(--main-background-color);
-webkit-transition: .4s;
transition: .4s;
}
.slider.checked {
background-color: var(--main-text-color);
}
.slider.checked:before {
transform: translateX(26px);
}
</style>
<button type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle">
@@ -19,6 +63,22 @@ const TPL = `
<span class="caret"></span>
</button>
<div class="dropdown-menu dropdown-menu-right">
<div class="dropdown-item protect-button">
Protect the note
<span title="Note is not protected, click to make it protected">
<label class="switch">
<span class="slider"></span>
</span>
</div>
<div class="dropdown-item unprotect-button">
Unprotect the note
<span title="Note is protected, click to make it unprotected">
<label class="switch">
<span class="slider checked"></span>
</span>
</div>
<a data-trigger-command="showNoteRevisions" class="dropdown-item show-note-revisions-button">Revisions</a>
<a data-trigger-command="showAttributes" class="dropdown-item show-attributes-button"><kbd data-command="showAttributes"></kbd> Attributes</a>
<a data-trigger-command="showLinkMap" class="dropdown-item show-link-map-button"><kbd data-command="showLinkMap"></kbd> Link map</a>
@@ -48,6 +108,12 @@ export default class NoteActionsWidget extends TabAwareWidget {
this.$importNoteButton = this.$widget.find('.import-files-button');
this.$importNoteButton.on("click", () => import('../dialogs/import.js').then(d => d.showDialog(this.noteId)));
this.$protectButton = this.$widget.find(".protect-button");
this.$protectButton.on('click', () => protectedSessionService.protectNote(this.noteId, true, false));
this.$unprotectButton = this.$widget.find(".unprotect-button");
this.$unprotectButton.on('click', () => protectedSessionService.protectNote(this.noteId, false, false));
return this.$widget;
}
@@ -64,5 +130,14 @@ export default class NoteActionsWidget extends TabAwareWidget {
else {
this.$exportNoteButton.attr('disabled', 'disabled');
}
this.$protectButton.toggle(!note.isProtected);
this.$unprotectButton.toggle(!!note.isProtected);
}
entitiesReloadedEvent({loadResults}) {
if (loadResults.isNoteReloaded(this.noteId)) {
this.refresh();
}
}
}

View File

@@ -6,7 +6,7 @@ import server from "../services/server.js";
import libraryLoader from "../services/library_loader.js";
import EmptyTypeWidget from "./type_widgets/empty.js";
import EditableTextTypeWidget from "./type_widgets/editable_text.js";
import CodeTypeWidget from "./type_widgets/code.js";
import EditableCodeTypeWidget from "./type_widgets/editable_code.js";
import FileTypeWidget from "./type_widgets/file.js";
import ImageTypeWidget from "./type_widgets/image.js";
import SearchTypeWidget from "./type_widgets/search.js";
@@ -19,6 +19,7 @@ import keyboardActionsService from "../services/keyboard_actions.js";
import noteCreateService from "../services/note_create.js";
import DeletedTypeWidget from "./type_widgets/deleted.js";
import ReadOnlyTextTypeWidget from "./type_widgets/read_only_text.js";
import ReadOnlyCodeTypeWidget from "./type_widgets/read_only_code.js";
const TPL = `
<div class="note-detail">
@@ -38,7 +39,8 @@ const typeWidgetClasses = {
'deleted': DeletedTypeWidget,
'editable-text': EditableTextTypeWidget,
'read-only-text': ReadOnlyTextTypeWidget,
'code': CodeTypeWidget,
'editable-code': EditableCodeTypeWidget,
'read-only-code': ReadOnlyCodeTypeWidget,
'file': FileTypeWidget,
'image': ImageTypeWidget,
'search': SearchTypeWidget,
@@ -61,6 +63,8 @@ export default class NoteDetailWidget extends TabAwareWidget {
const dto = note.dto;
dto.content = this.getTypeWidget().getContent();
protectedSessionHolder.touchProtectedSessionIfNecessary(note);
await server.put('notes/' + noteId, dto, this.componentId);
});
}
@@ -182,15 +186,32 @@ export default class NoteDetailWidget extends TabAwareWidget {
const noteComplement = await this.tabContext.getNoteComplement();
if (note.hasLabel('readOnly') ||
(noteComplement.content && noteComplement.content.length > 10000)) {
(noteComplement.content
&& noteComplement.content.length > 10000)
&& !note.hasLabel('autoReadOnlyDisabled')) {
type = 'read-only-text';
}
}
if (type === 'code' && !this.tabContext.codePreviewDisabled) {
const noteComplement = await this.tabContext.getNoteComplement();
if (note.hasLabel('readOnly') ||
(noteComplement.content
&& noteComplement.content.length > 30000)
&& !note.hasLabel('autoReadOnlyDisabled')) {
type = 'read-only-code';
}
}
if (type === 'text') {
type = 'editable-text';
}
if (type === 'code') {
type = 'editable-code';
}
if (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) {
type = 'protected-session';
}
@@ -274,6 +295,12 @@ export default class NoteDetailWidget extends TabAwareWidget {
}
}
codePreviewDisabledEvent({tabContext}) {
if (this.isTab(tabContext.tabId)) {
this.refresh();
}
}
async cutIntoNoteCommand() {
const note = appContext.tabManager.getActiveTabNote();

View File

@@ -84,6 +84,7 @@ export default class NotePathsWidget extends TabAwareWidget {
this.$currentPath.append(
$("<a>")
.attr('href', '#' + curPath)
.attr('data-note-path', curPath)
.addClass('no-tooltip-preview')
.text(await treeService.getNoteTitle(noteId, parentNoteId))
);

View File

@@ -17,6 +17,10 @@ const TPL = `
min-width: 5em;
width: 100%;
}
.note-title-container input.note-title.protected {
text-shadow: 4px 4px 4px var(--muted-text-color);
}
</style>
<input autocomplete="off" value="" class="note-title" tabindex="1">
@@ -29,6 +33,8 @@ export default class NoteTitleWidget extends TabAwareWidget {
this.spacedUpdate = new SpacedUpdate(async () => {
const title = this.$noteTitle.val();
protectedSessionHolder.touchProtectedSessionIfNecessary(this.note);
await server.put(`notes/${this.noteId}/change-title`, {title});
});
}
@@ -50,6 +56,12 @@ export default class NoteTitleWidget extends TabAwareWidget {
this.$noteTitle.val(note.title);
this.$noteTitle.prop("readonly", note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable());
this.setProtectedStatus(note);
}
setProtectedStatus(note) {
this.$noteTitle.toggleClass("protected", !!note.isProtected);
}
async beforeNoteSwitchEvent({tabContext}) {
@@ -78,6 +90,13 @@ export default class NoteTitleWidget extends TabAwareWidget {
}
}
entitiesReloadedEvent({loadResults}) {
if (loadResults.isNoteReloaded(this.noteId)) {
// not updating the title specifically since the synced title might be older than what the user is currently typing
this.setProtectedStatus(this.note);
}
}
beforeUnloadEvent() {
this.spacedUpdate.updateNowIfNecessary();
}

View File

@@ -3,7 +3,6 @@ import treeService from "../services/tree.js";
import utils from "../services/utils.js";
import contextMenu from "../services/context_menu.js";
import treeCache from "../services/tree_cache.js";
import treeBuilder from "../services/tree_builder.js";
import branchService from "../services/branches.js";
import ws from "../services/ws.js";
import TabAwareWidget from "./tab_aware_widget.js";
@@ -15,17 +14,24 @@ import keyboardActionsService from "../services/keyboard_actions.js";
import clipboard from "../services/clipboard.js";
import protectedSessionService from "../services/protected_session.js";
import syncService from "../services/sync.js";
import options from "../services/options.js";
const TPL = `
<div class="tree">
<div class="tree-wrapper">
<style>
.tree {
overflow: auto;
.tree-wrapper {
flex-grow: 1;
flex-shrink: 1;
flex-basis: 60%;
font-family: var(--tree-font-family);
font-size: var(--tree-font-size);
position: relative;
min-height: 0;
}
.tree {
height: 100%;
overflow: auto;
}
.refresh-search-button {
@@ -40,19 +46,81 @@ const TPL = `
.refresh-search-button:hover {
border-color: var(--button-border-color);
}
.tree-settings-button {
position: absolute;
top: 10px;
right: 20px;
z-index: 100;
}
.tree-settings-popup {
display: none;
position: absolute;
background-color: var(--accented-background-color);
border: 1px solid var(--main-border-color);
padding: 20px;
z-index: 1000;
width: 320px;
border-radius: 10px 0 10px 10px;
}
</style>
<button class="btn btn-sm icon-button bx bx-cog tree-settings-button" title="Tree settings"></button>
<div class="tree-settings-popup">
<div class="form-check">
<label class="form-check-label">
<input class="form-check-input hide-archived-notes" type="checkbox" value="">
Hide archived notes
</label>
</div>
<div class="form-check">
<label class="form-check-label">
<input class="form-check-input hide-included-images" type="checkbox" value="">
Hide images included in a note
<span class="bx bx-info-circle"
title="Images which are shown in the parent text note will not be displayed in the tree"></span>
</label>
</div>
<br/>
<button class="btn btn-sm btn-primary save-tree-settings-button" type="submit">Save & apply changes</button>
</div>
<div class="tree"></div>
</div>
`;
const NOTE_TYPE_ICONS = {
"file": "bx bx-file",
"image": "bx bx-image",
"code": "bx bx-code",
"render": "bx bx-extension",
"search": "bx bx-file-find",
"relation-map": "bx bx-map-alt",
"book": "bx bx-book"
};
export default class NoteTreeWidget extends TabAwareWidget {
constructor(treeName) {
super();
this.treeName = treeName;
}
doRender() {
this.$widget = $(TPL);
this.$tree = this.$widget.find('.tree');
this.$widget.on("click", ".unhoist-button", hoistedNoteService.unhoist);
this.$widget.on("click", ".refresh-search-button", () => this.refreshSearch());
this.$tree.on("click", ".unhoist-button", hoistedNoteService.unhoist);
this.$tree.on("click", ".refresh-search-button", () => this.refreshSearch());
// fancytree doesn't support middle click so this is a way to support it
this.$widget.on('mousedown', '.fancytree-title', e => {
this.$tree.on('mousedown', '.fancytree-title', e => {
if (e.which === 2) {
const node = $.ui.fancytree.getNode(e);
@@ -67,20 +135,82 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
});
this.$treeSettingsPopup = this.$widget.find('.tree-settings-popup');
this.$hideArchivedNotesCheckbox = this.$treeSettingsPopup.find('.hide-archived-notes');
this.$hideIncludedImages = this.$treeSettingsPopup.find('.hide-included-images');
this.$treeSettingsButton = this.$widget.find('.tree-settings-button');
this.$treeSettingsButton.on("click", e => {
if (this.$treeSettingsPopup.is(":visible")) {
this.$treeSettingsPopup.hide();
return;
}
this.$hideArchivedNotesCheckbox.prop("checked", this.hideArchivedNotes);
this.$hideIncludedImages.prop("checked", this.hideIncludedImages);
let top = this.$treeSettingsButton[0].offsetTop;
let left = this.$treeSettingsButton[0].offsetLeft;
top += this.$treeSettingsButton.outerHeight();
left += this.$treeSettingsButton.outerWidth() - this.$treeSettingsPopup.outerWidth();
if (left < 0) {
left = 0;
}
this.$treeSettingsPopup.css({
display: "block",
top: top,
left: left
}).addClass("show");
return false;
});
this.$treeSettingsPopup.on("click", e => { e.stopPropagation(); });
$(document).on('click', () => this.$treeSettingsPopup.hide());
this.$saveTreeSettingsButton = this.$treeSettingsPopup.find('.save-tree-settings-button');
this.$saveTreeSettingsButton.on('click', async () => {
await this.setHideArchivedNotes(this.$hideArchivedNotesCheckbox.prop("checked"));
await this.setHideIncludedImages(this.$hideIncludedImages.prop("checked"));
this.$treeSettingsPopup.hide();
this.reloadTreeFromCache();
});
this.initialized = this.initFancyTree();
return this.$widget;
}
async initFancyTree() {
const treeData = [await treeBuilder.prepareRootNode()];
get hideArchivedNotes() {
return options.is("hideArchivedNotes_" + this.treeName);
}
this.$widget.fancytree({
async setHideArchivedNotes(val) {
await options.save("hideArchivedNotes_" + this.treeName, val.toString());
}
get hideIncludedImages() {
return options.is("hideIncludedImages_" + this.treeName);
}
async setHideIncludedImages(val) {
await options.save("hideIncludedImages_" + this.treeName, val.toString());
}
async initFancyTree() {
const treeData = [await this.prepareRootNode()];
this.$tree.fancytree({
autoScroll: true,
keyboard: false, // we takover keyboard handling in the hotkeys plugin
extensions: utils.isMobile() ? ["dnd5", "clones"] : ["hotkeys", "dnd5", "clones"],
source: treeData,
scrollParent: this.$widget,
scrollParent: this.$tree,
minExpandLevel: 2, // root can't be collapsed
click: (event, data) => {
const targetType = data.targetType;
@@ -177,7 +307,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
// This function MUST be defined to enable dropping of items on the tree.
// data.hitMode is 'before', 'after', or 'over'.
const selectedBranchIds = this.getSelectedNodes().map(node => node.data.branchId);
const selectedBranchIds = this.getSelectedOrActiveNodes().map(node => node.data.branchId);
if (data.hitMode === "before") {
branchService.moveBeforeBranch(selectedBranchIds, node.data.branchId);
@@ -191,10 +321,20 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
}
},
lazyLoad: function(event, data) {
const noteId = data.node.data.noteId;
lazyLoad: (event, data) => {
const {noteId, noteType} = data.node.data;
data.result = treeCache.getNote(noteId).then(note => treeBuilder.prepareBranch(note));
if (noteType === 'search') {
const notePath = treeService.getNotePath(data.node.getParent());
// this is a search cycle (search note is a descendant of its own search result)
if (notePath.includes(noteId)) {
data.result = [];
return;
}
}
data.result = treeCache.getNote(noteId).then(note => this.prepareChildren(note));
},
clones: {
highlightActiveClones: true
@@ -220,7 +360,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
$span.append(refreshSearchButton);
}
},
// this is done to automatically lazy load all expanded search notes after tree load
// this is done to automatically lazy load all expanded notes after tree load
loadChildren: (event, data) => {
data.node.visit((subNode) => {
// Load all lazy/unloaded child nodes
@@ -232,7 +372,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
});
this.$widget.on('contextmenu', '.fancytree-node', e => {
this.$tree.on('contextmenu', '.fancytree-node', e => {
const node = $.ui.fancytree.getNode(e);
import("../services/tree_context_menu.js").then(({default: TreeContextMenu}) => {
@@ -243,7 +383,185 @@ export default class NoteTreeWidget extends TabAwareWidget {
return false; // blocks default browser right click menu
});
this.tree = $.ui.fancytree.getTree(this.$widget);
this.tree = $.ui.fancytree.getTree(this.$tree);
}
async prepareRootNode() {
await treeCache.initializedPromise;
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
let hoistedBranch;
if (hoistedNoteId === 'root') {
hoistedBranch = treeCache.getBranch('root');
}
else {
const hoistedNote = await treeCache.getNote(hoistedNoteId);
hoistedBranch = (await hoistedNote.getBranches())[0];
}
return await this.prepareNode(hoistedBranch);
}
async prepareChildren(note) {
if (note.type === 'search') {
return await this.prepareSearchNoteChildren(note);
}
else {
return await this.prepareNormalNoteChildren(note);
}
}
getIconClass(note) {
const labels = note.getLabels('iconClass');
return labels.map(l => l.value).join(' ');
}
getIcon(note, isFolder) {
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
const iconClass = this.getIconClass(note);
if (iconClass) {
return iconClass;
}
else if (note.noteId === 'root') {
return "bx bx-chevrons-right";
}
else if (note.noteId === hoistedNoteId) {
return "bx bxs-arrow-from-bottom";
}
else if (note.type === 'text') {
if (isFolder) {
return "bx bx-folder";
}
else {
return "bx bx-note";
}
}
else {
return NOTE_TYPE_ICONS[note.type];
}
}
async prepareNode(branch) {
const note = await branch.getNote();
if (!note) {
throw new Error(`Branch has no note ` + branch.noteId);
}
const title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title;
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
const isFolder = this.isFolder(note);
const node = {
noteId: note.noteId,
parentNoteId: branch.parentNoteId,
branchId: branch.branchId,
isProtected: note.isProtected,
noteType: note.type,
title: utils.escapeHtml(title),
extraClasses: this.getExtraClasses(note),
icon: this.getIcon(note, isFolder),
refKey: note.noteId,
lazy: true,
folder: isFolder,
expanded: branch.isExpanded || hoistedNoteId === note.noteId,
key: utils.randomString(12) // this should prevent some "duplicate key" errors
};
if (node.folder && node.expanded) {
node.children = await this.prepareChildren(note);
}
return node;
}
isFolder(note) {
if (note.type === 'search') {
return true;
}
else {
const childBranches = this.getChildBranches(note);
return childBranches.length > 0;
}
}
async prepareNormalNoteChildren(parentNote) {
utils.assertArguments(parentNote);
const noteList = [];
for (const branch of this.getChildBranches(parentNote)) {
const node = await this.prepareNode(branch);
noteList.push(node);
}
return noteList;
}
getChildBranches(parentNote) {
let childBranches = parentNote.getChildBranches();
if (!childBranches) {
ws.logError(`No children for ${parentNote}. This shouldn't happen.`);
return;
}
if (this.hideIncludedImages) {
const imageLinks = parentNote.getRelations('imageLink');
// image is already visible in the parent note so no need to display it separately in the book
childBranches = childBranches.filter(branch => !imageLinks.find(rel => rel.value === branch.noteId));
}
return childBranches;
}
async prepareSearchNoteChildren(note) {
await treeCache.reloadNotes([note.noteId]);
const newNote = await treeCache.getNote(note.noteId);
return await this.prepareNormalNoteChildren(newNote);
}
getExtraClasses(note) {
utils.assertArguments(note);
const extraClasses = [];
if (note.isProtected) {
extraClasses.push("protected");
}
if (note.getParentNoteIds().length > 1) {
extraClasses.push("multiple-parents");
}
const cssClass = note.getCssClass();
if (cssClass) {
extraClasses.push(cssClass);
}
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));
}
if (note.hasLabel('archived')) {
extraClasses.push("archived");
}
return extraClasses.join(" ");
}
/** @return {FancytreeNode[]} */
@@ -262,16 +580,32 @@ export default class NoteTreeWidget extends TabAwareWidget {
return notes;
}
collapseTree(node = null) {
async setExpandedStatusForSubtree(node, isExpanded) {
if (!node) {
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
node = this.getNodesByNoteId(hoistedNoteId)[0];
}
node.setExpanded(false);
const {branchIds} = await server.put(`branches/${node.data.branchId}/expanded-subtree/${isExpanded ? 1 : 0}`);
node.visit(node => node.setExpanded(false));
treeCache.getBranches(branchIds, true).forEach(branch => branch.isExpanded = isExpanded);
await this.batchUpdate(async () => {
await node.load(true);
if (node.data.noteId !== 'root') { // root is always expanded
await node.setExpanded(isExpanded, {noEvents: true});
}
});
}
async expandTree(node = null) {
await this.setExpandedStatusForSubtree(node, true);
}
async collapseTree(node = null) {
await this.setExpandedStatusForSubtree(node, false);
}
/**
@@ -387,11 +721,13 @@ export default class NoteTreeWidget extends TabAwareWidget {
const note = treeCache.getNoteFromCache(node.data.noteId);
const branch = treeCache.getBranch(node.data.branchId);
const isFolder = this.isFolder(note);
node.data.isProtected = note.isProtected;
node.data.noteType = note.type;
node.folder = note.type === 'search' || note.getChildNoteIds().length > 0;
node.icon = treeBuilder.getIcon(note);
node.extraClasses = treeBuilder.getExtraClasses(note);
node.folder = isFolder;
node.icon = this.getIcon(note, isFolder);
node.extraClasses = this.getExtraClasses(note);
node.title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title;
node.renderTitle();
}
@@ -413,12 +749,6 @@ export default class NoteTreeWidget extends TabAwareWidget {
return list ? list : []; // if no nodes with this refKey are found, fancy tree returns null
}
async reload() {
const rootNode = await treeBuilder.prepareRootNode();
await this.tree.reload([rootNode]);
}
// must be event since it's triggered from outside the tree
collapseTreeEvent() { this.collapseTree(); }
@@ -460,6 +790,18 @@ export default class NoteTreeWidget extends TabAwareWidget {
toastService.showMessage("Saved search note refreshed.");
}
async batchUpdate(cb) {
try {
// disable rendering during update for increased performance
this.tree.enableUpdate(false);
await cb();
}
finally {
this.tree.enableUpdate(true);
}
}
async entitiesReloadedEvent({loadResults}) {
const activeNode = this.getActiveNode();
const activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null;
@@ -481,6 +823,15 @@ export default class NoteTreeWidget extends TabAwareWidget {
// missing handling of things inherited from template
noteIdsToReload.add(attr.noteId);
}
else if (attr.type === 'relation' && attr.name === 'imageLink') {
const note = treeCache.getNoteFromCache(attr.noteId);
if (note && note.getChildNoteIds().includes(attr.value)) {
// there's new/deleted imageLink betwen note and its image child - which can show/hide
// the image (if there is a imageLink relation between parent and child then it is assumed to be "contained" in the note and thus does not have to be displayed in the tree)
noteIdsToReload.add(attr.noteId);
}
}
}
for (const branch of loadResults.getBranches()) {
@@ -496,7 +847,9 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
}
node.remove();
if (node.getParent()) {
node.remove();
}
noteIdsToUpdate.add(branch.parentNoteId);
}
@@ -524,37 +877,40 @@ export default class NoteTreeWidget extends TabAwareWidget {
noteIdsToUpdate.add(noteId);
}
for (const noteId of noteIdsToReload) {
for (const node of this.getNodesByNoteId(noteId)) {
await node.load(true);
await this.batchUpdate(async () => {
for (const noteId of noteIdsToReload) {
for (const node of this.getNodesByNoteId(noteId)) {
await node.load(true);
this.updateNode(node);
noteIdsToUpdate.add(noteId);
}
}
}
for (const parentNoteId of loadResults.getNoteReorderings()) {
for (const node of this.getNodesByNoteId(parentNoteId)) {
if (node.isLoaded()) {
node.sortChildren((nodeA, nodeB) => {
const branchA = treeCache.branches[nodeA.data.branchId];
const branchB = treeCache.branches[nodeB.data.branchId];
if (!branchA || !branchB) {
return 0;
}
return branchA.notePosition - branchB.notePosition;
});
}
}
}
});
// for some reason node update cannot be in the batchUpdate() block (node is not re-rendered)
for (const noteId of noteIdsToUpdate) {
for (const node of this.getNodesByNoteId(noteId)) {
this.updateNode(node);
}
}
for (const parentNoteId of loadResults.getNoteReorderings()) {
for (const node of this.getNodesByNoteId(parentNoteId)) {
if (node.isLoaded()) {
node.sortChildren((nodeA, nodeB) => {
const branchA = treeCache.branches[nodeA.data.branchId];
const branchB = treeCache.branches[nodeB.data.branchId];
if (!branchA || !branchB) {
return 0;
}
return branchA.notePosition - branchB.notePosition;
});
}
}
}
if (activeNotePath) {
let node = await this.expandToNote(activeNotePath);
@@ -586,7 +942,11 @@ export default class NoteTreeWidget extends TabAwareWidget {
const activeNotePath = activeNode !== null ? treeService.getNotePath(activeNode) : null;
await this.reload();
const rootNode = await this.prepareRootNode();
await this.batchUpdate(async () => {
await this.tree.reload([rootNode]);
});
if (activeNotePath) {
const node = await this.getNodeFromPath(activeNotePath, true);
@@ -740,6 +1100,10 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
}
expandSubtreeCommand({node}) {
this.expandTree(node);
}
collapseSubtreeCommand({node}) {
this.collapseTree(node);
}

View File

@@ -1,42 +0,0 @@
import protectedSessionService from "../services/protected_session.js";
import TabAwareWidget from "./tab_aware_widget.js";
const TPL = `
<div class="btn-group btn-group-xs">
<button type="button"
class="btn btn-sm icon-button bx bx-check-shield protect-button"
title="Protected note can be viewed and edited only after entering password">
</button>
<button type="button"
class="btn btn-sm icon-button bx bx-shield-x unprotect-button"
title="Not protected note can be viewed without entering password">
</button>
</div>`;``;
export default class ProtectedNoteSwitchWidget extends TabAwareWidget {
doRender() {
this.$widget = $(TPL);
this.$protectButton = this.$widget.find(".protect-button");
this.$protectButton.on('click', () => protectedSessionService.protectNote(this.noteId, true, false));
this.$unprotectButton = this.$widget.find(".unprotect-button");
this.$unprotectButton.on('click', () => protectedSessionService.protectNote(this.noteId, false, false));
return this.$widget;
}
refreshWithNote(note) {
this.$protectButton.toggleClass("active", note.isProtected);
this.$protectButton.prop("disabled", note.isProtected);
this.$unprotectButton.toggleClass("active", !note.isProtected);
this.$unprotectButton.prop("disabled", !note.isProtected);
}
async entitiesReloadedEvent({loadResults}) {
if (loadResults.isNoteReloaded(this.noteId)) {
this.refresh();
}
}
}

View File

@@ -3,10 +3,12 @@ import TabAwareWidget from "./tab_aware_widget.js";
const TPL = `
<div style="display: inline-flex;">
<button class="btn btn-sm icon-button bx bx-play-circle render-button"
data-trigger-command="renderActiveNote"
title="Render"></button>
<button class="btn btn-sm icon-button bx bx-play-circle execute-script-button"
title="Execute (Ctrl+Enter)"></button>
data-trigger-command="runActiveNote"
title="Execute"></button>
</div>`;
export default class RunScriptButtonsWidget extends TabAwareWidget {
@@ -21,6 +23,12 @@ export default class RunScriptButtonsWidget extends TabAwareWidget {
refreshWithNote(note) {
this.$renderButton.toggle(note.type === 'render');
this.$executeScriptButton.toggle(note.mime.startsWith('application/javascript'));
this.$executeScriptButton.toggle(note.type === 'code' && note.mime.startsWith('application/javascript'));
}
async entitiesReloadedEvent({loadResults}) {
if (loadResults.isNoteReloaded(this.noteId)) {
this.refresh();
}
}
}

View File

@@ -57,29 +57,39 @@ export default class TabCachingWidget extends TabAwareWidget {
}
}
/**
* widget.hasBeenAlreadyShown is intended for lazy loading of cached tabs - initial note switches of new tabs
* are not executed, we're waiting for the first tab activation and then we update the tab. After this initial
* activation further note switches are always propagated to the tabs.
*/
handleEventInChildren(name, data) {
// stop propagation of the event to the children, individual tab widget should not know about tab switching
// since they are per-tab
if (name === 'tabNoteSwitchedAndActivated') {
name = 'tabNoteSwitched';
}
if (name === 'tabNoteSwitched') {
if (['tabNoteSwitched', 'tabNoteSwitchedAndActivated'].includes(name)) {
// this event is propagated only to the widgets of a particular tab
const widget = this.widgets[data.tabContext.tabId];
if (widget) {
return widget.handleEvent(name, data);
if (widget && (widget.hasBeenAlreadyShown || name === 'tabNoteSwitchedAndActivated')) {
widget.hasBeenAlreadyShown = true;
return widget.handleEvent('tabNoteSwitched', data);
}
else {
return Promise.resolve();
}
}
if (name !== 'activeTabChanged') {
if (name === 'activeTabChanged') {
const widget = this.widgets[data.tabContext.tabId];
if (widget.hasBeenAlreadyShown) {
return Promise.resolve();
}
else {
widget.hasBeenAlreadyShown = true;
return widget.handleEvent(name, data);
}
} else {
return super.handleEventInChildren(name, data);
}
return Promise.resolve();
}
}

View File

@@ -29,11 +29,11 @@ const TAB_TPL = `
<div class="note-tab-wrapper">
<div class="note-tab-title"></div>
<div class="note-tab-drag-handle"></div>
<div class="note-tab-close kb-in-title" title="Close tab" data-command="closeActiveTab"><span>×</span></div>
<div class="note-tab-close" title="Close tab" data-trigger-command="closeActiveTab"><span>×</span></div>
</div>
</div>`;
const NEW_TAB_BUTTON_TPL = `<div class="note-new-tab kb-in-title" data-command="openNewTab" title="Add new tab">+</div>`;
const NEW_TAB_BUTTON_TPL = `<div class="note-new-tab" data-trigger-command="openNewTab" title="Add new tab">+</div>`;
const FILLER_TPL = `<div class="tab-row-filler">
<div class="tab-row-border"></div>
</div>`;
@@ -258,8 +258,9 @@ export default class TabRowWidget extends BasicWidget {
x: e.pageX,
y: e.pageY,
items: [
{title: "Close all tabs", command: "removeAllTabs", uiIcon: "empty"},
{title: "Close all tabs except for this", command: "removeAllTabsExceptForThis", uiIcon: "empty"}
{title: "Move this tab to a new window", command: "moveTabToNewWindow", uiIcon: "window-open"},
{title: "Close all tabs", command: "removeAllTabs", uiIcon: "x"},
{title: "Close all tabs except for this", command: "removeAllTabsExceptForThis", uiIcon: "x"},
],
selectMenuItemHandler: ({command}) => {
this.triggerCommand(command, {tabId});
@@ -393,10 +394,13 @@ export default class TabRowWidget extends BasicWidget {
this.setupDraggabilly();
}
setTabCloseEvent($tab) {
$tab.find('.note-tab-close')
.on('click', _ => appContext.tabManager.removeTab($tab.attr('data-tab-id')));
closeActiveTabCommand({$el}) {
const tabId = $el.closest(".note-tab").attr('data-tab-id');
appContext.tabManager.removeTab(tabId);
}
setTabCloseEvent($tab) {
$tab.on('mousedown', e => {
if (e.which === 2) {
appContext.tabManager.removeTab($tab.attr('data-tab-id'));
@@ -557,8 +561,6 @@ export default class TabRowWidget extends BasicWidget {
this.$newTab = $(NEW_TAB_BUTTON_TPL);
this.$tabContainer.append(this.$newTab);
this.$newTab.on('click', _ => this.triggerCommand('openNewTab'));
}
setupFiller() {

View File

@@ -7,7 +7,15 @@ const TPL = `
<style>
.title-bar-buttons {
margin-top: 4px;
min-width: 100px;
flex-shrink: 0;
}
.title-bar-buttons button {
border: none !important;
background: none !important;
font-size: 150%;
padding-left: 10px;
padding-right: 10px;
}
</style>

View File

@@ -1,9 +1,6 @@
import libraryLoader from "../../services/library_loader.js";
import bundleService from "../../services/bundle.js";
import toastService from "../../services/toast.js";
import server from "../../services/server.js";
import keyboardActionService from "../../services/keyboard_actions.js";
import TypeWidget from "./type_widget.js";
import keyboardActionService from "../../services/keyboard_actions.js";
const TPL = `
<div class="note-detail-code note-detail-printable">
@@ -21,17 +18,14 @@ const TPL = `
<div class="note-detail-code-editor"></div>
</div>`;
export default class CodeTypeWidget extends TypeWidget {
static getType() { return "code"; }
export default class EditableCodeTypeWidget extends TypeWidget {
static getType() { return "editable-code"; }
doRender() {
this.$widget = $(TPL);
this.$editor = this.$widget.find('.note-detail-code-editor');
this.$executeScriptButton = this.$widget.find(".execute-script-button");
keyboardActionService.setElementActionHandler(this.$widget, 'runActiveNote', () => this.executeCurrentNote());
this.$executeScriptButton.on('click', () => this.executeCurrentNote());
keyboardActionService.setupActionsForElement('code-detail', this.$widget, this);
this.initialized = this.initEditor();
@@ -106,26 +100,6 @@ export default class CodeTypeWidget extends TypeWidget {
this.codeEditor.focus();
}
async executeCurrentNote() {
// ctrl+enter is also used elsewhere so make sure we're running only when appropriate
if (this.note.type !== 'code') {
return;
}
// make sure note is saved so we load latest changes
await this.spacedUpdate.updateNowIfNecessary();
if (this.note.mime.endsWith("env=frontend")) {
await bundleService.getAndExecuteBundle(this.noteId);
}
if (this.note.mime.endsWith("env=backend")) {
await server.post('script/run/' + this.noteId);
}
toastService.showMessage("Note executed");
}
cleanup() {
if (this.codeEditor) {
this.spacedUpdate.allowUpdateWithoutChange(() => {

View File

@@ -1,13 +1,9 @@
import libraryLoader from "../../services/library_loader.js";
import noteAutocompleteService from '../../services/note_autocomplete.js';
import mimeTypesService from '../../services/mime_types.js';
import TypeWidget from "./type_widget.js";
import utils from "../../services/utils.js";
import appContext from "../../services/app_context.js";
import keyboardActionService from "../../services/keyboard_actions.js";
import treeCache from "../../services/tree_cache.js";
import linkService from "../../services/link.js";
import noteContentRenderer from "../../services/note_content_renderer.js";
import AbstractTextTypeWidget from "./abstract_text_type_widget.js";
const ENABLE_INSPECTOR = false;
@@ -60,6 +56,7 @@ const TPL = `
overflow: auto;
height: 100%;
font-family: var(--detail-text-font-family);
padding-left: 12px;
}
.note-detail-text-editor {

View File

@@ -0,0 +1,44 @@
import TypeWidget from "./type_widget.js";
const TPL = `
<div class="note-detail-read-only-code note-detail-printable">
<style>
.note-detail-read-only-code {
overflow: auto;
height: 100%;
}
.note-detail-read-only-code-content {
padding: 10px;
}
</style>
<div class="alert alert-warning no-print" style="margin-bottom: 0;">
Read only code view is shown. <a href="#" class="edit-note">Click here</a> to edit the note.
</div>
<pre class="note-detail-read-only-code-content"></pre>
</div>`;
export default class ReadOnlyCodeTypeWidget extends TypeWidget {
static getType() { return "read-only-code"; }
doRender() {
this.$widget = $(TPL);
this.$content = this.$widget.find('.note-detail-read-only-code-content');
this.$widget.find('a.edit-note').on('click', () => {
this.tabContext.codePreviewDisabled = true;
this.triggerEvent('codePreviewDisabled', {tabContext: this.tabContext});
});
return this.$widget;
}
async doRefresh(note) {
const noteComplement = await this.tabContext.getNoteComplement();
this.$content.text(noteComplement.content);
}
}

View File

@@ -25,9 +25,6 @@ export default class RenderTypeWidget extends TypeWidget {
this.$widget = $(TPL);
this.$noteDetailRenderHelp = this.$widget.find('.note-detail-render-help');
this.$noteDetailRenderContent = this.$widget.find('.note-detail-render-content');
this.$renderButton = this.$widget.find('.render-button');
this.$renderButton.on('click', () => this.refresh());
return this.$widget;
}
@@ -46,4 +43,10 @@ export default class RenderTypeWidget extends TypeWidget {
cleanup() {
this.$noteDetailRenderContent.empty();
}
renderActiveNoteEvent() {
if (this.tabContext.isActive()) {
this.refresh();
}
}
}

View File

@@ -48,4 +48,10 @@ export default class TypeWidget extends TabAwareWidget {
this.refresh();
}
}
codePreviewDisabledEvent({tabContext}) {
if (this.isTab(tabContext.tabId)) {
this.refresh();
}
}
}

View File

@@ -101,7 +101,7 @@ async function updateNoteAttributes(req) {
|| (attribute.type === 'relation' && attribute.value !== attributeEntity.value)) {
if (attribute.type !== 'relation' || !!attribute.value.trim()) {
const newAttribute = attribute.createClone(attribute.type, attribute.name, attribute.value);
const newAttribute = attributeEntity.createClone(attribute.type, attribute.name, attribute.value);
await newAttribute.save();
}
@@ -139,6 +139,7 @@ async function updateNoteAttributes(req) {
}
const note = await repository.getNote(noteId);
note.invalidateAttributeCache();
return await note.getAttributes();
}

View File

@@ -114,8 +114,34 @@ async function moveBranchAfterNote(req) {
async function setExpanded(req) {
const {branchId, expanded} = req.params;
await sql.execute("UPDATE branches SET isExpanded = ? WHERE branchId = ?", [expanded, branchId]);
// we don't sync expanded label
if (branchId !== 'root') {
await sql.execute("UPDATE branches SET isExpanded = ? WHERE branchId = ?", [expanded, branchId]);
// we don't sync expanded label
}
}
async function setExpandedForSubtree(req) {
const {branchId, expanded} = req.params;
let branchIds = await sql.getColumn(`
WITH RECURSIVE
tree(branchId, noteId) AS (
SELECT branchId, noteId FROM branches WHERE branchId = ?
UNION
SELECT branches.branchId, branches.noteId FROM branches
JOIN tree ON branches.parentNoteId = tree.noteId
WHERE branches.isDeleted = 0
)
SELECT branchId FROM tree`, [branchId]);
// root is always expanded
branchIds = branchIds.filter(branchId => branchId !== 'root');
await sql.executeMany(`UPDATE branches SET isExpanded = ${expanded} WHERE branchId IN (???)`, branchIds);
return {
branchIds
};
}
async function deleteBranch(req) {
@@ -149,6 +175,7 @@ module.exports = {
moveBranchBeforeNote,
moveBranchAfterNote,
setExpanded,
setExpandedForSubtree,
deleteBranch,
setPrefix
};

View File

@@ -27,7 +27,7 @@ async function loginSync(req) {
// 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' }];
return [400, { message: 'Auth request time is out of sync, please check that both client and server have correct time.' }];
}
const syncVersion = req.body.syncVersion;
@@ -42,7 +42,7 @@ async function loginSync(req) {
const givenHash = req.body.hash;
if (expectedHash !== givenHash) {
return [400, { message: "Sync login credentials are incorrect." }];
return [400, { message: "Sync login credentials are incorrect. It looks like you're trying to sync two different initialized documents which is not possible." }];
}
req.session.loggedIn = true;

View File

@@ -18,13 +18,11 @@ async function getNoteRevision(req) {
if (noteRevision.type === 'file') {
if (noteRevision.isStringNote()) {
await noteRevision.getContent();
noteRevision.content = noteRevision.content.substr(0, 10000);
noteRevision.content = (await noteRevision.getContent()).substr(0, 10000);
}
}
else {
await noteRevision.getContent();
noteRevision.content = await noteRevision.getContent();
if (noteRevision.content && noteRevision.type === 'image') {
noteRevision.content = noteRevision.content.toString('base64');

View File

@@ -110,7 +110,9 @@ async function getUserThemes() {
function isAllowed(name) {
return ALLOWED_OPTIONS.has(name)
|| name.startsWith("keyboardShortcuts")
|| name.endsWith("Collapsed");
|| name.endsWith("Collapsed")
|| name.startsWith("hideArchivedNotes")
|| name.startsWith("hideIncludedImages");
}
module.exports = {

View File

@@ -13,17 +13,17 @@ async function getRecentChanges(req) {
SELECT * FROM (
SELECT note_revisions.noteId,
note_revisions.noteRevisionId,
note_revisions.utcDateCreated AS date
note_revisions.dateLastEdited AS date
FROM note_revisions
ORDER BY note_revisions.utcDateCreated DESC
ORDER BY note_revisions.dateLastEdited DESC
)
UNION ALL SELECT * FROM (
SELECT
notes.noteId,
NULL AS noteRevisionId,
utcDateModified AS date
dateModified AS date
FROM notes
ORDER BY utcDateModified DESC
ORDER BY dateModified DESC
)
ORDER BY date DESC`);
@@ -44,7 +44,7 @@ async function getRecentChanges(req) {
notes.title AS current_title,
notes.isProtected AS current_isProtected,
note_revisions.title,
note_revisions.utcDateCreated AS date
note_revisions.dateCreated AS date
FROM
note_revisions
JOIN notes USING(noteId)
@@ -60,7 +60,7 @@ async function getRecentChanges(req) {
notes.title AS current_title,
notes.isProtected AS current_isProtected,
notes.title,
notes.utcDateModified AS date
notes.dateModified AS date
FROM
notes
WHERE noteId = ?`, [noteRow.noteId]));

View File

@@ -11,7 +11,7 @@ const env = require('../services/env');
async function index(req, res) {
const options = await optionService.getOptionsMap();
const view = req.cookies['trilium-device'] === 'mobile' ? 'mobile' : 'desktop';
let view = req.cookies['trilium-device'] === 'mobile' ? 'mobile' : 'desktop';
const csrfToken = req.csrfToken();
log.info(`Generated CSRF token ${csrfToken} with secret ${res.getHeader('set-cookie')}`);
@@ -26,7 +26,8 @@ async function index(req, res) {
maxSyncIdAtLoad: await sql.getValue("SELECT MAX(id) FROM sync"),
instanceName: config.General ? config.General.instanceName : null,
appCssNoteIds: await getAppCssNoteIds(),
isDev: env.isDev()
isDev: env.isDev(),
isMainWindow: !req.query.extra
});
}

View File

@@ -1,3 +1,5 @@
"use strict";
const setupRoute = require('./setup');
const loginRoute = require('./login');
const indexRoute = require('./index');
@@ -82,6 +84,7 @@ function route(method, path, middleware, routeHandler, resultHandler, transactio
try {
const result = await cls.init(async () => {
cls.namespace.set('sourceId', req.headers['trilium-source-id']);
cls.namespace.set('localNowDateTime', req.headers['`trilium-local-now-datetime`']);
protectedSessionService.setProtectedSessionId(req);
if (transactional) {
@@ -124,6 +127,7 @@ function register(app) {
apiRoute(PUT, '/api/branches/:branchId/move-before/:beforeBranchId', branchesApiRoute.moveBranchBeforeNote);
apiRoute(PUT, '/api/branches/:branchId/move-after/:afterBranchId', branchesApiRoute.moveBranchAfterNote);
apiRoute(PUT, '/api/branches/:branchId/expanded/:expanded', branchesApiRoute.setExpanded);
apiRoute(PUT, '/api/branches/:branchId/expanded-subtree/:expanded', branchesApiRoute.setExpandedForSubtree);
apiRoute(DELETE, '/api/branches/:branchId', branchesApiRoute.deleteBranch);
apiRoute(GET, '/api/autocomplete', autocompleteApiRoute.getAutocomplete);

View File

@@ -3,10 +3,11 @@
const sqlInit = require('../services/sql_init');
const setupService = require('../services/setup');
const utils = require('../services/utils');
const windowService = require('../services/window');
async function setupPage(req, res) {
if (await sqlInit.isDbInitialized()) {
const windowService = require('../services/window');
if (utils.isElectron()) {
await windowService.createMainWindow();
windowService.closeSetupWindow();

View File

@@ -19,6 +19,7 @@ const BUILTIN_ATTRIBUTES = [
{ type: 'label', name: 'appTheme' },
{ type: 'label', name: 'hidePromotedAttributes' },
{ type: 'label', name: 'readOnly' },
{ type: 'label', name: 'autoReadOnlyDisabled' },
{ type: 'label', name: 'cssClass' },
{ type: 'label', name: 'iconClass' },
{ type: 'label', name: 'keyboardShortcut' },

View File

@@ -1 +1 @@
module.exports = { buildDate:"2020-04-15T23:00:13+02:00", buildRevision: "dadcc93ae37fc03918837630c0a5a27a69af3495" };
module.exports = { buildDate:"2020-05-06T23:24:13+02:00", buildRevision: "54ecd2ee75d1177cedadf9fee10319687feee5f0" };

View File

@@ -76,16 +76,16 @@ module.exports = function(filters, selectedColumns = 'notes.*') {
let where = '1';
const params = [];
for (const filter of filters) {
if (['isarchived', 'in', 'orderby', 'limit'].includes(filter.name.toLowerCase())) {
continue; // these are not real filters
}
for (const filter of filters) {
if (['isarchived', 'in', 'orderby', 'limit'].includes(filter.name.toLowerCase())) {
continue; // these are not real filters
}
where += " " + filter.relation + " ";
const accessor = getAccessor(filter.name);
const accessor = getAccessor(filter.name);
if (filter.operator === 'exists') {
if (filter.operator === 'exists') {
where += `${accessor} IS NOT NULL`;
}
else if (filter.operator === 'not-exists') {
@@ -93,58 +93,53 @@ module.exports = function(filters, selectedColumns = 'notes.*') {
}
else if (filter.operator === '=' || filter.operator === '!=') {
where += `${accessor} ${filter.operator} ?`;
params.push(filter.value);
}
else if (filter.operator === '*=' || filter.operator === '!*=') {
params.push(filter.value);
} else if (filter.operator === '*=' || filter.operator === '!*=') {
where += `${accessor}`
+ (filter.operator.includes('!') ? ' NOT' : '')
+ ` LIKE ` + utils.prepareSqlForLike('%', filter.value, '');
}
else if (filter.operator === '=*' || filter.operator === '!=*') {
} else if (filter.operator === '=*' || filter.operator === '!=*') {
where += `${accessor}`
+ (filter.operator.includes('!') ? ' NOT' : '')
+ ` LIKE ` + utils.prepareSqlForLike('', filter.value, '%');
}
else if (filter.operator === '*=*' || filter.operator === '!*=*') {
const columns = filter.name === 'text' ? [getAccessor("title"), getAccessor("content")] : [accessor];
} else if (filter.operator === '*=*' || filter.operator === '!*=*') {
const columns = filter.name === 'text' ? [getAccessor("title"), getAccessor("content")] : [accessor];
let condition = "(" + columns.map(column =>
`${column}` + ` LIKE ` + utils.prepareSqlForLike('%', filter.value, '%'))
.join(" OR ") + ")";
let condition = "(" + columns.map(column =>
`${column}` + ` LIKE ` + utils.prepareSqlForLike('%', filter.value, '%'))
.join(" OR ") + ")";
if (filter.operator.includes('!')) {
condition = "NOT(" + condition + ")";
}
if (filter.operator.includes('!')) {
condition = "NOT(" + condition + ")";
}
if (['text', 'title', 'content'].includes(filter.name)) {
// for title/content search does not make sense to search for protected notes
condition = `(${condition} AND notes.isProtected = 0)`;
}
if (['text', 'title', 'content'].includes(filter.name)) {
// for title/content search does not make sense to search for protected notes
condition = `(${condition} AND notes.isProtected = 0)`;
}
where += condition;
}
else if ([">", ">=", "<", "<="].includes(filter.operator)) {
let floatParam;
let floatParam;
// from https://stackoverflow.com/questions/12643009/regular-expression-for-floating-point-numbers
if (/^[+-]?([0-9]*[.])?[0-9]+$/.test(filter.value)) {
floatParam = parseFloat(filter.value);
}
// from https://stackoverflow.com/questions/12643009/regular-expression-for-floating-point-numbers
if (/^[+-]?([0-9]*[.])?[0-9]+$/.test(filter.value)) {
floatParam = parseFloat(filter.value);
}
if (floatParam === undefined || isNaN(floatParam)) {
// if the value can't be parsed as float then we assume that string comparison should be used instead of numeric
if (floatParam === undefined || isNaN(floatParam)) {
// if the value can't be parsed as float then we assume that string comparison should be used instead of numeric
where += `${accessor} ${filter.operator} ?`;
params.push(filter.value);
}
else {
params.push(filter.value);
} else {
where += `CAST(${accessor} AS DECIMAL) ${filter.operator} ?`;
params.push(floatParam);
params.push(floatParam);
}
} else {
throw new Error("Unknown operator " + filter.operator);
}
}
else {
throw new Error("Unknown operator " + filter.operator);
}
}
if (orderBy.length === 0) {
// if no ordering is given then order at least by note title

View File

@@ -13,6 +13,10 @@ function getSourceId() {
return namespace.get('sourceId');
}
function getLocalNowDateTime() {
return namespace.get('localNowDateTime');
}
function disableEntityEvents() {
namespace.set('disableEntityEvents', true);
}
@@ -50,6 +54,7 @@ module.exports = {
wrap,
namespace,
getSourceId,
getLocalNowDateTime,
disableEntityEvents,
isEntityEventsDisabled,
reset,

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