Significant updates to editor.js.

This commit is contained in:
voussoir 2021-01-01 19:46:06 -08:00
parent 89195d3449
commit 84a5e2b4e1
4 changed files with 172 additions and 192 deletions

View file

@ -3,30 +3,40 @@ const editor = {};
editor.PARAGRAPH_TYPES = new Set(["P", "PRE"]); editor.PARAGRAPH_TYPES = new Set(["P", "PRE"]);
editor.Editor = editor.Editor =
function Editor(elements, on_open, on_save, on_cancel) function Editor(element_argss, on_open, on_save, on_cancel)
{ {
/* /*
This class wraps around display elements like headers and paragraphs, and This class wraps around display elements like spans, headers, and
creates edit elements like inputs and textareas to edit them with. 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 element_argss should be a list of dicts. Each dict is required to have "id"
affect their corresponding edit elements: which is unique amongst its peers, and "element" which is the display
data-editor-empty-text: If the display element contains this text, then element. Additionally, you may add the following properties to change the
the edit element will be set to "" when opened. 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 If the edit element contains "", then the display element will
contain this text when saved. 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: "hide_when_empty": true
1. This editor object. If the element does not have any text, it will get the "hidden" css
2. The edit elements as either: class after saving / closing.
If ALL of the display elements have a data-editor-id,
then a dictionary of {data-editor-id: edit_element, ...}. "placeholder": string
Otherwise, an array of [edit_element, ...] in the order they were The placeholder attribute of the edit element.
given to the constructor.
3. The display elements as either the map or the array, similarly. 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` When your callbacks are used, the default `open`, `save`, `cancel`
methods are not called automatically. You should call them from within methods are not called automatically. You should call them from within
@ -41,10 +51,13 @@ function Editor(elements, on_open, on_save, on_cancel)
this.close = function() this.close = function()
{ {
for (let index = 0; index < this.display_elements.length; index += 1) for (const element of Object.values(this.elements))
{ {
this.display_elements[index].classList.remove("hidden"); element.edit.classList.add("hidden");
this.edit_elements[index].classList.add("hidden"); if (! (element.display.innerText === "" && element.hide_when_empty))
{
element.display.classList.remove("hidden");
}
} }
this.hide_spinner(); this.hide_spinner();
this.hide_error(); this.hide_error();
@ -65,22 +78,23 @@ function Editor(elements, on_open, on_save, on_cancel)
this.open = function() this.open = function()
{ {
for (let index = 0; index < this.display_elements.length; index += 1) for (const element of Object.values(this.elements))
{ {
const display_element = this.display_elements[index]; element.display.classList.add("hidden");
const edit_element = this.edit_elements[index]; element.edit.classList.remove("hidden");
display_element.classList.add("hidden"); if (element.autofocus)
edit_element.classList.remove("hidden");
const empty_text = display_element.dataset.editorEmptyText;
if (empty_text !== undefined && display_element.innerText == empty_text)
{ {
edit_element.value = ""; element.edit.focus();
}
if (element.empty_text !== undefined && element.display.innerText == element.empty_text)
{
element.edit.value = "";
} }
else else
{ {
edit_element.value = display_element.innerText; element.edit.value = element.display.innerText;
} }
} }
this.open_button.classList.add("hidden"); this.open_button.classList.add("hidden");
@ -90,18 +104,15 @@ function Editor(elements, on_open, on_save, on_cancel)
this.save = function() this.save = function()
{ {
for (let index = 0; index < this.display_elements.length; index += 1) for (const element of Object.values(this.elements))
{ {
const display_element = this.display_elements[index]; if (element.empty_text !== undefined && element.edit.value == "")
const edit_element = this.edit_elements[index];
if (display_element.dataset.editorEmptyText !== undefined && edit_element.value == "")
{ {
display_element.innerText = display_element.dataset.editorEmptyText; element.display.innerText = element.empty_text;
} }
else else
{ {
display_element.innerText = edit_element.value; element.display.innerText = element.edit.value;
} }
} }
@ -121,64 +132,54 @@ function Editor(elements, on_open, on_save, on_cancel)
this.spinner.show(delay); this.spinner.show(delay);
}; };
this.display_elements = []; this.elements = {};
this.edit_elements = [];
this.can_use_element_map = true;
this.display_element_map = {};
this.edit_element_map = {};
// End-user can put anything they want in here.
this.misc_data = {}; this.misc_data = {};
for (const display_element of elements) // Keep track of last edit element so we can put the toolbox after it.
let last_element;
for (const element_args of element_argss)
{ {
let edit_element; const element = {};
if (editor.PARAGRAPH_TYPES.has(display_element.tagName)) 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))
{ {
edit_element = document.createElement("textarea"); element.edit = document.createElement("textarea");
edit_element.rows = 6; element.edit.rows = 6;
} }
else else
{ {
edit_element = document.createElement("input"); element.edit = document.createElement("input");
edit_element.type = "text"; element.edit.type = "text";
} }
edit_element.classList.add("editor_input");
edit_element.classList.add("hidden");
if (display_element.dataset.editorPlaceholder !== undefined) element.edit.classList.add("editor_input");
element.edit.classList.add("hidden");
if (element_args.placeholder !== undefined)
{ {
edit_element.placeholder = display_element.dataset.editorPlaceholder; element.edit.placeholder = element_args.placeholder;
} }
if (this.can_use_element_map) element.display.parentElement.insertBefore(element.edit, element.display.nextSibling);
{ last_element = element.edit;
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) this.binder = function(func, fallback)
{ {
/* /*
Given a function that takes an Editor as its first argument, and the Given a function that takes an Editor as its first argument,
element arrays/maps as the second and third, return a new function return a new function which requires no arguments and calls the
which requires no arguments and calls the given function with the function with this editor.
correct data.
This is done so that the new function can be used in an event handler. This is done so that the new function can be used in an event handler.
*/ */
@ -187,25 +188,19 @@ function Editor(elements, on_open, on_save, on_cancel)
return fallback.bind(this); return fallback.bind(this);
} }
if (this.can_use_element_map) const bindable = () => func(this);
{ return bindable.bind(this);
const bindable = () => func(this, this.edit_element_map, this.display_element_map);
return bindable.bind(this);
}
else
{
const bindable = () => func(this, this.edit_elements, this.display_elements);
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"); const placeholders = document.getElementsByClassName("editor_toolbox_placeholder");
for (const placeholder of placeholders) for (const placeholder of placeholders)
{ {
placeholder.parentElement.removeChild(placeholder); placeholder.parentElement.removeChild(placeholder);
} }
const last_element = this.edit_elements[this.edit_elements.length - 1];
const toolbox = document.createElement("div"); const toolbox = document.createElement("div");
toolbox.classList.add("editor_toolbox"); toolbox.classList.add("editor_toolbox");
last_element.parentElement.insertBefore(toolbox, last_element.nextSibling); last_element.parentElement.insertBefore(toolbox, last_element.nextSibling);
@ -249,15 +244,9 @@ function Editor(elements, on_open, on_save, on_cancel)
this.spinner = new spinner.Spinner(spinner_element); this.spinner = new spinner.Spinner(spinner_element);
toolbox.appendChild(spinner_element); toolbox.appendChild(spinner_element);
for (const edit_element of this.edit_elements) for (const element of Object.values(this.elements))
{ {
if (edit_element.tagName == "TEXTAREA") const ctrl_enter = element.edit.tagName == "TEXTAREA";
{ common.bind_box_to_button(element.edit, this.save_button, ctrl_enter);
common.bind_box_to_button(edit_element, this.save_button, true);
}
else
{
common.bind_box_to_button(edit_element, this.save_button, false);
}
} }
} }

