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,119 @@
|
|
|
1
|
+
function editHighlight() {
|
|
2
|
+
const selection = describeSelection();
|
|
3
|
+
if (selection) {
|
|
4
|
+
createHighlight(selection);
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
highlightModalReset();
|
|
9
|
+
const id = this.dataset.highlightId;
|
|
10
|
+
const highlight = highlights['' + id];
|
|
11
|
+
|
|
12
|
+
if (!highlight) {
|
|
13
|
+
log('Highlight not found: ' + id, 'error');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
document.getElementById('highlight-add-id').value = id;
|
|
18
|
+
document.getElementById('highlight-add-start').value = highlight.start_offset;
|
|
19
|
+
document.getElementById('highlight-add-end').value = highlight.end_offset;
|
|
20
|
+
|
|
21
|
+
const hl_tags = highlight.tags;
|
|
22
|
+
const allNodes = treeview_tags_highlight.treeview('getEnabled');
|
|
23
|
+
|
|
24
|
+
for (const tagId of hl_tags) {
|
|
25
|
+
highlightSelectedTags.push(tagId);
|
|
26
|
+
const node = allNodes.find(n => n.id === tagId);
|
|
27
|
+
if (node) treeview_tags_highlight.treeview('selectNode', node.nodeId);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
showHighlightTagSelection();
|
|
31
|
+
$(highlight_add_modal).modal('show').drags({handle: '.modal-header'});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function addHighlight(highlight, documentId) {
|
|
35
|
+
const targetDoc = documentId || current_document;
|
|
36
|
+
if (targetDoc !== current_document) {
|
|
37
|
+
updateDocumentHighlightsCount(targetDoc, +1);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const id = '' + highlight.id;
|
|
41
|
+
const isNew = !highlights[id];
|
|
42
|
+
highlights[id] = highlight;
|
|
43
|
+
renderHighlights(Object.values(highlights));
|
|
44
|
+
if (isNew) {
|
|
45
|
+
updateDocumentHighlightsCount(targetDoc, +1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function removeHighlight(highlightId, documentId) {
|
|
50
|
+
const targetDoc = documentId || current_document;
|
|
51
|
+
if (targetDoc !== current_document) {
|
|
52
|
+
updateDocumentHighlightsCount(targetDoc, -1);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const id = '' + highlightId;
|
|
56
|
+
if (!highlights[id]) return;
|
|
57
|
+
delete highlights[id];
|
|
58
|
+
renderHighlights(Object.values(highlights));
|
|
59
|
+
updateDocumentHighlightsCount(targetDoc, -1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function updateDocumentHighlightsCount(documentId, delta) {
|
|
63
|
+
if (!documentId) return;
|
|
64
|
+
const doc = documents['' + documentId];
|
|
65
|
+
if (!doc) return;
|
|
66
|
+
doc.highlights_count = Math.max(0, (doc.highlights_count || 0) + delta);
|
|
67
|
+
if (typeof initDocumentsTreeview === 'function') {
|
|
68
|
+
initDocumentsTreeview();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function saveHighlightDragDrop(tagId) {
|
|
73
|
+
const selection = globalThis.getSelection();
|
|
74
|
+
if (!selection?.rangeCount || selection.isCollapsed) return;
|
|
75
|
+
|
|
76
|
+
const range = selection.getRangeAt(0);
|
|
77
|
+
const container = document.getElementById('highlight-content');
|
|
78
|
+
if (!container?.contains(range.commonAncestorContainer)) return;
|
|
79
|
+
|
|
80
|
+
const snippet = range.toString().trim();
|
|
81
|
+
if (!snippet) return;
|
|
82
|
+
|
|
83
|
+
const beforeRange = document.createRange();
|
|
84
|
+
beforeRange.selectNodeContents(container);
|
|
85
|
+
beforeRange.setEnd(range.startContainer, range.startOffset);
|
|
86
|
+
|
|
87
|
+
const afterRange = document.createRange();
|
|
88
|
+
afterRange.selectNodeContents(container);
|
|
89
|
+
afterRange.setStart(range.endContainer, range.endOffset);
|
|
90
|
+
|
|
91
|
+
const startOffset = lengthUTF8(beforeRange.toString());
|
|
92
|
+
const endOffset = startOffset + lengthUTF8(snippet);
|
|
93
|
+
|
|
94
|
+
submitHighLightCreation(
|
|
95
|
+
{ selection: [startOffset, endOffset], tags: [tagId] },
|
|
96
|
+
reply => addHighlight(reply),
|
|
97
|
+
error => {
|
|
98
|
+
log("Failed to create highlight", 'error', error);
|
|
99
|
+
const createHighlightErrorMessage = gettext("Couldn't create highlight!");
|
|
100
|
+
showAlert(`${createHighlightErrorMessage}`, 'error');
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function submitHighLightCreation(highlight, callbackSuccess, callbackFail) {
|
|
106
|
+
const { id, selection, tags } = highlight;
|
|
107
|
+
const requestData = {
|
|
108
|
+
start_offset: selection[0],
|
|
109
|
+
end_offset: selection[1],
|
|
110
|
+
tags: tags,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const url = id
|
|
114
|
+
? `/api/project/${project_id}/document/${current_document}/highlight/${id}`
|
|
115
|
+
: `/api/project/${project_id}/document/${current_document}/highlight/new`;
|
|
116
|
+
|
|
117
|
+
showSpinner();
|
|
118
|
+
postJSON(url, requestData).then(callbackSuccess).catch(callbackFail).finally(hideSpinner);
|
|
119
|
+
}
|
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
function showTagUnderlineTooltip(event, tagId, color) {
|
|
2
|
+
let tooltip = document.getElementById('tag-underline-tooltip');
|
|
3
|
+
|
|
4
|
+
if (!tooltip) {
|
|
5
|
+
tooltip = document.createElement('div');
|
|
6
|
+
tooltip.id = 'tag-underline-tooltip';
|
|
7
|
+
document.body.appendChild(tooltip);
|
|
8
|
+
|
|
9
|
+
Object.assign(tooltip.style, {
|
|
10
|
+
position: 'fixed',
|
|
11
|
+
padding: '10px',
|
|
12
|
+
background: '#fff',
|
|
13
|
+
border: '2px solid #333',
|
|
14
|
+
borderRadius: '6px',
|
|
15
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.2)',
|
|
16
|
+
fontSize: '13px',
|
|
17
|
+
pointerEvents: 'none',
|
|
18
|
+
zIndex: '10000',
|
|
19
|
+
maxWidth: '300px',
|
|
20
|
+
wordWrap: 'break-word',
|
|
21
|
+
overflowWrap: 'break-word',
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const tag = findTag(tagId);
|
|
26
|
+
if (!tag) return;
|
|
27
|
+
|
|
28
|
+
tooltip.innerHTML = `
|
|
29
|
+
<div style="display: flex; gap: 4px; flex-wrap: wrap; max-width: 280px;">
|
|
30
|
+
<div style="
|
|
31
|
+
display: inline-block;
|
|
32
|
+
padding: 4px 8px;
|
|
33
|
+
border-radius: 3px;
|
|
34
|
+
background-color: ${color};
|
|
35
|
+
color: white;
|
|
36
|
+
font-size: 11px;
|
|
37
|
+
max-width: 100%;
|
|
38
|
+
word-wrap: break-word;
|
|
39
|
+
overflow-wrap: break-word;
|
|
40
|
+
">
|
|
41
|
+
${tag.path}
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
tooltip.style.display = 'block';
|
|
47
|
+
tooltip.style.left = (event.clientX + 10) + 'px';
|
|
48
|
+
tooltip.style.top = (event.clientY + 10) + 'px';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function hideTagUnderlineTooltip() {
|
|
52
|
+
const tooltip = document.getElementById('tag-underline-tooltip');
|
|
53
|
+
if (tooltip) tooltip.style.display = 'none';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Apply colored SVG underlines to a content div based on tag_ranges.
|
|
58
|
+
* Uses Range.getClientRects() to get pixel positions, then draws SVG lines.
|
|
59
|
+
*/
|
|
60
|
+
function applyTagUnderlines(contentDiv, tagRanges, principalTagIds) {
|
|
61
|
+
if (!tagRanges || tagRanges.length === 0) return;
|
|
62
|
+
|
|
63
|
+
// Make contentDiv a positioning context for the SVG overlay
|
|
64
|
+
contentDiv.style.position = 'relative';
|
|
65
|
+
|
|
66
|
+
const textNodes = collectTextNodes(contentDiv);
|
|
67
|
+
const fullText = textNodes.map(n => n.nodeValue).join('');
|
|
68
|
+
if (fullText.length === 0) return;
|
|
69
|
+
|
|
70
|
+
// Build char index -> { node, offsetInNode } mapping
|
|
71
|
+
const charMap = [];
|
|
72
|
+
for (const textNode of textNodes) {
|
|
73
|
+
const nodeText = textNode.nodeValue;
|
|
74
|
+
for (let j = 0; j < nodeText.length; j++) {
|
|
75
|
+
charMap.push({ node: textNode, offset: j });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Convert UTF-8 byte offsets to char indices
|
|
80
|
+
const utf8Map = buildUTF8ToCharMap(fullText);
|
|
81
|
+
const ranges = tagRanges.map(tr => ({
|
|
82
|
+
tag_id: tr.tag_id,
|
|
83
|
+
color: tr.color,
|
|
84
|
+
startChar: getCharIndexFromUTF8Index(fullText, tr.start, utf8Map),
|
|
85
|
+
endChar: getCharIndexFromUTF8Index(fullText, tr.end, utf8Map),
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
// Sort by start, then larger ranges first
|
|
89
|
+
ranges.sort((a, b) => a.startChar - b.startChar || (b.endChar - b.startChar) - (a.endChar - a.startChar));
|
|
90
|
+
|
|
91
|
+
// Assign lanes for overlapping ranges
|
|
92
|
+
const laneEnds = [];
|
|
93
|
+
const rangeLanes = ranges.map(range => {
|
|
94
|
+
for (let lane = 0; lane < laneEnds.length; lane++) {
|
|
95
|
+
if (laneEnds[lane] <= range.startChar) {
|
|
96
|
+
laneEnds[lane] = range.endChar;
|
|
97
|
+
return lane;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
laneEnds.push(range.endChar);
|
|
101
|
+
return laneEnds.length - 1;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const totalLanes = laneEnds.length;
|
|
105
|
+
const LINE_HEIGHT = 3;
|
|
106
|
+
const LINE_GAP = 2;
|
|
107
|
+
const BASE_OFFSET = 2;
|
|
108
|
+
const extraSpace = BASE_OFFSET + totalLanes * (LINE_HEIGHT + LINE_GAP);
|
|
109
|
+
|
|
110
|
+
// Add bottom padding so SVG lines don't overlap the badges
|
|
111
|
+
contentDiv.style.paddingBottom = extraSpace + 'px';
|
|
112
|
+
|
|
113
|
+
// Increase line-height to make room for stacked underlines between text lines
|
|
114
|
+
if (totalLanes > 0) {
|
|
115
|
+
contentDiv.style.lineHeight = (1.6 + totalLanes * 0.3) + 'em';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Create SVG overlay
|
|
119
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
120
|
+
svg.classList.add('tag-underline-svg');
|
|
121
|
+
svg.style.position = 'absolute';
|
|
122
|
+
svg.style.left = '0';
|
|
123
|
+
svg.style.top = '0';
|
|
124
|
+
svg.style.width = '100%';
|
|
125
|
+
svg.style.height = '100%';
|
|
126
|
+
svg.style.pointerEvents = 'none';
|
|
127
|
+
svg.style.overflow = 'visible';
|
|
128
|
+
|
|
129
|
+
const containerRect = contentDiv.getBoundingClientRect();
|
|
130
|
+
|
|
131
|
+
for (let ri = 0; ri < ranges.length; ri++) {
|
|
132
|
+
const range = ranges[ri];
|
|
133
|
+
const lane = rangeLanes[ri];
|
|
134
|
+
if (range.startChar >= range.endChar || range.startChar >= charMap.length) continue;
|
|
135
|
+
|
|
136
|
+
const effectiveEnd = Math.min(range.endChar, charMap.length);
|
|
137
|
+
|
|
138
|
+
// Create a DOM Range to get bounding rects
|
|
139
|
+
const domRange = document.createRange();
|
|
140
|
+
const startEntry = charMap[range.startChar];
|
|
141
|
+
const endEntry = charMap[effectiveEnd - 1];
|
|
142
|
+
domRange.setStart(startEntry.node, startEntry.offset);
|
|
143
|
+
domRange.setEnd(endEntry.node, endEntry.offset + 1);
|
|
144
|
+
|
|
145
|
+
const rawRects = Array.from(domRange.getClientRects()).map(r => ({
|
|
146
|
+
x: r.left,
|
|
147
|
+
y: r.top,
|
|
148
|
+
width: r.width,
|
|
149
|
+
height: r.height,
|
|
150
|
+
}));
|
|
151
|
+
const rects = deduplicateRects(rawRects);
|
|
152
|
+
|
|
153
|
+
for (const rect of rects) {
|
|
154
|
+
const x = rect.x - containerRect.left;
|
|
155
|
+
const y = (rect.y + rect.height) - containerRect.top + BASE_OFFSET + lane * (LINE_HEIGHT + LINE_GAP);
|
|
156
|
+
|
|
157
|
+
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
158
|
+
line.setAttribute('x1', x);
|
|
159
|
+
line.setAttribute('y1', y + LINE_HEIGHT / 2);
|
|
160
|
+
line.setAttribute('x2', x + rect.width);
|
|
161
|
+
line.setAttribute('y2', y + LINE_HEIGHT / 2);
|
|
162
|
+
line.setAttribute('stroke', range.color);
|
|
163
|
+
line.setAttribute('stroke-width', LINE_HEIGHT);
|
|
164
|
+
line.setAttribute('stroke-linecap', 'round');
|
|
165
|
+
if (!principalTagIds.has(range.tag_id)) {
|
|
166
|
+
line.setAttribute('stroke-dasharray', '8 6');
|
|
167
|
+
line.setAttribute('stroke-linecap', 'butt');
|
|
168
|
+
line.setAttribute('data-overlay-tag-id', range.tag_id);
|
|
169
|
+
line.style.display = 'none';
|
|
170
|
+
}
|
|
171
|
+
line.classList.add('smooth-stroke');
|
|
172
|
+
line.style.pointerEvents = 'all';
|
|
173
|
+
line.style.cursor = 'pointer';
|
|
174
|
+
line.style.transition = 'stroke-width 0.2s';
|
|
175
|
+
|
|
176
|
+
line.addEventListener('mouseenter', function (e) {
|
|
177
|
+
line.setAttribute('stroke-width', LINE_HEIGHT + 2);
|
|
178
|
+
showTagUnderlineTooltip(e, range.tag_id, range.color);
|
|
179
|
+
});
|
|
180
|
+
line.addEventListener('mouseleave', function () {
|
|
181
|
+
line.setAttribute('stroke-width', LINE_HEIGHT);
|
|
182
|
+
hideTagUnderlineTooltip();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
svg.appendChild(line);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
contentDiv.appendChild(svg);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function loadTag(tag_path, page, minSpinnerMs) {
|
|
193
|
+
if (page === undefined) {
|
|
194
|
+
page = 1;
|
|
195
|
+
}
|
|
196
|
+
let spinnerShownAt;
|
|
197
|
+
const useProgressBar = tag_path !== undefined && minSpinnerMs !== undefined;
|
|
198
|
+
if(tag_path !== undefined) {
|
|
199
|
+
spinnerShownAt = Date.now();
|
|
200
|
+
showSpinner(null, useProgressBar);
|
|
201
|
+
if (useProgressBar) {
|
|
202
|
+
// Show the progress bar alone, not the spinner circle, for this cue.
|
|
203
|
+
document.getElementById('spinner').style.display = 'none';
|
|
204
|
+
|
|
205
|
+
// Drive the bar to 100% ourselves over minSpinnerMs, since this isn't
|
|
206
|
+
// a real byte-progress upload — just a deliberate "computing" cue.
|
|
207
|
+
const bar = document.querySelector('#progress .progress-bar');
|
|
208
|
+
bar.style.transition = 'none';
|
|
209
|
+
bar.style.width = '0%';
|
|
210
|
+
void bar.offsetWidth; // force reflow so the 0% is committed before animating
|
|
211
|
+
bar.style.transition = `width ${minSpinnerMs}ms linear`;
|
|
212
|
+
bar.style.width = '100%';
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
getJSON(
|
|
216
|
+
`/api/project/${project_id}/highlights/${encodeURIComponent(tag_path)}?page=${page}`
|
|
217
|
+
)
|
|
218
|
+
.then(function (result) {
|
|
219
|
+
log(`Loaded highlights for tag ${tag_path}`);
|
|
220
|
+
document_contents.style.direction = 'ltr';
|
|
221
|
+
current_tag = tag_path;
|
|
222
|
+
// current_document = null;
|
|
223
|
+
let document_links = document.getElementsByClassName('document-link-current');
|
|
224
|
+
for (let i = document_links.length - 1; i >= 0; --i) {
|
|
225
|
+
document_links[i].classList.remove('document-link-current');
|
|
226
|
+
}
|
|
227
|
+
// No need to clear the 'tag-current', we are calling updateTagsList() below
|
|
228
|
+
document_contents.innerHTML = '';
|
|
229
|
+
highlights = {};
|
|
230
|
+
for (const hl of result.highlights) {
|
|
231
|
+
let content = document.createElement('div');
|
|
232
|
+
if (hl.text_direction === 'RIGHT_TO_LEFT') {
|
|
233
|
+
content.style.direction = 'rtl';
|
|
234
|
+
} else {
|
|
235
|
+
content.style.direction = 'ltr';
|
|
236
|
+
}
|
|
237
|
+
content.innerHTML = hl.content;
|
|
238
|
+
let elem = document.createElement('div');
|
|
239
|
+
elem.className = 'highlight-entry';
|
|
240
|
+
elem.setAttribute('id', 'highlight-entry-' + hl.id);
|
|
241
|
+
elem.appendChild(content);
|
|
242
|
+
elem.appendChild(document.createTextNode(' '));
|
|
243
|
+
|
|
244
|
+
// Separate principal tags (the tag being viewed) from associated tags
|
|
245
|
+
// When tag_path contains operators (∩∪¬), extract individual tag paths
|
|
246
|
+
const principalTagIds = new Set();
|
|
247
|
+
const pathParts = tag_path.split(/[∩∪¬]/);
|
|
248
|
+
for (const part of pathParts) {
|
|
249
|
+
const trimmed = part.trim();
|
|
250
|
+
if (trimmed) {
|
|
251
|
+
const t = findTag(trimmed);
|
|
252
|
+
if (t) principalTagIds.add(t.id);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Remove NOT-excluded tags from principal set
|
|
256
|
+
const notParts = tag_path.split('¬').slice(1);
|
|
257
|
+
for (const part of notParts) {
|
|
258
|
+
const trimmed = part.split(/[∩∪]/)[0].trim();
|
|
259
|
+
if (trimmed) {
|
|
260
|
+
const t = findTag(trimmed);
|
|
261
|
+
if (t) principalTagIds.delete(t.id);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
let principalTags = [];
|
|
266
|
+
let associatedTags = [];
|
|
267
|
+
hl.tags.forEach(function (tagId) {
|
|
268
|
+
const tag = findTag(tagId);
|
|
269
|
+
if (tag) {
|
|
270
|
+
if (principalTagIds.has(tag.id)) {
|
|
271
|
+
principalTags.push(tag);
|
|
272
|
+
} else {
|
|
273
|
+
associatedTags.push(tag);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Row 1: doclink + principal tag badges
|
|
279
|
+
let badgeRow = document.createElement('div');
|
|
280
|
+
badgeRow.className = 'd-flex flex-wrap gap-1 align-items-center';
|
|
281
|
+
|
|
282
|
+
let doclink = document.createElement('a');
|
|
283
|
+
doclink.className = 'badge badge-primary';
|
|
284
|
+
const documentPath = buildDocumentPath(hl.document_id);
|
|
285
|
+
doclink.textContent = documentPath;
|
|
286
|
+
linkDocument(doclink, hl.document_id, hl.id);
|
|
287
|
+
badgeRow.appendChild(doclink);
|
|
288
|
+
|
|
289
|
+
for (let j = 0; j < principalTags.length; j++) {
|
|
290
|
+
let taglink = document.createElement('a');
|
|
291
|
+
taglink.className = 'badge';
|
|
292
|
+
taglink.setAttribute('style', `background-color: ${principalTags[j].color}; color: white; font-weight: bold;`);
|
|
293
|
+
taglink.textContent = principalTags[j].path;
|
|
294
|
+
badgeRow.appendChild(taglink);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
elem.appendChild(badgeRow);
|
|
298
|
+
|
|
299
|
+
// Row 2: "Associated tags:" label + associated tag badges (only if any)
|
|
300
|
+
if (associatedTags.length > 0) {
|
|
301
|
+
let assocRow = document.createElement('div');
|
|
302
|
+
assocRow.className = 'd-flex flex-wrap gap-2 align-items-center mt-2';
|
|
303
|
+
|
|
304
|
+
let label = document.createElement('small');
|
|
305
|
+
label.style.color = '#6c757d';
|
|
306
|
+
label.style.cursor = 'pointer';
|
|
307
|
+
let allCheck = document.createElement('i');
|
|
308
|
+
allCheck.className = 'far fa-square me-1';
|
|
309
|
+
label.appendChild(allCheck);
|
|
310
|
+
label.appendChild(document.createTextNode(gettext('Overlapping')));
|
|
311
|
+
assocRow.appendChild(label);
|
|
312
|
+
|
|
313
|
+
const syncAllCheck = () => {
|
|
314
|
+
const badges = assocRow.querySelectorAll('.badge-outline-tag');
|
|
315
|
+
const allPinned = badges.length > 0 && Array.from(badges).every(b => b.classList.contains('badge-outline-tag--pinned'));
|
|
316
|
+
allCheck.className = allPinned ? 'fas fa-check-square me-1' : 'far fa-square me-1';
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
label.addEventListener('click', function () {
|
|
320
|
+
const badges = assocRow.querySelectorAll('.badge-outline-tag');
|
|
321
|
+
const allPinned = Array.from(badges).every(b => b.classList.contains('badge-outline-tag--pinned'));
|
|
322
|
+
const newPinned = !allPinned;
|
|
323
|
+
badges.forEach(function (badge) {
|
|
324
|
+
badge.classList.toggle('badge-outline-tag--pinned', newPinned);
|
|
325
|
+
const icon = badge.querySelector('.badge-outline-tag-check');
|
|
326
|
+
icon.className = newPinned
|
|
327
|
+
? 'fas fa-check-square me-1 badge-outline-tag-check'
|
|
328
|
+
: 'far fa-square me-1 badge-outline-tag-check';
|
|
329
|
+
const tagId = parseInt(badge.getAttribute('data-overlay-tag-id'));
|
|
330
|
+
content.querySelectorAll('line[data-overlay-tag-id="' + tagId + '"]').forEach(function (l) {
|
|
331
|
+
l.style.display = newPinned ? '' : 'none';
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
syncAllCheck();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
associatedTags.sort((a, b) => a.path.localeCompare(b.path));
|
|
338
|
+
for (let j = 0; j < associatedTags.length; j++) {
|
|
339
|
+
let taglink = document.createElement('a');
|
|
340
|
+
taglink.className = 'badge badge-outline-tag';
|
|
341
|
+
taglink.setAttribute('style',
|
|
342
|
+
`border: 2px dashed ${associatedTags[j].color}; color: ${associatedTags[j].color}; background: transparent; font-weight: bold; cursor: pointer;`
|
|
343
|
+
);
|
|
344
|
+
let checkIcon = document.createElement('i');
|
|
345
|
+
checkIcon.className = 'far fa-square me-1 badge-outline-tag-check';
|
|
346
|
+
taglink.appendChild(checkIcon);
|
|
347
|
+
taglink.appendChild(document.createTextNode(associatedTags[j].path));
|
|
348
|
+
taglink.setAttribute('data-overlay-tag-id', associatedTags[j].id);
|
|
349
|
+
taglink.addEventListener('mouseenter', function () {
|
|
350
|
+
const tagId = parseInt(this.getAttribute('data-overlay-tag-id'));
|
|
351
|
+
content.querySelectorAll('line[data-overlay-tag-id="' + tagId + '"]').forEach(function (l) {
|
|
352
|
+
l.style.display = '';
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
taglink.addEventListener('mouseleave', function () {
|
|
356
|
+
if (this.classList.contains('badge-outline-tag--pinned')) return;
|
|
357
|
+
const tagId = parseInt(this.getAttribute('data-overlay-tag-id'));
|
|
358
|
+
content.querySelectorAll('line[data-overlay-tag-id="' + tagId + '"]').forEach(function (l) {
|
|
359
|
+
l.style.display = 'none';
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
taglink.addEventListener('click', function () {
|
|
363
|
+
const tagId = parseInt(this.getAttribute('data-overlay-tag-id'));
|
|
364
|
+
const pinned = this.classList.toggle('badge-outline-tag--pinned');
|
|
365
|
+
const icon = this.querySelector('.badge-outline-tag-check');
|
|
366
|
+
icon.className = pinned
|
|
367
|
+
? 'fas fa-check-square me-1 badge-outline-tag-check'
|
|
368
|
+
: 'far fa-square me-1 badge-outline-tag-check';
|
|
369
|
+
content.querySelectorAll('line[data-overlay-tag-id="' + tagId + '"]').forEach(function (l) {
|
|
370
|
+
l.style.display = pinned ? '' : 'none';
|
|
371
|
+
});
|
|
372
|
+
syncAllCheck();
|
|
373
|
+
});
|
|
374
|
+
assocRow.appendChild(taglink);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
elem.appendChild(assocRow);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
document_contents.appendChild(elem);
|
|
381
|
+
|
|
382
|
+
// Apply SVG underlines after element is in the DOM (needed for getBoundingClientRect)
|
|
383
|
+
if (hl.tag_ranges && hl.tag_ranges.length > 0) {
|
|
384
|
+
applyTagUnderlines(content, hl.tag_ranges, principalTagIds);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (result.highlights.length === 0) {
|
|
388
|
+
document_contents.innerHTML = '<p style="font-style: oblique; text-align: center;">' + gettext("No highlights with this tag yet.") + '</p>';
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Pagination controls
|
|
392
|
+
function makePageLink(page_nb, label, current, enabled) {
|
|
393
|
+
let item;
|
|
394
|
+
if (current) {
|
|
395
|
+
item = document.createElement('li');
|
|
396
|
+
item.className = 'page-item active';
|
|
397
|
+
item.innerHTML = '<span class="page-link">' + label + '<span class="sr-only">(current)</span></span>';
|
|
398
|
+
} else if (!enabled) {
|
|
399
|
+
item = document.createElement('li');
|
|
400
|
+
item.className = 'page-item disabled';
|
|
401
|
+
item.innerHTML = '<span class="page-link">' + label + '</span>';
|
|
402
|
+
} else {
|
|
403
|
+
item = document.createElement('li');
|
|
404
|
+
item.className = 'page-item';
|
|
405
|
+
let link = document.createElement('a');
|
|
406
|
+
link.className = 'page-link';
|
|
407
|
+
link.setAttribute('href', '#');
|
|
408
|
+
link.innerText = label;
|
|
409
|
+
link.addEventListener('click', function (e) {
|
|
410
|
+
e.preventDefault();
|
|
411
|
+
loadTag(tag_path, page_nb);
|
|
412
|
+
});
|
|
413
|
+
item.appendChild(link);
|
|
414
|
+
}
|
|
415
|
+
return item;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (result.pages > 1 || page !== 1) {
|
|
419
|
+
if (result.pages === undefined && page !== 1) {
|
|
420
|
+
result.pages = 1;
|
|
421
|
+
}
|
|
422
|
+
let pagination = document.createElement('nav');
|
|
423
|
+
pagination.setAttribute('aria-label', "Page navigation");
|
|
424
|
+
let pagination_ul = document.createElement('ul');
|
|
425
|
+
pagination_ul.className = 'pagination justify-content-center';
|
|
426
|
+
const min_page = Math.max(2, page - 2);
|
|
427
|
+
const max_page = Math.min(result.pages - 1, page + 2);
|
|
428
|
+
|
|
429
|
+
// Previous button
|
|
430
|
+
pagination_ul.appendChild(makePageLink(page - 1, "Previous", false, page > 1));
|
|
431
|
+
// Page 1
|
|
432
|
+
pagination_ul.appendChild(makePageLink(1, 1, 1 === page, 1 !== page));
|
|
433
|
+
// "..." between 1 and other pages, if appropriate
|
|
434
|
+
if (min_page > 2) {
|
|
435
|
+
pagination_ul.appendChild(makePageLink(page, "...", false, false));
|
|
436
|
+
}
|
|
437
|
+
// Other pages
|
|
438
|
+
for (let i = min_page; i <= max_page; ++i) {
|
|
439
|
+
pagination_ul.appendChild(makePageLink(i, i, i === page, i !== page));
|
|
440
|
+
}
|
|
441
|
+
// "..." between other pages and last page, if appropriate
|
|
442
|
+
if (max_page < result.pages - 1) {
|
|
443
|
+
pagination_ul.appendChild(makePageLink(page, "...", false, false));
|
|
444
|
+
}
|
|
445
|
+
// Last page
|
|
446
|
+
pagination_ul.appendChild(makePageLink(result.pages, result.pages, result.pages === page, result.pages !== page));
|
|
447
|
+
// Next button
|
|
448
|
+
pagination_ul.appendChild(makePageLink(page + 1, "Next", false, page < result.pages));
|
|
449
|
+
|
|
450
|
+
pagination.appendChild(pagination_ul);
|
|
451
|
+
document_contents.appendChild(pagination);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Update export button
|
|
455
|
+
const items = document.querySelectorAll('#document-export-dropdown-menu a');
|
|
456
|
+
for (const item of items) {
|
|
457
|
+
const ext = item.dataset.extension;
|
|
458
|
+
if (item.dataset.highlights === 'false') {
|
|
459
|
+
item.style.display = 'none';
|
|
460
|
+
} else {
|
|
461
|
+
item.setAttribute('href', base_path + '/project/' + project_id + '/export/highlights/' + encodeURIComponent(tag_path) + '.' + ext);
|
|
462
|
+
item.style.display = '';
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
toggleToolbarActions(true)
|
|
467
|
+
|
|
468
|
+
// Scroll up
|
|
469
|
+
window.setTimeout(function () {
|
|
470
|
+
window.scrollTo(0, 0);
|
|
471
|
+
}, 0);
|
|
472
|
+
})
|
|
473
|
+
.catch(function (error) {
|
|
474
|
+
console.error("Failed to load tag highlights", error);
|
|
475
|
+
log("Failed to load tag highlights", 'error', {error});
|
|
476
|
+
const loadTagHighlightsErrorMessage = gettext("Error loading tag highlights!");
|
|
477
|
+
showAlert(`${loadTagHighlightsErrorMessage} \n \n`, 'error', {error})
|
|
478
|
+
})
|
|
479
|
+
.then(function (result) {
|
|
480
|
+
if (minSpinnerMs === undefined || spinnerShownAt === undefined) {
|
|
481
|
+
hideSpinner();
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const remaining = minSpinnerMs - (Date.now() - spinnerShownAt);
|
|
485
|
+
setTimeout(function () {
|
|
486
|
+
if (useProgressBar) {
|
|
487
|
+
// Reset the inline styles so a later real upload's spinner
|
|
488
|
+
// circle and progress bar behave as before.
|
|
489
|
+
document.getElementById('spinner').style.display = '';
|
|
490
|
+
const bar = document.querySelector('#progress .progress-bar');
|
|
491
|
+
bar.style.transition = '';
|
|
492
|
+
bar.style.width = '0%';
|
|
493
|
+
}
|
|
494
|
+
hideSpinner();
|
|
495
|
+
}, Math.max(0, remaining));
|
|
496
|
+
});
|
|
497
|
+
}
|