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.
- CHANGELOG.md +481 -0
- libreqda-1.2.0.dist-info/LICENSE.txt +28 -0
- libreqda-1.2.0.dist-info/METADATA +131 -0
- libreqda-1.2.0.dist-info/RECORD +255 -0
- libreqda-1.2.0.dist-info/WHEEL +4 -0
- libreqda-1.2.0.dist-info/entry_points.txt +4 -0
- scripts/babelrc +8 -0
- scripts/dist/LICENSE.txt +33 -0
- scripts/dist/RELEASE.txt +23 -0
- scripts/dist/macos/libreqda.icns +0 -0
- scripts/dist/macos/macos.spec +43 -0
- scripts/dist/macos/plist.patch +10 -0
- scripts/dist/macos/taguette.icns +0 -0
- scripts/dist/macos/taguette_console_wrapper +4 -0
- scripts/dist/macos.sh +32 -0
- scripts/dist/pyinstaller_entrypoint.py +19 -0
- scripts/dist/windows/windows-installer.iss +51 -0
- scripts/dist/windows/windows.spec +39 -0
- scripts/edugain/worker_update_edugain_idps.py +89 -0
- scripts/edugain/worker_update_edugain_idps.sh +9 -0
- scripts/make_password_reset_link.py +56 -0
- scripts/make_theme.py +99 -0
- scripts/new_db_revision.sh +8 -0
- scripts/register_user.py +76 -0
- scripts/update_pot.sh +33 -0
- scripts/update_translations.sh +17 -0
- scripts/validate.py +140 -0
- scripts/wipe_and_disable_user.py +90 -0
- taguette/__init__.py +13 -0
- taguette/access_control.py +145 -0
- taguette/convert.py +623 -0
- taguette/database/__init__.py +177 -0
- taguette/database/base.py +9 -0
- taguette/database/copy.py +433 -0
- taguette/database/models.py +920 -0
- taguette/demodata.py +688 -0
- taguette/export.py +500 -0
- taguette/extract.py +287 -0
- taguette/import_codebook.py +110 -0
- taguette/l10n/de_DE/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/de_DE/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/el_EL/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/el_GR/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/el_GR/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/en_US/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/en_US/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/es_ES/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/es_ES/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/fr_FR/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/fr_FR/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/id_ID/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/id_ID/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/it_IT/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/it_IT/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/nl_NL/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/nl_NL/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/pt_PT/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/pt_PT/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/sv_SE/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/sv_SE/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/loki_handler.py +107 -0
- taguette/main.py +649 -0
- taguette/migrations/env.py +72 -0
- taguette/migrations/script.py.mako +24 -0
- taguette/migrations/versions/09c662cd9483_make_documents_filename_not_null.py +39 -0
- taguette/migrations/versions/1b29e68900c1_add_user_roles.py +55 -0
- taguette/migrations/versions/1b96a606551d_update_user_to_have_a_display_name.py +34 -0
- taguette/migrations/versions/2467bab602b7_add_color_column_to_tags_table.py +33 -0
- taguette/migrations/versions/382db36f20f4_add_user_disabled.py +66 -0
- taguette/migrations/versions/43d6c240309d_remove_document_groups.py +81 -0
- taguette/migrations/versions/447d636f72c5_cascades.py +40 -0
- taguette/migrations/versions/491de2dc7cd7_add_indexes.py +51 -0
- taguette/migrations/versions/4c55b3b3ff30_user_login_length.py +58 -0
- taguette/migrations/versions/61a4abcc8769_user_with_parent_for_permissions.py +40 -0
- taguette/migrations/versions/6489b5f9cfb5_increase_password_hash_size.py +34 -0
- taguette/migrations/versions/679f625e6e6a_fix_unique_tags.py +59 -0
- taguette/migrations/versions/77c4543a5b9b_add_parent_id_column_to_collection_table.py +41 -0
- taguette/migrations/versions/7c94b7a31e69_add_is_archived_column_to_projets.py +39 -0
- taguette/migrations/versions/7f56de97e3c8_change_edugain_displayname_length.py +42 -0
- taguette/migrations/versions/807c0cc0ecf6_remove_invalid_hltags.py +43 -0
- taguette/migrations/versions/80b1cc9d4c22_add_user_email.py +35 -0
- taguette/migrations/versions/91ade71ccf4d_add_user_language.py +28 -0
- taguette/migrations/versions/955db2e523d1_add_collections_tables_and_associations_.py +60 -0
- taguette/migrations/versions/a1b2c3d4e5f6_add_user_hide_code_counts.py +28 -0
- taguette/migrations/versions/b23f3b7a638e_add_user_password_change_date.py +36 -0
- taguette/migrations/versions/b7c1e2d3f4a5_add_project_compact_layout.py +28 -0
- taguette/migrations/versions/bc8e0e0677e9_rename_command_fields.py +126 -0
- taguette/migrations/versions/bce44849c2f2_lowercase_logins.py +96 -0
- taguette/migrations/versions/ca076140922f_update_user_for_external_logins.py +32 -0
- taguette/migrations/versions/d86c45d7aa8a_add_document_direction.py +127 -0
- taguette/migrations/versions/db5e31a0233d_remove_blank_highlights.py +88 -0
- taguette/migrations/versions/dd37d9027a24_unique_tags.py +68 -0
- taguette/migrations/versions/de678cef2a94_add_project_id_to_collections_and_.py +41 -0
- taguette/migrations/versions/e459991d960e_add_table_edugainidps.py +39 -0
- taguette/migrations/versions/e4cf92942271_add_index.py +29 -0
- taguette/migrations/versions/e4e090a5b511_remove_commands_document_id_fk.py +38 -0
- taguette/migrations/versions/e9a9f1c77971_add_favorite_column_to_document.py +32 -0
- taguette/migrations/versions/ecb4065de575_don_t_use_server_default_now_timezone_.py +44 -0
- taguette/migrations/versions/fbdc29566f62_edugain_fix_field_type.py +42 -0
- taguette/migrations/versions/fdc6eb54b034_add_parent_id_column_to_tags_table.py +31 -0
- taguette/static/README.md +34 -0
- taguette/static/alt_templates/.gitkeep +0 -0
- taguette/static/assets/test/logos/monsters-university-logo.png +0 -0
- taguette/static/css/custom.css +1696 -0
- taguette/static/css/fa-all.min.css +5 -0
- taguette/static/css/themed.css +3 -0
- taguette/static/css/themed.css.map +25 -0
- taguette/static/css/vendor/bootstrap-treeview.min.css +1 -0
- taguette/static/css/vendor/bootstrap.min.css +6 -0
- taguette/static/css/vendor/font/context-menu-icons.eot +0 -0
- taguette/static/css/vendor/font/context-menu-icons.ttf +0 -0
- taguette/static/css/vendor/font/context-menu-icons.woff +0 -0
- taguette/static/css/vendor/font/context-menu-icons.woff2 +0 -0
- taguette/static/css/vendor/hierarchy-select.min.css +1 -0
- taguette/static/css/vendor/introjs.min.css +2 -0
- taguette/static/css/vendor/jquery.contextMenu.min.css +16 -0
- taguette/static/favicon.ico +0 -0
- taguette/static/js/codebook-viz.js +428 -0
- taguette/static/js/collection/context-menu.js +180 -0
- taguette/static/js/collection/hierarchy-select.js +71 -0
- taguette/static/js/collection/manager.js +217 -0
- taguette/static/js/document/context-menu.js +92 -0
- taguette/static/js/document/custom-select.js +82 -0
- taguette/static/js/document/manager.js +943 -0
- taguette/static/js/document/treeview.js +593 -0
- taguette/static/js/highlight/README.md +18 -0
- taguette/static/js/highlight/crud.js +119 -0
- taguette/static/js/highlight/extracts.js +497 -0
- taguette/static/js/highlight/loader.js +109 -0
- taguette/static/js/highlight/renderer.js +405 -0
- taguette/static/js/login.js +15 -0
- taguette/static/js/permissions.js +153 -0
- taguette/static/js/polling-event.js +145 -0
- taguette/static/js/project/intro.js +524 -0
- taguette/static/js/project/manager.js +0 -0
- taguette/static/js/project/search.js +124 -0
- taguette/static/js/tag/context-menu.js +133 -0
- taguette/static/js/tag/explore-codes.js +400 -0
- taguette/static/js/tag/hierarchy-select.js +69 -0
- taguette/static/js/tag/manager.js +545 -0
- taguette/static/js/tag/treeview.js +613 -0
- taguette/static/js/taguette.js +1790 -0
- taguette/static/js/utilities.js +714 -0
- taguette/static/js/vendor/bootstrap-treeview-custum.js +421 -0
- taguette/static/js/vendor/bootstrap-treeview-custum.min.js +1 -0
- taguette/static/js/vendor/bootstrap-treeview-default.min.js +1 -0
- taguette/static/js/vendor/bootstrap.bundle.min.js +7 -0
- taguette/static/js/vendor/bootstrap.bundle.min.js.map +1 -0
- taguette/static/js/vendor/hierarchy-select.min.js +1 -0
- taguette/static/js/vendor/introjs.min.js +17 -0
- taguette/static/js/vendor/jquery-3.7.1.js +2 -0
- taguette/static/js/vendor/jquery.contextMenu.min.js +2 -0
- taguette/static/libreqda.ico +0 -0
- taguette/static/logo-h.png +0 -0
- taguette/static/logo.png +0 -0
- taguette/static/logo_blanc.png +0 -0
- taguette/static/missing.png +0 -0
- taguette/static/robots.txt +2 -0
- taguette/static/scss/components/_badge.scss +42 -0
- taguette/static/scss/components/_button.scss +169 -0
- taguette/static/scss/components/_checkbox.scss +40 -0
- taguette/static/scss/components/_context_menu.scss +28 -0
- taguette/static/scss/components/_custom_select.scss +150 -0
- taguette/static/scss/components/_dropdown.scss +43 -0
- taguette/static/scss/components/_form.scss +54 -0
- taguette/static/scss/components/_modal.scss +57 -0
- taguette/static/scss/components/_project_card.scss +163 -0
- taguette/static/scss/components/_tag.scss +308 -0
- taguette/static/scss/components/_treeview.scss +76 -0
- taguette/static/scss/custom.scss +809 -0
- taguette/static/scss/layout/_credits.scss +62 -0
- taguette/static/scss/layout/_footer.scss +6 -0
- taguette/static/scss/layout/_navigation.scss +165 -0
- taguette/static/scss/layout/_sidebar.scss +37 -0
- taguette/static/scss/themes/default/_variables.scss +103 -0
- taguette/static/scss/themes/default/theme.css +3 -0
- taguette/static/scss/themes/default/theme.css.map +26 -0
- taguette/static/scss/themes/default/theme.scss +4 -0
- taguette/static/webfonts/BadScript-OFL.txt +94 -0
- taguette/static/webfonts/BadScript.ttf +0 -0
- taguette/static/webfonts/Montserrat-Light.ttf +0 -0
- taguette/static/webfonts/Montserrat-OFL.txt +93 -0
- taguette/static/webfonts/fa-brands-400.eot +0 -0
- taguette/static/webfonts/fa-brands-400.svg +1184 -0
- taguette/static/webfonts/fa-brands-400.ttf +0 -0
- taguette/static/webfonts/fa-brands-400.woff +0 -0
- taguette/static/webfonts/fa-brands-400.woff2 +0 -0
- taguette/static/webfonts/fa-regular-400.eot +0 -0
- taguette/static/webfonts/fa-regular-400.svg +467 -0
- taguette/static/webfonts/fa-regular-400.ttf +0 -0
- taguette/static/webfonts/fa-regular-400.woff +0 -0
- taguette/static/webfonts/fa-regular-400.woff2 +0 -0
- taguette/static/webfonts/fa-solid-900.eot +0 -0
- taguette/static/webfonts/fa-solid-900.svg +2618 -0
- taguette/static/webfonts/fa-solid-900.ttf +0 -0
- taguette/static/webfonts/fa-solid-900.woff +0 -0
- taguette/static/webfonts/fa-solid-900.woff2 +0 -0
- taguette/templates/alt_templates/.gitkeep +0 -0
- taguette/templates/archived_projects.html +50 -0
- taguette/templates/auth/account.html +74 -0
- taguette/templates/auth/cilogon.login.html +89 -0
- taguette/templates/auth/email_reset_password.html +5 -0
- taguette/templates/auth/external_account.html +60 -0
- taguette/templates/auth/login.html +144 -0
- taguette/templates/auth/reset_password.html +27 -0
- taguette/templates/auth/token_needed.html +7 -0
- taguette/templates/base.html +209 -0
- taguette/templates/bug_report.html +158 -0
- taguette/templates/cookies_prompt.html +15 -0
- taguette/templates/downloads.html +127 -0
- taguette/templates/email_bug_report.txt +9 -0
- taguette/templates/email_reset_password.txt +5 -0
- taguette/templates/error.html +8 -0
- taguette/templates/export_codebook.html +20 -0
- taguette/templates/export_document.html +20 -0
- taguette/templates/export_highlights.html +47 -0
- taguette/templates/index.html +347 -0
- taguette/templates/maintenance.html +8 -0
- taguette/templates/messages.js +74 -0
- taguette/templates/modal/change_document.html +68 -0
- taguette/templates/modal/create_collection.html +100 -0
- taguette/templates/modal/create_document.html +84 -0
- taguette/templates/modal/create_highlight.html +38 -0
- taguette/templates/modal/create_tag.html +73 -0
- taguette/templates/modal/explore_codes.html +58 -0
- taguette/templates/modal/manage_collaborators.html +44 -0
- taguette/templates/modal/merge_tag.html +49 -0
- taguette/templates/new_password.html +32 -0
- taguette/templates/partials/alert.html +16 -0
- taguette/templates/partials/codebook_viz_modal.html +34 -0
- taguette/templates/partials/confirm.html +17 -0
- taguette/templates/partials/document_toolbar.html +37 -0
- taguette/templates/partials/hlinfo.html +13 -0
- taguette/templates/partials/project_card.html +44 -0
- taguette/templates/partials/project_card_archived.html +41 -0
- taguette/templates/partials/project_sidebar.html +99 -0
- taguette/templates/partials/spinner.html +11 -0
- taguette/templates/permissions.html +54 -0
- taguette/templates/project.html +288 -0
- taguette/templates/project_delete.html +17 -0
- taguette/templates/project_import.html +122 -0
- taguette/templates/project_import_codebook.html +34 -0
- taguette/templates/project_import_codebook_confirm.html +49 -0
- taguette/templates/project_new.html +31 -0
- taguette/templates/tos.html +8 -0
- taguette/templates/trans.js +7 -0
- taguette/templates/welcome.html +15 -0
- taguette/utils.py +177 -0
- taguette/validate.py +141 -0
- taguette/version_check.py +102 -0
- taguette/web/__init__.py +252 -0
- taguette/web/api.py +1835 -0
- taguette/web/base.py +864 -0
- taguette/web/export.py +338 -0
- taguette/web/views.py +1618 -0
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// NEW selector
|
|
6
|
+
jQuery.expr[':'].contains_insensitive = function(a, i, m) {
|
|
7
|
+
return jQuery(a).text().toUpperCase()
|
|
8
|
+
.indexOf(m[3].toUpperCase()) >= 0;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
if (!Object.entries) {
|
|
12
|
+
Object.entries = function (obj) {
|
|
13
|
+
let ownProps = Object.keys(obj),
|
|
14
|
+
i = ownProps.length,
|
|
15
|
+
resArray = new Array(i); // preallocate the Array
|
|
16
|
+
while (i--) {
|
|
17
|
+
resArray[i] = [ownProps[i], obj[ownProps[i]]];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return resArray;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function sortByKey(array, key, reverse) {
|
|
25
|
+
let orderByX = 1;
|
|
26
|
+
if (reverse) {
|
|
27
|
+
orderByX = -1
|
|
28
|
+
}
|
|
29
|
+
array.sort(function (a, b) {
|
|
30
|
+
if (key(a) < key(b)) {
|
|
31
|
+
return -1 * orderByX;
|
|
32
|
+
} else if (key(a) > key(b)) {
|
|
33
|
+
return orderByX;
|
|
34
|
+
} else {
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function encodeGetParams(params) {
|
|
41
|
+
return Object.entries(params)
|
|
42
|
+
.filter(function (kv) {
|
|
43
|
+
return kv[1] !== undefined;
|
|
44
|
+
})
|
|
45
|
+
.map(function (kv) {
|
|
46
|
+
return kv.map(encodeURIComponent).join("=");
|
|
47
|
+
})
|
|
48
|
+
.join("&");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Don't use RegExp literals https://github.com/python-babel/babel/issues/640
|
|
52
|
+
let _escapeA = new RegExp('&', 'g'),
|
|
53
|
+
_escapeL = new RegExp('<', 'g'),
|
|
54
|
+
_escapeG = new RegExp('>', 'g'),
|
|
55
|
+
_escapeQ = new RegExp('"', 'g'),
|
|
56
|
+
_escapeP = new RegExp("'", 'g');
|
|
57
|
+
|
|
58
|
+
function escapeHtml(s) {
|
|
59
|
+
return s
|
|
60
|
+
.replace(_escapeA, "&")
|
|
61
|
+
.replace(_escapeL, "<")
|
|
62
|
+
.replace(_escapeG, ">")
|
|
63
|
+
.replace(_escapeQ, """)
|
|
64
|
+
.replace(_escapeP, "'");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function nextElement(node) {
|
|
68
|
+
while (node && !node.nextSibling) {
|
|
69
|
+
node = node.parentNode;
|
|
70
|
+
}
|
|
71
|
+
if (!node) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
node = node.nextSibling;
|
|
75
|
+
while (node.firstChild) {
|
|
76
|
+
node = node.firstChild;
|
|
77
|
+
}
|
|
78
|
+
return node;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let onlyWhitespace = new RegExp('^[\r\n\t]*$');
|
|
82
|
+
|
|
83
|
+
function getScrollPos() {
|
|
84
|
+
var doc = document.scrollingElement || document.documentElement, body = document.body;
|
|
85
|
+
var x = (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc.clientLeft || 0);
|
|
86
|
+
var y = (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc.clientTop || 0);
|
|
87
|
+
return {x: x, y: y};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Lets users start typing anywhere inside a container (e.g. a modal or panel)
|
|
91
|
+
// to jump into its search input, instead of requiring a click into the box first.
|
|
92
|
+
function enableTypeToFocusSearch(containerSelector, inputSelector) {
|
|
93
|
+
const container = document.querySelector(containerSelector);
|
|
94
|
+
const input = document.querySelector(inputSelector);
|
|
95
|
+
if (!container || !input) return;
|
|
96
|
+
|
|
97
|
+
const interactiveTags = ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON', 'A'];
|
|
98
|
+
|
|
99
|
+
container.addEventListener('keydown', function (event) {
|
|
100
|
+
if (document.activeElement === input) return;
|
|
101
|
+
if (event.ctrlKey || event.metaKey || event.altKey) return;
|
|
102
|
+
if (event.key.length !== 1) return; // ignore Tab/Escape/Enter/arrows/etc., only printable chars
|
|
103
|
+
|
|
104
|
+
const target = event.target;
|
|
105
|
+
if (interactiveTags.includes(target.tagName) || target.isContentEditable) return;
|
|
106
|
+
|
|
107
|
+
input.focus();
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getCookie(name) {
|
|
112
|
+
const r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
|
|
113
|
+
return r ? r[1] : undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function ApiError(response, message) {
|
|
117
|
+
if (!message) {
|
|
118
|
+
message = "Status " + response.status;
|
|
119
|
+
}
|
|
120
|
+
this.status = response.status;
|
|
121
|
+
this.message = message;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
ApiError.prototype.toString = function () {
|
|
125
|
+
return this.message;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
function getJSON(url = '', args) {
|
|
129
|
+
if (args) {
|
|
130
|
+
args = '?' + encodeGetParams(args);
|
|
131
|
+
} else {
|
|
132
|
+
args = '';
|
|
133
|
+
}
|
|
134
|
+
return fetch(
|
|
135
|
+
base_path + url + args,
|
|
136
|
+
{
|
|
137
|
+
credentials: 'same-origin',
|
|
138
|
+
mode: 'same-origin',
|
|
139
|
+
redirect: 'error'
|
|
140
|
+
}
|
|
141
|
+
).then(function (response) {
|
|
142
|
+
if (response.status !== 200) {
|
|
143
|
+
return response.json()
|
|
144
|
+
.then(
|
|
145
|
+
function (json) {
|
|
146
|
+
if ("error" in json) {
|
|
147
|
+
throw new ApiError(response, json.error);
|
|
148
|
+
} else {
|
|
149
|
+
throw new ApiError(response);
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
function () {
|
|
153
|
+
throw new ApiError(response);
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return response.json().catch(function () {
|
|
158
|
+
throw new ApiError(response, "Invalid JSON");
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function deleteURL(url = '', args) {
|
|
164
|
+
if (args) {
|
|
165
|
+
args = encodeGetParams(args);
|
|
166
|
+
} else {
|
|
167
|
+
args = '';
|
|
168
|
+
}
|
|
169
|
+
let xsrf = getCookie('_xsrf');
|
|
170
|
+
return fetch(
|
|
171
|
+
base_path + url + '?' + (xsrf !== undefined ? '_xsrf=' + encodeURIComponent(xsrf) + '&' : '') + args,
|
|
172
|
+
{
|
|
173
|
+
credentials: 'same-origin',
|
|
174
|
+
mode: 'same-origin',
|
|
175
|
+
cache: 'no-cache',
|
|
176
|
+
redirect: 'error',
|
|
177
|
+
method: 'DELETE'
|
|
178
|
+
}
|
|
179
|
+
).then(function (response) {
|
|
180
|
+
if (response.status !== 204) {
|
|
181
|
+
throw new ApiError(response);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function postJSON(url = '', data = {}, args) {
|
|
187
|
+
if (args) {
|
|
188
|
+
args = encodeGetParams(args);
|
|
189
|
+
} else {
|
|
190
|
+
args = '';
|
|
191
|
+
}
|
|
192
|
+
let xsrf = getCookie('_xsrf');
|
|
193
|
+
return fetch(
|
|
194
|
+
base_path + url + '?' + (xsrf !== undefined ? '_xsrf=' + encodeURIComponent(xsrf) + '&' : '') + args,
|
|
195
|
+
{
|
|
196
|
+
credentials: 'same-origin',
|
|
197
|
+
mode: 'same-origin',
|
|
198
|
+
cache: 'no-cache',
|
|
199
|
+
redirect: 'error',
|
|
200
|
+
method: 'POST',
|
|
201
|
+
headers: {
|
|
202
|
+
'Content-Type': 'application/json; charset=utf-8'
|
|
203
|
+
},
|
|
204
|
+
body: JSON.stringify(data)
|
|
205
|
+
}
|
|
206
|
+
).then(function (response) {
|
|
207
|
+
if (response.status !== 200) {
|
|
208
|
+
return response.json()
|
|
209
|
+
.then(
|
|
210
|
+
function (json) {
|
|
211
|
+
if ("error" in json) {
|
|
212
|
+
throw new ApiError(response, json.error);
|
|
213
|
+
} else {
|
|
214
|
+
throw new ApiError(response);
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
function () {
|
|
218
|
+
throw new ApiError(response);
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return response.json().catch(function () {
|
|
223
|
+
throw new ApiError(response, "Invalid JSON");
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function patchJSON(url = '', data = {}, args) {
|
|
229
|
+
if (args) {
|
|
230
|
+
args = encodeGetParams(args);
|
|
231
|
+
} else {
|
|
232
|
+
args = '';
|
|
233
|
+
}
|
|
234
|
+
let xsrf = getCookie('_xsrf');
|
|
235
|
+
return fetch(
|
|
236
|
+
base_path + url + '?' + (xsrf !== undefined ? '_xsrf=' + encodeURIComponent(xsrf) + '&' : '') + args,
|
|
237
|
+
{
|
|
238
|
+
credentials: 'same-origin',
|
|
239
|
+
mode: 'same-origin',
|
|
240
|
+
cache: 'no-cache',
|
|
241
|
+
redirect: 'error',
|
|
242
|
+
method: 'PATCH',
|
|
243
|
+
headers: {
|
|
244
|
+
'Content-Type': 'application/json; charset=utf-8'
|
|
245
|
+
},
|
|
246
|
+
body: JSON.stringify(data)
|
|
247
|
+
}
|
|
248
|
+
).then(function (response) {
|
|
249
|
+
if (response.status !== 204) {
|
|
250
|
+
return response.json()
|
|
251
|
+
.then(
|
|
252
|
+
function (json) {
|
|
253
|
+
if ("error" in json) {
|
|
254
|
+
throw new ApiError(response, json.error);
|
|
255
|
+
} else {
|
|
256
|
+
throw new ApiError(response);
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
function () {
|
|
260
|
+
throw new ApiError(response);
|
|
261
|
+
}
|
|
262
|
+
);
|
|
263
|
+
} else {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Build a lookup table mapping UTF-8 byte offset -> char index for a string.
|
|
270
|
+
function buildUTF8ToCharMap(str) {
|
|
271
|
+
const map = [];
|
|
272
|
+
let byteOffset = 0;
|
|
273
|
+
for (let i = 0; i < str.length; i++) {
|
|
274
|
+
const code = str.codePointAt(i);
|
|
275
|
+
let byteLen;
|
|
276
|
+
if (code <= 0x7f) byteLen = 1;
|
|
277
|
+
else if (code <= 0x7ff) byteLen = 2;
|
|
278
|
+
else if (code <= 0xffff) byteLen = 3;
|
|
279
|
+
else { byteLen = 4; i++; } // skip surrogate pair
|
|
280
|
+
for (let b = 0; b < byteLen; b++) {
|
|
281
|
+
map[byteOffset + b] = i;
|
|
282
|
+
}
|
|
283
|
+
byteOffset += byteLen;
|
|
284
|
+
}
|
|
285
|
+
map[byteOffset] = str.length; // sentinel for end offset
|
|
286
|
+
return map;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Returns the byte length of a string encoded in UTF-8
|
|
290
|
+
// https://stackoverflow.com/q/5515869/711380
|
|
291
|
+
function getCharIndexFromUTF8Index(str, utf8Index, utf8ToCharMap) {
|
|
292
|
+
if (utf8ToCharMap) {
|
|
293
|
+
return utf8ToCharMap[utf8Index] !== undefined ? utf8ToCharMap[utf8Index] : str.length;
|
|
294
|
+
}
|
|
295
|
+
let charIndex = 0;
|
|
296
|
+
let currentUTF8Index = 0;
|
|
297
|
+
while (currentUTF8Index < utf8Index && charIndex < str.length) {
|
|
298
|
+
currentUTF8Index += lengthUTF8(str.charAt(charIndex));
|
|
299
|
+
charIndex++;
|
|
300
|
+
}
|
|
301
|
+
return charIndex;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const _utf8Encoder = typeof TextEncoder !== 'undefined' ? new TextEncoder() : null;
|
|
305
|
+
|
|
306
|
+
function lengthUTF8(s) {
|
|
307
|
+
if (_utf8Encoder) {
|
|
308
|
+
return _utf8Encoder.encode(s).length;
|
|
309
|
+
} else {
|
|
310
|
+
let l = s.length;
|
|
311
|
+
for (let i = s.length - 1; i >= 0; --i) {
|
|
312
|
+
let code = s.charCodeAt(i);
|
|
313
|
+
if (code > 0x7f && code <= 0x7ff) ++l;
|
|
314
|
+
else if (code > 0x7ff && code <= 0xffff) l += 2;
|
|
315
|
+
if (code >= 0xDC00 && code <= 0xDFFF) i--; // trailing surrogate
|
|
316
|
+
}
|
|
317
|
+
return l;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Escape a string for use in a regexp
|
|
322
|
+
// https://stackoverflow.com/a/6969486/711380
|
|
323
|
+
function escapeRegExp(string) {
|
|
324
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
window.addEventListener('load', function () {
|
|
328
|
+
// https://css-tricks.com/snippets/jquery/draggable-without-jquery-ui/
|
|
329
|
+
(function ($) {
|
|
330
|
+
$.fn.drags = function (opt) {
|
|
331
|
+
opt = $.extend({handle: "", cursor: "move"}, opt);
|
|
332
|
+
let $el;
|
|
333
|
+
let $drag;
|
|
334
|
+
if (opt.handle === "") {
|
|
335
|
+
$el = this;
|
|
336
|
+
} else {
|
|
337
|
+
$el = this.find(opt.handle);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return $el.css('cursor', opt.cursor).on("mousedown", function (e) {
|
|
341
|
+
if (opt.handle === "") {
|
|
342
|
+
$drag = $(this).addClass('draggable');
|
|
343
|
+
} else {
|
|
344
|
+
$drag = $(this).addClass('active-handle').parent().addClass('draggable');
|
|
345
|
+
}
|
|
346
|
+
let z_idx = $drag.css('z-index'),
|
|
347
|
+
drg_h = $drag.outerHeight(),
|
|
348
|
+
drg_w = $drag.outerWidth(),
|
|
349
|
+
pos_y = $drag.offset().top + drg_h - e.pageY,
|
|
350
|
+
pos_x = $drag.offset().left + drg_w - e.pageX;
|
|
351
|
+
$drag.css('z-index', 1000).parents().on("mousemove", function (e) {
|
|
352
|
+
$('.draggable').offset({
|
|
353
|
+
top: e.pageY + pos_y - drg_h,
|
|
354
|
+
left: e.pageX + pos_x - drg_w
|
|
355
|
+
}).on("mouseup", function () {
|
|
356
|
+
$(this).removeClass('draggable').css('z-index', z_idx);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
e.preventDefault(); // disable selection
|
|
360
|
+
}).on("mouseup", function () {
|
|
361
|
+
if (opt.handle === "") {
|
|
362
|
+
$(this).removeClass('draggable');
|
|
363
|
+
} else {
|
|
364
|
+
$(this).removeClass('active-handle').parent().removeClass('draggable');
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
})(jQuery);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
function showAlert(message, status) {
|
|
372
|
+
|
|
373
|
+
const alertModal = $('#alertModal');
|
|
374
|
+
|
|
375
|
+
let modalTitle = '';
|
|
376
|
+
let alertClass = '';
|
|
377
|
+
let modalContent = '';
|
|
378
|
+
let iconClass = '';
|
|
379
|
+
|
|
380
|
+
switch (status) {
|
|
381
|
+
case 'info':
|
|
382
|
+
modalTitle = 'Information';
|
|
383
|
+
alertClass = 'alert-info';
|
|
384
|
+
modalContent = 'This is some information content.';
|
|
385
|
+
iconClass = 'fa-info-circle';
|
|
386
|
+
break;
|
|
387
|
+
case 'error':
|
|
388
|
+
modalTitle = 'Erreur';
|
|
389
|
+
alertClass = 'alert-danger';
|
|
390
|
+
modalContent = 'This is some error content.';
|
|
391
|
+
iconClass = 'fa-exclamation-triangle';
|
|
392
|
+
break;
|
|
393
|
+
case 'warning':
|
|
394
|
+
modalTitle = 'Avertissement';
|
|
395
|
+
alertClass = 'alert-warning';
|
|
396
|
+
modalContent = 'This is some warning content.';
|
|
397
|
+
iconClass = 'fa-exclamation-circle';
|
|
398
|
+
break;
|
|
399
|
+
default:
|
|
400
|
+
modalTitle = 'Alerte';
|
|
401
|
+
alertClass = 'alert-primary';
|
|
402
|
+
modalContent = 'This is some default content.';
|
|
403
|
+
iconClass = 'fa-info';
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
$('#alertModalLabel').text(modalTitle);
|
|
407
|
+
|
|
408
|
+
const alertMessageHtml = `
|
|
409
|
+
<div class="alert ${alertClass}" role="alert">
|
|
410
|
+
<i class="fa ${iconClass}"></i> <strong>${message}</strong>
|
|
411
|
+
</div>
|
|
412
|
+
`;
|
|
413
|
+
|
|
414
|
+
$('#alertMessage').html(alertMessageHtml)
|
|
415
|
+
alertModal.modal('show');
|
|
416
|
+
|
|
417
|
+
alertModal.on('hidden.bs.modal', function () {
|
|
418
|
+
$('#alertMessage').html('');
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function showConfirm(message, confirmCallback, show_danger_button=false) {
|
|
423
|
+
|
|
424
|
+
$('#confirmMessage').html(message);
|
|
425
|
+
|
|
426
|
+
if(show_danger_button) {
|
|
427
|
+
$('#confirmButton').addClass('btn-danger');
|
|
428
|
+
$('#confirmButton').removeClass('btn-primary');
|
|
429
|
+
} else {
|
|
430
|
+
$('#confirmButton').addClass('btn-primary');
|
|
431
|
+
$('#confirmButton').removeClass('btn-danger');
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
$('#confirmModal').modal('show');
|
|
435
|
+
|
|
436
|
+
$('#confirmButton').off('click').on('click', function () {
|
|
437
|
+
if (typeof confirmCallback === 'function') {
|
|
438
|
+
confirmCallback();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
$('#confirmModal').modal('hide');
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function log(message, status = 'info', context = {}) {
|
|
446
|
+
const timestamp = new Date().toLocaleString();
|
|
447
|
+
const logMessage = `${timestamp} [${status.toUpperCase()}]: ${message}`;
|
|
448
|
+
if (status.toLowerCase() === 'error') {
|
|
449
|
+
// console.error(logMessage, context);
|
|
450
|
+
} else {
|
|
451
|
+
console.log(logMessage, context);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function showSpinner(cancel_action=null, show_progress=false) {
|
|
456
|
+
let $spinner_modal = $('#spinner-modal')
|
|
457
|
+
if (typeof $spinner_modal.modal !== 'undefined') {
|
|
458
|
+
$spinner_modal.modal('show');
|
|
459
|
+
if(cancel_action !== null) {
|
|
460
|
+
$('button.cancel', $spinner_modal).show();
|
|
461
|
+
$('button.cancel', $spinner_modal).bind("click", cancel_action);
|
|
462
|
+
}
|
|
463
|
+
if(show_progress) {
|
|
464
|
+
$("#progress", $spinner_modal).show();
|
|
465
|
+
} else {
|
|
466
|
+
$("#progress", $spinner_modal).hide();
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function updateProgress(percent) {
|
|
472
|
+
function doUpdateProgress(){
|
|
473
|
+
let $spinner_modal = $('#spinner-modal');
|
|
474
|
+
if (typeof $spinner_modal.modal !== 'undefined') {
|
|
475
|
+
if (isNaN(percent) || (percent < 0)) {
|
|
476
|
+
percent = 0;
|
|
477
|
+
}
|
|
478
|
+
if (percent > 100) {
|
|
479
|
+
percent = 100;
|
|
480
|
+
}
|
|
481
|
+
$("#progress .progress-bar", $spinner_modal).css('width', percent + '%');
|
|
482
|
+
$("#progress .progress-bar", $spinner_modal).css('aria-valuenow', percent);
|
|
483
|
+
log(`Upload progress: ${percent}%`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
window.requestAnimationFrame(doUpdateProgress);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function hideSpinner() {
|
|
490
|
+
let $spinner_modal = $('#spinner-modal');
|
|
491
|
+
$spinner_modal.modal('hide');
|
|
492
|
+
$('button.cancel', $spinner_modal).hide();
|
|
493
|
+
$('button.cancel', $spinner_modal).unbind("click");
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
/*
|
|
498
|
+
* Selection stuff
|
|
499
|
+
*/
|
|
500
|
+
|
|
501
|
+
let chunk_offsets = [];
|
|
502
|
+
|
|
503
|
+
// Get the document offset from a position
|
|
504
|
+
function describePos(node, offset) {
|
|
505
|
+
// Convert current offset from character to bytes
|
|
506
|
+
offset = lengthUTF8(node.textContent.substring(0, offset));
|
|
507
|
+
while (!node.id) {
|
|
508
|
+
if (node.previousSibling) {
|
|
509
|
+
node = node.previousSibling;
|
|
510
|
+
offset += lengthUTF8(node.textContent);
|
|
511
|
+
} else {
|
|
512
|
+
node = node.parentNode;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (node.id.substring(0, 11) !== 'doc-offset-') {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
return parseInt(node.id.substring(11)) + offset;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Find a position from the document offset
|
|
522
|
+
function locatePos(pos) {
|
|
523
|
+
// Find the right chunk
|
|
524
|
+
let chunk_start = 0;
|
|
525
|
+
for (let i = 0; i < chunk_offsets.length; ++i) {
|
|
526
|
+
if (chunk_offsets[i] > pos) {
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
chunk_start = chunk_offsets[i];
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
let offset = pos - chunk_start;
|
|
533
|
+
let node = document.getElementById('doc-offset-' + chunk_start);
|
|
534
|
+
while (node.firstChild) {
|
|
535
|
+
node = node.firstChild;
|
|
536
|
+
}
|
|
537
|
+
while (offset > 0) {
|
|
538
|
+
if (lengthUTF8(node.textContent) >= offset) {
|
|
539
|
+
break;
|
|
540
|
+
} else {
|
|
541
|
+
offset -= lengthUTF8(node.textContent);
|
|
542
|
+
node = nextElement(node);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return [node, offset]
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
let current_selection = null;
|
|
549
|
+
let current_selection_text = null;
|
|
550
|
+
|
|
551
|
+
function describeSelection() {
|
|
552
|
+
const sel = window.getSelection();
|
|
553
|
+
if (sel.rangeCount === 0 || sel.isCollapsed) return null;
|
|
554
|
+
|
|
555
|
+
const range = sel.getRangeAt(0);
|
|
556
|
+
const container = document.getElementById('highlight-content');
|
|
557
|
+
if (!container || !container.contains(range.commonAncestorContainer)) return null;
|
|
558
|
+
|
|
559
|
+
const tempRange = document.createRange();
|
|
560
|
+
tempRange.selectNodeContents(container);
|
|
561
|
+
tempRange.setEnd(range.startContainer, range.startOffset);
|
|
562
|
+
const startOffset = lengthUTF8(tempRange.toString());
|
|
563
|
+
|
|
564
|
+
tempRange.selectNodeContents(container);
|
|
565
|
+
tempRange.setEnd(range.endContainer, range.endOffset);
|
|
566
|
+
const endOffset = lengthUTF8(tempRange.toString());
|
|
567
|
+
|
|
568
|
+
return [startOffset, endOffset];
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function splitAtPos(pos, after) {
|
|
572
|
+
let node = pos[0], idx = pos[1];
|
|
573
|
+
if (idx === 0) {
|
|
574
|
+
// Leftmost index: return current node
|
|
575
|
+
return node;
|
|
576
|
+
} else if (idx >= lengthUTF8(node.textContent)) {
|
|
577
|
+
// Rightmost index: return next node
|
|
578
|
+
return nextElement(node);
|
|
579
|
+
} else {
|
|
580
|
+
// Find the character index from the byte index
|
|
581
|
+
let idx_char = 0;
|
|
582
|
+
while (idx > 0) {
|
|
583
|
+
let code = node.textContent.charCodeAt(idx_char);
|
|
584
|
+
if (code <= 0x7f) idx -= 1;
|
|
585
|
+
else if (code <= 0x7ff) idx -= 2;
|
|
586
|
+
else if (code <= 0xffff) idx -= 3;
|
|
587
|
+
else idx -= 4;
|
|
588
|
+
idx_char += 1;
|
|
589
|
+
}
|
|
590
|
+
if (idx_char >= node.textContent.length) {
|
|
591
|
+
log("Error computing character position!", 'error');
|
|
592
|
+
idx_char = node.textContent.length - 1;
|
|
593
|
+
}
|
|
594
|
+
// Split, return right node
|
|
595
|
+
return node.splitText(idx_char);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Restore a described selection
|
|
600
|
+
function restoreSelection(saved) {
|
|
601
|
+
let sel = window.getSelection();
|
|
602
|
+
sel.removeAllRanges();
|
|
603
|
+
if (saved !== null) {
|
|
604
|
+
let range = document.createRange();
|
|
605
|
+
let start = locatePos(saved[0]);
|
|
606
|
+
let end = locatePos(saved[1]);
|
|
607
|
+
range.setStart(start[0], start[1]);
|
|
608
|
+
range.setEnd(end[0], end[1]);
|
|
609
|
+
sel.addRange(range);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function isFirefox() {
|
|
614
|
+
return navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function hasVerticalScrollbar() {
|
|
618
|
+
// Get the total height of the document content
|
|
619
|
+
const documentHeight = Math.max(
|
|
620
|
+
document.body.scrollHeight,
|
|
621
|
+
document.documentElement.scrollHeight,
|
|
622
|
+
document.body.offsetHeight,
|
|
623
|
+
document.documentElement.offsetHeight,
|
|
624
|
+
document.body.clientHeight,
|
|
625
|
+
document.documentElement.clientHeight
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
// Get the height of the viewport (visible area)
|
|
629
|
+
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
|
|
630
|
+
|
|
631
|
+
// Compare the document height with the viewport height
|
|
632
|
+
return documentHeight > viewportHeight;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/*
|
|
636
|
+
* Apply scroll behavior to all pages
|
|
637
|
+
* Previously page-project only (taguette.js)
|
|
638
|
+
*/
|
|
639
|
+
|
|
640
|
+
window.addEventListener('load', function() {
|
|
641
|
+
let lastScrollY = 0;
|
|
642
|
+
let scrollUpDistance = 0;
|
|
643
|
+
const bodyElement = document.querySelector('body');
|
|
644
|
+
if (!bodyElement) return;
|
|
645
|
+
|
|
646
|
+
window.addEventListener('scroll', function() {
|
|
647
|
+
const currentY = window.scrollY;
|
|
648
|
+
|
|
649
|
+
if (currentY < 50) {
|
|
650
|
+
bodyElement.classList.remove("scrolled");
|
|
651
|
+
scrollUpDistance = 0;
|
|
652
|
+
} else if (currentY > lastScrollY) {
|
|
653
|
+
// Scrolling down — hide navbar
|
|
654
|
+
scrollUpDistance = 0;
|
|
655
|
+
bodyElement.classList.add("scrolled");
|
|
656
|
+
} else {
|
|
657
|
+
// Scrolling up — show after 80px
|
|
658
|
+
scrollUpDistance += lastScrollY - currentY;
|
|
659
|
+
if (scrollUpDistance > 80) {
|
|
660
|
+
bodyElement.classList.remove("scrolled");
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
lastScrollY = currentY;
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
function initHelpPopover(elementId, sections) {
|
|
668
|
+
const contentParts = sections.map((section, index) => {
|
|
669
|
+
const itemsList = section.items.map(item => `<li>${gettext(item)}</li>`).join('');
|
|
670
|
+
const marginClass = index === sections.length - 1 ? 'mb-0' : 'mb-2';
|
|
671
|
+
return `
|
|
672
|
+
<p class="mb-2"><strong>${gettext(section.title)}</strong></p>
|
|
673
|
+
<ul class="${marginClass} ps-3">
|
|
674
|
+
${itemsList}
|
|
675
|
+
</ul>
|
|
676
|
+
`;
|
|
677
|
+
}).join('');
|
|
678
|
+
|
|
679
|
+
const helpContent = `<div class="text-start">${contentParts}</div>`;
|
|
680
|
+
|
|
681
|
+
const popoverElement = document.getElementById(elementId);
|
|
682
|
+
if (popoverElement) {
|
|
683
|
+
const existing = bootstrap.Popover.getInstance(popoverElement);
|
|
684
|
+
if (existing) existing.dispose();
|
|
685
|
+
|
|
686
|
+
const popover = new bootstrap.Popover(popoverElement, {
|
|
687
|
+
content: helpContent,
|
|
688
|
+
html: true,
|
|
689
|
+
sanitize: false,
|
|
690
|
+
trigger: 'manual',
|
|
691
|
+
placement: 'left',
|
|
692
|
+
fallbackPlacements: ['top', 'bottom', 'right']
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
if (popoverElement._helpClickHandler) {
|
|
696
|
+
popoverElement.removeEventListener('click', popoverElement._helpClickHandler);
|
|
697
|
+
}
|
|
698
|
+
popoverElement._helpClickHandler = (e) => {
|
|
699
|
+
e.stopPropagation();
|
|
700
|
+
popover.toggle();
|
|
701
|
+
};
|
|
702
|
+
popoverElement.addEventListener('click', popoverElement._helpClickHandler);
|
|
703
|
+
|
|
704
|
+
if (!popoverElement._helpOutsideHandler) {
|
|
705
|
+
popoverElement._helpOutsideHandler = (e) => {
|
|
706
|
+
const tip = document.querySelector('.popover');
|
|
707
|
+
if (tip && !tip.contains(e.target) && e.target !== popoverElement) {
|
|
708
|
+
popover.hide();
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
document.addEventListener('click', popoverElement._helpOutsideHandler);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|