mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	Compare commits
	
		
			108 Commits
		
	
	
		
			v0.41.3-be
			...
			v0.42.5
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 38723e0189 | ||
|  | 8c88ce6f65 | ||
|  | 4d22959e28 | ||
|  | 50a28d8c51 | ||
|  | e25b633ec4 | ||
|  | 75bd395669 | ||
|  | 5e353a5612 | ||
|  | 6b359b7796 | ||
|  | 13f9d037dc | ||
|  | 1911d64c1c | ||
|  | d4c3f1b3f2 | ||
|  | 3db84daf94 | ||
|  | 2526715aa4 | ||
|  | 04c573e212 | ||
|  | 58f4f5d1e6 | ||
|  | fa5d982a55 | ||
|  | 108afe8896 | ||
|  | 37da0adb8a | ||
|  | 4f50864ec8 | ||
|  | 30b9ef8604 | ||
|  | b063b4c528 | ||
|  | e08b0141a4 | ||
|  | 9d8b8e26a1 | ||
|  | cb70109ee7 | ||
|  | e541abbd60 | ||
|  | 940a70adc5 | ||
|  | 88e8eb7e9c | ||
|  | b365c186a1 | ||
|  | 64c9734f05 | ||
|  | 48c843c087 | ||
|  | 0e4eec10b9 | ||
|  | a3661cb763 | ||
|  | 115879ec4a | ||
|  | df11b076bc | ||
|  | 54ecd2ee75 | ||
|  | 2369bcf9fc | ||
|  | 5d8808a2ad | ||
|  | 62b993f06f | ||
|  | 8aa5608085 | ||
|  | b452d7e5c5 | ||
|  | 9b9d6d86d0 | ||
|  | 7f2755d4a0 | ||
|  | 3b268cc8eb | ||
|  | 6dfe335707 | ||
|  | c7125d2b50 | ||
|  | e8a33a5ee7 | ||
|  | deb0b24c4c | ||
|  | cafcb67a8a | ||
|  | 4c6e9480e4 | ||
|  | 109bead1c7 | ||
|  | ae1220b970 | ||
|  | b89a2df462 | ||
|  | 8b5536ee3a | ||
|  | 647790885d | ||
|  | 227c3e4dcc | ||
|  | 4eb2407c73 | ||
|  | 43e12fbea2 | ||
|  | 2a3091f788 | ||
|  | 742df25bc2 | ||
|  | 9be1d1f697 | ||
|  | ed52f93bbb | ||
|  | 3466a19397 | ||
|  | fe53e2351c | ||
|  | 5c35b870eb | ||
|  | 90d091aedb | ||
|  | 0a05a40186 | ||
|  | 7482ed063b | ||
|  | 70e343f2fc | ||
|  | 358f3a7291 | ||
|  | 6161b1c193 | ||
|  | 94b57dadd7 | ||
|  | 81fbefb9cd | ||
|  | 6d6695e3a9 | ||
|  | 4c308ad68f | ||
|  | 989a003d2f | ||
|  | ccdb41841e | ||
|  | 0a94622413 | ||
|  | 5769587305 | ||
|  | ffbfccb701 | ||
|  | 56ce23fc36 | ||
|  | cba7e5a59f | ||
|  | 86cf8f3202 | ||
|  | 907cdd8fcb | ||
|  | 7ea53d468e | ||
|  | 586d6b4557 | ||
|  | 8f68ff1932 | ||
|  | a1ea2c9115 | ||
|  | e8ce81a133 | ||
|  | aff12950f0 | ||
|  | 75c58cbf79 | ||
|  | 87a1e98fa2 | ||
|  | d1eacbb574 | ||
|  | 71d248cd87 | ||
|  | ac608b9334 | ||
|  | 32020d78b5 | ||
|  | ff853c7d0a | ||
|  | 8526cb2315 | ||
|  | dc89f72e75 | ||
|  | 657ff16267 | ||
|  | ed759f5585 | ||
|  | a86177bb59 | ||
|  | 9f1b3cc892 | ||
|  | 8473f72ec8 | ||
|  | 666d202a3a | ||
|  | 988fae50cb | ||
|  | 98bbd17920 | ||
|  | dadcc93ae3 | ||
|  | 48e19d0149 | 
							
								
								
									
										4
									
								
								.idea/dataSources.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								.idea/dataSources.xml
									
									
									
										generated
									
									
									
								
							| @@ -1,11 +1,11 @@ | ||||