View file

@ -207,19 +207,10 @@ const ALBUM_ID = undefined;
<div id="left"> <div id="left">
<div id="hierarchy_self" class="panel"> <div id="hierarchy_self" class="panel">
<div id="album_metadata"> <div id="album_metadata">
<h2><span <h2><span id="title_text">{{-album.display_name-}}</span></h2>
id="title_text"
data-editor-id="title"
data-editor-empty-text="{{album.id}}"
data-editor-placeholder="title"
>
{{-album.display_name-}}
</span></h2>
<pre <pre
id="description_text" id="description_text"
data-editor-id="description"
data-editor-placeholder="description"
{% if not album.description %}class="hidden"{% endif %} {% if not album.description %}class="hidden"{% endif %}
> >
{{-album.description-}} {{-album.description-}}
@ -319,13 +310,9 @@ function unpaste_photo_clipboard()
api.albums.remove_photos(ALBUM_ID, photo_ids, common.refresh_or_alert); api.albums.remove_photos(ALBUM_ID, photo_ids, common.refresh_or_alert);
} }
function rename_ed_on_open(ed, edit_element_map, display_element_map) rename_ed_on_open = undefined;
{
ed.open();
edit_element_map["title"].focus();
}
function rename_ed_on_save(ed, edit_element_map, display_element_map) function rename_ed_on_save(ed)
{ {
function callback(response) function callback(response)
{ {
@ -342,41 +329,41 @@ function rename_ed_on_save(ed, edit_element_map, display_element_map)
return; return;
} }
// The data coming back from the server will have been normalized.
ed.elements["title"].edit.value = response.data.title;
ed.save(); ed.save();
const title_display = display_element_map["title"]; document.title = ed.elements["title"].display.innerText + " | Albums";
const description_display = display_element_map["description"];
document.title = title_display.innerText + " | Albums";
if (description_display.innerText == "")
{
description_display.classList.add("hidden");
}
} }
edit_element_map["title"].value = edit_element_map["title"].value.trim(); const title = ed.elements["title"].edit.value;
const title = edit_element_map["title"].value; const description = ed.elements["description"].edit.value;
const description = edit_element_map["description"].value;
ed.show_spinner(); ed.show_spinner();
api.albums.edit(ALBUM_ID, title, description, callback); api.albums.edit(ALBUM_ID, title, description, callback);
} }
function rename_ed_on_cancel(ed, edit_element_map, display_element_map) const rename_ed_elements = [
{
ed.cancel();
if (display_element_map["description"].innerText == "")
{ {
display_element_map["description"].classList.add("hidden"); "id": "title",
} "element": document.getElementById("title_text"),
} "placeholder": "title",
"empty_text": ALBUM_ID,
"autofocus": true,
},
{
"id": "description",
"element": document.getElementById("description_text"),
"placeholder": "description",
"hide_when_empty": true,
},
];
const rename_ed = new editor.Editor( const rename_ed = new editor.Editor(
[document.getElementById("title_text"), document.getElementById("description_text")], rename_ed_elements,
rename_ed_on_open, rename_ed_on_open,
rename_ed_on_save, rename_ed_on_save,
rename_ed_on_cancel
); );
function add_album_datalist_on_load(datalist) function add_album_datalist_on_load(datalist)

