Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Iss 573 editable term listing #575

Merged
merged 13 commits into from
Jan 22, 2025
Merged
1 change: 1 addition & 0 deletions lute/models/term.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
31 changes: 23 additions & 8 deletions lute/static/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 ****************/

Expand Down
202 changes: 202 additions & 0 deletions lute/static/js/lute-tagify-utils.js
Original file line number Diff line number Diff line change
@@ -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 `<footer data-selector='tagify-suggestions-footer' class="${this.settings.classNames.dropdownFooter}">
(more items available, please refine your search.)</footer>`;
}
},

// 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
21 changes: 21 additions & 0 deletions lute/static/js/lute.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 3 additions & 1 deletion lute/static/vendor/tagify/tagify_overrides.css
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@
.tagify__dropdown {
background: white !important;
color: black !important;
min-width: 400px !important;
max-width: 600px;
text-overflow: ellipsis;
}

.tagify__tag {
Expand All @@ -84,4 +87,3 @@
padding: 0.1rem !important;
font-size: 0.8rem;
}

7 changes: 6 additions & 1 deletion lute/templates/read/reading_menu.html
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
<ul>
<li>
<a id="readmenu_bookmark_index" class="reading-menu-item" href="/bookmarks/{{ book.id }}" title="Index">
List
List bookmarks
</a>
</li>
<li>
Expand All @@ -91,6 +91,11 @@
</ul>
</div>
</li>
<li>
<a id="listTermsOnCurrentPage" class="reading-menu-item" href="" onclick="open_term_list_for_current_page(); return false;">
Term list
</a>
</li>
<li>
<!-- mobile users need this link, since they don't have keyboard shortcuts. -->
<a id="translateSentence" class="reading-menu-item" href="" onclick="handle_translate('sentence-id'); return false;">
Expand Down
2 changes: 1 addition & 1 deletion lute/templates/read/term_bulk_edit_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Loading
Loading