mirror of https://github.com/snachodog/mybuddy.git
Create new javascript file for tags editor
This commit is contained in:
parent
f47d3c6b76
commit
747b398bd5
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
|
@ -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>
|
||||||
|
|
|
@ -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 %}"{{ t.name }}"{% if not forloop.last %},{% endif %}{% endfor %}"
|
value="{% for t in widget.value %}"{{ t.name }}"{% 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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue