libreqda 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. CHANGELOG.md +481 -0
  2. libreqda-1.2.0.dist-info/LICENSE.txt +28 -0
  3. libreqda-1.2.0.dist-info/METADATA +131 -0
  4. libreqda-1.2.0.dist-info/RECORD +255 -0
  5. libreqda-1.2.0.dist-info/WHEEL +4 -0
  6. libreqda-1.2.0.dist-info/entry_points.txt +4 -0
  7. scripts/babelrc +8 -0
  8. scripts/dist/LICENSE.txt +33 -0
  9. scripts/dist/RELEASE.txt +23 -0
  10. scripts/dist/macos/libreqda.icns +0 -0
  11. scripts/dist/macos/macos.spec +43 -0
  12. scripts/dist/macos/plist.patch +10 -0
  13. scripts/dist/macos/taguette.icns +0 -0
  14. scripts/dist/macos/taguette_console_wrapper +4 -0
  15. scripts/dist/macos.sh +32 -0
  16. scripts/dist/pyinstaller_entrypoint.py +19 -0
  17. scripts/dist/windows/windows-installer.iss +51 -0
  18. scripts/dist/windows/windows.spec +39 -0
  19. scripts/edugain/worker_update_edugain_idps.py +89 -0
  20. scripts/edugain/worker_update_edugain_idps.sh +9 -0
  21. scripts/make_password_reset_link.py +56 -0
  22. scripts/make_theme.py +99 -0
  23. scripts/new_db_revision.sh +8 -0
  24. scripts/register_user.py +76 -0
  25. scripts/update_pot.sh +33 -0
  26. scripts/update_translations.sh +17 -0
  27. scripts/validate.py +140 -0
  28. scripts/wipe_and_disable_user.py +90 -0
  29. taguette/__init__.py +13 -0
  30. taguette/access_control.py +145 -0
  31. taguette/convert.py +623 -0
  32. taguette/database/__init__.py +177 -0
  33. taguette/database/base.py +9 -0
  34. taguette/database/copy.py +433 -0
  35. taguette/database/models.py +920 -0
  36. taguette/demodata.py +688 -0
  37. taguette/export.py +500 -0
  38. taguette/extract.py +287 -0
  39. taguette/import_codebook.py +110 -0
  40. taguette/l10n/de_DE/LC_MESSAGES/taguette_javascript.mo +0 -0
  41. taguette/l10n/de_DE/LC_MESSAGES/taguette_main.mo +0 -0
  42. taguette/l10n/el_EL/LC_MESSAGES/taguette_javascript.mo +0 -0
  43. taguette/l10n/el_GR/LC_MESSAGES/taguette_javascript.mo +0 -0
  44. taguette/l10n/el_GR/LC_MESSAGES/taguette_main.mo +0 -0
  45. taguette/l10n/en_US/LC_MESSAGES/taguette_javascript.mo +0 -0
  46. taguette/l10n/en_US/LC_MESSAGES/taguette_main.mo +0 -0
  47. taguette/l10n/es_ES/LC_MESSAGES/taguette_javascript.mo +0 -0
  48. taguette/l10n/es_ES/LC_MESSAGES/taguette_main.mo +0 -0
  49. taguette/l10n/fr_FR/LC_MESSAGES/taguette_javascript.mo +0 -0
  50. taguette/l10n/fr_FR/LC_MESSAGES/taguette_main.mo +0 -0
  51. taguette/l10n/id_ID/LC_MESSAGES/taguette_javascript.mo +0 -0
  52. taguette/l10n/id_ID/LC_MESSAGES/taguette_main.mo +0 -0
  53. taguette/l10n/it_IT/LC_MESSAGES/taguette_javascript.mo +0 -0
  54. taguette/l10n/it_IT/LC_MESSAGES/taguette_main.mo +0 -0
  55. taguette/l10n/nl_NL/LC_MESSAGES/taguette_javascript.mo +0 -0
  56. taguette/l10n/nl_NL/LC_MESSAGES/taguette_main.mo +0 -0
  57. taguette/l10n/pt_PT/LC_MESSAGES/taguette_javascript.mo +0 -0
  58. taguette/l10n/pt_PT/LC_MESSAGES/taguette_main.mo +0 -0
  59. taguette/l10n/sv_SE/LC_MESSAGES/taguette_javascript.mo +0 -0
  60. taguette/l10n/sv_SE/LC_MESSAGES/taguette_main.mo +0 -0
  61. taguette/loki_handler.py +107 -0
  62. taguette/main.py +649 -0
  63. taguette/migrations/env.py +72 -0
  64. taguette/migrations/script.py.mako +24 -0
  65. taguette/migrations/versions/09c662cd9483_make_documents_filename_not_null.py +39 -0
  66. taguette/migrations/versions/1b29e68900c1_add_user_roles.py +55 -0
  67. taguette/migrations/versions/1b96a606551d_update_user_to_have_a_display_name.py +34 -0
  68. taguette/migrations/versions/2467bab602b7_add_color_column_to_tags_table.py +33 -0
  69. taguette/migrations/versions/382db36f20f4_add_user_disabled.py +66 -0
  70. taguette/migrations/versions/43d6c240309d_remove_document_groups.py +81 -0
  71. taguette/migrations/versions/447d636f72c5_cascades.py +40 -0
  72. taguette/migrations/versions/491de2dc7cd7_add_indexes.py +51 -0
  73. taguette/migrations/versions/4c55b3b3ff30_user_login_length.py +58 -0
  74. taguette/migrations/versions/61a4abcc8769_user_with_parent_for_permissions.py +40 -0
  75. taguette/migrations/versions/6489b5f9cfb5_increase_password_hash_size.py +34 -0
  76. taguette/migrations/versions/679f625e6e6a_fix_unique_tags.py +59 -0
  77. taguette/migrations/versions/77c4543a5b9b_add_parent_id_column_to_collection_table.py +41 -0
  78. taguette/migrations/versions/7c94b7a31e69_add_is_archived_column_to_projets.py +39 -0
  79. taguette/migrations/versions/7f56de97e3c8_change_edugain_displayname_length.py +42 -0
  80. taguette/migrations/versions/807c0cc0ecf6_remove_invalid_hltags.py +43 -0
  81. taguette/migrations/versions/80b1cc9d4c22_add_user_email.py +35 -0
  82. taguette/migrations/versions/91ade71ccf4d_add_user_language.py +28 -0
  83. taguette/migrations/versions/955db2e523d1_add_collections_tables_and_associations_.py +60 -0
  84. taguette/migrations/versions/a1b2c3d4e5f6_add_user_hide_code_counts.py +28 -0
  85. taguette/migrations/versions/b23f3b7a638e_add_user_password_change_date.py +36 -0
  86. taguette/migrations/versions/b7c1e2d3f4a5_add_project_compact_layout.py +28 -0
  87. taguette/migrations/versions/bc8e0e0677e9_rename_command_fields.py +126 -0
  88. taguette/migrations/versions/bce44849c2f2_lowercase_logins.py +96 -0
  89. taguette/migrations/versions/ca076140922f_update_user_for_external_logins.py +32 -0
  90. taguette/migrations/versions/d86c45d7aa8a_add_document_direction.py +127 -0
  91. taguette/migrations/versions/db5e31a0233d_remove_blank_highlights.py +88 -0
  92. taguette/migrations/versions/dd37d9027a24_unique_tags.py +68 -0
  93. taguette/migrations/versions/de678cef2a94_add_project_id_to_collections_and_.py +41 -0
  94. taguette/migrations/versions/e459991d960e_add_table_edugainidps.py +39 -0
  95. taguette/migrations/versions/e4cf92942271_add_index.py +29 -0
  96. taguette/migrations/versions/e4e090a5b511_remove_commands_document_id_fk.py +38 -0
  97. taguette/migrations/versions/e9a9f1c77971_add_favorite_column_to_document.py +32 -0
  98. taguette/migrations/versions/ecb4065de575_don_t_use_server_default_now_timezone_.py +44 -0
  99. taguette/migrations/versions/fbdc29566f62_edugain_fix_field_type.py +42 -0
  100. taguette/migrations/versions/fdc6eb54b034_add_parent_id_column_to_tags_table.py +31 -0
  101. taguette/static/README.md +34 -0
  102. taguette/static/alt_templates/.gitkeep +0 -0
  103. taguette/static/assets/test/logos/monsters-university-logo.png +0 -0
  104. taguette/static/css/custom.css +1696 -0
  105. taguette/static/css/fa-all.min.css +5 -0
  106. taguette/static/css/themed.css +3 -0
  107. taguette/static/css/themed.css.map +25 -0
  108. taguette/static/css/vendor/bootstrap-treeview.min.css +1 -0
  109. taguette/static/css/vendor/bootstrap.min.css +6 -0
  110. taguette/static/css/vendor/font/context-menu-icons.eot +0 -0
  111. taguette/static/css/vendor/font/context-menu-icons.ttf +0 -0
  112. taguette/static/css/vendor/font/context-menu-icons.woff +0 -0
  113. taguette/static/css/vendor/font/context-menu-icons.woff2 +0 -0
  114. taguette/static/css/vendor/hierarchy-select.min.css +1 -0
  115. taguette/static/css/vendor/introjs.min.css +2 -0
  116. taguette/static/css/vendor/jquery.contextMenu.min.css +16 -0
  117. taguette/static/favicon.ico +0 -0
  118. taguette/static/js/codebook-viz.js +428 -0
  119. taguette/static/js/collection/context-menu.js +180 -0
  120. taguette/static/js/collection/hierarchy-select.js +71 -0
  121. taguette/static/js/collection/manager.js +217 -0
  122. taguette/static/js/document/context-menu.js +92 -0
  123. taguette/static/js/document/custom-select.js +82 -0
  124. taguette/static/js/document/manager.js +943 -0
  125. taguette/static/js/document/treeview.js +593 -0
  126. taguette/static/js/highlight/README.md +18 -0
  127. taguette/static/js/highlight/crud.js +119 -0
  128. taguette/static/js/highlight/extracts.js +497 -0
  129. taguette/static/js/highlight/loader.js +109 -0
  130. taguette/static/js/highlight/renderer.js +405 -0
  131. taguette/static/js/login.js +15 -0
  132. taguette/static/js/permissions.js +153 -0
  133. taguette/static/js/polling-event.js +145 -0
  134. taguette/static/js/project/intro.js +524 -0
  135. taguette/static/js/project/manager.js +0 -0
  136. taguette/static/js/project/search.js +124 -0
  137. taguette/static/js/tag/context-menu.js +133 -0
  138. taguette/static/js/tag/explore-codes.js +400 -0
  139. taguette/static/js/tag/hierarchy-select.js +69 -0
  140. taguette/static/js/tag/manager.js +545 -0
  141. taguette/static/js/tag/treeview.js +613 -0
  142. taguette/static/js/taguette.js +1790 -0
  143. taguette/static/js/utilities.js +714 -0
  144. taguette/static/js/vendor/bootstrap-treeview-custum.js +421 -0
  145. taguette/static/js/vendor/bootstrap-treeview-custum.min.js +1 -0
  146. taguette/static/js/vendor/bootstrap-treeview-default.min.js +1 -0
  147. taguette/static/js/vendor/bootstrap.bundle.min.js +7 -0
  148. taguette/static/js/vendor/bootstrap.bundle.min.js.map +1 -0
  149. taguette/static/js/vendor/hierarchy-select.min.js +1 -0
  150. taguette/static/js/vendor/introjs.min.js +17 -0
  151. taguette/static/js/vendor/jquery-3.7.1.js +2 -0
  152. taguette/static/js/vendor/jquery.contextMenu.min.js +2 -0
  153. taguette/static/libreqda.ico +0 -0
  154. taguette/static/logo-h.png +0 -0
  155. taguette/static/logo.png +0 -0
  156. taguette/static/logo_blanc.png +0 -0
  157. taguette/static/missing.png +0 -0
  158. taguette/static/robots.txt +2 -0
  159. taguette/static/scss/components/_badge.scss +42 -0
  160. taguette/static/scss/components/_button.scss +169 -0
  161. taguette/static/scss/components/_checkbox.scss +40 -0
  162. taguette/static/scss/components/_context_menu.scss +28 -0
  163. taguette/static/scss/components/_custom_select.scss +150 -0
  164. taguette/static/scss/components/_dropdown.scss +43 -0
  165. taguette/static/scss/components/_form.scss +54 -0
  166. taguette/static/scss/components/_modal.scss +57 -0
  167. taguette/static/scss/components/_project_card.scss +163 -0
  168. taguette/static/scss/components/_tag.scss +308 -0
  169. taguette/static/scss/components/_treeview.scss +76 -0
  170. taguette/static/scss/custom.scss +809 -0
  171. taguette/static/scss/layout/_credits.scss +62 -0
  172. taguette/static/scss/layout/_footer.scss +6 -0
  173. taguette/static/scss/layout/_navigation.scss +165 -0
  174. taguette/static/scss/layout/_sidebar.scss +37 -0
  175. taguette/static/scss/themes/default/_variables.scss +103 -0
  176. taguette/static/scss/themes/default/theme.css +3 -0
  177. taguette/static/scss/themes/default/theme.css.map +26 -0
  178. taguette/static/scss/themes/default/theme.scss +4 -0
  179. taguette/static/webfonts/BadScript-OFL.txt +94 -0
  180. taguette/static/webfonts/BadScript.ttf +0 -0
  181. taguette/static/webfonts/Montserrat-Light.ttf +0 -0
  182. taguette/static/webfonts/Montserrat-OFL.txt +93 -0
  183. taguette/static/webfonts/fa-brands-400.eot +0 -0
  184. taguette/static/webfonts/fa-brands-400.svg +1184 -0
  185. taguette/static/webfonts/fa-brands-400.ttf +0 -0
  186. taguette/static/webfonts/fa-brands-400.woff +0 -0
  187. taguette/static/webfonts/fa-brands-400.woff2 +0 -0
  188. taguette/static/webfonts/fa-regular-400.eot +0 -0
  189. taguette/static/webfonts/fa-regular-400.svg +467 -0
  190. taguette/static/webfonts/fa-regular-400.ttf +0 -0
  191. taguette/static/webfonts/fa-regular-400.woff +0 -0
  192. taguette/static/webfonts/fa-regular-400.woff2 +0 -0
  193. taguette/static/webfonts/fa-solid-900.eot +0 -0
  194. taguette/static/webfonts/fa-solid-900.svg +2618 -0
  195. taguette/static/webfonts/fa-solid-900.ttf +0 -0
  196. taguette/static/webfonts/fa-solid-900.woff +0 -0
  197. taguette/static/webfonts/fa-solid-900.woff2 +0 -0
  198. taguette/templates/alt_templates/.gitkeep +0 -0
  199. taguette/templates/archived_projects.html +50 -0
  200. taguette/templates/auth/account.html +74 -0
  201. taguette/templates/auth/cilogon.login.html +89 -0
  202. taguette/templates/auth/email_reset_password.html +5 -0
  203. taguette/templates/auth/external_account.html +60 -0
  204. taguette/templates/auth/login.html +144 -0
  205. taguette/templates/auth/reset_password.html +27 -0
  206. taguette/templates/auth/token_needed.html +7 -0
  207. taguette/templates/base.html +209 -0
  208. taguette/templates/bug_report.html +158 -0
  209. taguette/templates/cookies_prompt.html +15 -0
  210. taguette/templates/downloads.html +127 -0
  211. taguette/templates/email_bug_report.txt +9 -0
  212. taguette/templates/email_reset_password.txt +5 -0
  213. taguette/templates/error.html +8 -0
  214. taguette/templates/export_codebook.html +20 -0
  215. taguette/templates/export_document.html +20 -0
  216. taguette/templates/export_highlights.html +47 -0
  217. taguette/templates/index.html +347 -0
  218. taguette/templates/maintenance.html +8 -0
  219. taguette/templates/messages.js +74 -0
  220. taguette/templates/modal/change_document.html +68 -0
  221. taguette/templates/modal/create_collection.html +100 -0
  222. taguette/templates/modal/create_document.html +84 -0
  223. taguette/templates/modal/create_highlight.html +38 -0
  224. taguette/templates/modal/create_tag.html +73 -0
  225. taguette/templates/modal/explore_codes.html +58 -0
  226. taguette/templates/modal/manage_collaborators.html +44 -0
  227. taguette/templates/modal/merge_tag.html +49 -0
  228. taguette/templates/new_password.html +32 -0
  229. taguette/templates/partials/alert.html +16 -0
  230. taguette/templates/partials/codebook_viz_modal.html +34 -0
  231. taguette/templates/partials/confirm.html +17 -0
  232. taguette/templates/partials/document_toolbar.html +37 -0
  233. taguette/templates/partials/hlinfo.html +13 -0
  234. taguette/templates/partials/project_card.html +44 -0
  235. taguette/templates/partials/project_card_archived.html +41 -0
  236. taguette/templates/partials/project_sidebar.html +99 -0
  237. taguette/templates/partials/spinner.html +11 -0
  238. taguette/templates/permissions.html +54 -0
  239. taguette/templates/project.html +288 -0
  240. taguette/templates/project_delete.html +17 -0
  241. taguette/templates/project_import.html +122 -0
  242. taguette/templates/project_import_codebook.html +34 -0
  243. taguette/templates/project_import_codebook_confirm.html +49 -0
  244. taguette/templates/project_new.html +31 -0
  245. taguette/templates/tos.html +8 -0
  246. taguette/templates/trans.js +7 -0
  247. taguette/templates/welcome.html +15 -0
  248. taguette/utils.py +177 -0
  249. taguette/validate.py +141 -0
  250. taguette/version_check.py +102 -0
  251. taguette/web/__init__.py +252 -0
  252. taguette/web/api.py +1835 -0
  253. taguette/web/base.py +864 -0
  254. taguette/web/export.py +338 -0
  255. taguette/web/views.py +1618 -0
