Made widget translateable, added German translation

This commit is contained in:
Paul Konstantin Gerke 2022-02-27 19:41:27 +01:00
parent a7f461551c
commit cfdb9e1ade
4 changed files with 726 additions and 212 deletions

View File

@ -6,6 +6,12 @@
return parseInt(x, 16); return parseInt(x, 16);
} }
/**
* Attempt to compute a high-contrast color from a background color.
*
* (This probably should be researched better because this was
* hand-crafted ad-hoc.)
*/
function computeComplementaryColor(colorStr) { function computeComplementaryColor(colorStr) {
let avgColor = 0.0; let avgColor = 0.0;
avgColor += hexParse(colorStr.substring(1, 3)) * -0.5; avgColor += hexParse(colorStr.substring(1, 3)) * -0.5;
@ -19,6 +25,8 @@
} }
} }
// CSRF token should always be present because it is auto-included with
// every tag-editor widget
const CSRF_TOKEN = document.querySelector('input[name="csrfmiddlewaretoken"]').value; const CSRF_TOKEN = document.querySelector('input[name="csrfmiddlewaretoken"]').value;
function doReq(method, uri, data, success, fail) { function doReq(method, uri, data, success, fail) {
@ -46,6 +54,13 @@
req.send(data); req.send(data);
} }
/**
* Base class allowing generic operations on the tag lists, like:
*
* - Adding tags to a tag list
* - Updating or creating new tags with a set name and color
* - Controlling the error modal
*/
class TaggingBase { class TaggingBase {
constructor(widget) { constructor(widget) {
this.prototype = widget.querySelector('.prototype-tag'); this.prototype = widget.querySelector('.prototype-tag');
@ -118,8 +133,23 @@
/** /**
* Handler for the edit field allowing to dynamically create new tags. * Handler for the edit field allowing to dynamically create new tags.
*
* Handles user inputs for the editor. Calls the 'onInsertNewTag' callback
* when the craetion of a new tag has been requested. All backend handling
* like guareteening that the requested tag exists is handled by this class,
* the only task left is to add the new tag to the tags-list when
* 'onInsertNewTag' is called.
*/ */
class AddNewTagControl { class AddNewTagControl {
/**
* @param widget
* The root DOM element of the widget
* @param taggingBase
* Reference to a common TaggingBase class to be used by this widget
* @param onInsertNewTag
* Callback that is called when a new tag should be added to the
* tags widget.
*/
constructor(widget, taggingBase, onInsertNewTag) { constructor(widget, taggingBase, onInsertNewTag) {
this.widget = widget; this.widget = widget;
this.taggingBase = taggingBase; this.taggingBase = taggingBase;
@ -155,7 +185,7 @@
const fail = (msg) => { const fail = (msg) => {
this.addTagInput.select(); this.addTagInput.select();
this.taggingBase.showModal(msg || "error-creating-tag"); this.taggingBase.showModal(msg || "generic");
}; };
if (!tagName) { if (!tagName) {
@ -192,7 +222,19 @@
} }
}; };
/**
* JavaScript implementation for the tags editor.
*
* This class uses TaggingBase and AddNewTagControl to provide the custom
* tag editor controls. This mainly consists of updating the hidden
* input values with the current list of tags and adding/removing
* tags from the current-tags- or recently-used-lists.
*/
class TagsEditor { class TagsEditor {
/**
* @param tagEditorRoot
* The root DOM element of the widget.
*/
constructor(tagEditorRoot) { constructor(tagEditorRoot) {
this.widget = tagEditorRoot; this.widget = tagEditorRoot;
this.taggingBase = new TaggingBase(this.widget); this.taggingBase = new TaggingBase(this.widget);
@ -217,6 +259,13 @@
); );
} }
/**
* Insert a new tag into the "current tag" list.
*
* Makes sure that no duplicates are present in the widget before adding
* the new tag. If a duplicate is found, the old tag is removed before
* the new one is added.
*/
insertNewTag(tag) { insertNewTag(tag) {
const name = tag.getAttribute("data-value"); const name = tag.getAttribute("data-value");
@ -229,6 +278,12 @@
this.configureRemoveTag(tag); this.configureRemoveTag(tag);
} }
/**
* Registeres a click-callback for a given node.
*
* The callback chain-calls another callback "onClicked" after
* moving the clicked tag from the old tag-list to a new tag list.
*/
registerNewCallback(tag, newParent, onClicked) { registerNewCallback(tag, newParent, onClicked) {
function callback(event) { function callback(event) {
tag.parentNode.removeChild(tag); tag.parentNode.removeChild(tag);
@ -240,6 +295,12 @@
tag.addEventListener('click', callback.bind(this)); tag.addEventListener('click', callback.bind(this));
} }
/**
* Updates the value of the hidden input element.
*
* Sets the value from the list of tags added to the currentTags
* DOM element.
*/
updateInputList() { updateInputList() {
const names = []; const names = [];
for (const tag of this.currentTags.querySelectorAll(".tag")) { for (const tag of this.currentTags.querySelectorAll(".tag")) {
@ -249,13 +310,19 @@
this.inputElement.value = names.join(","); this.inputElement.value = names.join(",");
} }
/**
* Configure a tag-DOM element as a "add tag" button.
*/
configureAddTag(tag) { configureAddTag(tag) {
this.taggingBase.updateTag(tag, null, null, "+"); this.taggingBase.updateTag(tag, null, null, "+");
this.registerNewCallback(tag, this.currentTags, () => this.configureRemoveTag(tag)); this.registerNewCallback(tag, this.currentTags, () => this.configureRemoveTag(tag));
this.updateInputList(); this.updateInputList();
} }
configureRemoveTag(tag) { /**
* Configure a tag-DOM element as a "remove tag" button.
*/
configureRemoveTag(tag) {
this.taggingBase.updateTag(tag, null, null, "-"); this.taggingBase.updateTag(tag, null, null, "-");
this.registerNewCallback(tag, this.newTags, () => this.configureAddTag(tag)); this.registerNewCallback(tag, this.newTags, () => this.configureAddTag(tag));
this.updateInputList(); this.updateInputList();

View File

@ -1,10 +1,12 @@
{% load i18n %}
<div data-tags-url="{% url 'api:api-root' %}tags/" <div data-tags-url="{% url 'api:api-root' %}tags/"
{% for k, v in widget.attrs.items %} {% for k, v in widget.attrs.items %}
{{ k }}="{{ v }}" {{ k }}="{{ v }}"
{% endfor %}> {% endfor %}>
{% csrf_token %} {% csrf_token %}
<span class="prototype-tag btn badge badge-pill cursor-pointer mr-1" style="display: none;"> <span class="prototype-tag btn badge badge-pill cursor-pointer mr-1" style="display: none;">
PROTOTYPE UNINITIALIZED PROTOTYPE
<span class="add-remove-icon pl-1 pr-1">+ or -</span> <span class="add-remove-icon pl-1 pr-1">+ or -</span>
</span> </span>
<div class="current_tags" style="min-height: 2em;"> <div class="current_tags" style="min-height: 2em;">
@ -17,12 +19,12 @@
</div> </div>
<div class="new-tags"> <div class="new-tags">
<div class="create-tag-inputs input-group"> <div class="create-tag-inputs input-group">
<input class="form-control" type="text" name="" placeholder="Tag name"> <input class="form-control" type="text" name="" placeholder="{% trans "Tag name" %}">
<div class="input-group-append"> <div class="input-group-append">
<button class="btn btn-outline-primary bg-dark btn-add-new-tag" type="button">Add</button> <button class="btn btn-outline-primary bg-dark btn-add-new-tag" type="button">{% trans "Add" %}</button>
</div> </div>
</div> </div>
<span>Recently used:</span> <span>{% trans "Recently used:" %}</span>
{% for t in widget.tag_suggestions.quick %} {% for t in widget.tag_suggestions.quick %}
<span data-value="{{ t.name }}" data-color="{{ t.color }}" class="tag btn badge badge-pill cursor-pointer mr-1" style="background-color: {{ t.color }};"> <span data-value="{{ t.name }}" data-color="{{ t.color }}" class="tag btn badge badge-pill cursor-pointer mr-1" style="background-color: {{ t.color }};">
{{ t.name }} {{ t.name }}
@ -32,7 +34,7 @@
</div> </div>
<input <input
type="hidden" type="hidden"
name="tags" name="{{ widget.name }}"
value="{% for t in widget.value %}&quot;{{ t.name }}&quot;{% if not forloop.last %},{% endif %}{% endfor %}" value="{% for t in widget.value %}&quot;{{ t.name }}&quot;{% if not forloop.last %},{% endif %}{% endfor %}"
> >
@ -40,20 +42,21 @@
<div class="modal-dialog modal-sm" role="document"> <div class="modal-dialog modal-sm" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title">Error</h4> <h4 class="modal-title">{% trans "Error" context "Error modal" %}</h4>
<button type="button" class="close" data-dismiss="modal">&times;</button> <button type="button" class="close" data-dismiss="modal">&times;</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<span data-message="generic">An error ocurred</span> <span data-message="generic">{% trans "An error ocurred." context "Error modal" %}</span>
<span data-message="error-creating-tag">Error creating tag.</span> <span data-message="invalid-tag-name">{% trans "Invalid tag name." context "Error modal" %}</span>
<span data-message="invalid-tag-name">Invalid tag name.</span> <span data-message="tag-creation-failed">{% trans "Failed to create tag." context "Error modal" %}</span>
<span data-message="tag-creation-failed">Failed to create tag.</span> <span data-message="tag-checking-failed">{% trans "Failed to obtain tag data." context "Error modal" %}</span>
<span data-message="tag-checking-failed">Failed to obtain tag data.</span>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-danger" data-dismiss="modal">Close</button> <button type="button" class="btn btn-danger" data-dismiss="modal">
{% trans "Close" context "Error modal" %}
</button>
</div> </div>
</div> </div>
</div> </div>

File diff suppressed because it is too large Load Diff

201
static/babybuddy/js/tags_editor.js generated Normal file
View File

@ -0,0 +1,201 @@
(function() {
class TagsEditor {
constructor(tagEditorRoot) {
this.tagEditorRoot = tagEditorRoot;
}
};
window.addEventListener('load', () => {
const widget = document.getElementById('{{ widget.attrs.id }}');
const csrfToken = document.querySelector('input[name="csrfmiddlewaretoken"]').value;
const prototype = widget.querySelector('.prototype-tag');
const currentTags = widget.querySelector('.current_tags');
const newTags = widget.querySelector('.new-tags');
const inputElement = widget.querySelector('input[type="hidden"]');
const apiTagsUrl = widget.getAttribute('data-tags-url');
const createTagInputs = widget.querySelector('.create-tag-inputs');
const addTagInput = createTagInputs.querySelector('input[type="text"]');
const addTagButton = createTagInputs.querySelector('.btn-add-new-tag');
function doReq(method, uri, data, success, fail) {
const req = new XMLHttpRequest();
req.addEventListener('load', () => {
if ((req.status >= 200) && (req.status < 300)) {
success(req.responseText, req);
} else {
fail(req.responseText, req);
}
});
for (const name of ["error", "timeout", "abort"]) {
req.addEventListener(name, () => {
fail(req.responseText, req);
});
}
req.timeout = 20000;
req.open(method, uri);
req.setRequestHeader("Content-Type", "application/json");
req.setRequestHeader("Accept", "application/json");
req.setRequestHeader("X-CSRFTOKEN", csrfToken);
req.send(data);
}
function createTagClicked() {
const tagName = addTagInput.value.trim();
const uriTagName = encodeURIComponent(tagName);
function success() {
addTagInput.value = "";
}
function fail(msg) {
msg = msg || "Error creating tag";
addTagInput.select()
alert(msg);
}
if (!tagName) {
fail('Not a valid tag name');
return;
}
const data = JSON.stringify({
'name': addTagInput.value
});
function addTag(name, color) {
const foundTag = widget.querySelector(`span[data-value="${name}"]`);
if (foundTag) {
foundTag.parentNode.removeChild(foundTag);
}
const tag = createNewTag(name, color, "-");
insertTag(currentTags, tag);
removeTagCallback(tag);
success();
}
doReq("GET", `${apiTagsUrl}?name=${uriTagName}`, null,
(text) => {
const json = JSON.parse(text);
if (json.count) {
const tagJson = json.results[0];
addTag(tagJson.name, tagJson.color);
} else {
doReq("POST", apiTagsUrl, data,
(text) => {
const tagJson = JSON.parse(text);
addTag(tagJson.name, tagJson.color);
}, fail
);
}
}, fail
);
}
addTagButton.addEventListener('click', createTagClicked);
addTagInput.addEventListener('keydown', (e) => {
const key = e.key.toLowerCase();
if (key === "enter") {
e.preventDefault();
createTagClicked();
}
});
function hexParse(x) {
return parseInt(x, 16);
}
function computeComplementaryColor(colorStr) {
let avgColor = 0.0;
avgColor += hexParse(colorStr.substring(1, 3)) * -0.5;
avgColor += hexParse(colorStr.substring(3, 5)) * 1.5;
avgColor += hexParse(colorStr.substring(5, 7)) * 1.0;
if (avgColor > 200) {
return "#101010";
} else {
return "#E0E0E0";
}
}
function updateTag(tag, name, color, actionSymbol) {
const actionTextNode = tag.querySelector('.add-remove-icon').childNodes[0];
name = name || tag.getAttribute("data-value");
color = color || tag.getAttribute("data-color");
actionSymbol = actionSymbol || actionTextNode.textContent;
tag.childNodes[0].textContent = name;
tag.setAttribute("data-value", name);
tag.setAttribute("data-color", color);
const textColor = computeComplementaryColor(color);
tag.setAttribute('style', `background-color: ${color}; color: ${textColor};`);
actionTextNode.textContent = actionSymbol;
}
function createNewTag(name, color, actionSymbol) {
const tag = prototype.cloneNode(true);
tag.classList.remove("prototype-tag");
tag.classList.add("tag");
updateTag(tag, name, color, actionSymbol);
return tag;
}
function insertTag(list, tag) {
list.appendChild(tag);
updateInputList();
}
function registerNewCallback(tag, newParent, newSymbol, onClicked) {
function callback(event) {
tag.parentNode.removeChild(tag);
updateTag(
tag,
null,
tag.getAttribute("data-color"),
newSymbol
);
insertTag(newParent, tag);
tag.removeEventListener('click', callback);
onClicked(tag);
}
tag.addEventListener('click', callback);
}
function updateInputList() {
const names = [];
for (const tag of currentTags.querySelectorAll(".tag")) {
const name = tag.getAttribute("data-value");
names.push(`"${name}"`);
}
inputElement.value = names.join(",");
}
function addTagCallback(tag) {
registerNewCallback(tag, currentTags, "-", removeTagCallback);
updateInputList();
}
function removeTagCallback(tag) {
registerNewCallback(tag, newTags, "+", addTagCallback);
updateInputList();
}
for (const tag of newTags.querySelectorAll(".tag")) {
updateTag(tag);
addTagCallback(tag);
}
for (const tag of currentTags.querySelectorAll(".tag")) {
updateTag(tag);
removeTagCallback(tag);
}
});
})();