mirror of https://github.com/snachodog/mybuddy.git
Made widget translateable, added German translation
This commit is contained in:
parent
a7f461551c
commit
cfdb9e1ade
|
@ -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();
|
||||||
|
|
|
@ -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 %}"{{ t.name }}"{% if not forloop.last %},{% endif %}{% endfor %}"
|
value="{% for t in widget.value %}"{{ t.name }}"{% 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">×</button>
|
<button type="button" class="close" data-dismiss="modal">×</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
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
Loading…
Reference in New Issue