@@ -0,0 +1,593 @@
1
+ const treeview_documents = $('#treeview-documents');
2
+
3
+ function flattenCollections(collectionsTree) {
4
+ const flat = {};
5
+
6
+ function addToFlat(nodes) {
7
+ for (let node of nodes) {
8
+ flat[node.id] = node;
9
+ if (node.children && node.children.length > 0) {
10
+ addToFlat(node.children);
11
+ }
12
+ }
13
+ }
14
+
15
+ addToFlat(collectionsTree);
16
+ return flat;
17
+ }
18
+
19
+ function convertCollectionsToTreeNode(data) {
20
+ return data.map(collection => {
21
+ const collectionNode = {
22
+ id: 'collection_' + collection.id,
23
+ text: collection.name,
24
+ selectable: false,
25
+ nodes: []
26
+ };
27
+
28
+ if (collection.document_ids && collection.document_ids.length > 0 && typeof documents !== 'undefined') {
29
+ for (let docId of collection.document_ids) {
30
+ const document = documents[docId.toString()];
31
+ if (document) {
32
+ collectionNode.nodes.push({
33
+ id: 'document_' + docId,
34
+ text: document.name,
35
+ selectable: true,
36
+ document_id: parseInt(docId),
37
+ tags: [(document.highlights_count || 0).toString()]
38
+ });
39
+ }
40
+ }
41
+ }
42
+
43
+ if (collection.children && collection.children.length > 0) {
44
+ const childCollections = convertCollectionsToTreeNode(collection.children);
45
+ collectionNode.nodes.push(...childCollections);
46
+ }
47
+
48
+ const documentNodes = collectionNode.nodes.filter(node => node.document_id !== undefined);
49
+ const subCollections = collectionNode.nodes.filter(node => node.document_id === undefined);
50
+
51
+ documentNodes.sort((a, b) => a.text.localeCompare(b.text));
52
+ subCollections.sort((a, b) => a.text.localeCompare(b.text));
53
+
54
+ collectionNode.nodes = [...documentNodes, ...subCollections];
55
+
56
+ // fake node to force chevron display (for icon alignment)
57
+ if (collectionNode.nodes.length === 0) {
58
+ collectionNode.nodes.push({
59
+ id: 'dummy_' + collection.id,
60
+ text: '',
61
+ icon: '',
62
+ selectable: false,
63
+ hideCheckbox: true,
64
+ isDummy: true,
65
+ state: {
66
+ disabled: true
67
+ }
68
+ });
69
+ }
70
+
71
+ const documentCount = collection.document_ids ? collection.document_ids.length : 0;
72
+ collectionNode.tags = [documentCount.toString()];
73
+
74
+ return collectionNode;
75
+ });
76
+ }
77
+
78
+ function convertDocumentsToTreeNode(filteredCollections = null) {
79
+ const treeData = [];
80
+
81
+ const collectionsToUse = filteredCollections || collections;
82
+
83
+ if (collectionsToUse && collectionsToUse.length > 0) {
84
+ const collectionNodes = convertCollectionsToTreeNode(collectionsToUse);
85
+ treeData.push(...collectionNodes);
86
+ }
87
+
88
+ const shouldShowUncategorized = true;
89
+
90
+ if (shouldShowUncategorized) {
91
+ const uncategorizedDocs = [];
92
+ for (let [docId, document] of Object.entries(documents)) {
93
+ const docCollections = document.collections || [];
94
+ if (docCollections.length === 0) {
95
+ // If filtering, only include documents that match the search
96
+ if (filteredCollections) {
97
+ const searchText = window.document.getElementById("input-search-titles")?.value.toLowerCase() || '';
98
+ if (searchText && !document.name.toLowerCase().includes(searchText)) {
99
+ continue;
100
+ }
101
+ }
102
+
103
+ uncategorizedDocs.push({
104
+ id: 'document_' + docId,
105
+ text: document.name,
106
+ selectable: true,
107
+ document_id: parseInt(docId),
108
+ tags: [(document.highlights_count || 0).toString()]
109
+ });
110
+ }
111
+ }
112
+
113
+ if (uncategorizedDocs.length > 0) {
114
+ uncategorizedDocs.sort((a, b) => a.text.localeCompare(b.text));
115
+ treeData.push({
116
+ id: 'uncategorized',
117
+ text: gettext('Without collection'),
118
+ icon: 'fas fa-folder',
119
+ selectable: false,
120
+ nodes: uncategorizedDocs,
121
+ tags: [uncategorizedDocs.length.toString()]
122
+ });
123
+ }
124
+ }
125
+
126
+ return treeData;
127
+ }
128
+
129
+ function customDocumentNodeRow() {
130
+ return {
131
+ customContent: true,
132
+ nodeContentSelector: 'a',
133
+ nodeContentAttributes: {
134
+ href: '#',
135
+ class: 'document-link-a',
136
+ style: 'text-decoration: none;'
137
+ },
138
+ customizeNode: function(element, node, tree) {
139
+ element.attr('id', `treeview-node-${node.id}`);
140
+
141
+ if (node.document_id) {
142
+ element.attr('data-document-id', node.document_id);
143
+ element.attr('id', `document-link-${node.document_id}`);
144
+ element.addClass('document-link');
145
+ element.css('position', 'relative');
146
+
147
+ const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
148
+ const cmdKey = isMac ? 'Cmd' : 'Ctrl';
149
+ const tooltipText = gettext('Click to open • %(key)s+Click to select • Right-click for options', {key: cmdKey});
150
+ const tooltip = `${node.text}\n\n${tooltipText}`;
151
+ element.attr('title', tooltip);
152
+
153
+ if (node.document_id === current_document) {
154
+ element.addClass('document-link-current');
155
+ }
156
+ element.addClass('document-context-menu');
157
+
158
+ linkDocument(element[0], node.document_id);
159
+ }
160
+
161
+ setTimeout(function() {
162
+ if (node.id && (node.id.startsWith('collection_') || node.id === 'uncategorized')) {
163
+ element.addClass('collection-context-menu');
164
+ }
165
+ }, 10);
166
+ }
167
+ };
168
+ }
169
+
170
+ function initDocumentsTreeview(filteredCollections = null, callback = null) {
171
+ const expandedIds = saveExpandedState();
172
+
173
+ const treeData = convertDocumentsToTreeNode(filteredCollections);
174
+ const customNodeOptions = customDocumentNodeRow();
175
+
176
+ const options = {
177
+ data: treeData,
178
+ ...customNodeOptions,
179
+ showIcon: false,
180
+ showBorder: true,
181
+ showTags: true,
182
+ levels: 1,
183
+ collapseIcon: 'fas fa-chevron-down',
184
+ expandIcon: 'fas fa-chevron-right',
185
+ customizeBadges: function(container, node) {
186
+ container.addClass('document-badges');
187
+ if (node.document_id) {
188
+ container.addClass('document-badges-left');
189
+ }
190
+ if (!hideCodeCounts && node.tags && node.tags.length > 0) {
191
+ container.append($('<span>', {
192
+ class: 'badge badge-secondary badge-fixed-width',
193
+ title: node.document_id ? gettext('Highlights count') : gettext('Documents count'),
194
+ text: node.tags[0]
195
+ }));
196
+ }
197
+ }
198
+ };
199
+
200
+ options.multiSelect = true;
201
+
202
+ treeview_documents.treeview(options);
203
+
204
+ if (callback) {
205
+ setTimeout(() => {
206
+ if (expandedIds.length > 0) {
207
+ restoreExpandedState(expandedIds, callback);
208
+ } else {
209
+ callback();
210
+ }
211
+ }, 50);
212
+ } else if (expandedIds.length > 0) {
213
+ restoreExpandedState(expandedIds, null);
214
+ }
215
+
216
+ if (filteredCollections) {
217
+ treeview_documents.treeview('expandAll');
218
+ } else if (expandedIds.length === 0) {
219
+ treeview_documents.treeview('collapseAll');
220
+ }
221
+
222
+ treeview_documents.on('nodeExpanded', function(event, node) {
223
+ if (node.nodes && node.nodes.length === 1 && node.nodes[0].isDummy) {
224
+ event.preventDefault();
225
+ event.stopImmediatePropagation();
226
+ treeview_documents.treeview('collapseNode', [node.nodeId, {silent: true}]);
227
+ return false;
228
+ }
229
+ });
230
+
231
+ treeview_documents.on('nodeSelected nodeUnselected nodeCollapsed nodeExpanded', function (event, node) {
232
+ if (event.type === 'nodeCollapsed' || event.type === 'nodeExpanded') {
233
+ deselectAllDocumentNodes();
234
+ hideDragHint();
235
+ }
236
+
237
+ if (event.type === 'nodeSelected' || event.type === 'nodeUnselected') {
238
+ const selectedNodes = treeview_documents.treeview('getSelected');
239
+ const selectedDocuments = selectedNodes.filter(node =>
240
+ node.id && node.id.startsWith('document_')
241
+ );
242
+
243
+ if (event.type === 'nodeSelected' && node.id && node.id.startsWith('document_')) {
244
+ showDragHint(selectedDocuments.length, node.nodeId);
245
+ } else if (selectedDocuments.length <= 1) {
246
+ hideDragHint();
247
+ }
248
+ }
249
+
250
+ setTimeout(function () {
251
+ addDocumentDragDrop();
252
+
253
+ }, 10);
254
+ });
255
+
256
+ setTimeout(function() {
257
+ addDocumentDragDrop();
258
+ }, 10);
259
+ }
260
+
261
+ function updateDocumentTreeviewHighlight(current_document) {
262
+ if (current_document) {
263
+ const treeviewDocumentLink = document.getElementById('document-link-' + current_document);
264
+ if (treeviewDocumentLink) {
265
+ treeviewDocumentLink.classList.add('document-link-current');
266
+ }
267
+ }
268
+ }
269
+
270
+ function findDocumentParentCollection(documentId) {
271
+ if (!treeview_documents.length) return null;
272
+
273
+ const allNodes = treeview_documents.treeview('getEnabled');
274
+ const document = documents[documentId];
275
+
276
+ if (!document) return null;
277
+
278
+ const docCollections = document.collections || [];
279
+
280
+ if (docCollections.length > 0) {
281
+ const collectionId = docCollections[0];
282
+ const collectionNode = allNodes.find(node => node.id === `collection_${collectionId}`);
283
+ return collectionNode ? collectionNode.nodeId : null;
284
+ } else {
285
+ const uncategorizedNode = allNodes.find(node => node.id === 'uncategorized');
286
+ return uncategorizedNode ? uncategorizedNode.nodeId : null;
287
+ }
288
+ }
289
+
290
+ function findDocumentRootParent(collectionId) {
291
+ const collection = findCollection(collectionId);
292
+ if (collection && collection.parent) {
293
+ return findDocumentRootParent(collection.parent);
294
+ }
295
+ return collectionId;
296
+ }
297
+
298
+ function expandDocumentNodeParent(collectionParent) {
299
+ const rootParentId = findDocumentRootParent(collectionParent);
300
+ const nodeId = getNodeIdFromTreeviewByCollectionId(treeview_documents, rootParentId);
301
+ expandDocumentNode(treeview_documents, nodeId);
302
+ }
303
+
304
+ function expandDocumentNode(treeview, nodeId) {
305
+ const node = getNode(treeview, nodeId);
306
+ if (!node.state.expanded) {
307
+ treeview.treeview('expandNode', [parseInt(nodeId, 10),
308
+ {levels: 5, silent: false}]);
309
+ }
310
+ }
311
+
312
+ function highlightCollection(collectionId) {
313
+ const nodeId = getNodeIdFromTreeviewByCollectionId(treeview_documents, collectionId);
314
+ const nodeElement = treeview_documents.find(`li[data-nodeid="${nodeId}"]`);
315
+
316
+ if (nodeElement.length > 0) {
317
+ nodeElement[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
318
+ }
319
+
320
+ nodeElement.addClass('highlight-collection');
321
+ setTimeout(() => {
322
+ nodeElement.addClass('fade-out');
323
+ setTimeout(() => {
324
+ nodeElement.removeClass('highlight-collection fade-out');
325
+ }, 1000);
326
+ }, 3000);
327
+ }
328
+
329
+ function highlightDocument(documentId) {
330
+ const nodeId = getNodeIdFromTreeviewByDocumentId(treeview_documents, documentId);
331
+ const nodeElement = treeview_documents.find(`li[data-nodeid="${nodeId}"]`);
332
+
333
+ if (nodeElement.length > 0) {
334
+ nodeElement[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
335
+ }
336
+
337
+ nodeElement.addClass('highlight-document');
338
+ setTimeout(() => {
339
+ nodeElement.addClass('fade-out');
340
+ }, 3000);
341
+ }
342
+
343
+ function getNodeIdFromTreeviewByCollectionId(treeview, collectionId) {
344
+ const allNodes = treeview.treeview('getEnabled');
345
+ const node = allNodes.find(node => node.id === `collection_${collectionId}`);
346
+ return node ? node.nodeId : null;
347
+ }
348
+
349
+ function getNodeIdFromTreeviewByDocumentId(treeview, documentId) {
350
+ const allNodes = treeview.treeview('getEnabled');
351
+ const node = allNodes.find(node => node.id === `document_${documentId}`);
352
+ return node ? node.nodeId : null;
353
+ }
354
+
355
+ function getDocumentIdFromTreeviewNode(treeview, nodeId) {
356
+ const allNodes = treeview.treeview('getEnabled');
357
+ const node = allNodes.find(item => item.nodeId === parseInt(nodeId));
358
+
359
+ if (node && node.id && node.id.startsWith('document_')) {
360
+ return parseInt(node.id.replace('document_', ''));
361
+ }
362
+
363
+ log('getDocumentIdFromTreeviewNode - node not a document:', 'warn', node);
364
+ return null;
365
+ }
366
+
367
+ function deselectAllDocumentNodes() {
368
+ const allNodes = treeview_documents.treeview('getEnabled');
369
+ allNodes.forEach(node => {
370
+ if (node.state.selected) {
371
+ treeview_documents.treeview('unselectNode', [node.nodeId, {silent: false}]);
372
+ }
373
+ });
374
+ }
375
+
376
+ function showDragHint(selectedCount, lastSelectedNodeId) {
377
+
378
+ $('#drag-hint').remove();
379
+
380
+ if (selectedCount > 1) {
381
+ const message = gettext("You can drag and drop %s documents to a collection").replace('%s', selectedCount);
382
+ const popup = $('<div id="drag-hint" class="drag-hint-popup">' +
383
+ '<i class="fas fa-grip-vertical"></i> ' + message +
384
+ '</div>');
385
+
386
+ $('#treeview-documents').before(popup);
387
+
388
+ setTimeout(() => {
389
+ $('#drag-hint').fadeOut(300, function() {
390
+ $(this).remove();
391
+ });
392
+ }, 3000);
393
+ }
394
+ }
395
+
396
+ function hideDragHint() {
397
+ $('#drag-hint').fadeOut(200, function() {
398
+ $(this).remove();
399
+ });
400
+ }
401
+
402
+ function addDocumentDragDrop() {
403
+
404
+ $('.node-treeview-documents').on('dragstart', function (event) {
405
+ const nodeId = $(this).data('nodeid');
406
+ const documentId = getDocumentIdFromTreeviewNode(treeview_documents, nodeId);
407
+
408
+ // Only allow dragging documents, not collections
409
+ if (!documentId) {
410
+ event.preventDefault();
411
+ return false;
412
+ }
413
+
414
+ // Get all selected documents
415
+ const selectedNodes = treeview_documents.treeview('getSelected');
416
+ const selectedDocumentIds = selectedNodes
417
+ .map(node => getDocumentIdFromTreeviewNode(treeview_documents, node.nodeId))
418
+ .filter(id => id !== null);
419
+
420
+ // If no documents are selected, use only the dragged document
421
+ const documentsToMove = selectedDocumentIds.length > 0 ? selectedDocumentIds : [documentId];
422
+
423
+ // Store document IDs in dataTransfer
424
+ event.originalEvent.dataTransfer.setData('text', JSON.stringify(documentsToMove));
425
+ });
426
+
427
+ $(".node-treeview-documents").each(function (index, element) {
428
+
429
+ $(element).off('drop');
430
+ $(element).off('dragover');
431
+
432
+ $(element).on('dragover', function (event) {
433
+ event.preventDefault();
434
+ });
435
+
436
+ $(element).on('drop', function (event) {
437
+ event.preventDefault();
438
+
439
+ const collectionId = getCollectionIdFromTreeviewNode(treeview_documents, $(element).data("nodeid"));
440
+ const draggedData = event.originalEvent.dataTransfer.getData('text');
441
+
442
+ // Hide drag hint after drop
443
+ hideDragHint();
444
+
445
+ try {
446
+ // Try to parse as JSON (multiple documents)
447
+ const selectedDocumentIds = JSON.parse(draggedData);
448
+ if (Array.isArray(selectedDocumentIds)) {
449
+ // Move all selected documents with sequential delay
450
+ selectedDocumentIds.forEach((documentId, index) => {
451
+ // Check if document is already in this collection
452
+ const document = documents[documentId.toString()];
453
+ const currentCollections = document ? document.collections || [] : [];
454
+ const isAlreadyInCollection = currentCollections.includes(collectionId);
455
+
456
+ if (!isAlreadyInCollection) {
457
+ setTimeout(() => {
458
+ moveDocumentToCollection(documentId, collectionId);
459
+ }, index * 200); // 200ms delay between each document
460
+ }
461
+ });
462
+ }
463
+ } catch (e) {
464
+ // Fallback to single document (old format)
465
+ const documentId = parseInt(draggedData);
466
+ if (!isNaN(documentId)) {
467
+ // Check if document is already in this collection
468
+ const document = documents[documentId.toString()];
469
+ const currentCollections = document ? document.collections || [] : [];
470
+ const isAlreadyInCollection = currentCollections.includes(collectionId);
471
+
472
+ if (!isAlreadyInCollection) {
473
+ moveDocumentToCollection(documentId, collectionId);
474
+ }
475
+ }
476
+ }
477
+ });
478
+ });
479
+ }
480
+
481
+ function getCollectionIdFromTreeviewNode(treeview, nodeId) {
482
+ const allNodes = treeview.treeview('getEnabled');
483
+ const node = allNodes.find(item => item.nodeId === parseInt(nodeId));
484
+
485
+ if (node && node.id && node.id.startsWith('collection_')) {
486
+ return parseInt(node.id.replace('collection_', ''));
487
+ }
488
+
489
+ return null;
490
+ }
491
+
492
+ function isUncategorizedNode(nodeId) {
493
+ const allNodes = treeview_documents.treeview('getEnabled');
494
+ const node = allNodes.find(item => item.nodeId === parseInt(nodeId));
495
+ return node && node.id === 'uncategorized';
496
+ }
497
+
498
+ function moveDocumentToCollection(documentId, targetCollectionId) {
499
+ const document = documents[documentId.toString()];
500
+ if (!document) {
501
+ return;
502
+ }
503
+ if (targetCollectionId === undefined || targetCollectionId === null) {
504
+ log('Target collection ID not provided');
505
+ return;
506
+ }
507
+
508
+ const updatedDocument = {
509
+ id: documentId,
510
+ collections: targetCollectionId !== null ? [targetCollectionId] : []
511
+ };
512
+
513
+ submitDocumentUpdate(updatedDocument,
514
+ (response) => {
515
+ log('Document moved successfully:', 'success', {response});
516
+ },
517
+ (error) => {
518
+ log('Error moving document:', 'error', {error});
519
+ }
520
+ );
521
+ }
522
+
523
+ $(document).ready(function() {
524
+ const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
525
+ const cmdKey = isMac ? 'Cmd' : 'Ctrl';
526
+
527
+ const sections = [
528
+ {
529
+ title: gettext('Right-click on a collection:'),
530
+ items: [
531
+ gettext('Add sub-collection'),
532
+ gettext('Edit collection'),
533
+ gettext('Remove parent'),
534
+ gettext('Delete collection')
535
+ ]
536
+ },
537
+ {
538
+ title: gettext('Right-click on a document:'),
539
+ items: [
540
+ gettext('Edit document'),
541
+ gettext('Toggle bookmark'),
542
+ gettext('Delete document')
543
+ ]
544
+ },
545
+ {
546
+ title: gettext('Select multiple documents:'),
547
+ items: [
548
+ gettext('Hold %(key)s and click documents', { key: cmdKey }),
549
+ gettext('Drag selected documents to a collection')
550
+ ]
551
+ }
552
+ ];
553
+
554
+ initHelpPopover('treeview-help-icon', sections);
555
+ });
556
+
557
+
558
+ function saveExpandedState() {
559
+ if (!treeview_documents.length) return [];
560
+ if (!treeview_documents.data('treeview')) return [];
561
+
562
+ try {
563
+ const expandedNodes = treeview_documents.treeview('getExpanded');
564
+ return expandedNodes.map(node => node.id).filter(id => id);
565
+ } catch (e) {
566
+ return [];
567
+ }
568
+ }
569
+
570
+ function restoreExpandedState(expandedIds, callback) {
571
+ if (!expandedIds || expandedIds.length === 0) {
572
+ if (callback) setTimeout(callback, 50);
573
+ return;
574
+ }
575
+
576
+ setTimeout(() => {
577
+ const allNodes = treeview_documents.treeview('getEnabled');
578
+ expandedIds.forEach(id => {
579
+ const node = allNodes.find(n => n.id === id);
580
+ if (node && !node.state.expanded) {
581
+ treeview_documents.treeview('expandNode', [node.nodeId, {silent: true}]);
582
+ }
583
+ });
584
+ addDocumentDragDrop();
585
+
586
+ if (callback) {
587
+ setTimeout(() => {
588
+ callback();
589
+ }, 150);
590
+ }
591
+ }, 50);
592
+ }
593
+
@@ -0,0 +1,18 @@
1
+ # Highlight System
2
+
3
+ SVG overlay approach — the HTML document stays intact, highlights are drawn as lines on top.
4
+
5
+ ## How it works
6
+
7
+ 1. **loader.js** fetches the document and builds two layers: a content div (text) and an SVG element (highlights)
8
+ 2. **renderer.js** converts UTF-8 byte offsets to DOM positions using a char map, then uses `Range.getClientRects()` to get pixel coordinates for each highlight
9
+ 3. Only block elements (`<p>`, `<h1>`-`<h6>`, etc.) that contain highlights get extra line-height — the rest stays normal
10
+ 4. Overlapping highlights are stacked in lanes so they don't collide
11
+ 5. Multi-tag highlights use SVG gradients
12
+ 6. **crud.js** handles create/edit/delete via the API
13
+
14
+ ## Files
15
+
16
+ - `renderer.js` — rendering pipeline, smart lanes, tooltips
17
+ - `loader.js` — document loading, ResizeObserver
18
+ - `crud.js` — CRUD operations