view libervia/pages/_browser/editor.py @ 1466:cff720e26089

pages (blog/view): activate pagination when a single item is shown: `previous_page_url` and `next_page_url` are set when `item_id` is used. For now, they are both activated even if there is no item before or after, as it would request to make extra request to check it. This may be improved in 0.9 by using internal cache. fix 399
author Goffi <goffi@goffi.org>
date Thu, 30 Sep 2021 17:04:22 +0200
parents 925a7c498cda
children 106bae41f5c8
line wrap: on
line source

"""text edition management"""

from browser import document, window, aio, bind, timer
from browser.local_storage import storage
from browser.object_storage import ObjectStorage
from javascript import JSON
from aio_bridge import Bridge, BridgeException
from template import Template
import dialog

bridge = Bridge()
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 = ""


class BlogEditor:
    """Editor class, handling tabs, preview, and submit loading button

    It's using and HTML form as source
    The form must have:
        - a "title" text input
        - a "body" textarea
        - an optional "tags" text input with comma separated tags (may be using Tags
          Editor)
        - a "tab_preview" tab element
    """

    def __init__(self, form_id="blog_post_edit"):
        self.tab_select = window.tab_select
        self.item_tpl = Template('blog/item.html')
        self.form = document[form_id]
        for elt in document.select(".click_to_edit"):
            elt.bind("click", self.on_edit)
        for elt in document.select('.click_to_preview'):
            elt.bind("click", lambda evt: aio.run(self.on_preview(evt)))
        self.form.bind("submit", self.on_submit)


    def on_edit(self, evt):
        self.tab_select(evt.target, "tab_edit", "is-active")

    async def on_preview(self, evt):
        """Generate a blog preview from HTML form

        """
        print("on preview OK")
        elt = evt.target
        tab_preview = document["tab_preview"]
        tab_preview.clear()
        data = {
            "content_rich": self.form.select_one('textarea[name="body"]').value.strip()
        }
        title = self.form.select_one('input[name="title"]').value.strip()
        if title:
            data["title_rich"] = title
        tags_input_elt = self.form.select_one('input[name="tags"]')
        if tags_input_elt is not None:
            tags = tags_input_elt.value.strip()
            if tags:
                data['tags'] = [t.strip() for t in tags.split(',') if t.strip()]
        try:
            preview_data = JSON.parse(
                await bridge.mbPreview("", "", JSON.stringify(data))
            )
        except BridgeException as e:
            dialog.notification.show(
                f"Can't generate item preview: {e.message}",
                level="error"
            )
        else:
            self.tab_select(elt, "tab_preview", "is-active")
            item_elt = self.item_tpl.get_elt({
                "item": preview_data,
                "dates_format": "short",
            })
            tab_preview <= item_elt

    def on_submit(self, evt):
        submit_btn = document.select_one("button[type='submit']")
        submit_btn.classList.add("is-loading")