| <?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="document.db" uuid="b0b03187-36c8-4ec1-bdab-fd4273cd692e"> | ||||
|       <driver-ref>sqlite.xerial</driver-ref> | ||||
|       <synchronize>true</synchronize> | ||||
|       <jdbc-driver>org.sqlite.JDBC</jdbc-driver> | ||||
|       <jdbc-url>jdbc:sqlite:$PROJECT_DIR$/../trilium-data/document.db</jdbc-url> | ||||
|       <jdbc-url>jdbc:sqlite:$USER_HOME$/trilium-data/document.db</jdbc-url> | ||||
|     </data-source> | ||||
|   </component> | ||||
| </project> | ||||
| @@ -1,662 +0,0 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <dataSource name="document.db"> | ||||
|   <database-model serializer="dbm" dbms="SQLITE" family-id="SQLITE" format-version="4.17"> | ||||
|     <root id="1"> | ||||
|       <ServerVersion>3.25.1</ServerVersion> | ||||
|     </root> | ||||
|     <schema id="2" parent="1" name="main"> | ||||
|       <Current>1</Current> | ||||
|     </schema> | ||||
|     <collation id="3" parent="1" name="BINARY"/> | ||||
|     <collation id="4" parent="1" name="NOCASE"/> | ||||
|     <collation id="5" parent="1" name="RTRIM"/> | ||||
|     <table id="6" parent="2" name="api_tokens"/> | ||||
|     <table id="7" parent="2" name="attributes"/> | ||||
|     <table id="8" parent="2" name="branches"/> | ||||
|     <table id="9" parent="2" name="note_contents"/> | ||||
|     <table id="10" parent="2" name="note_revision_contents"/> | ||||
|     <table id="11" parent="2" name="note_revisions"/> | ||||
|     <table id="12" parent="2" name="notes"/> | ||||
|     <table id="13" parent="2" name="options"/> | ||||
|     <table id="14" parent="2" name="recent_notes"/> | ||||
|     <table id="15" parent="2" name="source_ids"/> | ||||
|     <table id="16" parent="2" name="sqlite_master"> | ||||
|       <System>1</System> | ||||
|     </table> | ||||
|     <table id="17" parent="2" name="sqlite_sequence"> | ||||
|       <System>1</System> | ||||
|     </table> | ||||
|     <table id="18" parent="2" name="sync"/> | ||||
|     <column id="19" parent="6" name="apiTokenId"> | ||||
|       <Position>1</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="20" parent="6" name="token"> | ||||
|       <Position>2</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="21" parent="6" name="utcDateCreated"> | ||||
|       <Position>3</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="22" parent="6" name="isDeleted"> | ||||
|       <Position>4</Position> | ||||
|       <DataType>INT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|       <DefaultExpression>0</DefaultExpression> | ||||
|     </column> | ||||
|     <column id="23" parent="6" name="hash"> | ||||
|       <Position>5</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|       <DefaultExpression>""</DefaultExpression> | ||||
|     </column> | ||||
|     <index id="24" parent="6" name="sqlite_autoindex_api_tokens_1"> | ||||
|       <NameSurrogate>1</NameSurrogate> | ||||
|       <ColNames>apiTokenId</ColNames> | ||||
|       <Unique>1</Unique> | ||||
|     </index> | ||||
|     <key id="25" parent="6"> | ||||
|       <ColNames>apiTokenId</ColNames> | ||||
|       <Primary>1</Primary> | ||||
|       <UnderlyingIndexName>sqlite_autoindex_api_tokens_1</UnderlyingIndexName> | ||||
|     </key> | ||||
|     <column id="26" parent="7" name="attributeId"> | ||||
|       <Position>1</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="27" parent="7" name="noteId"> | ||||
|       <Position>2</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="28" parent="7" name="type"> | ||||
|       <Position>3</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="29" parent="7" name="name"> | ||||
|       <Position>4</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="30" parent="7" name="value"> | ||||
|       <Position>5</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|       <DefaultExpression>''</DefaultExpression> | ||||
|     </column> | ||||
|     <column id="31" parent="7" name="position"> | ||||
|       <Position>6</Position> | ||||
|       <DataType>INT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|       <DefaultExpression>0</DefaultExpression> | ||||
|     </column> | ||||
|     <column id="32" parent="7" name="utcDateCreated"> | ||||
|       <Position>7</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="33" parent="7" name="utcDateModified"> | ||||
|       <Position>8</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="34" parent="7" name="isDeleted"> | ||||
|       <Position>9</Position> | ||||
|       <DataType>INT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="35" parent="7" name="deleteId"> | ||||
|       <Position>10</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <DefaultExpression>NULL</DefaultExpression> | ||||
|     </column> | ||||
|     <column id="36" parent="7" name="hash"> | ||||
|       <Position>11</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|       <DefaultExpression>""</DefaultExpression> | ||||
|     </column> | ||||
|     <column id="37" parent="7" name="isInheritable"> | ||||
|       <Position>12</Position> | ||||
|       <DataType>int|0s</DataType> | ||||
|       <DefaultExpression>0</DefaultExpression> | ||||
|     </column> | ||||
|     <index id="38" parent="7" name="sqlite_autoindex_attributes_1"> | ||||
|       <NameSurrogate>1</NameSurrogate> | ||||
|       <ColNames>attributeId</ColNames> | ||||
|       <Unique>1</Unique> | ||||
|     </index> | ||||
|     <index id="39" parent="7" name="IDX_attributes_noteId_index"> | ||||
|       <ColNames>noteId</ColNames> | ||||
|     </index> | ||||
|     <index id="40" parent="7" name="IDX_attributes_name_value"> | ||||
|       <ColNames>name | ||||
| value</ColNames> | ||||
|     </index> | ||||
|     <index id="41" parent="7" name="IDX_attributes_value_index"> | ||||
|       <ColNames>value</ColNames> | ||||
|     </index> | ||||
|     <key id="42" parent="7"> | ||||
|       <ColNames>attributeId</ColNames> | ||||
|       <Primary>1</Primary> | ||||
|       <UnderlyingIndexName>sqlite_autoindex_attributes_1</UnderlyingIndexName> | ||||
|     </key> | ||||
|     <column id="43" parent="8" name="branchId"> | ||||
|       <Position>1</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="44" parent="8" name="noteId"> | ||||
|       <Position>2</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="45" parent="8" name="parentNoteId"> | ||||
|       <Position>3</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="46" parent="8" name="notePosition"> | ||||
|       <Position>4</Position> | ||||
|       <DataType>INTEGER|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="47" parent="8" name="prefix"> | ||||
|       <Position>5</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|     </column> | ||||
|     <column id="48" parent="8" name="isExpanded"> | ||||
|       <Position>6</Position> | ||||
|       <DataType>INTEGER|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|       <DefaultExpression>0</DefaultExpression> | ||||
|     </column> | ||||
|     <column id="49" parent="8" name="isDeleted"> | ||||
|       <Position>7</Position> | ||||
|       <DataType>INTEGER|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|       <DefaultExpression>0</DefaultExpression> | ||||
|     </column> | ||||
|     <column id="50" parent="8" name="deleteId"> | ||||
|       <Position>8</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <DefaultExpression>NULL</DefaultExpression> | ||||
|     </column> | ||||
|     <column id="51" parent="8" name="utcDateModified"> | ||||
|       <Position>9</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="52" parent="8" name="utcDateCreated"> | ||||
|       <Position>10</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="53" parent="8" name="hash"> | ||||
|       <Position>11</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|       <DefaultExpression>""</DefaultExpression> | ||||
|     </column> | ||||
|     <index id="54" parent="8" name="sqlite_autoindex_branches_1"> | ||||
|       <NameSurrogate>1</NameSurrogate> | ||||
|       <ColNames>branchId</ColNames> | ||||
|       <Unique>1</Unique> | ||||
|     </index> | ||||
|     <index id="55" parent="8" name="IDX_branches_noteId_parentNoteId"> | ||||
|       <ColNames>noteId | ||||
| parentNoteId</ColNames> | ||||
|     </index> | ||||
|     <index id="56" parent="8" name="IDX_branches_parentNoteId"> | ||||
|       <ColNames>parentNoteId</ColNames> | ||||
|     </index> | ||||
|     <key id="57" parent="8"> | ||||
|       <ColNames>branchId</ColNames> | ||||
|       <Primary>1</Primary> | ||||
|       <UnderlyingIndexName>sqlite_autoindex_branches_1</UnderlyingIndexName> | ||||
|     </key> | ||||
|     <column id="58" parent="9" name="noteId"> | ||||
|       <Position>1</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="59" parent="9" name="content"> | ||||
|       <Position>2</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <DefaultExpression>NULL</DefaultExpression> | ||||
|     </column> | ||||
|     <column id="60" parent="9" name="hash"> | ||||
|       <Position>3</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|       <DefaultExpression>""</DefaultExpression> | ||||
|     </column> | ||||
|     <column id="61" parent="9" name="utcDateModified"> | ||||
|       <Position>4</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <index id="62" parent="9" name="sqlite_autoindex_note_contents_1"> | ||||
|       <NameSurrogate>1</NameSurrogate> | ||||
|       <ColNames>noteId</ColNames> | ||||
|       <Unique>1</Unique> | ||||
|     </index> | ||||
|     <key id="63" parent="9"> | ||||
|       <ColNames>noteId</ColNames> | ||||
|       <Primary>1</Primary> | ||||
|       <UnderlyingIndexName>sqlite_autoindex_note_contents_1</UnderlyingIndexName> | ||||
|     </key> | ||||
|     <column id="64" parent="10" name="noteRevisionId"> | ||||
|       <Position>1</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="65" parent="10" name="content"> | ||||
|       <Position>2</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|     </column> | ||||
|     <column id="66" parent="10" name="hash"> | ||||
|       <Position>3</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|       <DefaultExpression>''</DefaultExpression> | ||||
|     </column> | ||||
|     <column id="67" parent="10" name="utcDateModified"> | ||||
|       <Position>4</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <index id="68" parent="10" name="sqlite_autoindex_note_revision_contents_1"> | ||||
|       <NameSurrogate>1</NameSurrogate> | ||||
|       <ColNames>noteRevisionId</ColNames> | ||||
|       <Unique>1</Unique> | ||||
|     </index> | ||||
|     <key id="69" parent="10"> | ||||
|       <ColNames>noteRevisionId</ColNames> | ||||
|       <Primary>1</Primary> | ||||
|       <UnderlyingIndexName>sqlite_autoindex_note_revision_contents_1</UnderlyingIndexName> | ||||
|     </key> | ||||
|     <column id="70" parent="11" name="noteRevisionId"> | ||||
|       <Position>1</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="71" parent="11" name="noteId"> | ||||
|       <Position>2</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="72" parent="11" name="title"> | ||||
|       <Position>3</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|     </column> | ||||
|     <column id="73" parent="11" name="contentLength"> | ||||
|       <Position>4</Position> | ||||
|       <DataType>INT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="74" parent="11" name="isErased"> | ||||
|       <Position>5</Position> | ||||
|       <DataType>INT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|       <DefaultExpression>0</DefaultExpression> | ||||
|     </column> | ||||
|     <column id="75" parent="11" name="isProtected"> | ||||
|       <Position>6</Position> | ||||
|       <DataType>INT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|       <DefaultExpression>0</DefaultExpression> | ||||
|     </column> | ||||
|     <column id="76" parent="11" name="utcDateLastEdited"> | ||||
|       <Position>7</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="77" parent="11" name="utcDateCreated"> | ||||
|       <Position>8</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="78" parent="11" name="utcDateModified"> | ||||
|       <Position>9</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="79" parent="11" name="dateLastEdited"> | ||||
|       <Position>10</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="80" parent="11" name="dateCreated"> | ||||
|       <Position>11</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="81" parent="11" name="type"> | ||||
|       <Position>12</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|       <DefaultExpression>''</DefaultExpression> | ||||
|     </column> | ||||
|     <column id="82" parent="11" name="mime"> | ||||
|       <Position>13</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|       <DefaultExpression>''</DefaultExpression> | ||||
|     </column> | ||||
|     <column id="83" parent="11" name="hash"> | ||||
|       <Position>14</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|       <DefaultExpression>''</DefaultExpression> | ||||
|     </column> | ||||
|     <index id="84" parent="11" name="sqlite_autoindex_note_revisions_1"> | ||||
|       <NameSurrogate>1</NameSurrogate> | ||||
|       <ColNames>noteRevisionId</ColNames> | ||||
|       <Unique>1</Unique> | ||||
|     </index> | ||||
|     <index id="85" parent="11" name="IDX_note_revisions_noteId"> | ||||
|       <ColNames>noteId</ColNames> | ||||
|     </index> | ||||
|     <index id="86" parent="11" name="IDX_note_revisions_utcDateLastEdited"> | ||||
|       <ColNames>utcDateLastEdited</ColNames> | ||||
|     </index> | ||||
|     <index id="87" parent="11" name="IDX_note_revisions_utcDateCreated"> | ||||
|       <ColNames>utcDateCreated</ColNames> | ||||
|     </index> | ||||
|     <index id="88" parent="11" name="IDX_note_revisions_dateLastEdited"> | ||||
|       <ColNames>dateLastEdited</ColNames> | ||||
|     </index> | ||||
|     <index id="89" parent="11" name="IDX_note_revisions_dateCreated"> | ||||
|       <ColNames>dateCreated</ColNames> | ||||
|     </index> | ||||
|     <key id="90" parent="11"> | ||||
|       <ColNames>noteRevisionId</ColNames> | ||||
|       <Primary>1</Primary> | ||||
|       <UnderlyingIndexName>sqlite_autoindex_note_revisions_1</UnderlyingIndexName> | ||||
|     </key> | ||||
|     <column id="91" parent="12" name="noteId"> | ||||
|       <Position>1</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="92" parent="12" name="title"> | ||||
|       <Position>2</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|       <DefaultExpression>"note"</DefaultExpression> | ||||
|     </column> | ||||
|     <column id="93" parent="12" name="contentLength"> | ||||
|       <Position>3</Position> | ||||
|       <DataType>INT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="94" parent="12" name="isProtected"> | ||||
|       <Position>4</Position> | ||||
|       <DataType>INT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|       <DefaultExpression>0</DefaultExpression> | ||||
|     </column> | ||||
|     <column id="95" parent="12" name="type"> | ||||
|       <Position>5</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|       <DefaultExpression>'text'</DefaultExpression> | ||||
|     </column> | ||||
|     <column id="96" parent="12" name="mime"> | ||||
|       <Position>6</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|       <DefaultExpression>'text/html'</DefaultExpression> | ||||
|     </column> | ||||
|     <column id="97" parent="12" name="hash"> | ||||
|       <Position>7</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|       <DefaultExpression>""</DefaultExpression> | ||||
|     </column> | ||||
|     <column id="98" parent="12" name="isDeleted"> | ||||
|       <Position>8</Position> | ||||
|       <DataType>INT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|       <DefaultExpression>0</DefaultExpression> | ||||
|     </column> | ||||
|     <column id="99" parent="12" name="deleteId"> | ||||
|       <Position>9</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <DefaultExpression>NULL</DefaultExpression> | ||||
|     </column> | ||||
|     <column id="100" parent="12" name="isErased"> | ||||
|       <Position>10</Position> | ||||
|       <DataType>INT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|       <DefaultExpression>0</DefaultExpression> | ||||
|     </column> | ||||
|     <column id="101" parent="12" name="dateCreated"> | ||||
|       <Position>11</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="102" parent="12" name="dateModified"> | ||||
|       <Position>12</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="103" parent="12" name="utcDateCreated"> | ||||
|       <Position>13</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="104" parent="12" name="utcDateModified"> | ||||
|       <Position>14</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <index id="105" parent="12" name="sqlite_autoindex_notes_1"> | ||||
|       <NameSurrogate>1</NameSurrogate> | ||||
|       <ColNames>noteId</ColNames> | ||||
|       <Unique>1</Unique> | ||||
|     </index> | ||||
|     <index id="106" parent="12" name="IDX_notes_title"> | ||||
|       <ColNames>title</ColNames> | ||||
|     </index> | ||||
|     <index id="107" parent="12" name="IDX_notes_type"> | ||||
|       <ColNames>type</ColNames> | ||||
|     </index> | ||||
|     <index id="108" parent="12" name="IDX_notes_isDeleted"> | ||||
|       <ColNames>isDeleted</ColNames> | ||||
|     </index> | ||||
|     <index id="109" parent="12" name="IDX_notes_dateCreated"> | ||||
|       <ColNames>dateCreated</ColNames> | ||||
|     </index> | ||||
|     <index id="110" parent="12" name="IDX_notes_dateModified"> | ||||
|       <ColNames>dateModified</ColNames> | ||||
|     </index> | ||||
|     <index id="111" parent="12" name="IDX_notes_utcDateCreated"> | ||||
|       <ColNames>utcDateCreated</ColNames> | ||||
|     </index> | ||||
|     <index id="112" parent="12" name="IDX_notes_utcDateModified"> | ||||
|       <ColNames>utcDateModified</ColNames> | ||||
|     </index> | ||||
|     <key id="113" parent="12"> | ||||
|       <ColNames>noteId</ColNames> | ||||
|       <Primary>1</Primary> | ||||
|       <UnderlyingIndexName>sqlite_autoindex_notes_1</UnderlyingIndexName> | ||||
|     </key> | ||||
|     <column id="114" parent="13" name="name"> | ||||
|       <Position>1</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="115" parent="13" name="value"> | ||||
|       <Position>2</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|     </column> | ||||
|     <column id="116" parent="13" name="isSynced"> | ||||
|       <Position>3</Position> | ||||
|       <DataType>INTEGER|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|       <DefaultExpression>0</DefaultExpression> | ||||
|     </column> | ||||
|     <column id="117" parent="13" name="hash"> | ||||
|       <Position>4</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|       <DefaultExpression>""</DefaultExpression> | ||||
|     </column> | ||||
|     <column id="118" parent="13" name="utcDateCreated"> | ||||
|       <Position>5</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="119" parent="13" name="utcDateModified"> | ||||
|       <Position>6</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <index id="120" parent="13" name="sqlite_autoindex_options_1"> | ||||
|       <NameSurrogate>1</NameSurrogate> | ||||
|       <ColNames>name</ColNames> | ||||
|       <Unique>1</Unique> | ||||
|     </index> | ||||
|     <key id="121" parent="13"> | ||||
|       <ColNames>name</ColNames> | ||||
|       <Primary>1</Primary> | ||||
|       <UnderlyingIndexName>sqlite_autoindex_options_1</UnderlyingIndexName> | ||||
|     </key> | ||||
|     <column id="122" parent="14" name="noteId"> | ||||
|       <Position>1</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="123" parent="14" name="notePath"> | ||||
|       <Position>2</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="124" parent="14" name="hash"> | ||||
|       <Position>3</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|       <DefaultExpression>""</DefaultExpression> | ||||
|     </column> | ||||
|     <column id="125" parent="14" name="utcDateCreated"> | ||||
|       <Position>4</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="126" parent="14" name="isDeleted"> | ||||
|       <Position>5</Position> | ||||
|       <DataType>INT|0s</DataType> | ||||
|     </column> | ||||
|     <index id="127" parent="14" name="sqlite_autoindex_recent_notes_1"> | ||||
|       <NameSurrogate>1</NameSurrogate> | ||||
|       <ColNames>noteId</ColNames> | ||||
|       <Unique>1</Unique> | ||||
|     </index> | ||||
|     <key id="128" parent="14"> | ||||
|       <ColNames>noteId</ColNames> | ||||
|       <Primary>1</Primary> | ||||
|       <UnderlyingIndexName>sqlite_autoindex_recent_notes_1</UnderlyingIndexName> | ||||
|     </key> | ||||
|     <column id="129" parent="15" name="sourceId"> | ||||
|       <Position>1</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="130" parent="15" name="utcDateCreated"> | ||||
|       <Position>2</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <index id="131" parent="15" name="sqlite_autoindex_source_ids_1"> | ||||
|       <NameSurrogate>1</NameSurrogate> | ||||
|       <ColNames>sourceId</ColNames> | ||||
|       <Unique>1</Unique> | ||||
|     </index> | ||||
|     <index id="132" parent="15" name="IDX_source_ids_utcDateCreated"> | ||||
|       <ColNames>utcDateCreated</ColNames> | ||||
|     </index> | ||||
|     <key id="133" parent="15"> | ||||
|       <ColNames>sourceId</ColNames> | ||||
|       <Primary>1</Primary> | ||||
|       <UnderlyingIndexName>sqlite_autoindex_source_ids_1</UnderlyingIndexName> | ||||
|     </key> | ||||
|     <column id="134" parent="16" name="type"> | ||||
|       <Position>1</Position> | ||||
|       <DataType>text|0s</DataType> | ||||
|     </column> | ||||
|     <column id="135" parent="16" name="name"> | ||||
|       <Position>2</Position> | ||||
|       <DataType>text|0s</DataType> | ||||
|     </column> | ||||
|     <column id="136" parent="16" name="tbl_name"> | ||||
|       <Position>3</Position> | ||||
|       <DataType>text|0s</DataType> | ||||
|     </column> | ||||
|     <column id="137" parent="16" name="rootpage"> | ||||
|       <Position>4</Position> | ||||
|       <DataType>int|0s</DataType> | ||||
|     </column> | ||||
|     <column id="138" parent="16" name="sql"> | ||||
|       <Position>5</Position> | ||||
|       <DataType>text|0s</DataType> | ||||
|     </column> | ||||
|     <column id="139" parent="17" name="name"> | ||||
|       <Position>1</Position> | ||||
|     </column> | ||||
|     <column id="140" parent="17" name="seq"> | ||||
|       <Position>2</Position> | ||||
|     </column> | ||||
|     <column id="141" parent="18" name="id"> | ||||
|       <Position>1</Position> | ||||
|       <DataType>INTEGER|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|       <SequenceIdentity>1</SequenceIdentity> | ||||
|     </column> | ||||
|     <column id="142" parent="18" name="entityName"> | ||||
|       <Position>2</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="143" parent="18" name="entityId"> | ||||
|       <Position>3</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="144" parent="18" name="sourceId"> | ||||
|       <Position>4</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <column id="145" parent="18" name="isSynced"> | ||||
|       <Position>5</Position> | ||||
|       <DataType>INTEGER|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|       <DefaultExpression>0</DefaultExpression> | ||||
|     </column> | ||||
|     <column id="146" parent="18" name="utcSyncDate"> | ||||
|       <Position>6</Position> | ||||
|       <DataType>TEXT|0s</DataType> | ||||
|       <NotNull>1</NotNull> | ||||
|     </column> | ||||
|     <index id="147" parent="18" name="IDX_sync_entityName_entityId"> | ||||
|       <ColNames>entityName | ||||
| entityId</ColNames> | ||||
|       <Unique>1</Unique> | ||||
|     </index> | ||||
|     <index id="148" parent="18" name="IDX_sync_utcSyncDate"> | ||||
|       <ColNames>utcSyncDate</ColNames> | ||||
|     </index> | ||||
|     <key id="149" parent="18"> | ||||
|       <ColNames>id</ColNames> | ||||
|       <Primary>1</Primary> | ||||
|     </key> | ||||
|   </database-model> | ||||
| </dataSource> | ||||
| @@ -1,2 +0,0 @@ | ||||
| #n:main | ||||
| !<md> [0, 0, null, null, -2147483648, -2147483648] | ||||
							
								
								
									
										1
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
									
									
										generated
									
									
									
								
							| @@ -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
									
									
									
								
							
							
						
						
									
										1
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
								
							| @@ -2,5 +2,6 @@ | ||||
