mirror of https://github.com/snachodog/mybuddy.git
Update static files so tests run
This commit is contained in:
parent
8a37b44839
commit
bd78149db3
|
@ -10586,6 +10586,10 @@ h3 {
|
|||
font-size: 1.65em;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
color: #343a40;
|
||||
}
|
||||
|
||||
#view-core\:child .child-photo {
|
||||
max-width: 150px;
|
||||
}
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,337 @@
|
|||
(function() {
|
||||
/**
|
||||
* Parse a string as hexadecimal number
|
||||
*/
|
||||
function hexParse(x) {
|
||||
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) {
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
function doReq(method, uri, data, success, fail) {
|
||||
// TODO: prefer jQuery based requests for now
|
||||
|
||||
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", CSRF_TOKEN);
|
||||
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 {
|
||||
constructor(widget) {
|
||||
this.prototype = widget.querySelector('.prototype-tag');
|
||||
this.listeners = [];
|
||||
|
||||
this.modalElement = widget.querySelector('.tag-editor-error-modal');
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
createNewTag(name, color, actionSymbol) {
|
||||
const tag = this.prototype.cloneNode(true);
|
||||
tag.classList.remove("prototype-tag");
|
||||
tag.classList.add("tag");
|
||||
this.updateTag(tag, name, color, actionSymbol);
|
||||
return tag;
|
||||
}
|
||||
|
||||
insertTag(list, tag) {
|
||||
list.appendChild(tag);
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback called when the the "Add" button of the add-tag input is
|
||||
* clicked or enter is pressed in the editor.
|
||||
*/
|
||||
onCreateTagClicked() {
|
||||
// TODO: Make promise based
|
||||
|
||||
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)
|
||||
);
|
||||
|
||||
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);
|
||||
onClicked(tag);
|
||||
}
|
||||
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() {
|
||||
const names = [];
|
||||
for (const tag of this.currentTags.querySelectorAll(".tag")) {
|
||||
const name = tag.getAttribute("data-value");
|
||||
names.push(`"${name}"`);
|
||||
}
|
||||
this.inputElement.value = names.join(",");
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a tag-DOM element as a "add tag" button.
|
||||
*/
|
||||
configureAddTag(tag) {
|
||||
this.taggingBase.updateTag(tag, null, null, "+");
|
||||
this.registerNewCallback(tag, this.currentTags, () => this.configureRemoveTag(tag));
|
||||
this.updateInputList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a tag-DOM element as a "remove tag" button.
|
||||
*/
|
||||
configureRemoveTag(tag) {
|
||||
this.taggingBase.updateTag(tag, null, null, "-");
|
||||
this.registerNewCallback(tag, this.newTags, () => this.configureAddTag(tag));
|
||||
this.updateInputList();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
for (const el of document.querySelectorAll('.babybuddy-tags-editor')) {
|
||||
new TagsEditor(el);
|
||||
}
|
||||
});
|
||||
})();
|
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue