Create new javascript file for tags editor

This commit is contained in:
Paul Konstantin Gerke 2022-02-22 19:40:27 +01:00
parent f47d3c6b76
commit 747b398bd5
6 changed files with 233 additions and 202 deletions

View File

@ -0,0 +1,206 @@
(function() {
class TagsEditor {
constructor(tagEditorRoot) {
this.tagEditorRoot = tagEditorRoot;
}
};
window.addEventListener('load', () => {
for (const el of document.querySelectorAll('.babybuddy-tags-editor')) {
new TagsEditor(el);
}
return;
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);
}
});
})();

View File

@ -1,9 +1,12 @@
{% load i18n widget_tweaks %} {% load i18n widget_tweaks %}
{# Load any form-javascript files #}
{{ form.media.js }}
<div class="container-fluid pb-5"> <div class="container-fluid pb-5">
<form role="form" method="post" enctype="multipart/form-data"> <form role="form" method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
{% for field in form %} {% for field in form %}
{{ field.widget }}
<div class="form-group row"> <div class="form-group row">
{% include 'babybuddy/form_field.html' %} {% include 'babybuddy/form_field.html' %}
</div> </div>

View File

@ -1,6 +1,8 @@
<div class="form_control" data-tags-url="{% url 'api:api-root' %}tags/" {{ widget }}
<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;">
@ -35,199 +37,4 @@
name="tags" name="tags"
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 %}"
> >
<script>
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);
}
});
</script>
</div> </div>

View File

@ -1,7 +1,11 @@
from django.forms import Media
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from django.forms import Widget from django.forms import Widget
class TagsEditor(Widget): class TagsEditor(Widget):
class Media:
js = ("babybuddy/js/tags_editor.js",)
input_type = 'hidden' input_type = 'hidden'
template_name = 'core/widget_tag_editor.html' template_name = 'core/widget_tag_editor.html'
@ -14,21 +18,23 @@ class TagsEditor(Widget):
value = [self.__unpack_tag(tag) for tag in value] value = [self.__unpack_tag(tag) for tag in value]
return value return value
def build_attrs(self, base_attrs, extra_attrs=None):
attrs = super().build_attrs(base_attrs, extra_attrs)
attrs['class'] = attrs.get('class', '') + ' babybuddy-tags-editor'
return attrs
def get_context(self, name: str, value: Any, attrs) -> Dict[str, Any]: def get_context(self, name: str, value: Any, attrs) -> Dict[str, Any]:
from . import models from . import models
most_tags = models.BabyBuddyTag.objects.order_by( most_tags = models.BabyBuddyTag.objects.order_by(
'-last_used' '-last_used'
).all()[:256] ).all()[:256]
quick_suggestion_tags = models.BabyBuddyTag.objects.order_by(
'-last_used'
).all()
result = super().get_context(name, value, attrs) result = super().get_context(name, value, attrs)
tag_names = set(x['name'] for x in (result.get('widget', {}).get('value', None) or [])) tag_names = set(x['name'] for x in (result.get('widget', {}).get('value', None) or []))
quick_suggestion_tags = [ quick_suggestion_tags = [
t for t in quick_suggestion_tags t for t in most_tags
if t.name not in tag_names if t.name not in tag_names
][:5] ][:5]
@ -41,4 +47,4 @@ class TagsEditor(Widget):
self.__unpack_tag(t) for t in most_tags self.__unpack_tag(t) for t in most_tags
] ]
} }
return result return result

View File

@ -66,6 +66,9 @@ module.exports = {
'api/static_src/js/*.js', 'api/static_src/js/*.js',
'core/static_src/js/*.js', 'core/static_src/js/*.js',
'dashboard/static_src/js/*.js' 'dashboard/static_src/js/*.js'
],
tags_editor: [
'babybuddy/static_src/js/tags_editor.js'
] ]
}, },
stylesConfig: { stylesConfig: {

View File

@ -195,6 +195,12 @@ function scripts(cb) {
concat('app.js'), concat('app.js'),
gulp.dest(config.scriptsConfig.dest) gulp.dest(config.scriptsConfig.dest)
], cb); ], cb);
pump([
gulp.src(config.scriptsConfig.tags_editor),
concat('tags_editor.js'),
gulp.dest(config.scriptsConfig.dest)
], cb);
} }
/** /**