View file

@ -63,9 +63,6 @@
<a <a
href="{{bookmark.url}}" href="{{bookmark.url}}"
class="bookmark_title" class="bookmark_title"
data-editor-id="title"
data-editor-placeholder="title (optional)"
data-editor-empty-text="{{bookmark.id}}"
> >
{{-bookmark.display_name-}} {{-bookmark.display_name-}}
</a> </a>
@ -73,8 +70,6 @@
<a <a
href="{{bookmark.url}}" href="{{bookmark.url}}"
class="bookmark_url" class="bookmark_url"
data-editor-id="url"
data-editor-placeholder="url"
> >
{{-bookmark.url-}} {{-bookmark.url-}}
</a> </a>
@ -117,51 +112,65 @@ function delete_bookmark_form(event)
api.bookmarks.delete(id, common.refresh_or_alert); api.bookmarks.delete(id, common.refresh_or_alert);
} }
function on_open(ed, edit_element_map) ed_on_open = undefined;
{
ed.open();
edit_element_map["title"].focus();
}
function on_save(ed, edit_element_map, display_element_map) function ed_on_save(ed)
{ {
function callback(response) function callback(response)
{ {
ed.hide_spinner();
if (response.meta.status != 200) if (response.meta.status != 200)
{ {
ed.show_error("Status: " + response.meta.status); ed.show_error("Status: " + response.meta.status);
return; return;
} }
// The data coming back from the server will have been normalized.
ed.elements["title"].edit.value = response.data.title;
ed.save(); ed.save();
display_element_map["title"].href = response.data.url;
display_element_map["url"].href = response.data.url; ed.elements["title"].display.href = response.data.url;
ed.elements["url"].display.href = response.data.url;
} }
edit_element_map["url"].value = edit_element_map["url"].value.trim(); ed.elements["url"].edit.value = ed.elements["url"].edit.value.trim();
if (!edit_element_map["url"].value) if (! ed.elements["url"].edit.value)
{ {
return; return;
} }
const bookmark_id = ed.misc_data["bookmark_id"]; const bookmark_id = ed.misc_data["bookmark_id"];
const title = edit_element_map["title"].value; const title = ed.elements["title"].edit.value;
const url = edit_element_map["url"].value; const url = ed.elements["url"].edit.value;
ed.show_spinner(); ed.show_spinner();
api.bookmarks.edit(bookmark_id, title, url, callback); api.bookmarks.edit(bookmark_id, title, url, callback);
} }
on_cancel = undefined; ed_on_cancel = undefined;
function create_editors() function create_editors()
{ {
const cards = document.getElementsByClassName("bookmark_card"); const cards = document.getElementsByClassName("bookmark_card");
for (const card of cards) for (const card of cards)
{ {
const title_div = card.getElementsByClassName("bookmark_title")[0]; const ed_elements = [
const url_div = card.getElementsByClassName("bookmark_url")[0]; {
ed = new editor.Editor([title_div, url_div], on_open, on_save, on_cancel); "id": "title",
"element": card.getElementsByClassName("bookmark_title")[0],
"placeholder": "title (optional)",
"empty_text": card.dataset.id,
"autofocus": true,
},
{
"id": "url",
"element": card.getElementsByClassName("bookmark_url")[0],
"placeholder": "url",
},
];
ed = new editor.Editor(ed_elements, ed_on_open, ed_on_save, ed_on_cancel);
ed.misc_data["bookmark_id"] = card.dataset.id; ed.misc_data["bookmark_id"] = card.dataset.id;
} }
} }

