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,16 @@
1
+ @charset "UTF-8";/*!
2
+ * jQuery contextMenu - Plugin for simple contextMenu handling
3
+ *
4
+ * Version: v2.7.0
5
+ *
6
+ * Authors: Björn Brala (SWIS.nl), Rodney Rehm, Addy Osmani (patches for FF)
7
+ * Web: http://swisnl.github.io/jQuery-contextMenu/
8
+ *
9
+ * Copyright (c) 2011-2018 SWIS BV and contributors
10
+ *
11
+ * Licensed under
12
+ * MIT License http://www.opensource.org/licenses/mit-license
13
+ *
14
+ * Date: 2018-10-02T14:29:27.829Z
15
+ */@-webkit-keyframes cm-spin{0%{-webkit-transform:translateY(-50%) rotate(0);transform:translateY(-50%) rotate(0)}100%{-webkit-transform:translateY(-50%) rotate(359deg);transform:translateY(-50%) rotate(359deg)}}@-o-keyframes cm-spin{0%{-webkit-transform:translateY(-50%) rotate(0);-o-transform:translateY(-50%) rotate(0);transform:translateY(-50%) rotate(0)}100%{-webkit-transform:translateY(-50%) rotate(359deg);-o-transform:translateY(-50%) rotate(359deg);transform:translateY(-50%) rotate(359deg)}}@keyframes cm-spin{0%{-webkit-transform:translateY(-50%) rotate(0);-o-transform:translateY(-50%) rotate(0);transform:translateY(-50%) rotate(0)}100%{-webkit-transform:translateY(-50%) rotate(359deg);-o-transform:translateY(-50%) rotate(359deg);transform:translateY(-50%) rotate(359deg)}}@font-face{font-family:context-menu-icons;font-style:normal;font-weight:400;src:url(font/context-menu-icons.eot?2gb3e);src:url(font/context-menu-icons.eot?2gb3e#iefix) format("embedded-opentype"),url(font/context-menu-icons.woff2?2gb3e) format("woff2"),url(font/context-menu-icons.woff?2gb3e) format("woff"),url(font/context-menu-icons.ttf?2gb3e) format("truetype")}.context-menu-icon-add:before{content:"\EA01"}.context-menu-icon-copy:before{content:"\EA02"}.context-menu-icon-cut:before{content:"\EA03"}.context-menu-icon-delete:before{content:"\EA04"}.context-menu-icon-edit:before{content:"\EA05"}.context-menu-icon-loading:before{content:"\EA06"}.context-menu-icon-paste:before{content:"\EA07"}.context-menu-icon-quit:before{content:"\EA08"}.context-menu-icon::before{position:absolute;top:50%;left:0;width:2em;font-family:context-menu-icons;font-size:1em;font-style:normal;font-weight:400;line-height:1;color:#018849;text-align:center;-webkit-transform:translateY(-50%);-ms-transform:translateY(-50%);-o-transform:translateY(-50%);transform:translateY(-50%);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.context-menu-icon.context-menu-hover:before{color:#fff}.context-menu-icon.context-menu-disabled::before{color:#bbb}.context-menu-icon.context-menu-icon-loading:before{-webkit-animation:cm-spin 2s infinite;-o-animation:cm-spin 2s infinite;animation:cm-spin 2s infinite}.context-menu-icon.context-menu-icon--fa{display:list-item;font-family:inherit;line-height:inherit}.context-menu-icon.context-menu-icon--fa::before{position:absolute;top:50%;left:0;width:2em;font-family:FontAwesome;font-size:1em;font-style:normal;font-weight:400;line-height:1;color:#018849;text-align:center;-webkit-transform:translateY(-50%);-ms-transform:translateY(-50%);-o-transform:translateY(-50%);transform:translateY(-50%);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.context-menu-icon.context-menu-icon--fa.context-menu-hover:before{color:#fff}.context-menu-icon.context-menu-icon--fa.context-menu-disabled::before{color:#bbb}.context-menu-icon.context-menu-icon--fa5{display:list-item;font-family:inherit;line-height:inherit}.context-menu-icon.context-menu-icon--fa5 i,.context-menu-icon.context-menu-icon--fa5 svg{position:absolute;top:.3em;left:.5em;color:#018849}.context-menu-icon.context-menu-icon--fa5.context-menu-hover>i,.context-menu-icon.context-menu-icon--fa5.context-menu-hover>svg{color:#fff}.context-menu-icon.context-menu-icon--fa5.context-menu-disabled i,.context-menu-icon.context-menu-icon--fa5.context-menu-disabled svg{color:#bbb}.context-menu-list{position:absolute;display:inline-block;min-width:13em;max-width:26em;padding:.25em 0;margin:.3em;font-family:inherit;font-size:inherit;list-style-type:none;background:#fff;border:1px solid #bebebe;border-radius:.2em;-webkit-box-shadow:0 2px 5px rgba(0,0,0,.5);box-shadow:0 2px 5px rgba(0,0,0,.5)}.context-menu-item{position:relative;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;padding:.2em 2em;color:#2f2f2f;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#fff}.context-menu-separator{padding:0;margin:.35em 0;border-bottom:1px solid #e6e6e6}.context-menu-item>label>input,.context-menu-item>label>textarea{-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text}.context-menu-item.context-menu-hover{color:#fff;cursor:pointer;background-color:#018849}.context-menu-item.context-menu-disabled{color:#bbb;cursor:default;background-color:#fff}.context-menu-input.context-menu-hover{color:#2f2f2f;cursor:default}.context-menu-submenu:after{position:absolute;top:50%;right:.5em;z-index:1;width:0;height:0;content:'';border-color:transparent transparent transparent #2f2f2f;border-style:solid;border-width:.25em 0 .25em .25em;-webkit-transform:translateY(-50%);-ms-transform:translateY(-50%);-o-transform:translateY(-50%);transform:translateY(-50%)}.context-menu-item.context-menu-input{padding:.3em .6em}.context-menu-input>label>*{vertical-align:top}.context-menu-input>label>input[type=checkbox],.context-menu-input>label>input[type=radio]{position:relative;top:.12em;margin-right:.4em}.context-menu-input>label{margin:0}.context-menu-input>label,.context-menu-input>label>input[type=text],.context-menu-input>label>select,.context-menu-input>label>textarea{display:block;width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.context-menu-input>label>textarea{height:7em}.context-menu-item>.context-menu-list{top:.3em;right:-.3em;display:none}.context-menu-item.context-menu-visible>.context-menu-list{display:block}.context-menu-accesskey{text-decoration:underline}
16
+ /*# sourceMappingURL=jquery.contextMenu.min.css.map */
Binary file
@@ -0,0 +1,428 @@
1
+ (function () {
2
+ 'use strict';
3
+
4
+ const NODE_W = 160;
5
+ const NODE_H = 40; // minimum badge height (single-line)
6
+ const INDENT = 190; // horizontal offset per depth level (NODE_W + 30px gap)
7
+ const V_GAP = 14; // vertical gap between nodes
8
+ const LINE_H = 16; // px per text line
9
+ const TEXT_PAD = 12; // top + bottom padding inside badge
10
+ const MAX_CHARS = 16; // chars per line before wrapping
11
+ const PADDING = 24;
12
+ const COUNT_R = 11;
13
+ const SVG_NS = 'http://www.w3.org/2000/svg';
14
+
15
+ let nodeMap = {};
16
+ let rootNodes = [];
17
+ let allEdges = [];
18
+
19
+ // ── Text wrapping ────────────────────────────────────────────────────────────
20
+
21
+ function wrapText(text, maxChars) {
22
+ const words = text.split(/\s+/);
23
+ const lines = [];
24
+ let current = '';
25
+ for (const word of words) {
26
+ if (!current) {
27
+ current = word;
28
+ } else if ((current + ' ' + word).length <= maxChars) {
29
+ current += ' ' + word;
30
+ } else {
31
+ lines.push(current);
32
+ current = word;
33
+ }
34
+ }
35
+ if (current) lines.push(current);
36
+ return lines.length ? lines : [''];
37
+ }
38
+
39
+ function nodeHeight(lines) {
40
+ return Math.max(NODE_H, TEXT_PAD * 2 + lines.length * LINE_H);
41
+ }
42
+
43
+ // ── Tree building ───────────────────────────────────────────────────────────
44
+
45
+ function processTag(tag, parentId) {
46
+ const lines = wrapText(tag.path, MAX_CHARS);
47
+ const node = {
48
+ id: tag.id,
49
+ path: tag.path,
50
+ lines: lines,
51
+ height: nodeHeight(lines),
52
+ color: (tag.color && /^#[0-9a-fA-F]{6}$/.test(tag.color)) ? tag.color : '#6c757d',
53
+ count: tag.count || 0,
54
+ parentId: parentId,
55
+ children: [],
56
+ x: 0,
57
+ y: 0,
58
+ _groupEl: null,
59
+ };
60
+ nodeMap[tag.id] = node;
61
+ for (const child of (tag.children || [])) {
62
+ node.children.push(processTag(child, tag.id));
63
+ }
64
+ return node;
65
+ }
66
+
67
+ function buildNodes(tagsData) {
68
+ nodeMap = {};
69
+ rootNodes = [];
70
+ for (const tag of tagsData) {
71
+ rootNodes.push(processTag(tag, null));
72
+ }
73
+ }
74
+
75
+ // ── Layout ──────────────────────────────────────────────────────────────────
76
+
77
+ function applyDefaultLayout() {
78
+ let y = PADDING;
79
+
80
+ function place(node, depth) {
81
+ node.x = PADDING + depth * INDENT;
82
+ node.y = y;
83
+ y += node.height + V_GAP;
84
+ for (const child of node.children) place(child, depth + 1);
85
+ }
86
+
87
+ rootNodes.forEach((root, i) => {
88
+ if (i > 0) y += V_GAP * 2; // extra gap between separate root trees
89
+ place(root, 0);
90
+ });
91
+ }
92
+
93
+ // ── SVG helpers ─────────────────────────────────────────────────────────────
94
+
95
+ function mk(tag, attrs, children) {
96
+ const e = document.createElementNS(SVG_NS, tag);
97
+ if (attrs) for (const [k, v] of Object.entries(attrs)) e.setAttribute(k, String(v));
98
+ if (children) {
99
+ for (const c of children) {
100
+ e.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
101
+ }
102
+ }
103
+ return e;
104
+ }
105
+
106
+ function luminance(c) {
107
+ if (!c || c.length !== 7 || c[0] !== '#') return 0.5;
108
+ const r = parseInt(c.slice(1, 3), 16);
109
+ const g = parseInt(c.slice(3, 5), 16);
110
+ const b = parseInt(c.slice(5, 7), 16);
111
+ return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
112
+ }
113
+
114
+ function edgePath(parent, child) {
115
+ // S-curve from parent's right-center to child's left-center.
116
+ const x1 = parent.x + NODE_W, y1 = parent.y + parent.height / 2;
117
+ const x2 = child.x, y2 = child.y + child.height / 2;
118
+ const mx = (x1 + x2) / 2;
119
+ return `M${x1.toFixed(1)},${y1.toFixed(1)} C${mx.toFixed(1)},${y1.toFixed(1)} ${mx.toFixed(1)},${y2.toFixed(1)} ${x2.toFixed(1)},${y2.toFixed(1)}`;
120
+ }
121
+
122
+ // ── Delete ──────────────────────────────────────────────────────────────────
123
+
124
+ function deleteNode(node) {
125
+ delete nodeMap[node.id];
126
+ allEdges = allEdges.filter(edge => {
127
+ if (edge.p.id === node.id || edge.c.id === node.id) {
128
+ edge.el.remove();
129
+ return false;
130
+ }
131
+ return true;
132
+ });
133
+ node._groupEl.remove();
134
+ }
135
+
136
+ // ── Drag ────────────────────────────────────────────────────────────────────
137
+
138
+ function getDescendants(node) {
139
+ const result = [node];
140
+ for (const c of node.children) result.push(...getDescendants(c));
141
+ return result;
142
+ }
143
+
144
+ function expandSvg(svg) {
145
+ const allNodes = Object.values(nodeMap);
146
+ const newW = allNodes.reduce((m, n) => Math.max(m, n.x + NODE_W), 0) + PADDING;
147
+ const newH = allNodes.reduce((m, n) => Math.max(m, n.y + n.height), 0) + PADDING;
148
+ const curW = parseFloat(svg.getAttribute('width') || 0);
149
+ const curH = parseFloat(svg.getAttribute('height') || 0);
150
+ if (newW > curW || newH > curH) {
151
+ const w = Math.max(newW, curW);
152
+ const h = Math.max(newH, curH);
153
+ svg.setAttribute('width', w);
154
+ svg.setAttribute('height', h);
155
+ const bg = svg.querySelector('#viz-bg');
156
+ if (bg) { bg.setAttribute('width', w); bg.setAttribute('height', h); }
157
+ }
158
+ }
159
+
160
+ function setupDrag(g, node, svg) {
161
+ let dragging = false;
162
+ let startCX = 0, startCY = 0;
163
+ let startPos = null;
164
+
165
+ g.addEventListener('pointerdown', e => {
166
+ if (e.button !== 0) return;
167
+ e.preventDefault();
168
+ e.stopPropagation();
169
+ dragging = true;
170
+ startCX = e.clientX;
171
+ startCY = e.clientY;
172
+ const desc = getDescendants(node);
173
+ startPos = new Map(desc.map(d => [d.id, { x: d.x, y: d.y }]));
174
+ g.style.cursor = 'grabbing';
175
+ g.setPointerCapture(e.pointerId);
176
+ });
177
+
178
+ g.addEventListener('pointermove', e => {
179
+ if (!dragging) return;
180
+ e.preventDefault();
181
+ const dx = e.clientX - startCX;
182
+ const dy = e.clientY - startCY;
183
+ const desc = getDescendants(node);
184
+ const movedIds = new Set(desc.map(d => d.id));
185
+
186
+ for (const d of desc) {
187
+ const s = startPos.get(d.id);
188
+ d.x = s.x + dx;
189
+ d.y = s.y + dy;
190
+ d._groupEl.setAttribute('transform', `translate(${d.x.toFixed(1)},${d.y.toFixed(1)})`);
191
+ }
192
+ for (const edge of allEdges) {
193
+ if (movedIds.has(edge.p.id) || movedIds.has(edge.c.id)) {
194
+ edge.el.setAttribute('d', edgePath(edge.p, edge.c));
195
+ }
196
+ }
197
+ expandSvg(svg);
198
+ });
199
+
200
+ const end = () => { dragging = false; startPos = null; g.style.cursor = 'grab'; };
201
+ g.addEventListener('pointerup', end);
202
+ g.addEventListener('pointercancel', end);
203
+ }
204
+
205
+ // ── Render ──────────────────────────────────────────────────────────────────
206
+
207
+ function renderViz(container) {
208
+ allEdges = [];
209
+ const allNodes = Object.values(nodeMap);
210
+
211
+ if (!allNodes.length) {
212
+ container.innerHTML = '<p class="text-muted text-center p-5">No codes defined.</p>';
213
+ return null;
214
+ }
215
+
216
+ const maxX = allNodes.reduce((m, n) => Math.max(m, n.x + NODE_W), 0) + PADDING;
217
+ const maxY = allNodes.reduce((m, n) => Math.max(m, n.y + n.height), 0) + PADDING;
218
+
219
+ const svg = mk('svg', {
220
+ xmlns: 'http://www.w3.org/2000/svg',
221
+ width: maxX, height: maxY,
222
+ 'font-family': 'Arial, Helvetica, sans-serif',
223
+ });
224
+
225
+ svg.appendChild(mk('rect', { id: 'viz-bg', width: maxX, height: maxY, fill: '#ffffff' }));
226
+
227
+ const edgeLayer = mk('g');
228
+ const nodeLayer = mk('g');
229
+ svg.appendChild(edgeLayer);
230
+ svg.appendChild(nodeLayer);
231
+
232
+ // Edges
233
+ for (const node of allNodes) {
234
+ for (const child of node.children) {
235
+ const pathEl = mk('path', {
236
+ d: edgePath(node, child),
237
+ fill: 'none',
238
+ stroke: '#ced4da',
239
+ 'stroke-width': 2,
240
+ });
241
+ edgeLayer.appendChild(pathEl);
242
+ allEdges.push({ el: pathEl, p: node, c: child });
243
+ }
244
+ }
245
+
246
+ // Nodes
247
+ for (const node of allNodes) {
248
+ const h = node.height;
249
+ const lum = luminance(node.color);
250
+ const textColor = lum > 0.55 ? '#212529' : '#ffffff';
251
+ const countStr = node.count > 999 ? '999+' : String(node.count);
252
+ const bx = NODE_W - COUNT_R - 3;
253
+ const by = COUNT_R + 3;
254
+
255
+ const g = mk('g', {
256
+ transform: `translate(${node.x.toFixed(1)},${node.y.toFixed(1)})`,
257
+ cursor: 'grab',
258
+ });
259
+
260
+ const title = document.createElementNS(SVG_NS, 'title');
261
+ title.textContent = `${node.path} — ${node.count} highlight${node.count !== 1 ? 's' : ''}`;
262
+ g.appendChild(title);
263
+
264
+ // Badge background
265
+ g.appendChild(mk('rect', { width: NODE_W, height: h, rx: 8, ry: 8, fill: node.color }));
266
+
267
+ // Wrapped label — lines vertically centered in the badge
268
+ const totalTextH = node.lines.length * LINE_H;
269
+ const firstBaseline = (h - totalTextH) / 2 + LINE_H * 0.78;
270
+ const textEl = mk('text', {
271
+ 'text-anchor': 'middle',
272
+ fill: textColor,
273
+ 'font-size': 12,
274
+ 'font-weight': 500,
275
+ });
276
+ node.lines.forEach((line, i) => {
277
+ const tspan = document.createElementNS(SVG_NS, 'tspan');
278
+ tspan.setAttribute('x', NODE_W / 2);
279
+ tspan.setAttribute('y', (firstBaseline + i * LINE_H).toFixed(1));
280
+ tspan.textContent = line;
281
+ textEl.appendChild(tspan);
282
+ });
283
+ g.appendChild(textEl);
284
+
285
+ // Count badge
286
+ g.appendChild(mk('circle', {
287
+ cx: bx, cy: by, r: COUNT_R,
288
+ fill: '#ffffff', stroke: node.color, 'stroke-width': 1.5,
289
+ }));
290
+ g.appendChild(mk('text', {
291
+ x: bx, y: by,
292
+ 'dominant-baseline': 'middle', 'text-anchor': 'middle',
293
+ fill: '#212529', 'font-size': 9, 'font-weight': 'bold',
294
+ }, [countStr]));
295
+
296
+ // Delete button (top-left corner, mirrors count badge at top-right)
297
+ const delBx = COUNT_R + 3, delBy = COUNT_R + 3;
298
+ const delBtn = mk('g', { cursor: 'pointer', 'data-viz-delete': '1' });
299
+ delBtn.appendChild(mk('circle', {
300
+ cx: delBx, cy: delBy, r: COUNT_R,
301
+ fill: '#ffffff', stroke: node.color, 'stroke-width': 1.5,
302
+ }));
303
+ delBtn.appendChild(mk('text', {
304
+ x: delBx, y: delBy,
305
+ 'dominant-baseline': 'middle', 'text-anchor': 'middle',
306
+ fill: '#666', 'font-size': 13, 'pointer-events': 'none',
307
+ }, ['×']));
308
+ delBtn.addEventListener('pointerdown', e => e.stopPropagation());
309
+ delBtn.addEventListener('click', e => { e.stopPropagation(); deleteNode(node); });
310
+ g.appendChild(delBtn);
311
+
312
+ node._groupEl = g;
313
+ nodeLayer.appendChild(g);
314
+ setupDrag(g, node, svg);
315
+ }
316
+
317
+ container.innerHTML = '';
318
+ container.appendChild(svg);
319
+ return svg;
320
+ }
321
+
322
+ // ── Export ──────────────────────────────────────────────────────────────────
323
+
324
+ function exportBounds() {
325
+ const allNodes = Object.values(nodeMap);
326
+ const pad = PADDING / 2;
327
+ return {
328
+ minX: allNodes.reduce((m, n) => Math.min(m, n.x), Infinity) - pad,
329
+ minY: allNodes.reduce((m, n) => Math.min(m, n.y), Infinity) - pad,
330
+ maxX: allNodes.reduce((m, n) => Math.max(m, n.x + NODE_W), -Infinity) + pad,
331
+ maxY: allNodes.reduce((m, n) => Math.max(m, n.y + n.height), -Infinity) + pad,
332
+ };
333
+ }
334
+
335
+ function cloneForExport(svg, transparent) {
336
+ const { minX, minY, maxX, maxY } = exportBounds();
337
+ const w = maxX - minX, h = maxY - minY;
338
+ const clone = svg.cloneNode(true);
339
+ clone.setAttribute('width', w);
340
+ clone.setAttribute('height', h);
341
+ clone.setAttribute('viewBox', `${minX} ${minY} ${w} ${h}`);
342
+ clone.querySelectorAll('[data-viz-delete]').forEach(el => el.remove());
343
+ const bg = clone.querySelector('#viz-bg');
344
+ if (transparent) {
345
+ if (bg) bg.remove();
346
+ } else if (bg) {
347
+ bg.setAttribute('x', minX); bg.setAttribute('y', minY);
348
+ bg.setAttribute('width', w); bg.setAttribute('height', h);
349
+ }
350
+ return clone;
351
+ }
352
+
353
+ function exportSVG(svg, transparent) {
354
+ if (!svg || !Object.keys(nodeMap).length) return;
355
+ const clone = cloneForExport(svg, transparent);
356
+ const svgStr = '<?xml version="1.0" encoding="UTF-8"?>\n' + new XMLSerializer().serializeToString(clone);
357
+ download(new Blob([svgStr], { type: 'image/svg+xml;charset=utf-8' }), `${datePrefix()}_codebook.svg`);
358
+ }
359
+
360
+ function exportPNG(svg, transparent) {
361
+ if (!svg || !Object.keys(nodeMap).length) return;
362
+ const { minX, minY, maxX, maxY } = exportBounds();
363
+ const w = maxX - minX, h = maxY - minY;
364
+ const scale = 2;
365
+
366
+ const clone = cloneForExport(svg, transparent);
367
+ const svgStr = new XMLSerializer().serializeToString(clone);
368
+ const svgUrl = URL.createObjectURL(new Blob([svgStr], { type: 'image/svg+xml;charset=utf-8' }));
369
+
370
+ const img = new Image();
371
+ img.onload = () => {
372
+ const canvas = document.createElement('canvas');
373
+ canvas.width = Math.round(w * scale);
374
+ canvas.height = Math.round(h * scale);
375
+ const ctx = canvas.getContext('2d');
376
+ if (!transparent) { ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, canvas.width, canvas.height); }
377
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
378
+ URL.revokeObjectURL(svgUrl);
379
+ canvas.toBlob(blob => download(blob, `${datePrefix()}_codebook.png`), 'image/png');
380
+ };
381
+ img.src = svgUrl;
382
+ }
383
+
384
+ function download(blob, filename) {
385
+ const url = URL.createObjectURL(blob);
386
+ const a = Object.assign(document.createElement('a'), { href: url, download: filename });
387
+ document.body.appendChild(a);
388
+ a.click();
389
+ document.body.removeChild(a);
390
+ URL.revokeObjectURL(url);
391
+ }
392
+
393
+ function datePrefix() {
394
+ const d = new Date();
395
+ return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
396
+ }
397
+
398
+ // ── Init ────────────────────────────────────────────────────────────────────
399
+
400
+ document.addEventListener('DOMContentLoaded', () => {
401
+ const modal = document.getElementById('codebook-viz-modal');
402
+ const container = document.getElementById('codebook-viz-container');
403
+ if (!modal || !container) return;
404
+
405
+ let currentSvg = null;
406
+
407
+ const trigger = document.getElementById('codebook-viz-trigger');
408
+ if (trigger) {
409
+ trigger.addEventListener('click', (e) => {
410
+ e.stopPropagation();
411
+ const toggleEl = trigger.closest('.dropdown')?.querySelector('[data-bs-toggle="dropdown"]');
412
+ bootstrap.Dropdown.getInstance(toggleEl)?.hide();
413
+ buildNodes(tags);
414
+ applyDefaultLayout();
415
+ currentSvg = renderViz(container);
416
+ bootstrap.Modal.getOrCreateInstance(modal).show();
417
+ });
418
+ }
419
+
420
+ const transparentChk = document.getElementById('codebook-viz-transparent');
421
+ const getTransparent = () => transparentChk ? transparentChk.checked : false;
422
+
423
+ const btnSvg = document.getElementById('codebook-viz-export-svg');
424
+ const btnPng = document.getElementById('codebook-viz-export-png');
425
+ if (btnSvg) btnSvg.addEventListener('click', () => exportSVG(currentSvg, getTransparent()));
426
+ if (btnPng) btnPng.addEventListener('click', () => exportPNG(currentSvg, getTransparent()));
427
+ });
428
+ })();
@@ -0,0 +1,180 @@
1
+ function addCollectionContextMenu() {
2
+ $.contextMenu({
3
+ selector: '.collection-context-menu',
4
+ build: function ($trigger) {
5
+ const nodeId = $trigger.attr('id');
6
+ const collectionId = extractCollectionId(nodeId);
7
+
8
+ if (!collectionId) return false;
9
+
10
+ const collection = findCollection(collectionId);
11
+ if (!collection) return false;
12
+
13
+ let items = generateCollectionContextMenuItems(collection);
14
+
15
+ return {
16
+ callback: (key, options) => collectionContextMenuAction(key, options.$trigger.attr("id")),
17
+ items
18
+ };
19
+ }
20
+ });
21
+ }
22
+
23
+ function generateCollectionContextMenuItems(collection) {
24
+ const items = {
25
+ "addChildCollection": {
26
+ name: gettext('Add sub-collection'),
27
+ icon: 'fas fa-plus-circle',
28
+ callback: (key, options) => collectionContextMenuAction('addChildCollection', collection.id)
29
+ },
30
+ "edit": {
31
+ name: gettext('Edit collection'),
32
+ icon: "fas fa-edit",
33
+ callback: (key, options) => collectionContextMenuAction('edit', collection.id)
34
+ }
35
+ };
36
+
37
+ if (collection.parent) {
38
+ items["removeParent"] = {
39
+ name: gettext('Remove parent'),
40
+ icon: "fas fa-level-up-alt",
41
+ callback: (key, options) => collectionContextMenuAction('removeParent', collection.id)
42
+ };
43
+ }
44
+
45
+ items["separator1"] = "---------";
46
+ items["delete"] = {
47
+ name: gettext('Delete collection'),
48
+ icon: "fas fa-trash",
49
+ callback: (key, options) => collectionContextMenuAction('delete', collection.id)
50
+ };
51
+
52
+ return items;
53
+ }
54
+
55
+ function collectionContextMenuAction(action, param) {
56
+ const actionFunction = collectionActions[action];
57
+
58
+ if (actionFunction) {
59
+ actionFunction(param);
60
+ } else {
61
+ log('No action found for:', action);
62
+ }
63
+ }
64
+
65
+ const collectionActions = {
66
+ addChildCollection: collectionId => createChildCollection(collectionId),
67
+ edit: collectionId => editCollection(collectionId),
68
+ removeParent: collectionId => removeCollectionParent(collectionId),
69
+ delete: collectionId => deleteCollection(collectionId)
70
+ };
71
+
72
+ function extractCollectionId(nodeId) {
73
+ if (!nodeId) return null;
74
+
75
+ const match = nodeId.match(/collection_(\d+)/);
76
+ return match ? parseInt(match[1]) : null;
77
+ }
78
+
79
+ function findCollection(collectionId) {
80
+
81
+ function searchInTree(collections) {
82
+ if (!collections || !Array.isArray(collections)) {
83
+ return null;
84
+ }
85
+
86
+ for (let collection of collections) {
87
+ if (collection.id === collectionId) {
88
+ return collection;
89
+ }
90
+ if (collection.children && collection.children.length > 0) {
91
+ const found = searchInTree(collection.children);
92
+ if (found) return found;
93
+ }
94
+ }
95
+ return null;
96
+ }
97
+
98
+ const result = searchInTree(collections);
99
+ return result;
100
+ }
101
+
102
+
103
+ let selectedCollectionId = null;
104
+
105
+
106
+ function handleCollectionKeyboardShortcuts(e) {
107
+
108
+ if ($('.collection-context-menu').length === 0) return;
109
+
110
+ // if (e.key.toLowerCase() === 'm') {
111
+ // e.preventDefault();
112
+ // if (selectedCollectionId) {
113
+ // const element = $(`.collection-context-menu[id*="collection_${selectedCollectionId}"]`);
114
+ // if (element.length > 0) {
115
+ // element.contextMenu();
116
+ // }
117
+ // }
118
+ // return;
119
+ // }
120
+
121
+ if (selectedCollectionId) {
122
+ switch(e.key.toLowerCase()) {
123
+ case 'e':
124
+ e.preventDefault();
125
+ editCollection(selectedCollectionId);
126
+ break;
127
+ case 'd':
128
+ e.preventDefault();
129
+ deleteCollection(selectedCollectionId);
130
+ break;
131
+ case 'c':
132
+ e.preventDefault();
133
+ createChildCollection(selectedCollectionId);
134
+ break;
135
+ case 'u':
136
+ e.preventDefault();
137
+ const collection = findCollection(selectedCollectionId);
138
+ if (collection && collection.parent) {
139
+ removeCollectionParent(selectedCollectionId);
140
+ }
141
+ break;
142
+ }
143
+ }
144
+ }
145
+
146
+ function updateSelectedCollection(collectionId) {
147
+ selectedCollectionId = collectionId;
148
+
149
+ $('.collection-context-menu').removeClass('collection-selected');
150
+
151
+ if (collectionId) {
152
+ const element = $(`.collection-context-menu[id*="collection_${collectionId}"]`);
153
+ if (element.length > 0) {
154
+ element.addClass('collection-selected');
155
+ }
156
+ }
157
+ }
158
+
159
+ function addCollectionSelectionHandlers() {
160
+ $(document).on('mouseenter', '.collection-context-menu', function() {
161
+ const nodeId = $(this).attr('id');
162
+ const collectionId = extractCollectionId(nodeId);
163
+ if (collectionId) {
164
+ updateSelectedCollection(collectionId);
165
+ }
166
+ });
167
+
168
+ $(document).on('click', function(e) {
169
+ if (!$(e.target).closest('.collection-context-menu').length) {
170
+ updateSelectedCollection(null);
171
+ }
172
+ });
173
+ }
174
+
175
+ $(document).ready(function() {
176
+ addCollectionContextMenu();
177
+ addCollectionSelectionHandlers();
178
+ $(document).on('keydown', handleCollectionKeyboardShortcuts);
179
+ });
180
+