diff --git a/lute/models/term.py b/lute/models/term.py index 9aad0cc50..ac89236b6 100644 --- a/lute/models/term.py +++ b/lute/models/term.py @@ -322,6 +322,7 @@ class Status(db.Model): # pylint: disable=too-few-public-methods UNKNOWN = 0 WELLKNOWN = 99 IGNORED = 98 + ALLOWED = [UNKNOWN, 1, 2, 3, 4, 5, IGNORED, WELLKNOWN] __tablename__ = "statuses" diff --git a/lute/static/css/styles.css b/lute/static/css/styles.css index dfc4d0ece..afeab21f0 100644 --- a/lute/static/css/styles.css +++ b/lute/static/css/styles.css @@ -2182,18 +2182,20 @@ input[name='status']:disabled + label { /** Term listing (/term) action dropdown. ********************/ -.term-action-container { - display: flex; +table#termtable .tagify, +table#termtable .translationDiv { + border: 1px solid transparent; /* Prevent elements pushing others around on hover. */ + box-sizing: border-box; } -/* -.term-action-dropdown, -.actionDiv { +table#termtable .tagify:hover, +table#termtable .translationDiv:hover { + border: 1px solid; +} + +.term-action-container { display: flex; - align-items: center; - margin-right: 10px; } -*/ #bulkEditDiv { margin: 0.5rem; @@ -2254,6 +2256,19 @@ div#termtable_wrapper div.dt-buttons { display: none; } +.ajax-saved-checkmark { + position: absolute; + width: 1.5em; /* Define the size of the circle */ + height: 1.5em; /* Make it a perfect circle */ + background: green; + color: white; + border-radius: 50%; /* Turns the square into a circle */ + font-size: 1em; /* Adjust size of the checkmark */ + text-align: center; + line-height: 1.5em; /* Vertically center the checkmark */ + box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.2); + z-index: 2004; +} /** Term image search ****************/ diff --git a/lute/static/js/lute-tagify-utils.js b/lute/static/js/lute-tagify-utils.js new file mode 100644 index 000000000..69e5bffe3 --- /dev/null +++ b/lute/static/js/lute-tagify-utils.js @@ -0,0 +1,202 @@ +/** + * 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); + } + + // Need a global tagify instance here + // so that hooks can use it. + var tagify_instance = null; + + 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'); + let e = { detail: { value: pastedData } }; + build_autocomplete_dropdown(tagify_instance, e); + resolve(); + }); + } + }, + }; + + let settings = { ...base_settings, ...override_base_settings }; + tagify_instance = new Tagify(input, settings); + return tagify_instance; + }; + + 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 diff --git a/lute/static/js/lute.js b/lute/static/js/lute.js index 79fbff1cc..4691c1597 100644 --- a/lute/static/js/lute.js +++ b/lute/static/js/lute.js @@ -825,6 +825,27 @@ let show_translation_for_text = function(text) { }; +// Get all the word ids on the current page, open new tab with just those terms. +function open_term_list_for_current_page() { + const ids = new Set(); + $('span.word').each(function () { + ids.add($(this).data("wid")); + }); + if (ids.length == 0) + return; // Nothing to do. + + const idarray = Array.from(ids); + const idlist = idarray.join('+'); + + // console.log('passing ids:'); + // console.log(idlist); + // let msg = idlist; + // window.alert(msg); + const url = `/term/index?termids=${idlist}`; + window.open(url); +} + + /** Show the translation using the next dictionary. */ function handle_translate(span_attribute) { const tis = get_textitems_spans(span_attribute); diff --git a/lute/static/vendor/tagify/tagify_overrides.css b/lute/static/vendor/tagify/tagify_overrides.css index ccd9cbb11..5c3f35e86 100644 --- a/lute/static/vendor/tagify/tagify_overrides.css +++ b/lute/static/vendor/tagify/tagify_overrides.css @@ -61,6 +61,9 @@ .tagify__dropdown { background: white !important; color: black !important; + min-width: 400px !important; + max-width: 600px; + text-overflow: ellipsis; } .tagify__tag { @@ -84,4 +87,3 @@ padding: 0.1rem !important; font-size: 0.8rem; } - diff --git a/lute/templates/read/reading_menu.html b/lute/templates/read/reading_menu.html index b24b545bc..b7cd82027 100644 --- a/lute/templates/read/reading_menu.html +++ b/lute/templates/read/reading_menu.html @@ -80,7 +80,7 @@ +
  • + + Term list + +
  • 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 @@ + + + +