etiquette/frontends/etiquette_flask/static/js/editor.js

266 lines
8.9 KiB
JavaScript

const editor = {};
editor.PARAGRAPH_TYPES = new Set(["P", "PRE"]);
editor.Editor =
function Editor(elements, on_open, on_save, on_cancel)
{
/*
This class wraps around display elements like headers and paragraphs, and
creates edit elements like inputs and textareas to edit them with.
You may add the following data- attributes to your display elements to
affect their corresponding edit elements:
data-editor-empty-text: If the display element contains this text, then
the edit element will be set to "" when opened.
If the edit element contains "", then the display element will
contain this text when saved.
data-editor-id: The string used as the key into display_element_map and
edit_element_map.
data-editor-placeholder: The placeholder attribute of the edit element.
Your on_open, on_save and on_cancel hooks will be called with:
1. This editor object.
2. The edit elements as either:
If ALL of the display elements have a data-editor-id,
then a dictionary of {data-editor-id: edit_element, ...}.
Otherwise, an array of [edit_element, ...] in the order they were
given to the constructor.
3. The display elements as either the map or the array, similarly.
When your callbacks are used, the default `open`, `save`, `cancel`
methods are not called automatically. You should call them from within
your function. That's because you may wish to do some of your own
normalization before the default handler, and some of your own cleanup
after it. So it is up to you when to call the default.
*/
this.cancel = function()
{
this.close();
};
this.close = function()
{
for (let index = 0; index < this.display_elements.length; index += 1)
{
this.display_elements[index].classList.remove("hidden");
this.edit_elements[index].classList.add("hidden");
}
this.hide_spinner();
this.hide_error();
this.open_button.classList.remove("hidden");
this.save_button.classList.add("hidden");
this.cancel_button.classList.add("hidden");
};
this.hide_error = function()
{
this.error_message.classList.add("hidden");
};
this.hide_spinner = function()
{
this.spinner.hide();
};
this.open = function()
{
for (let index = 0; index < this.display_elements.length; index += 1)
{
const display_element = this.display_elements[index];
const edit_element = this.edit_elements[index];
display_element.classList.add("hidden");
edit_element.classList.remove("hidden");
const empty_text = display_element.dataset.editorEmptyText;
if (empty_text !== undefined && display_element.innerText == empty_text)
{
edit_element.value = "";
}
else
{
edit_element.value = display_element.innerText;
}
}
this.open_button.classList.add("hidden");
this.save_button.classList.remove("hidden");
this.cancel_button.classList.remove("hidden");
};
this.save = function()
{
for (let index = 0; index < this.display_elements.length; index += 1)
{
const display_element = this.display_elements[index];
const edit_element = this.edit_elements[index];
if (display_element.dataset.editorEmptyText !== undefined && edit_element.value == "")
{
display_element.innerText = display_element.dataset.editorEmptyText;
}
else
{
display_element.innerText = edit_element.value;
}
}
this.close();
};
this.show_error = function(message)
{
this.hide_spinner();
this.error_message.innerText = message;
this.error_message.classList.remove("hidden");
};
this.show_spinner = function(delay)
{
this.hide_error();
this.spinner.show(delay);
};
this.display_elements = [];
this.edit_elements = [];
this.can_use_element_map = true;
this.display_element_map = {};
this.edit_element_map = {};
this.misc_data = {};
for (const display_element of elements)
{
let edit_element;
if (editor.PARAGRAPH_TYPES.has(display_element.tagName))
{
edit_element = document.createElement("textarea");
edit_element.rows = 6;
}
else
{
edit_element = document.createElement("input");
edit_element.type = "text";
}
edit_element.classList.add("editor_input");
edit_element.classList.add("hidden");
if (display_element.dataset.editorPlaceholder !== undefined)
{
edit_element.placeholder = display_element.dataset.editorPlaceholder;
}
if (this.can_use_element_map)
{
if (display_element.dataset.editorId !== undefined)
{
this.display_element_map[display_element.dataset.editorId] = display_element;
this.edit_element_map[display_element.dataset.editorId] = edit_element;
}
else
{
this.can_use_element_map = false;
this.edit_element_map = null;
this.display_element_map = null;
}
}
display_element.parentElement.insertBefore(edit_element, display_element.nextSibling);
this.display_elements.push(display_element);
this.edit_elements.push(edit_element);
}
this.binder = function(func, fallback)
{
/*
Given a function that takes an Editor as its first argument, and the
element arrays/maps as the second and third, return a new function
which requires no arguments and calls the given function with the
correct data.
This is done so that the new function can be used in an event handler.
*/
if (func == undefined)
{
return fallback.bind(this);
}
const bindable = function()
{
if (this.can_use_element_map)
{
func(this, this.edit_element_map, this.display_element_map);
}
else
{
func(this, this.edit_elements, this.display_elements);
}
}
return bindable.bind(this);
}
const placeholders = document.getElementsByClassName("editor_toolbox_placeholder");
for (const placeholder of placeholders)
{
placeholder.parentElement.removeChild(placeholder);
}
const last_element = this.edit_elements[this.edit_elements.length - 1];
const toolbox = document.createElement("div");
toolbox.classList.add("editor_toolbox");
last_element.parentElement.insertBefore(toolbox, last_element.nextSibling);
this.open_button = document.createElement("button");
this.open_button.innerText = "Edit";
this.open_button.classList.add("editor_button");
this.open_button.classList.add("editor_open_button");
this.open_button.classList.add("green_button");
this.open_button.onclick = this.binder(on_open, this.open);
toolbox.appendChild(this.open_button);
this.save_button = document.createElement("button");
this.save_button.innerText = "Save";
this.save_button.classList.add("editor_button");
this.save_button.classList.add("editor_save_button");
this.save_button.classList.add("green_button");
this.save_button.classList.add("hidden");
this.save_button.onclick = this.binder(on_save, this.save);
toolbox.appendChild(this.save_button);
toolbox.appendChild(document.createTextNode(" "));
this.cancel_button = document.createElement("button");
this.cancel_button.innerText = "Cancel";
this.cancel_button.classList.add("editor_button");
this.cancel_button.classList.add("editor_cancel_button");
this.cancel_button.classList.add("gray_button");
this.cancel_button.classList.add("hidden");
this.cancel_button.onclick = this.binder(on_cancel, this.cancel);
toolbox.appendChild(this.cancel_button);
this.error_message = document.createElement("span");
this.error_message.classList.add("editor_error");
this.error_message.classList.add("hidden");
toolbox.appendChild(this.error_message);
spinner_element = document.createElement("span");
spinner_element.innerText = "Submitting...";
spinner_element.classList.add("editor_spinner");
spinner_element.classList.add("hidden");
this.spinner = new spinner.Spinner(spinner_element);
toolbox.appendChild(spinner_element);
for (const edit_element of this.edit_elements)
{
if (edit_element.tagName == "TEXTAREA")
{
common.bind_box_to_button(edit_element, this.save_button, true);
}
else
{
common.bind_box_to_button(edit_element, this.save_button, false);
}
}
}