diff --git a/frontends/etiquette_flask/static/js/editor.js b/frontends/etiquette_flask/static/js/editor.js index cb7fe11..3d3ce83 100644 --- a/frontends/etiquette_flask/static/js/editor.js +++ b/frontends/etiquette_flask/static/js/editor.js @@ -3,30 +3,40 @@ const editor = {}; editor.PARAGRAPH_TYPES = new Set(["P", "PRE"]); 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 - creates edit elements like inputs and textareas to edit them with. + This class wraps around display elements like spans, 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. + 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. - 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. + "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 @@ -41,10 +51,13 @@ function Editor(elements, on_open, on_save, on_cancel) 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"); - this.edit_elements[index].classList.add("hidden"); + element.edit.classList.add("hidden"); + if (! (element.display.innerText === "" && element.hide_when_empty)) + { + element.display.classList.remove("hidden"); + } } this.hide_spinner(); this.hide_error(); @@ -65,22 +78,23 @@ function Editor(elements, on_open, on_save, on_cancel) 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]; - const edit_element = this.edit_elements[index]; + element.display.classList.add("hidden"); + element.edit.classList.remove("hidden"); - 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) + if (element.autofocus) { - edit_element.value = ""; + element.edit.focus(); + } + + if (element.empty_text !== undefined && element.display.innerText == element.empty_text) + { + element.edit.value = ""; } else { - edit_element.value = display_element.innerText; + element.edit.value = element.display.innerText; } } this.open_button.classList.add("hidden"); @@ -90,18 +104,15 @@ function Editor(elements, on_open, on_save, on_cancel) 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]; - const edit_element = this.edit_elements[index]; - - if (display_element.dataset.editorEmptyText !== undefined && edit_element.value == "") + if (element.empty_text !== undefined && element.edit.value == "") { - display_element.innerText = display_element.dataset.editorEmptyText; + element.display.innerText = element.empty_text; } 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.display_elements = []; - this.edit_elements = []; - - this.can_use_element_map = true; - this.display_element_map = {}; - this.edit_element_map = {}; + this.elements = {}; + // End-user can put anything they want in here. 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; - if (editor.PARAGRAPH_TYPES.has(display_element.tagName)) + 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)) { - edit_element = document.createElement("textarea"); - edit_element.rows = 6; + element.edit = document.createElement("textarea"); + element.edit.rows = 6; } else { - edit_element = document.createElement("input"); - edit_element.type = "text"; + element.edit = document.createElement("input"); + 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) - { - 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); + 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, 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. + 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. */ @@ -187,25 +188,19 @@ function Editor(elements, on_open, on_save, on_cancel) return fallback.bind(this); } - if (this.can_use_element_map) - { - 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); - } + 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 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); @@ -249,15 +244,9 @@ function Editor(elements, on_open, on_save, on_cancel) this.spinner = new spinner.Spinner(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") - { - common.bind_box_to_button(edit_element, this.save_button, true); - } - else - { - common.bind_box_to_button(edit_element, this.save_button, false); - } + const ctrl_enter = element.edit.tagName == "TEXTAREA"; + common.bind_box_to_button(element.edit, this.save_button, ctrl_enter); } } diff --git a/frontends/etiquette_flask/templates/album.html b/frontends/etiquette_flask/templates/album.html index c2ee3e9..02abffc 100644 --- a/frontends/etiquette_flask/templates/album.html +++ b/frontends/etiquette_flask/templates/album.html @@ -207,19 +207,10 @@ const ALBUM_ID = undefined;
{{-album.description-}} @@ -319,13 +310,9 @@ function unpaste_photo_clipboard() api.albums.remove_photos(ALBUM_ID, photo_ids, common.refresh_or_alert); } -function rename_ed_on_open(ed, edit_element_map, display_element_map) -{ - ed.open(); - edit_element_map["title"].focus(); -} +rename_ed_on_open = undefined; -function rename_ed_on_save(ed, edit_element_map, display_element_map) +function rename_ed_on_save(ed) { function callback(response) { @@ -342,41 +329,41 @@ function rename_ed_on_save(ed, edit_element_map, display_element_map) return; } + // The data coming back from the server will have been normalized. + ed.elements["title"].edit.value = response.data.title; + ed.save(); - const title_display = display_element_map["title"]; - const description_display = display_element_map["description"]; - - document.title = title_display.innerText + " | Albums"; - - if (description_display.innerText == "") - { - description_display.classList.add("hidden"); - } + document.title = ed.elements["title"].display.innerText + " | Albums"; } - edit_element_map["title"].value = edit_element_map["title"].value.trim(); - const title = edit_element_map["title"].value; - const description = edit_element_map["description"].value; + const title = ed.elements["title"].edit.value; + const description = ed.elements["description"].edit.value; ed.show_spinner(); api.albums.edit(ALBUM_ID, title, description, callback); } -function rename_ed_on_cancel(ed, edit_element_map, display_element_map) -{ - ed.cancel(); - if (display_element_map["description"].innerText == "") +const rename_ed_elements = [ { - 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( - [document.getElementById("title_text"), document.getElementById("description_text")], + rename_ed_elements, rename_ed_on_open, rename_ed_on_save, - rename_ed_on_cancel ); function add_album_datalist_on_load(datalist) diff --git a/frontends/etiquette_flask/templates/bookmarks.html b/frontends/etiquette_flask/templates/bookmarks.html index ec96484..6d5aa53 100644 --- a/frontends/etiquette_flask/templates/bookmarks.html +++ b/frontends/etiquette_flask/templates/bookmarks.html @@ -63,9 +63,6 @@ {{-bookmark.display_name-}} @@ -73,8 +70,6 @@ {{-bookmark.url-}} @@ -117,51 +112,65 @@ function delete_bookmark_form(event) api.bookmarks.delete(id, common.refresh_or_alert); } -function on_open(ed, edit_element_map) -{ - ed.open(); - edit_element_map["title"].focus(); -} +ed_on_open = undefined; -function on_save(ed, edit_element_map, display_element_map) +function ed_on_save(ed) { function callback(response) { + ed.hide_spinner(); + if (response.meta.status != 200) { ed.show_error("Status: " + response.meta.status); return; } + // The data coming back from the server will have been normalized. + ed.elements["title"].edit.value = response.data.title; + 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(); - if (!edit_element_map["url"].value) + ed.elements["url"].edit.value = ed.elements["url"].edit.value.trim(); + if (! ed.elements["url"].edit.value) { return; } const bookmark_id = ed.misc_data["bookmark_id"]; - const title = edit_element_map["title"].value; - const url = edit_element_map["url"].value; + const title = ed.elements["title"].edit.value; + const url = ed.elements["url"].edit.value; ed.show_spinner(); api.bookmarks.edit(bookmark_id, title, url, callback); } -on_cancel = undefined; +ed_on_cancel = undefined; function create_editors() { const cards = document.getElementsByClassName("bookmark_card"); for (const card of cards) { - const title_div = card.getElementsByClassName("bookmark_title")[0]; - const url_div = card.getElementsByClassName("bookmark_url")[0]; - ed = new editor.Editor([title_div, url_div], on_open, on_save, on_cancel); + const ed_elements = [ + { + "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; } } diff --git a/frontends/etiquette_flask/templates/tags.html b/frontends/etiquette_flask/templates/tags.html index cd5bef2..3d6b52d 100644 --- a/frontends/etiquette_flask/templates/tags.html +++ b/frontends/etiquette_flask/templates/tags.html @@ -99,15 +99,11 @@ h2, h3 specific_tag, link="search", id="name_text", - data_editor_id="name", - data_editor_placeholder="name", )}}{{-specific_tag.description-}} @@ -383,65 +379,64 @@ function tag_action_callback(response) } {% if specific_tag is not none %} -function rename_ed_on_open(ed, edit_element_map) -{ - ed.open(); - edit_element_map["name"].focus(); -} +rename_ed_on_open = undefined; -function rename_ed_on_save(ed, edit_element_map, display_element_map) +function rename_ed_on_save(ed) { function callback(response) { ed.hide_spinner(); + if (response.meta.status !== 200) { alert(JSON.stringify(response)); 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_description = response.data.description; - document.title = new_name + " | Tags"; + SPECIFIC_TAG = new_name; + document.title = new_name + " | Tags"; window.history.replaceState(null, null, "/tag/" + new_name); - name_editor.value = new_name; - name_display.href = "/search?tag_musts=" + new_name; - description_editor.value = new_description; + + ed.elements["name"].display.href = "/search?tag_musts=" + new_name; + ed.elements["name"].edit.value = new_name; + ed.elements["description"].edit.value = new_description; + 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(); + + 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); } -function rename_ed_on_cancel(ed, edit_element_map, display_element_map) -{ - ed.cancel(); - if (display_element_map["description"].innerText == "") +const rename_ed_elements = [ { - 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( - [document.getElementById("name_text"), document.getElementById("description_text")], + rename_ed_elements, rename_ed_on_open, rename_ed_on_save, - rename_ed_on_cancel, ); {% endif %}