From eb0e5ac7be5d5160233713d8a9fd85ab809a6411 Mon Sep 17 00:00:00 2001 From: Jeff Zohrab Date: Tue, 21 Jan 2025 23:38:52 -0600 Subject: [PATCH 01/13] Add tagify utils for creating parent and term tag tagify controls. --- lute/static/js/lute-tagify-utils.js | 198 ++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 lute/static/js/lute-tagify-utils.js diff --git a/lute/static/js/lute-tagify-utils.js b/lute/static/js/lute-tagify-utils.js new file mode 100644 index 000000000..569be2c11 --- /dev/null +++ b/lute/static/js/lute-tagify-utils.js @@ -0,0 +1,198 @@ +/** + * Tagify helpers. + * + * Lute uses Tagify (https://github.com/yairEO/tagify) + * for parent terms and term tags. + */ + + +/** + * Build a parent term tagify with autocomplete. + * + * args: + * - input: the input box tagify will control + * - language_id_func: zero-arg function that returns the language. + * + * notes: + * + * - language_id_func is passed, rather than a language_id, because in + * some cases such as bulk term editing the language isn't known at + * tagify setup time. The func delegates the check until it's + * actually needed, in _fetch_whitelist. + */ +function lute_tagify_utils_setup_parent_tagify( + input, + language_id_func, // if returns null, autocomplete does nothing + this_term_text = null, // set to non-null to filter whitelist + override_base_settings = {} +) { + if (input._tagify) { + // console.log('Tagify already initialized for this input.'); + return input._tagify; + } + + // Do the fetch and build the whitelist. + const _fetch_whitelist = function(mytagify, e_detail_value, controller) { + const language_id = language_id_func(); + if (language_id == null) { + console.log("language_id not set or not consistent"); + mytagify.loading(false); + return; + } + + // Create entry like "cat (a furry thing...)" + const _make_dropdown_entry = function(hsh) { + const txt = decodeURIComponent(hsh.text); + let translation = hsh.translation ?? ''; + translation = translation. + replaceAll("\n", "; "). + replaceAll("\r", ""). + trim(); + if (translation == '') + return txt; + const max_translation_len = 40; + if (translation.length > max_translation_len) + translation = translation.slice(0, max_translation_len) + "..."; + translation = translation ? `(${translation})` : ''; + return [txt, translation].join(' '); + }; + + // Build whitelist from returned ajax data. + const _build_whitelist = function(data) { + const _make_hash = function(a) { + return { + "value": a.text, + "id": a.id, + "suggestion": _make_dropdown_entry(a), + "status": a.status, + }; + }; + return data.map((a) => _make_hash(a)); + }; + + const encoded_value = encodeURIComponent(e_detail_value); + const url = `/term/search/${encoded_value}/${language_id ?? -1}`; + mytagify.loading(true); + fetch(url, {signal:controller.signal}) + .then(RES => RES.json()) + .then(function(data) { + // Update whitelist and render in place. + let whitelist = _build_whitelist(data); + whitelist = whitelist.filter(hsh => hsh.value != this_term_text); + mytagify.whitelist = whitelist; + mytagify.loading(false).dropdown.show(e_detail_value); + }).catch(err => { + if (err.name === 'AbortError') { + // Do nothing, fetch was aborted due to another fetch. + // console.log('AbortError: Fetch request aborted'); + } + else { + console.log(`error: ${err}`); + } + mytagify.loading(false); + }); + }; + + // Controller to handle cancellations/aborts of calls. + // https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort + var controller; + + // Build whitelist in response to user input. + function build_autocomplete_dropdown(mytagify, e) { + if (e.detail.value == '' || e.detail.value.length < 1) { + controller && controller.abort(); + mytagify.whitelist = []; + mytagify.loading(false).dropdown.hide(); + return; + } + controller && controller.abort() + controller = new AbortController() + _fetch_whitelist(mytagify, e.detail.value, controller); + } + + const make_Tagify_for = function(input) { + const base_settings = { + editTags: false, + pasteAsTags: false, + backspace: true, + addTagOnBlur: true, // note different + autoComplete: { enabled: true, rightKey: true, tabKey: true }, + delimiters: ';;', // special delimiter to handle parents with commas. + enforceWhitelist: false, + whitelist: [], + dropdown: { + enabled: 1, + maxItems: 15, + mapValueTo: 'suggestion', + placeAbove: false, + }, + templates: { + dropdownFooter(suggestions) { + var hasMore = suggestions.length - this.settings.dropdown.maxItems; + if (hasMore <= 0) + return ''; + return ``; + } + }, + + // Use a hook to force build_autocomplete_dropdown. + // Pasting from the clipboard doesnt fire the + // tagify.on('input') event, so intercept it and handle + // it manually. + hooks: { + beforePaste : function(content) { + return new Promise((resolve, reject) => { + clipboardData = content.clipboardData || window.clipboardData; + pastedData = clipboardData.getData('Text'); + // console.log("pasting => " + pastedData); + let e = { detail: { value: pastedData } }; + build_autocomplete_dropdown(ret, e); + resolve(); + }); + } + }, + }; + + let settings = { ...base_settings, ...override_base_settings }; + return new Tagify(input, settings); + }; + + const tagify = make_Tagify_for(input); + tagify.on('input', function (e) { + build_autocomplete_dropdown(tagify, e) + }); + + return tagify; +} // end lute_tagify_utils_setup_parent_tagify + + +/** + * Build a term tag tagify with autocomplete. + * + * args: + * - input: the input box tagify will control + * - tags: the tags array + * - override_base_settings: {} to override + */ +function lute_tagify_utils_setup_term_tag_tagify( + input, + tags, + override_base_settings = {} +) { + if (input._tagify) { + return input._tagify; + } + + const base_settings = { + delimiters: ';;', // special delim to handle tags w/ commas + editTags: false, + autoComplete: { enabled: true, rightKey: true, tabKey: true }, + dropdown: { enabled: 1 }, + enforceWhitelist: false, + whitelist: tags, + }; + const settings = { ...base_settings, ...override_base_settings }; + const tagify = new Tagify(input, settings); + return tagify; +} // end lute_tagify_utils_setup_term_tag_tagify From 9bc7dead68c20f69502f35c7f55d90e9e97fbcc2 Mon Sep 17 00:00:00 2001 From: Jeff Zohrab Date: Tue, 21 Jan 2025 23:41:25 -0600 Subject: [PATCH 02/13] Use tagify utility methods in term form, bulk edit form. --- lute/templates/read/term_bulk_edit_form.html | 2 +- .../term/_bulk_edit_form_fields.html | 128 +------- lute/templates/term/_form.html | 291 ++++++------------ 3 files changed, 107 insertions(+), 314 deletions(-) diff --git a/lute/templates/read/term_bulk_edit_form.html b/lute/templates/read/term_bulk_edit_form.html index 2c37f00cf..6a9dfe3fa 100644 --- a/lute/templates/read/term_bulk_edit_form.html +++ b/lute/templates/read/term_bulk_edit_form.html @@ -40,7 +40,7 @@ const elements = $(parent.document).find("span.word"); if (elements.length == 0) { console.log("No words on page ..."); - return -999; // dummy + return null; // dummy } const first = elements.first(); return $(first).data("lang-id"); diff --git a/lute/templates/term/_bulk_edit_form_fields.html b/lute/templates/term/_bulk_edit_form_fields.html index 7fd1c44a2..06a2155b8 100644 --- a/lute/templates/term/_bulk_edit_form_fields.html +++ b/lute/templates/term/_bulk_edit_form_fields.html @@ -39,110 +39,11 @@ + + diff --git a/lute/templates/term/_form.html b/lute/templates/term/_form.html index 1794d9627..22f5f5cc2 100644 --- a/lute/templates/term/_form.html +++ b/lute/templates/term/_form.html @@ -3,8 +3,21 @@ @@ -84,6 +97,9 @@ + + + +