View file

@ -99,15 +99,11 @@ h2, h3
specific_tag, specific_tag,
link="search", link="search",
id="name_text", id="name_text",
data_editor_id="name",
data_editor_placeholder="name",
)}} )}}
</h2> </h2>
<pre <pre
id="description_text" id="description_text"
data-editor-id="description"
data-editor-placeholder="description"
{% if specific_tag.description == "" %}class="hidden"{% endif -%} {% if specific_tag.description == "" %}class="hidden"{% endif -%}
> >
{{-specific_tag.description-}} {{-specific_tag.description-}}
@ -383,65 +379,64 @@ function tag_action_callback(response)
} }
{% if specific_tag is not none %} {% if specific_tag is not none %}
function rename_ed_on_open(ed, edit_element_map) rename_ed_on_open = undefined;
{
ed.open();
edit_element_map["name"].focus();
}
function rename_ed_on_save(ed, edit_element_map, display_element_map) function rename_ed_on_save(ed)
{ {
function callback(response) function callback(response)
{ {
ed.hide_spinner(); ed.hide_spinner();
if (response.meta.status !== 200) if (response.meta.status !== 200)
{ {
alert(JSON.stringify(response)); alert(JSON.stringify(response));
return; return;
} }
// The data that comes back from the server will have been normalized,
// so we update some local state.
const new_name = response.data.name; const new_name = response.data.name;
const new_description = response.data.description; const new_description = response.data.description;
document.title = new_name + " | Tags";
SPECIFIC_TAG = new_name; SPECIFIC_TAG = new_name;
document.title = new_name + " | Tags";
window.history.replaceState(null, null, "/tag/" + new_name); window.history.replaceState(null, null, "/tag/" + new_name);
name_editor.value = new_name;
name_display.href = "/search?tag_musts=" + new_name; ed.elements["name"].display.href = "/search?tag_musts=" + new_name;
description_editor.value = new_description; ed.elements["name"].edit.value = new_name;
ed.elements["description"].edit.value = new_description;
ed.save(); ed.save();
if (new_description === "")
{
description_display.classList.add("hidden");
}
} }
const name_display = display_element_map["name"];
const name_editor = edit_element_map["name"];
const description_display = display_element_map["description"];
const description_editor = edit_element_map["description"];
const tag_name = name_display.innerText;
const new_name = name_editor.value;
const new_description = description_editor.value;
ed.show_spinner(); ed.show_spinner();
const tag_name = ed.elements["name"].display.innerText;
const new_name = ed.elements["name"].edit.value;
const new_description = ed.elements["description"].edit.value;
api.tags.edit(tag_name, new_name, new_description, callback); api.tags.edit(tag_name, new_name, new_description, callback);
} }
function rename_ed_on_cancel(ed, edit_element_map, display_element_map) const rename_ed_elements = [
{
ed.cancel();
if (display_element_map["description"].innerText == "")
{ {
display_element_map["description"].classList.add("hidden"); "id": "name",
} "element": document.getElementById("name_text"),
} "placeholder": "name",
"autofocus": true,
},
{
"id": "description",
"element": document.getElementById("description_text"),
"placeholder": "description",
"hide_when_empty": true,
},
];
const rename_ed = new editor.Editor( const rename_ed = new editor.Editor(
[document.getElementById("name_text"), document.getElementById("description_text")], rename_ed_elements,
rename_ed_on_open, rename_ed_on_open,
rename_ed_on_save, rename_ed_on_save,
rename_ed_on_cancel,
); );
{% endif %} {% endif %}
</script> </script>