| <project version="4"> | ||||
|   <component name="VcsDirectoryMappings"> | ||||
|     <mapping directory="" vcs="Git" /> | ||||
|     <mapping directory="$PROJECT_DIR$/../.." vcs="Git" /> | ||||
|   </component> | ||||
| </project> | ||||
| @@ -1,4 +1,4 @@ | ||||
| FROM node:12.16.2-alpine | ||||
| FROM node:12.16.3-alpine | ||||
|  | ||||
| # Create app directory | ||||
| WORKDIR /usr/src/app | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -12,7 +12,7 @@ echo "Copying required linux-x64 binaries" | ||||
| rm -r $SRC_DIR/node_modules/sqlite3/lib/binding/* | ||||
| rm -r $SRC_DIR/node_modules/pngquant-bin/vendor/* | ||||
|  | ||||
| rm -r $SRC_DIR/src/public/dist/*.mobile.* | ||||
| rm -r $SRC_DIR/src/public/app-dist/*.mobile.* | ||||
|  | ||||
| cp -r bin/deps/linux-x64/sqlite/* $SRC_DIR/node_modules/sqlite3/lib/binding/ | ||||
| cp bin/deps/linux-x64/image/pngquant $SRC_DIR/node_modules/pngquant-bin/vendor/ | ||||
| @@ -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` | ||||
|  | ||||
|   | ||||
| @@ -19,7 +19,7 @@ cp bin/deps/mac-x64/image/cjpeg $SRC_DIR/node_modules/mozjpeg/vendor/ | ||||
| cp bin/deps/mac-x64/image/pngquant $SRC_DIR/node_modules/pngquant-bin/vendor/ | ||||
| cp bin/deps/mac-x64/image/gifsicle $SRC_DIR/node_modules/giflossy/vendor/ | ||||
|  | ||||
| rm -r $SRC_DIR/src/public/dist/*.mobile.* | ||||
| rm -r $SRC_DIR/src/public/app-dist/*.mobile.* | ||||
|  | ||||
| ./node_modules/.bin/electron-packager $SRC_DIR --asar --out=dist --executable-name=trilium --platform=darwin --arch=x64 --overwrite --icon=images/app-icons/mac/icon.icns | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| #!/usr/bin/env bash | ||||
|  | ||||
| PKG_DIR=dist/trilium-linux-x64-server | ||||
| NODE_VERSION=12.16.2 | ||||
| NODE_VERSION=12.16.3 | ||||
|  | ||||
| if [ "$1" != "DONTCOPY" ] | ||||
| then | ||||
|   | ||||
| @@ -19,7 +19,7 @@ cp bin/deps/win-x64/image/cjpeg.exe $SRC_DIR/node_modules/mozjpeg/vendor/ | ||||
| cp bin/deps/win-x64/image/pngquant.exe $SRC_DIR/node_modules/pngquant-bin/vendor/ | ||||
| cp bin/deps/win-x64/image/gifsicle.exe $SRC_DIR/node_modules/giflossy/vendor/ | ||||
|  | ||||
| rm -r $SRC_DIR/src/public/dist/*.mobile.* | ||||
| rm -r $SRC_DIR/src/public/app-dist/*.mobile.* | ||||
|  | ||||
| ./node_modules/.bin/electron-packager $SRC_DIR --asar --out=dist --executable-name=trilium --platform=win32  --arch=x64 --overwrite --icon=images/app-icons/win/icon.ico | ||||
|  | ||||
| @@ -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` | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
| @@ -33,3 +35,4 @@ rm -r $DIR/src/public/app | ||||
|  | ||||
| sed -i -e 's/app\/desktop.js/app-dist\/desktop.js/g' $DIR/src/views/desktop.ejs | ||||
| sed -i -e 's/app\/mobile.js/app-dist\/mobile.js/g' $DIR/src/views/mobile.ejs | ||||
| sed -i -e 's/app\/setup.js/app-dist\/setup.js/g' $DIR/src/views/setup.ejs | ||||
							
								
								
									
										4
									
								
								bin/tpl/portable-trilium.bat
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								bin/tpl/portable-trilium.bat
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										7
									
								
								bin/tpl/portable-trilium.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| #!/usr/bin/env sh | ||||
|  | ||||
| DIR=`dirname "$0"` | ||||
| export TRILIUM_DATA_DIR="$DIR/trilium-data" | ||||
|  | ||||
| "$DIR/trilium" | ||||
|  | ||||
| @@ -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"> | ||||
|   | ||||
| @@ -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"> | ||||
|   | ||||
| @@ -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"> | ||||
|   | ||||
| @@ -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"> | ||||
|   | ||||
| @@ -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"> | ||||
|   | ||||
| @@ -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"> | ||||
|   | ||||
| @@ -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"> | ||||
|   | ||||
| @@ -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"> | ||||
|   | ||||
| @@ -94,6 +94,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"> | ||||
|   | ||||
| @@ -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"> | ||||
|   | ||||
| @@ -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"> | ||||
|   | ||||
							
								
								
									
										151
									
								
								docs/frontend_api/widgets_collapsible_widget.js.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								docs/frontend_api/widgets_collapsible_widget.js.html
									
									
									
									
									
										Normal 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 = ` | ||||
| <div class="card widget"> | ||||
|     <div class="card-header"> | ||||
|         <div>            | ||||
|             <button class="btn btn-sm widget-title" data-toggle="collapse" data-target="#[to be set]"> | ||||
|                 Collapsible Group Item | ||||
|             </button> | ||||
|              | ||||
|             <a class="widget-help external no-arrow bx bx-info-circle"></a> | ||||
|         </div> | ||||
|          | ||||
|         <div class="widget-header-actions"></div> | ||||
|     </div> | ||||
|  | ||||
|     <div id="[to be set]" class="collapse body-wrapper" style="transition: none; "> | ||||
|         <div class="card-body"></div> | ||||
|     </div> | ||||
| </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> | ||||
| @@ -1,4 +1,4 @@ | ||||
| For bug reports, please mention **version of the application** and include **log files** from following location: | ||||
| For bug reports, **PLEASE mention version of Trilium you're using** and also include **log files** from following location: | ||||
|  | ||||
| * `/home/[user]/.local/share/trilium-data/log` for Linux | ||||
| * `C:\Users\[user]\AppData\Roaming\trilium-data\log` for Windows Vista and up | ||||
|   | ||||
							
								
								
									
										2
									
								
								libraries/ckeditor/ckeditor.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								libraries/ckeditor/ckeditor.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -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); | ||||
|   | ||||
							
								
								
									
										904
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										904
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										28
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								package.json
									
									
									
									
									
								
							| @@ -2,7 +2,7 @@ | ||||
|   "name": "trilium", | ||||
|   "productName": "Trilium Notes", | ||||
|   "description": "Trilium Notes", | ||||
|   "version": "0.41.3-beta", | ||||
|   "version": "0.42.5", | ||||
|   "license": "AGPL-3.0-only", | ||||
|   "main": "electron.js", | ||||
|   "bin": { | ||||
| @@ -16,28 +16,28 @@ | ||||
|     "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" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "async-mutex": "0.2.1", | ||||
|     "async-mutex": "0.2.2", | ||||
|     "axios": "0.19.2", | ||||
|     "body-parser": "1.19.0", | ||||
|     "cls-hooked": "4.2.2", | ||||
|     "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", | ||||
|     "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": { | ||||
|   | ||||
| @@ -105,7 +105,6 @@ class Attribute extends Entity { | ||||
|  | ||||
|     // cannot be static! | ||||
|     updatePojo(pojo) { | ||||
|         delete pojo.isOwned; | ||||
|         delete pojo.__note; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -411,10 +411,6 @@ class Note extends Entity { | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         for (const attr of filteredAttributes) { | ||||
|             attr.isOwned = attr.noteId === this.noteId; | ||||
|         } | ||||
|  | ||||
|         this.__attributeCache = filteredAttributes; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -5,8 +5,10 @@ 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"; | ||||
| import zoomService from './services/zoom.js'; | ||||
|  | ||||
| glob.setupGlobs(); | ||||
|  | ||||
| @@ -23,9 +25,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 +86,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", | ||||
| @@ -130,9 +134,11 @@ if (utils.isElectron()) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const zoomLevel = zoomService.getCurrentZoom(); | ||||
|  | ||||
|         contextMenu.show({ | ||||
|             x: params.x, | ||||
|             y: params.y, | ||||
|             x: params.x / zoomLevel, | ||||
|             y: params.y / zoomLevel, | ||||
|             items, | ||||
|             selectMenuItemHandler: ({command, spellingSuggestion}) => { | ||||
|                 if (command === 'replaceMisspelling') { | ||||
|   | ||||
| @@ -59,8 +59,8 @@ function AttributesModel() { | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     async function showAttributes(attributes) { | ||||
|         const ownedAttributes = attributes.filter(attr => attr.isOwned); | ||||
|     async function showAttributes(noteId, attributes) { | ||||
|         const ownedAttributes = attributes.filter(attr => attr.noteId === noteId); | ||||
|  | ||||
|         for (const attr of ownedAttributes) { | ||||
|             attr.labelValue = attr.type === 'label' ? attr.value : ''; | ||||
| @@ -86,7 +86,7 @@ function AttributesModel() { | ||||
|  | ||||
|         addLastEmptyRow(); | ||||
|  | ||||
|         const inheritedAttributes = attributes.filter(attr => !attr.isOwned); | ||||
|         const inheritedAttributes = attributes.filter(attr => attr.noteId !== noteId); | ||||
|  | ||||
|         self.inheritedAttributes(inheritedAttributes); | ||||
|     } | ||||
| @@ -96,7 +96,7 @@ function AttributesModel() { | ||||
|  | ||||
|         const attributes = await server.get('notes/' + noteId + '/attributes'); | ||||
|  | ||||
|         await showAttributes(attributes); | ||||
|         await showAttributes(noteId, attributes); | ||||
|  | ||||
|         // attribute might not be rendered immediatelly so could not focus | ||||
|         setTimeout(() => $(".attribute-type-select:last").trigger('focus'), 1000); | ||||
| @@ -166,7 +166,7 @@ function AttributesModel() { | ||||
|  | ||||
|         const attributes = await server.put('notes/' + noteId + '/attributes', attributesToSave); | ||||
|  | ||||
|         await showAttributes(attributes); | ||||
|         await showAttributes(noteId, attributes); | ||||
|  | ||||
|         toastService.showMessage("Attributes have been saved."); | ||||
|     }; | ||||
|   | ||||
| @@ -39,13 +39,14 @@ export async function showDialog(noteIds) { | ||||
| } | ||||
|  | ||||
| async function cloneNotesTo(notePath) { | ||||
|     const targetNoteId = treeService.getNoteIdFromNotePath(notePath); | ||||
|     const {noteId, parentNoteId} = treeService.getNoteIdAndParentIdFromNotePath(notePath); | ||||
|     const targetBranchId = await treeCache.getBranchId(parentNoteId, noteId); | ||||
|  | ||||
|     for (const cloneNoteId of clonedNoteIds) { | ||||
|         await branchService.cloneNoteTo(cloneNoteId, targetNoteId, $clonePrefix.val()); | ||||
|         await branchService.cloneNoteTo(cloneNoteId, targetBranchId, $clonePrefix.val()); | ||||
|  | ||||
|         const clonedNote = await treeCache.getNote(cloneNoteId); | ||||
|         const targetNote = await treeCache.getNote(targetNoteId); | ||||
|         const targetNote = await treeCache.getBranch(targetBranchId).getNote(); | ||||
|  | ||||
|         toastService.showMessage(`Note "${clonedNote.title}" has been cloned into ${targetNote.title}`); | ||||
|     } | ||||
|   | ||||
| @@ -32,10 +32,11 @@ export async function showDialog(branchIds) { | ||||
|     noteAutocompleteService.showRecentNotes($noteAutoComplete); | ||||
| } | ||||
|  | ||||
| async function moveNotesTo(parentNoteId) { | ||||
|     await branchService.moveToParentNote(movedBranchIds, parentNoteId); | ||||
| async function moveNotesTo(parentBranchId) { | ||||
|     await branchService.moveToParentNote(movedBranchIds, parentBranchId); | ||||
|  | ||||
|     const parentNote = await treeCache.getNote(parentNoteId); | ||||
|     const parentBranch = treeCache.getBranch(parentBranchId); | ||||
|     const parentNote = await parentBranch.getNote(); | ||||
|  | ||||
|     toastService.showMessage(`Selected notes have been moved into ${parentNote.title}`); | ||||
| } | ||||
| @@ -46,9 +47,8 @@ $form.on('submit', () => { | ||||
|     if (notePath) { | ||||
|         $dialog.modal('hide'); | ||||
|  | ||||
|         const noteId = treeService.getNoteIdFromNotePath(notePath); | ||||
|  | ||||
|         moveNotesTo(noteId); | ||||
|         const {noteId, parentNoteId} = treeService.getNoteIdAndParentIdFromNotePath(notePath); | ||||
|         treeCache.getBranchId(parentNoteId, noteId).then(branchId => moveNotesTo(branchId)); | ||||
|     } | ||||
|     else { | ||||
|         console.error("No path to move to."); | ||||
|   | ||||
| @@ -37,14 +37,18 @@ export async function showNoteRevisionsDialog(noteId, noteRevisionId) { | ||||
| async function loadNoteRevisions(noteId, noteRevId) { | ||||
|     $list.empty(); | ||||
|     $content.empty(); | ||||
|     $titleButtons.empty(); | ||||
|  | ||||
|     note = appContext.tabManager.getActiveTabNote(); | ||||
|     revisionItems = await server.get(`notes/${noteId}/revisions`); | ||||
|  | ||||
|     for (const item of revisionItems) { | ||||
|         $list.append($('<a class="dropdown-item" tabindex="0">') | ||||
|         $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'); | ||||
| @@ -59,6 +63,8 @@ async function loadNoteRevisions(noteId, noteRevId) { | ||||
|         $title.text("No revisions for this note yet..."); | ||||
|         noteRevisionId = null; | ||||
|     } | ||||
|  | ||||
|     $eraseAllRevisionsButton.toggle(revisionItems.length > 0); | ||||
| } | ||||
|  | ||||
| $dialog.on('shown.bs.modal', () => { | ||||
| @@ -76,6 +82,21 @@ async function setContentPane() { | ||||
|  | ||||
|     $title.html(revisionItem.title); | ||||
|  | ||||
|     const $restoreRevisionButton = $('<button class="btn btn-sm" type="button">Restore this revision</button>'); | ||||
|  | ||||
|     $restoreRevisionButton.on('click', async () => { | ||||
|         const confirmDialog = await import('../dialogs/confirm.js'); | ||||
|         const text = 'Do you want to restore this revision? This will overwrite current title/content of the note with this revision.'; | ||||
|  | ||||
|         if (await confirmDialog.confirm(text)) { | ||||
|             await server.put(`notes/${revisionItem.noteId}/restore-revision/${revisionItem.noteRevisionId}`); | ||||
|  | ||||
|             $dialog.modal('hide'); | ||||
|  | ||||
|             toastService.showMessage('Note revision has been restored.'); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     const $eraseRevisionButton = $('<button class="btn btn-sm" type="button">Delete this revision</button>'); | ||||
|  | ||||
|     $eraseRevisionButton.on('click', async () => { | ||||
| @@ -92,6 +113,8 @@ async function setContentPane() { | ||||
|     }); | ||||
|  | ||||
|     $titleButtons | ||||
|         .append($restoreRevisionButton) | ||||
|         .append('   ') | ||||
|         .append($eraseRevisionButton) | ||||
|         .append('   '); | ||||
|  | ||||
|   | ||||
| @@ -24,6 +24,13 @@ const TPL = ` | ||||
| <p>This action will create a new copy of the database and anonymise it (remove all note content and leave only structure and metadata) | ||||
|     for sharing online for debugging purposes without fear of leaking your personal data.</p> | ||||
|  | ||||
| <h4>Backup database</h4> | ||||
|  | ||||
| <button id="backup-database-button" class="btn">Backup database</button> | ||||
|  | ||||
| <br/> | ||||
| <br/> | ||||
|  | ||||
| <h4>Vacuum database</h4> | ||||
|  | ||||
| <p>This will rebuild database which will typically result in smaller database file. No data will be actually changed.</p> | ||||
| @@ -37,6 +44,7 @@ export default class AdvancedOptions { | ||||
|         this.$forceFullSyncButton = $("#force-full-sync-button"); | ||||
|         this.$fillSyncRowsButton = $("#fill-sync-rows-button"); | ||||
|         this.$anonymizeButton = $("#anonymize-button"); | ||||
|         this.$backupDatabaseButton = $("#backup-database-button"); | ||||
|         this.$vacuumDatabaseButton = $("#vacuum-database-button"); | ||||
|         this.$findAndFixConsistencyIssuesButton = $("#find-and-fix-consistency-issues-button"); | ||||
|  | ||||
| @@ -58,14 +66,20 @@ export default class AdvancedOptions { | ||||
|             toastService.showMessage("Created anonymized database"); | ||||
|         }); | ||||
|  | ||||
|         this.$backupDatabaseButton.on('click', async () => { | ||||
|             const {backupFile} = await server.post('database/backup-database'); | ||||
|  | ||||
|             toastService.showMessage("Database has been backed up to " + backupFile, 10000); | ||||
|         }); | ||||
|  | ||||
|         this.$vacuumDatabaseButton.on('click', async () => { | ||||
|             await server.post('cleanup/vacuum-database'); | ||||
|             await server.post('database/vacuum-database'); | ||||
|  | ||||
|             toastService.showMessage("Database has been vacuumed"); | ||||
|         }); | ||||
|  | ||||
|         this.$findAndFixConsistencyIssuesButton.on('click', async () => { | ||||
|             await server.post('cleanup/find-and-fix-consistency-issues'); | ||||
|             await server.post('database/find-and-fix-consistency-issues'); | ||||
|  | ||||
|             toastService.showMessage("Consistency issues should be fixed."); | ||||
|         }); | ||||
|   | ||||
| @@ -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 "en-US", "de-AT""> | ||||
|     </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', () => { | ||||
|   | ||||
| @@ -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, []); | ||||
|   | ||||
| @@ -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,6 +170,7 @@ class NoteShort { | ||||
|      * @returns {Attribute[]} all note's attributes, including inherited ones | ||||
|      */ | ||||
|     getAttributes(type, name) { | ||||
|         if (!(this.noteId in noteAttributeCache)) { | ||||
|             const ownedAttributes = this.getOwnedAttributes(); | ||||
|  | ||||
|             const attrArrs = [ | ||||
| @@ -174,7 +178,7 @@ class NoteShort { | ||||
|             ]; | ||||
|  | ||||
|             for (const templateAttr of ownedAttributes.filter(oa => oa.type === 'relation' && oa.name === 'template')) { | ||||
|             const templateNote = this.treeCache.getNoteFromCache(templateAttr.value); | ||||
|                 const templateNote = this.treeCache.notes[templateAttr.value]; | ||||
|  | ||||
|                 if (templateNote) { | ||||
|                     attrArrs.push(templateNote.getAttributes()); | ||||
| @@ -190,20 +194,21 @@ class NoteShort { | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|         const attributes = attrArrs.flat(); | ||||
|             noteAttributeCache.attributes[this.noteId] = 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; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
							
								
								
									
										46
									
								
								src/public/app/layouts/desktop_extra_window_layout.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/public/app/layouts/desktop_extra_window_layout.js
									
									
									
									
									
										Normal 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')) | ||||
|                 ) | ||||
|             ); | ||||
|     } | ||||
| } | ||||
| @@ -1,31 +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 appContext from "../services/app_context.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"; | ||||
| 
 | ||||
| const RIGHT_PANE_CSS = ` | ||||
| <style> | ||||
| @@ -98,13 +96,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) | ||||
| @@ -118,6 +116,7 @@ export default class DesktopLayout { | ||||
|                 .hideInZenMode()) | ||||
|             .child(new FlexContainer('row') | ||||
|                 .collapsible() | ||||
|                 .filling() | ||||
|                 .child(new SidePaneContainer('left') | ||||
|                     .hideInZenMode() | ||||
|                     .child(new GlobalButtonsWidget()) | ||||
| @@ -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()) | ||||
|                     ) | ||||
| @@ -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') | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -4,15 +4,22 @@ import DialogCommandExecutor from "./dialog_command_executor.js"; | ||||
| import Entrypoints from "./entrypoints.js"; | ||||
| import options from "./options.js"; | ||||
| import utils from "./utils.js"; | ||||
| import ZoomService from "./zoom.js"; | ||||
| import zoomService from "./zoom.js"; | ||||
| 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; | ||||
|         this.executors = []; | ||||
|     } | ||||
|  | ||||
|     setLayout(layout) { | ||||
|         this.layout = layout; | ||||
|     } | ||||
| @@ -39,10 +46,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(); | ||||
| @@ -65,7 +74,7 @@ class AppContext extends Component { | ||||
|         } | ||||
|  | ||||
|         if (utils.isElectron()) { | ||||
|             this.child(new ZoomService()); | ||||
|             this.child(zoomService); | ||||
|         } | ||||
|  | ||||
|         this.triggerEvent('initialRenderComplete'); | ||||
| @@ -86,6 +95,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 +105,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', () => { | ||||
|   | ||||
| @@ -45,7 +45,7 @@ async function moveAfterBranch(branchIdsToMove, afterBranchId) { | ||||
|     } | ||||
| } | ||||
|  | ||||
| async function moveToParentNote(branchIdsToMove, newParentNoteId) { | ||||
| async function moveToParentNote(branchIdsToMove, newParentBranchId) { | ||||
|     branchIdsToMove = filterRootNote(branchIdsToMove); | ||||
|  | ||||
|     for (const branchIdToMove of branchIdsToMove) { | ||||
| @@ -56,7 +56,7 @@ async function moveToParentNote(branchIdsToMove, newParentNoteId) { | ||||
|             continue; | ||||
|         } | ||||
|  | ||||
|         const resp = await server.put(`branches/${branchIdToMove}/move-to/${newParentNoteId}`); | ||||
|         const resp = await server.put(`branches/${branchIdToMove}/move-to/${newParentBranchId}`); | ||||
|  | ||||
|         if (!resp.success) { | ||||
|             alert(resp.message); | ||||
| @@ -198,8 +198,8 @@ ws.subscribeToMessages(async message => { | ||||
|     } | ||||
| }); | ||||
|  | ||||
| async function cloneNoteTo(childNoteId, parentNoteId, prefix) { | ||||
|     const resp = await server.put('notes/' + childNoteId + '/clone-to/' + parentNoteId, { | ||||
| async function cloneNoteTo(childNoteId, parentBranchId, prefix) { | ||||
|     const resp = await server.put(`notes/${childNoteId}/clone-to/${parentBranchId}`, { | ||||
|         prefix: prefix | ||||
|     }); | ||||
|  | ||||
|   | ||||
| @@ -33,13 +33,13 @@ async function pasteAfter(afterBranchId) { | ||||
|     } | ||||
| } | ||||
|  | ||||
| async function pasteInto(parentNoteId) { | ||||
| async function pasteInto(parentBranchId) { | ||||
|     if (isClipboardEmpty()) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     if (clipboardMode === 'cut') { | ||||
|         await branchService.moveToParentNote(clipboardBranchIds, parentNoteId); | ||||
|         await branchService.moveToParentNote(clipboardBranchIds, parentBranchId); | ||||
|  | ||||
|         clipboardBranchIds = []; | ||||
|         clipboardMode = null; | ||||
| @@ -50,7 +50,7 @@ async function pasteInto(parentNoteId) { | ||||
|         for (const clipboardBranch of clipboardBranches) { | ||||
|             const clipboardNote = await clipboardBranch.getNote(); | ||||
|  | ||||
|             await branchService.cloneNoteTo(clipboardNote.noteId, parentNoteId); | ||||
|             await branchService.cloneNoteTo(clipboardNote.noteId, parentBranchId); | ||||
|         } | ||||
|  | ||||
|         // copy will keep clipboardBranchIds and clipboardMode so it's possible to paste into multiple places | ||||
|   | ||||
| @@ -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"); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -81,24 +81,29 @@ function goToLink(e) { | ||||
|         } | ||||
|         else if (e.which === 1) { | ||||
|             const activeTabContext = appContext.tabManager.getActiveTabContext(); | ||||
|             activeTabContext.setNote(notePath) | ||||
|             activeTabContext.setNote(notePath); | ||||
|         } | ||||
|         else { | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
|     else { | ||||
|         if (e.which === 1) { | ||||
|             const address = $link.attr('href'); | ||||
|  | ||||
|             if (address && address.startsWith('http')) { | ||||
|                 window.open(address, '_blank'); | ||||
|             } | ||||
|         } | ||||
|         else { | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| function newTabContextMenu(e) { | ||||
| function linkContextMenu(e) { | ||||
|     const $link = $(e.target).closest("a"); | ||||
|  | ||||
|     const notePath = getNotePathFromLink($link); | ||||
| @@ -113,12 +118,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: "empty"}, | ||||
|             {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); | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
| } | ||||
| @@ -151,17 +160,20 @@ $(document).on('mousedown', '.note-detail-text a', function (e) { | ||||
|  | ||||
| $(document).on('mousedown', '.note-detail-book a', goToLink); | ||||
| $(document).on('mousedown', '.note-detail-render a', goToLink); | ||||
| $(document).on('mousedown', '.note-detail-text.ck-read-only a,.note-detail-text a.reference-link', goToLink); | ||||
| $(document).on('mousedown', '.note-detail-text a.reference-link', goToLink); | ||||
| $(document).on('mousedown', '.note-detail-readonly-text a', goToLink); | ||||
| $(document).on('mousedown', 'a.ck-link-actions__preview', goToLink); | ||||
| $(document).on('click', 'a.ck-link-actions__preview', e => { | ||||
|     e.preventDefault(); | ||||
|     e.stopPropagation(); | ||||
| }); | ||||
|  | ||||
| $(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', 'a.ck-link-actions__preview', linkContextMenu); | ||||
| $(document).on('contextmenu', '.note-detail-text a', linkContextMenu); | ||||
| $(document).on('contextmenu', '.note-detail-readonly-text a', linkContextMenu); | ||||
| $(document).on('contextmenu', "a[data-action='note']", linkContextMenu); | ||||
| $(document).on('contextmenu', ".note-detail-render a", linkContextMenu); | ||||
| $(document).on('contextmenu', ".note-paths-widget a", linkContextMenu); | ||||
|  | ||||
| export default { | ||||
|     getNotePathFromUrl, | ||||
|   | ||||
| @@ -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) | ||||
|      *          notably changes in note itself should not have any effect on attributes | ||||
|      */ | ||||
|     hasAttributeRelatedChanges() { | ||||
|         return this.branches.length === 0 | ||||
|             && this.attributes.length === 0; | ||||
|     } | ||||
|  | ||||
|     isEmpty() { | ||||
|         return Object.keys(this.noteIdToSourceId).length === 0 | ||||
|             && this.branches.length === 0 | ||||
|   | ||||
| @@ -38,10 +38,6 @@ export default class MainTreeExecutors extends Component { | ||||
|             isProtected: activeNote.isProtected, | ||||
|             saveSelection: false | ||||
|         }); | ||||
|  | ||||
|         await ws.waitForMaxKnownSyncId(); | ||||
|  | ||||
|         appContext.tabManager.getActiveTabContext().setNote(note.noteId); | ||||
|     } | ||||
|  | ||||
|     async createNoteAfterCommand() { | ||||
| @@ -53,15 +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); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										20
									
								
								src/public/app/services/note_attribute_cache.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/public/app/services/note_attribute_cache.js
									
									
									
									
									
										Normal 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; | ||||
| @@ -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}; | ||||
|   | ||||
| @@ -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."); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -39,9 +39,16 @@ function touchProtectedSession() { | ||||
|     } | ||||
| } | ||||
|  | ||||
| function touchProtectedSessionIfNecessary(note) { | ||||
|     if (note && note.isProtected && isProtectedSessionAvailable()) { | ||||
|         touchProtectedSession(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default { | ||||
|     setProtectedSessionId, | ||||
|     resetProtectedSession, | ||||
|     isProtectedSessionAvailable, | ||||
|     touchProtectedSession | ||||
|     touchProtectedSession, | ||||
|     touchProtectedSessionIfNecessary | ||||
| }; | ||||
| @@ -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 | ||||
|     }; | ||||
|  | ||||
|   | ||||
| @@ -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', { | ||||
|   | ||||
| @@ -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); | ||||
|  | ||||
|         if (notePath) { | ||||
|             await tabContext.setNote(notePath, !activate); // if activate is false then send normal noteSwitched event | ||||
|         } | ||||
|  | ||||
|         if (activate) { | ||||
|             this.activateTab(tabContext.tabId, false); | ||||
| @@ -240,6 +249,9 @@ export default class TabManager extends Component { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // close dangling autocompletes after closing the tab | ||||
|         $(".aa-input").autocomplete("close"); | ||||
|  | ||||
|         await this.triggerEvent('beforeTabRemove', {tabId}); | ||||
|  | ||||
|         if (this.tabContexts.length <= 1) { | ||||
| @@ -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; | ||||
|   | ||||
| @@ -162,6 +162,13 @@ function getNoteIdFromNotePath(notePath) { | ||||
| } | ||||
|  | ||||
| function getNoteIdAndParentIdFromNotePath(notePath) { | ||||
|     if (notePath === 'root') { | ||||
|         return { | ||||
|             noteId: 'root', | ||||
|             parentNoteId: 'none' | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     let parentNoteId = 'root'; | ||||
|     let noteId = ''; | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
| @@ -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} */ | ||||
|   | ||||
| @@ -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}); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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"); | ||||
|  | ||||
| @@ -36,6 +37,21 @@ function subscribeToMessages(messageHandler) { | ||||
| // used to serialize sync operations | ||||
| let consumeQueuePromise = null; | ||||
|  | ||||
| // most sync events are sent twice - once immediatelly after finishing the transaction and once during the scheduled ping | ||||
| // but we want to process only once | ||||
| const processedSyncIds = new Set(); | ||||
|  | ||||
| function logRows(syncRows) { | ||||
|     const filteredRows = syncRows.filter(row => | ||||
|         !processedSyncIds.has(row.id) | ||||
|         && row.entityName !== 'recent_notes' | ||||
|         && (row.entityName !== 'options' || row.entityId !== 'openTabs')); | ||||
|  | ||||
|     if (filteredRows.length > 0) { | ||||
|         console.debug(utils.now(), "Sync data: ", filteredRows); | ||||
|     } | ||||
| } | ||||
|  | ||||
| async function handleMessage(event) { | ||||
|     const message = JSON.parse(event.data); | ||||
|  | ||||
| @@ -50,13 +66,7 @@ async function handleMessage(event) { | ||||
|         $outstandingSyncsCount.html(message.outstandingSyncs); | ||||
|  | ||||
|         if (syncRows.length > 0) { | ||||
|             const filteredRows = syncRows.filter(row => | ||||
|                 row.entityName !== 'recent_notes' | ||||
|                 && (row.entityName !== 'options' || row.entityId !== 'openTabs')); | ||||
|  | ||||
|             if (filteredRows.length > 0) { | ||||
|                 console.debug(utils.now(), "Sync data: ", filteredRows); | ||||
|             } | ||||
|             logRows(syncRows); | ||||
|  | ||||
|             syncDataQueue.push(...syncRows); | ||||
|  | ||||
| @@ -132,13 +142,21 @@ async function runSafely(syncHandler, syncData) { | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * TODO: we should rethink the fact that each sync row is sent twice (once at the end of transaction, once periodically) | ||||
|  *       and we keep both lastProcessedSyncId and processedSyncIds | ||||
|  *       it even seems incorrect that when transaction sync rows are received, we incorrectly increase lastProcessedSyncId | ||||
|  *       and then some syncs might lost (or are *all* sync rows sent from transactions?) | ||||
|  */ | ||||
| async function consumeSyncData() { | ||||
|     if (syncDataQueue.length > 0) { | ||||
|         const allSyncData = syncDataQueue; | ||||
|         const allSyncRows = syncDataQueue; | ||||
|         syncDataQueue = []; | ||||
|  | ||||
|         const nonProcessedSyncRows = allSyncRows.filter(sync => !processedSyncIds.has(sync.id)); | ||||
|  | ||||
|         try { | ||||
|             await processSyncRows(allSyncData); | ||||
|             await processSyncRows(nonProcessedSyncRows); | ||||
|         } | ||||
|         catch (e) { | ||||
|             logError(`Encountered error ${e.message}: ${e.stack}, reloading frontend.`); | ||||
| @@ -147,7 +165,11 @@ async function consumeSyncData() { | ||||
|             utils.reloadApp(); | ||||
|         } | ||||
|  | ||||
|         lastProcessedSyncId = Math.max(lastProcessedSyncId, allSyncData[allSyncData.length - 1].id); | ||||
|         for (const syncRow of nonProcessedSyncRows) { | ||||
|             processedSyncIds.add(syncRow.id); | ||||
|         } | ||||
|  | ||||
|         lastProcessedSyncId = Math.max(lastProcessedSyncId, allSyncRows[allSyncRows.length - 1].id); | ||||
|     } | ||||
|  | ||||
|     checkSyncIdListeners(); | ||||
| @@ -169,7 +191,7 @@ function connectWebSocket() { | ||||
|  | ||||
| async function sendPing() { | ||||
|     if (Date.now() - lastPingTs > 30000) { | ||||
|         console.log(utils.now(), "Lost websocket connection to the backend"); | ||||
|         console.log(utils.now(), "Lost websocket connection to the backend. If you keep having this issue repeatedly, you might want to check your reverse proxy (nginx, apache) configuration and allow/unblock WebSocket."); | ||||
|     } | ||||
|  | ||||
|     if (ws.readyState === ws.OPEN) { | ||||
| @@ -211,7 +233,7 @@ subscribeToMessages(message => { | ||||
| async function processSyncRows(syncRows) { | ||||
|     const missingNoteIds = []; | ||||
|  | ||||
|     syncRows.forEach(({entityName, entity}) => { | ||||
|     for (const {entityName, entity} of syncRows) { | ||||
|         if (entityName === 'branches' && !(entity.parentNoteId in treeCache.notes)) { | ||||
|             missingNoteIds.push(entity.parentNoteId); | ||||
|         } | ||||
| @@ -222,7 +244,7 @@ async function processSyncRows(syncRows) { | ||||
|  | ||||
|             missingNoteIds.push(entity.value); | ||||
|         } | ||||
|     }); | ||||
|     } | ||||
|  | ||||
|     if (missingNoteIds.length > 0) { | ||||
|         await treeCache.reloadNotes(missingNoteIds); | ||||
| @@ -230,16 +252,16 @@ async function processSyncRows(syncRows) { | ||||
|  | ||||
|     const loadResults = new LoadResults(treeCache); | ||||
|  | ||||
|     syncRows.filter(sync => sync.entityName === 'notes').forEach(sync => { | ||||
|     for (const sync of syncRows.filter(sync => sync.entityName === 'notes')) { | ||||
|         const note = treeCache.notes[sync.entityId]; | ||||
|  | ||||
|         if (note) { | ||||
|             note.update(sync.entity); | ||||
|             loadResults.addNote(sync.entityId, sync.sourceId); | ||||
|         } | ||||
|     }); | ||||
|     } | ||||
|  | ||||
|     syncRows.filter(sync => sync.entityName === 'branches').forEach(sync => { | ||||
|     for (const sync of syncRows.filter(sync => sync.entityName === 'branches')) { | ||||
|         let branch = treeCache.branches[sync.entityId]; | ||||
|         const childNote = treeCache.notes[sync.entity.noteId]; | ||||
|         const parentNote = treeCache.notes[sync.entity.parentNoteId]; | ||||
| @@ -285,9 +307,9 @@ async function processSyncRows(syncRows) { | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
|     } | ||||
|  | ||||
|     syncRows.filter(sync => sync.entityName === 'note_reordering').forEach(sync => { | ||||
|     for (const sync of syncRows.filter(sync => sync.entityName === 'note_reordering')) { | ||||
|         for (const branchId in sync.positions) { | ||||
|             const branch = treeCache.branches[branchId]; | ||||
|  | ||||
| @@ -297,10 +319,10 @@ async function processSyncRows(syncRows) { | ||||
|         } | ||||
|  | ||||
|         loadResults.addNoteReordering(sync.entityId, sync.sourceId); | ||||
|     }); | ||||
|     } | ||||
|  | ||||
|     // missing reloading the relation target note | ||||
|     syncRows.filter(sync => sync.entityName === 'attributes').forEach(sync => { | ||||
|     for (const sync of syncRows.filter(sync => sync.entityName === 'attributes')) { | ||||
|         let attribute = treeCache.attributes[sync.entityId]; | ||||
|         const sourceNote = treeCache.notes[sync.entity.noteId]; | ||||
|         const targetNote = sync.entity.type === 'relation' && treeCache.notes[sync.entity.value]; | ||||
| @@ -336,29 +358,33 @@ async function processSyncRows(syncRows) { | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
|     } | ||||
|  | ||||
|     syncRows.filter(sync => sync.entityName === 'note_contents').forEach(sync => { | ||||
|     for (const sync of syncRows.filter(sync => sync.entityName === 'note_contents')) { | ||||
|         delete treeCache.noteComplementPromises[sync.entityId]; | ||||
|  | ||||
|         loadResults.addNoteContent(sync.entityId, sync.sourceId); | ||||
|     }); | ||||
|     } | ||||
|  | ||||
|     syncRows.filter(sync => sync.entityName === 'note_revisions').forEach(sync => { | ||||
|     for (const sync of syncRows.filter(sync => sync.entityName === 'note_revisions')) { | ||||
|         loadResults.addNoteRevision(sync.entityId, sync.noteId, sync.sourceId); | ||||
|     }); | ||||
|     } | ||||
|  | ||||
|     syncRows.filter(sync => sync.entityName === 'options').forEach(sync => { | ||||
|     for (const sync of syncRows.filter(sync => sync.entityName === 'options')) { | ||||
|         if (sync.entity.name === 'openTabs') { | ||||
|             return; // only noise | ||||
|             continue; // only noise | ||||
|         } | ||||
|  | ||||
|         options.set(sync.entity.name, sync.entity.value); | ||||
|  | ||||
|         loadResults.addOption(sync.entity.name); | ||||
|     }); | ||||
|     } | ||||
|  | ||||
|     if (!loadResults.isEmpty()) { | ||||
|         if (loadResults.hasAttributeRelatedChanges()) { | ||||
|             noteAttributeCache.invalidate(); | ||||
|         } | ||||
|  | ||||
|         const appContext = (await import("./app_context.js")).default; | ||||
|         await appContext.triggerEvent('entitiesReloaded', {loadResults}); | ||||
|     } | ||||
|   | ||||
| @@ -5,11 +5,15 @@ import utils from "../services/utils.js"; | ||||
| const MIN_ZOOM = 0.5; | ||||
| const MAX_ZOOM = 2.0; | ||||
|  | ||||
| export default class ZoomService extends Component { | ||||
| class ZoomService extends Component { | ||||
|     constructor() { | ||||
|         super(); | ||||
|  | ||||
|         if (utils.isElectron()) { | ||||
|             options.initializedPromise.then(() => { | ||||
|                 this.setZoomFactor(options.getFloat('zoomFactor')); | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     setZoomFactor(zoomFactor) { | ||||
| @@ -46,3 +50,7 @@ export default class ZoomService extends Component { | ||||
|         this.setZoomFactorAndSave(zoomFactor); | ||||
|     } | ||||
| } | ||||
|  | ||||
| const zoomService = new ZoomService(); | ||||
|  | ||||
| export default zoomService; | ||||
|   | ||||
| @@ -30,6 +30,11 @@ class BasicWidget extends Component { | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     filling() { | ||||
|         this.css('flex-grow', '1'); | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     hideInZenMode() { | ||||
|         this.class('hide-in-zen-mode'); | ||||
|         return this; | ||||
| @@ -57,7 +62,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 { | ||||
|   | ||||
| @@ -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"); | ||||
|   | ||||
| @@ -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(); | ||||
|         }); | ||||
| 
 | ||||
| @@ -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"> | ||||
| @@ -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"; } | ||||
| @@ -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, | ||||
| @@ -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,16 +21,16 @@ const TPL = ` | ||||
|     </style> | ||||
| 
 | ||||
|     <tr> | ||||
|         <th nowrap>Note ID:</th> | ||||
|         <td nowrap colspan="3" class="note-info-note-id"></td> | ||||
|         <th>Note ID:</th> | ||||
|         <td colspan="3" class="note-info-note-id"></td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|         <th nowrap>Created:</th> | ||||
|         <td nowrap colspan="3" style="overflow: hidden; text-overflow: ellipsis;" class="note-info-date-created"></td> | ||||
|         <th>Created:</th> | ||||
|         <td colspan="3" class="note-info-date-created"></td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|         <th nowrap>Modified:</th> | ||||
|         <td nowrap colspan="3" style="overflow: hidden; text-overflow: ellipsis;" class="note-info-date-modified"></td> | ||||
|         <th>Modified:</th> | ||||
|         <td colspan="3" class="note-info-date-modified"></td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|         <th>Type:</th> | ||||
| @@ -60,11 +60,11 @@ export default class NoteInfoWidget extends CollapsibleWidget { | ||||
| 
 | ||||
|         this.$noteId.text(note.noteId); | ||||
|         this.$dateCreated | ||||
|             .text(noteComplement.dateCreated) | ||||
|             .text(noteComplement.dateCreated.substr(0, 16)) | ||||
|             .attr("title", noteComplement.dateCreated); | ||||
| 
 | ||||
|         this.$dateModified | ||||
|             .text(noteComplement.dateModified) | ||||
|             .text(noteComplement.dateModified.substr(0, 16)) | ||||
|             .attr("title", noteComplement.dateCreated); | ||||
| 
 | ||||
|         this.$type.text(note.type); | ||||
| @@ -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))); | ||||
| 
 | ||||
