changeset 1415:8415d882b686

browser: new `editor` module: This module provide tools for dynamtic item edition. For now it implements forms autosave, and a tags editor.
author Goffi <goffi@goffi.org>
date Thu, 29 Apr 2021 16:45:39 +0200
parents 97b8ce9ce54b
children 0554103ec700
files libervia/pages/_browser/editor.py
diffstat 1 files changed, 163 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/pages/_browser/editor.py	Thu Apr 29 16:45:39 2021 +0200
@@ -0,0 +1,163 @@
+"""text edition management"""
+
+from browser import document, window, bind, timer
+from browser.local_storage import storage
+from browser.object_storage import ObjectStorage
+from template import Template
+
+object_storage = ObjectStorage(storage)
+profile = window.profile
+
+# how often we save forms, in seconds
+AUTOSAVE_FREQUENCY = 20
+
+
+def serialise_form(form_elt):
+    ret = {}
+    for elt in form_elt.elements:
+        if elt.tagName == "INPUT":
+            if elt.type in ("hidden", "submit"):
+                continue
+            elif elt.type == "text":
+                ret[elt.name] = elt.value
+            else:
+                print(f"elt.type not managet yet: {elt.type}")
+                continue
+        elif elt.tagName == "TEXTAREA":
+            ret[elt.name] = elt.value
+        elif elt.tagName in ("BUTTON",):
+            continue
+        else:
+            print(f"tag not managet yet: {elt.tagName}")
+            continue
+    return ret
+
+
+def restore_form(form_elt, data):
+    for elt in form_elt.elements:
+        if elt.tagName not in ("INPUT", "TEXTAREA"):
+            continue
+        try:
+            value = data[elt.name]
+        except KeyError:
+            continue
+        else:
+            elt.value = value
+
+
+def set_form_autosave(form_id):
+    """Save locally form data regularly and restore it until it's submitted
+
+    form is saved every AUTOSAVE_FREQUENCY seconds and when visibility is lost.
+    Saved data is restored when the method is called.
+    Saved data is cleared when the form is submitted.
+    """
+    if profile is None:
+        print(f"No session started, won't save and restore form {form_id}")
+        return
+
+    form_elt = document[form_id]
+    submitted = False
+
+    key = {"profile": profile, "type": "form_autosave", "form": form_id}
+    try:
+        form_saved_data = object_storage[key]
+    except KeyError:
+        last_serialised = None
+    else:
+        print(f"restoring content of form {form_id!r}")
+        last_serialised = form_saved_data
+        restore_form(form_elt, form_saved_data)
+
+    def save_form():
+        if not submitted:
+            nonlocal last_serialised
+            serialised = serialise_form(form_elt)
+            if serialised != last_serialised:
+                last_serialised = serialised
+                print(f"saving content of form {form_id!r}")
+                object_storage[key] = serialised
+
+    @bind(form_elt, "submit")
+    def on_submit(evt):
+        nonlocal submitted
+        submitted = True
+        print(f"clearing stored content of form {form_id!r}")
+        try:
+            del object_storage[key]
+        except KeyError:
+            print("key error")
+            pass
+
+    @bind(document, "visibilitychange")
+    def on_visibiliy_change(evt):
+        print("visibility change")
+        if document.visibilityState != "visible":
+            save_form()
+
+    timer.set_interval(save_form, AUTOSAVE_FREQUENCY * 1000)
+
+
+class TagsEditor:
+
+    def __init__(self, input_selector):
+        print("installing Tags Editor")
+        self.input_elt = document.select_one(input_selector)
+        self.input_elt.style.display = "none"
+        tags_editor_tpl = Template('editor/tags_editor.html')
+        self.tag_tpl = Template('editor/tag.html')
+
+        editor_elt = tags_editor_tpl.get_elt()
+        self.input_elt.parent <= editor_elt
+        self.tag_input_elt = editor_elt.select_one(".tag_input")
+        self.tag_input_elt.bind("keydown", self.on_key_down)
+        self._current_tags = None
+        self.tags_map = {}
+        for tag in self.current_tags:
+            self.add_tag(tag, force=True)
+
+    @property
+    def current_tags(self):
+        if self._current_tags is None:
+            self._current_tags = {
+                t.strip() for t in self.input_elt.value.split(',') if t.strip()
+            }
+        return self._current_tags
+
+    @current_tags.setter
+    def current_tags(self, tags):
+        self._current_tags = tags
+
+    def add_tag(self, tag, force=False):
+        tag = tag.strip()
+        if not force and (not tag or tag in self.current_tags):
+            return
+        self.current_tags = self.current_tags | {tag}
+        self.input_elt.value = ','.join(self.current_tags)
+        tag_elt = self.tag_tpl.get_elt({"label": tag})
+        self.tags_map[tag] = tag_elt
+        self.tag_input_elt.parent.insertBefore(tag_elt, self.tag_input_elt)
+        tag_elt.select_one(".click_to_delete").bind(
+            "click", lambda evt: self.on_tag_click(evt, tag)
+        )
+
+    def remove_tag(self, tag):
+        try:
+            tag_elt = self.tags_map[tag]
+        except KeyError:
+            print(f"trying to remove an inexistant tag: {tag}")
+        else:
+            self.current_tags = self.current_tags - {tag}
+            self.input_elt.value = ','.join(self.current_tags)
+            tag_elt.remove()
+
+    def on_tag_click(self, evt, tag):
+        evt.stopPropagation()
+        self.remove_tag(tag)
+
+    def on_key_down(self, evt):
+        if evt.key in (",", "Enter"):
+            evt.stopPropagation()
+            evt.preventDefault()
+            self.add_tag(self.tag_input_elt.value)
+            self.tag_input_elt.value = ""