Add german translation and refactor tags_editor.js

This commit is contained in:
Paul Konstantin Gerke 2022-02-27 19:50:05 +01:00
parent cfdb9e1ade
commit 897fb7c2d1
4 changed files with 285 additions and 145 deletions

Binary file not shown.

View File

@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Baby Buddy\n" "Project-Id-Version: Baby Buddy\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-02-27 18:38+0000\n" "POT-Creation-Date: 2022-02-27 18:44+0000\n"
"Language: de\n" "Language: de\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"

View File

@ -10586,6 +10586,10 @@ h3 {
font-size: 1.65em; font-size: 1.65em;
} }
.modal-content {
color: #343a40;
}
#view-core\:child .child-photo { #view-core\:child .child-photo {
max-width: 150px; max-width: 150px;
} }

View File

@ -1,26 +1,37 @@
(function() { (function() {
class TagsEditor { /**
constructor(tagEditorRoot) { * Parse a string as hexadecimal number
this.tagEditorRoot = tagEditorRoot; */
function hexParse(x) {
return parseInt(x, 16);
} }
};
window.addEventListener('load', () => { /**
const widget = document.getElementById('{{ widget.attrs.id }}'); * Attempt to compute a high-contrast color from a background color.
const csrfToken = document.querySelector('input[name="csrfmiddlewaretoken"]').value; *
* (This probably should be researched better because this was
* hand-crafted ad-hoc.)
*/
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;
const prototype = widget.querySelector('.prototype-tag'); if (avgColor > 200) {
const currentTags = widget.querySelector('.current_tags'); return "#101010";
const newTags = widget.querySelector('.new-tags'); } else {
return "#E0E0E0";
}
}
const inputElement = widget.querySelector('input[type="hidden"]'); // CSRF token should always be present because it is auto-included with
// every tag-editor widget
const apiTagsUrl = widget.getAttribute('data-tags-url'); const CSRF_TOKEN = document.querySelector('input[name="csrfmiddlewaretoken"]').value;
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) { function doReq(method, uri, data, success, fail) {
// TODO: prefer jQuery based requests for now
const req = new XMLHttpRequest(); const req = new XMLHttpRequest();
req.addEventListener('load', () => { req.addEventListener('load', () => {
if ((req.status >= 200) && (req.status < 300)) { if ((req.status >= 200) && (req.status < 300)) {
@ -39,91 +50,58 @@
req.open(method, uri); req.open(method, uri);
req.setRequestHeader("Content-Type", "application/json"); req.setRequestHeader("Content-Type", "application/json");
req.setRequestHeader("Accept", "application/json"); req.setRequestHeader("Accept", "application/json");
req.setRequestHeader("X-CSRFTOKEN", csrfToken); req.setRequestHeader("X-CSRFTOKEN", CSRF_TOKEN);
req.send(data); req.send(data);
} }
function createTagClicked() { /**
const tagName = addTagInput.value.trim(); * Base class allowing generic operations on the tag lists, like:
const uriTagName = encodeURIComponent(tagName); *
* - Adding tags to a tag list
* - Updating or creating new tags with a set name and color
* - Controlling the error modal
*/
class TaggingBase {
constructor(widget) {
this.prototype = widget.querySelector('.prototype-tag');
this.listeners = [];
function success() { this.modalElement = widget.querySelector('.tag-editor-error-modal');
addTagInput.value = ""; this.modalBodyNode = this.modalElement.querySelector('.modal-body');
// Clean whitespace text nodes between spans
for (const n of this.modalBodyNode.childNodes) {
if (n.nodeType === Node.TEXT_NODE) {
this.modalBodyNode.removeChild(n);
} }
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) { showModal(msg) {
const selectedMessage = this.modalBodyNode.querySelector(`span[data-message='${msg}']`);
if (!selectedMessage) {
selectedMessage = this.modalBodyNode.childNodes[0];
}
for (const n of this.modalBodyNode.childNodes) {
n.classList.add('d-none');
}
selectedMessage.classList.remove('d-none');
jQuery(this.modalElement).modal('show');
}
addTagListUpdatedListener(c) {
this.listeners.push(c);
}
callTagListUpdatedListeners() {
for (const l of this.listeners) {
l();
}
}
updateTag(tag, name, color, actionSymbol) {
const actionTextNode = tag.querySelector('.add-remove-icon').childNodes[0]; const actionTextNode = tag.querySelector('.add-remove-icon').childNodes[0];
name = name || tag.getAttribute("data-value"); name = name || tag.getAttribute("data-value");
@ -139,63 +117,221 @@
actionTextNode.textContent = actionSymbol; actionTextNode.textContent = actionSymbol;
} }
function createNewTag(name, color, actionSymbol) { createNewTag(name, color, actionSymbol) {
const tag = prototype.cloneNode(true); const tag = this.prototype.cloneNode(true);
tag.classList.remove("prototype-tag"); tag.classList.remove("prototype-tag");
tag.classList.add("tag"); tag.classList.add("tag");
updateTag(tag, name, color, actionSymbol); this.updateTag(tag, name, color, actionSymbol);
return tag; return tag;
} }
function insertTag(list, tag) { insertTag(list, tag) {
list.appendChild(tag); list.appendChild(tag);
updateInputList(); this.callTagListUpdatedListeners();
}
};
/**
* 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 {
/**
* @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) {
this.widget = widget;
this.taggingBase = taggingBase;
this.apiTagsUrl = widget.getAttribute('data-tags-url');
this.createTagInputs = widget.querySelector('.create-tag-inputs');
this.addTagInput = this.createTagInputs.querySelector('input[type="text"]');
this.addTagButton = this.createTagInputs.querySelector('.btn-add-new-tag');
this.addTagInput.value = "";
this.onInsertNewTag = onInsertNewTag;
this.addTagButton.addEventListener('click', () => this.onCreateTagClicked());
this.addTagInput.addEventListener('keydown', (e) => {
const key = e.key.toLowerCase();
if (key === "enter") {
e.preventDefault();
this.onCreateTagClicked();
}
});
} }
function registerNewCallback(tag, newParent, newSymbol, onClicked) { /**
function callback(event) { * Callback called when the the "Add" button of the add-tag input is
tag.parentNode.removeChild(tag); * clicked or enter is pressed in the editor.
updateTag( */
tag, onCreateTagClicked() {
null, // TODO: Make promise based
tag.getAttribute("data-color"),
newSymbol const tagName = this.addTagInput.value.trim();
const uriTagName = encodeURIComponent(tagName);
const fail = (msg) => {
this.addTagInput.select();
this.taggingBase.showModal(msg || "generic");
};
if (!tagName) {
fail('invalid-tag-name');
return;
}
const addTag = (name, color) => {
const tag = this.taggingBase.createNewTag(name, color, "-");
this.addTagInput.value = "";
this.onInsertNewTag(tag);
};
const data = JSON.stringify({
'name': this.addTagInput.value
});
doReq("GET", `${this.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", this.apiTagsUrl, data,
(text) => {
const tagJson = JSON.parse(text);
addTag(tagJson.name, tagJson.color);
}, () => fail("tag-creation-failed")
);
}
}, () => fail("tag-checking-failed")
);
}
};
/**
* 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 {
/**
* @param tagEditorRoot
* The root DOM element of the widget.
*/
constructor(tagEditorRoot) {
this.widget = tagEditorRoot;
this.taggingBase = new TaggingBase(this.widget);
this.addTagControl = new AddNewTagControl(
this.widget, this.taggingBase, (t) => this.insertNewTag(t)
); );
insertTag(newParent, tag); this.currentTags = this.widget.querySelector('.current_tags');
this.newTags = this.widget.querySelector('.new-tags');
this.inputElement = this.widget.querySelector('input[type="hidden"]');
for (const tag of this.newTags.querySelectorAll(".tag")) {
this.configureAddTag(tag);
}
for (const tag of this.currentTags.querySelectorAll(".tag")) {
this.configureRemoveTag(tag);
}
this.updateInputList();
this.taggingBase.addTagListUpdatedListener(
() => this.updateInputList()
);
}
/**
* 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) {
const name = tag.getAttribute("data-value");
const oldTag = this.widget.querySelector(`span[data-value="${name}"]`);
if (oldTag) {
oldTag.parentNode.removeChild(oldTag);
}
this.taggingBase.insertTag(this.currentTags, 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) {
function callback(event) {
tag.parentNode.removeChild(tag);
this.taggingBase.insertTag(newParent, tag);
tag.removeEventListener('click', callback); tag.removeEventListener('click', callback);
onClicked(tag); onClicked(tag);
} }
tag.addEventListener('click', callback); tag.addEventListener('click', callback.bind(this));
} }
function updateInputList() { /**
* Updates the value of the hidden input element.
*
* Sets the value from the list of tags added to the currentTags
* DOM element.
*/
updateInputList() {
const names = []; const names = [];
for (const tag of currentTags.querySelectorAll(".tag")) { for (const tag of this.currentTags.querySelectorAll(".tag")) {
const name = tag.getAttribute("data-value"); const name = tag.getAttribute("data-value");
names.push(`"${name}"`); names.push(`"${name}"`);
} }
inputElement.value = names.join(","); this.inputElement.value = names.join(",");
} }
function addTagCallback(tag) { /**
registerNewCallback(tag, currentTags, "-", removeTagCallback); * Configure a tag-DOM element as a "add tag" button.
updateInputList(); */
configureAddTag(tag) {
this.taggingBase.updateTag(tag, null, null, "+");
this.registerNewCallback(tag, this.currentTags, () => this.configureRemoveTag(tag));
this.updateInputList();
} }
function removeTagCallback(tag) { /**
registerNewCallback(tag, newTags, "+", addTagCallback); * Configure a tag-DOM element as a "remove tag" button.
updateInputList(); */
configureRemoveTag(tag) {
this.taggingBase.updateTag(tag, null, null, "-");
this.registerNewCallback(tag, this.newTags, () => this.configureAddTag(tag));
this.updateInputList();
} }
};
for (const tag of newTags.querySelectorAll(".tag")) { window.addEventListener('load', () => {
updateTag(tag); for (const el of document.querySelectorAll('.babybuddy-tags-editor')) {
addTagCallback(tag); new TagsEditor(el);
}
for (const tag of currentTags.querySelectorAll(".tag")) {
updateTag(tag);
removeTagCallback(tag);
} }
}); });
})(); })();