| @@ -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"; } | ||||
| @@ -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(); | ||||
|         }); | ||||
| 
 | ||||
| @@ -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(); | ||||
| @@ -82,6 +82,10 @@ export default class Component { | ||||
|         let release; | ||||
|  | ||||
|         try { | ||||
|             if (this.mutex.isLocked()) { | ||||
|                 console.debug("Mutex locked for", this.constructor.name); | ||||
|             } | ||||
|  | ||||
|             release = await this.mutex.acquire(); | ||||
|  | ||||
|             await fun.call(this, data); | ||||
|   | ||||
| @@ -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"  | ||||
|        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> | ||||
| `; | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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"> | ||||
| @@ -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>`; | ||||
| 
 | ||||
| @@ -1,4 +1,4 @@ | ||||
| import BasicWidget from "./basic_widget.js"; | ||||
| import BasicWidget from "../basic_widget.js"; | ||||
| 
 | ||||
| const WIDGET_TPL = ` | ||||
| <div id="global-buttons"> | ||||
| @@ -1,4 +1,4 @@ | ||||
| import Component from "./component.js"; | ||||
| import Component from "../component.js"; | ||||
| 
 | ||||
| export default class MobileScreenSwitcherExecutor extends Component { | ||||
|     setActiveScreenCommand({screen}) { | ||||
| @@ -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) { | ||||
| @@ -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(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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(); | ||||
|  | ||||
| @@ -281,7 +308,8 @@ export default class NoteDetailWidget extends TabAwareWidget { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         await noteCreateService.createNote(note.noteId, { | ||||
|         // without await as this otherwise causes deadlock through component mutex | ||||
|         noteCreateService.createNote(note.noteId, { | ||||
|             isProtected: note.isProtected, | ||||
|             saveSelection: true | ||||
|         }); | ||||
|   | ||||
| @@ -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)) | ||||
|                 ); | ||||
|   | ||||
| @@ -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(); | ||||
|     } | ||||
|   | ||||
| @@ -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; | ||||
| @@ -121,8 +251,8 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|                     this.triggerCommand('setActiveScreen', {screen:'detail'}); | ||||
|                 } | ||||
|             }, | ||||
|             expand: (event, data) => this.setExpandedToServer(data.node.data.branchId, true), | ||||
|             collapse: (event, data) => this.setExpandedToServer(data.node.data.branchId, false), | ||||
|             expand: (event, data) => this.setExpanded(data.node.data.branchId, true), | ||||
|             collapse: (event, data) => this.setExpanded(data.node.data.branchId, false), | ||||
|             hotkeys: utils.isMobile() ? undefined : { keydown: await this.getHotKeys() }, | ||||
|             dnd5: { | ||||
|                 autoExpandMS: 600, | ||||
| @@ -135,6 +265,7 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|  | ||||
|                     const notes = this.getSelectedOrActiveNodes(node).map(node => ({ | ||||
|                         noteId: node.data.noteId, | ||||
|                         branchId: node.data.branchId, | ||||
|                         title: node.title | ||||
|                     })); | ||||
|  | ||||
| @@ -174,27 +305,48 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|                         }); | ||||
|                     } | ||||
|                     else { | ||||
|                         const jsonStr = dataTransfer.getData("text"); | ||||
|                         let notes = null; | ||||
|  | ||||
|                         try { | ||||
|                             notes = JSON.parse(jsonStr); | ||||
|                         } | ||||
|                         catch (e) { | ||||
|                             console.error(`Cannot parse ${jsonStr} into notes for drop`); | ||||
|                             return; | ||||
|                         } | ||||
|  | ||||
|                         // 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 = notes.map(note => note.branchId); | ||||
|  | ||||
|                         if (data.hitMode === "before") { | ||||
|                             branchService.moveBeforeBranch(selectedBranchIds, node.data.branchId); | ||||
|                         } else if (data.hitMode === "after") { | ||||
|                             branchService.moveAfterBranch(selectedBranchIds, node.data.branchId); | ||||
|                         } else if (data.hitMode === "over") { | ||||
|                             branchService.moveToParentNote(selectedBranchIds, node.data.noteId); | ||||
|                             branchService.moveToParentNote(selectedBranchIds, node.data.branchId); | ||||
|                         } else { | ||||
|                             throw new Error("Unknown hitMode=" + data.hitMode); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             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 +372,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 +384,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 +395,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[]} */ | ||||
| @@ -253,25 +583,46 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|  | ||||
|     /** @return {FancytreeNode[]} */ | ||||
|     getSelectedOrActiveNodes(node = null) { | ||||
|         const notes = this.getSelectedNodes(true); | ||||
|         const nodes = this.getSelectedNodes(true); | ||||
|  | ||||
|         if (notes.length === 0) { | ||||
|             notes.push(node ? node : this.getActiveNode()); | ||||
|         // the node you start dragging should be included even if not selected | ||||
|         if (node && !nodes.find(n => n.key === node.key)) { | ||||
|             nodes.push(node); | ||||
|         } | ||||
|  | ||||
|         return notes; | ||||
|         if (nodes.length === 0) { | ||||
|             nodes.push(this.getActiveNode()); | ||||
|         } | ||||
|  | ||||
|     collapseTree(node = null) { | ||||
|         return nodes; | ||||
|     } | ||||
|  | ||||
|     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,12 +738,15 @@ 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.setExpanded(branch.isExpanded, {noEvents:true}); | ||||
|         node.renderTitle(); | ||||
|     } | ||||
|  | ||||
| @@ -413,12 +767,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,9 +808,23 @@ 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 nextNode = activeNode ? (activeNode.getNextSibling() || activeNode.getPrevSibling() || activeNode.getParent()) : null; | ||||
|         const activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null; | ||||
|         const nextNotePath = nextNode ? treeService.getNotePath(nextNode) : null; | ||||
|         const activeNoteId = activeNode ? activeNode.data.noteId : null; | ||||
|  | ||||
|         const noteIdsToUpdate = new Set(); | ||||
| @@ -481,6 +843,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 +867,9 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     if (node.getParent()) { | ||||
|                         node.remove(); | ||||
|                     } | ||||
|  | ||||
|                     noteIdsToUpdate.add(branch.parentNoteId); | ||||
|                 } | ||||
| @@ -524,17 +897,12 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|             noteIdsToUpdate.add(noteId); | ||||
|         } | ||||
|  | ||||
|         await this.batchUpdate(async () => { | ||||
|             for (const noteId of noteIdsToReload) { | ||||
|                 for (const node of this.getNodesByNoteId(noteId)) { | ||||
|                     await node.load(true); | ||||
|  | ||||
|                 this.updateNode(node); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         for (const noteId of noteIdsToUpdate) { | ||||
|             for (const node of this.getNodesByNoteId(noteId)) { | ||||
|                 this.updateNode(node); | ||||
|                     noteIdsToUpdate.add(noteId); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
| @@ -554,6 +922,14 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         // 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); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (activeNotePath) { | ||||
|             let node = await this.expandToNote(activeNotePath); | ||||
| @@ -570,15 +946,27 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|             if (node) { | ||||
|                 node.setActive(true, {noEvents: true}); | ||||
|             } | ||||
|             else { | ||||
|                 // this is used when original note has been deleted and we want to move the focus to the note above/below | ||||
|                 node = await this.expandToNote(nextNotePath); | ||||
|  | ||||
|                 if (node) { | ||||
|                     this.tree.setFocus(); | ||||
|                     node.setFocus(true); | ||||
|  | ||||
|                     await appContext.tabManager.getActiveTabContext().setNote(nextNotePath); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async setExpandedToServer(branchId, isExpanded) { | ||||
|     async setExpanded(branchId, isExpanded) { | ||||
|         utils.assertArguments(branchId); | ||||
|  | ||||
|         const expandedNum = isExpanded ? 1 : 0; | ||||
|         const branch = treeCache.getBranch(branchId); | ||||
|         branch.isExpanded = isExpanded; | ||||
|  | ||||
|         await server.put('branches/' + branchId + '/expanded/' + expandedNum); | ||||
|         await server.put(`branches/${branchId}/expanded/${isExpanded ? 1 : 0}`); | ||||
|     } | ||||
|  | ||||
|     async reloadTreeFromCache() { | ||||
| @@ -586,7 +974,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); | ||||
| @@ -688,7 +1080,7 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|         const toNode = node.getPrevSibling(); | ||||
|  | ||||
|         if (toNode !== null) { | ||||
|             branchService.moveToParentNote([node.data.branchId], toNode.data.noteId); | ||||
|             branchService.moveToParentNote([node.data.branchId], toNode.data.branchId); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -740,6 +1132,10 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     expandSubtreeCommand({node}) { | ||||
|         this.expandTree(node); | ||||
|     } | ||||
|  | ||||
|     collapseSubtreeCommand({node}) { | ||||
|         this.collapseTree(node); | ||||
|     } | ||||
| @@ -769,7 +1165,7 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|     } | ||||
|  | ||||
|     pasteNotesFromClipboardCommand({node}) { | ||||
|         clipboard.pasteInto(node.data.noteId); | ||||
|         clipboard.pasteInto(node.data.branchId); | ||||
|     } | ||||
|  | ||||
|     pasteNotesAfterFromClipboard({node}) { | ||||
|   | ||||
| @@ -19,6 +19,7 @@ const TPL = ` | ||||
|      | ||||
|     .promoted-attributes td, .promoted-attributes th { | ||||
|         padding: 5px; | ||||
|         min-width: 50px; /* otherwise checkboxes can collapse into 0 width (if there are only checkboxes) */ | ||||
|     } | ||||
|     </style> | ||||
|      | ||||
| @@ -98,7 +99,7 @@ export default class PromotedAttributesWidget extends TabAwareWidget { | ||||
|         const $labelCell = $("<th>").append(valueAttr.name); | ||||
|         const $input = $("<input>") | ||||
|             .prop("tabindex", definitionAttr.position) | ||||
|             .prop("attribute-id", valueAttr.isOwned ? valueAttr.attributeId : '') // if not owned, we'll force creation of a new attribute instead of updating the inherited one | ||||
|             .prop("attribute-id", valueAttr.noteId === this.noteId ? valueAttr.attributeId : '') // if not owned, we'll force creation of a new attribute instead of updating the inherited one | ||||
|             .prop("attribute-type", valueAttr.type) | ||||
|             .prop("attribute-name", valueAttr.name) | ||||
|             .prop("value", valueAttr.value) | ||||
|   | ||||
| @@ -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(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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') { | ||||
|             return super.handleEventInChildren(name, data); | ||||
|         } | ||||
|         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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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() { | ||||
| @@ -600,18 +602,23 @@ export default class TabRowWidget extends BasicWidget { | ||||
|     } | ||||
|  | ||||
|     updateTab($tab, note) { | ||||
|         if (!note || !$tab.length) { | ||||
|         if (!$tab.length) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         this.updateTitle($tab, note.title); | ||||
|  | ||||
|         for (const clazz of Array.from($tab[0].classList)) { // create copy to safely iterate over while removing classes | ||||
|             if (clazz !== 'note-tab') { | ||||
|                 $tab.removeClass(clazz); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (!note) { | ||||
|             this.updateTitle($tab, 'New tab'); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         this.updateTitle($tab, note.title); | ||||
|  | ||||
|         $tab.addClass(note.getCssClass()); | ||||
|         $tab.addClass(utils.getNoteTypeClass(note.type)); | ||||
|         $tab.addClass(utils.getMimeTypeClass(note.mime)); | ||||
|   | ||||
| @@ -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> | ||||
|  | ||||
|   | ||||
| @@ -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(() => { | ||||
| @@ -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 { | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user