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

252 lines
8.2 KiB
JavaScript

const editor = {};
editor.PARAGRAPH_TYPES = new Set(["P", "PRE"]);
editor.Editor =
function Editor(element_argss, on_open, on_save, on_cancel)
{
/*
This class wraps around display elements like spans, headers, and
paragraphs, and creates edit elements like inputs and textareas to edit
them with.
element_argss should be a list of dicts. Each dict is required to have "id"
which is unique amongst its peers, and "element" which is the display
element. Additionally, you may add the following properties to change the
element's behavior:
"autofocus": true
When the user opens the editor, this element will get .focus().
Only one element should have this.
"empty_text": string
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.
"hide_when_empty": true
If the element does not have any text, it will get the "hidden" css
class after saving / closing.
"placeholder": string
The placeholder attribute of the edit element.
The editor object will contain a dict called elements that maps IDs to the
display element, edit elements, and your other options.
Your on_open, on_save and on_cancel hooks will be called with the editor
object as the only argument.
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 (const element of Object.values(this.elements))
{
element.edit.classList.add("hidden");
if (! (element.display.innerText === "" && element.hide_when_empty))
{
element.display.classList.remove("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 (const element of Object.values(this.elements))
{
element.display.classList.add("hidden");
element.edit.classList.remove("hidden");
if (element.autofocus)
{
element.edit.focus();
}
if (element.empty_text !== undefined && element.display.innerText == element.empty_text)
{
element.edit.value = "";
}
else
{
element.edit.value = element.display.innerText;
}
}
this.open_button.classList.add("hidden");
this.save_button.classList.remove("hidden");
this.cancel_button.classList.remove("hidden");
};
this.save = function()
{
for (const element of Object.values(this.elements))
{
if (element.empty_text !== undefined && element.edit.value == "")
{
element.display.innerText = element.empty_text;
}
else
{
element.display.innerText = element.edit.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.elements = {};
// End-user can put anything they want in here.
this.misc_data = {};
// Keep track of last edit element so we can put the toolbox after it.
let last_element;
for (const element_args of element_argss)
{
const element = {};
element.id = element_args.id;
this.elements[element.id] = element;
element.display = element_args.element;
element.empty_text = element_args.empty_text;
element.hide_when_empty = element_args.hide_when_empty;
element.autofocus = element_args.autofocus;
if (editor.PARAGRAPH_TYPES.has(element.display.tagName))
{
element.edit = document.createElement("textarea");
element.edit.rows = 6;
}
else
{
element.edit = document.createElement("input");
element.edit.type = "text";
}
element.edit.classList.add("editor_input");
element.edit.classList.add("hidden");
if (element_args.placeholder !== undefined)
{
element.edit.placeholder = element_args.placeholder;
}
element.display.parentElement.insertBefore(element.edit, element.display.nextSibling);
last_element = element.edit;
}
this.binder = function(func, fallback)
{
/*
Given a function that takes an Editor as its first argument,
return a new function which requires no arguments and calls the
function with this editor.
This is done so that the new function can be used in an event handler.
*/
if (func == undefined)
{
return fallback.bind(this);
}
const bindable = () => func(this);
return bindable.bind(this);
}
// In order to prevent page jumping on load, you can add an element with
// class editor_toolbox_placeholder to the page and size it so it matches
// the buttons that are going to get placed there.
const placeholders = document.getElementsByClassName("editor_toolbox_placeholder");
for (const placeholder of placeholders)
{
placeholder.parentElement.removeChild(placeholder);
}
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 element of Object.values(this.elements))
{
const ctrl_enter = element.edit.tagName == "TEXTAREA";
common.bind_box_to_button(element.edit, this.save_button, ctrl_enter);
}
}