brython-dev / brython

Brython (Browser Python) is an implementation of Python 3 running in the browser
BSD 3-Clause "New" or "Revised" License
6.4k stars 512 forks source link

Unbinding an event handler results in the following error: `TypeError: function is not an event callback`. #2498

Open moepnse opened 1 month ago

moepnse commented 1 month ago

Hello,

unbinding an event handler that is a method of a web component instance does not work and results in an error.

Below is a demo of the problem.

To reproduce the error, you need to click on the red area of the page and a popup will appear. To close the popup, you have to click anywhere on the page. This should also result in the event handler being unregistered, but instead an error is displayed in the console.

Using removeEventListener instead of unbind will silently fail.

Thanks in advance!

Error

Traceback (most recent call last):
  File "C:/Users/test/issues/brython_issue_202401017_web_component.html#__main__", line 102, in close
    document.body.unbind("click", self.close)
TypeError: function is not an event callback

Issue Demo

<!DOCTYPE html>
<html>
    <head>
        <!-- Required meta tags-->
        <meta charset="utf-8">

        <title>Issue Demo</title>

        <style>
html, body {
    height: 100%;
}
.modal-in {
    displaY: block;
    border: 1px solid black;
    padding: 5px;
    background-color: rgb(112, 248, 112);
}

ui-popup {
    display: none;
    position: absolute;
}

#show_popup {
    background-color: red;
    font-weight: bold;
    padding: 5px;
    cursor: pointer;
}
        </style>

        <!-- Brython -->
        <script src="https://raw.githack.com/brython-dev/brython/master/www/src/brython.js"></script>
        <script src="https://raw.githack.com/brython-dev/brython/master/www/src/brython_stdlib.js"></script>
        <script type="text/python">
import copy
from browser import webcomponent, html, window, console, document

class BaseComponent:

    _registry = []
    _initialized = False
    _logic_obj = None
    _is_container: bool = False
    _observe_attributes = {"childList": True}
    _create_observer = False

    def __init__(self):
        # Create a shadow root
        shadow = self._shadow = self.attachShadow({'mode': 'open'})
        default_slot = self._default_slot = document.createElement("slot")
        shadow <= default_slot

    def mutation(self, records, observer):
        console.debug("mutation detected:", records)
        for record in records:
            for node in record.addedNodes:
                console.debug("node:", node)
                # NodeType 3 is a TextNode
                if node.nodeType in (3, 8):
                    continue
                self.append_child(node)

    def connectedCallback(self):
        if not self._initialized:
            self._initialized = True
        if self._create_observer:
            console.debug("creating observer for:", self)
            self._observer = observer = window.MutationObserver.new(self.mutation)
            observer.observe(self, self._observe_attributes)
        self.__bind_events__()

    def disconnectedCallback(self):
        self.__unbind_events__()

    def __bind_events__(self):
        pass

    def __unbind_events__(self):
        pass

    @staticmethod
    def un_camel(word: str) -> str:
        upper_chars: str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
        last_char: str = word[0]
        output: list = [last_char.lower()]
        for c in word[1:]:
            if c == "_":
                output.append("-")
                continue
            if c in upper_chars:
                if last_char not in upper_chars:
                    output.append('-')
                output.append(c.lower())
            else:
                output.append(c)
            last_char = c
        return "".join(output)

    @classmethod
    def __init_subclass__(cls, **kwargs):
        BaseComponent._registry.append(cls)

    @classmethod
    def remove_from_registry(cls, component):
        print(cls._registry, component)
        if component in cls._registry:
            cls._registry.remove(component)

    @classmethod
    def register(cls):
        registry = cls._registry
        for web_component in registry:
            web_component_name = cls.un_camel(web_component.__name__)
            component_name = f"ui-{web_component_name}"
            console.debug(f"registering web component {web_component} as {component_name}...")
            webcomponent.define(component_name, web_component)

    def append_child(self, child):
        console.debug("child:", child)

class Popup(BaseComponent):

    def open(self, ev=None):
        if ev is not None:
            self.style.left = f"{ev.clientX}px"
            self.style.top = f"{ev.clientY}px"
            ev.stopPropagation()
        self.classList.add("modal-in")
        document.body.bind("click", self.close)

    def close(self, ev=None):
        if ev is not None:
            # does not work.
            document.body.unbind("click", self.close)
        self.classList.remove("modal-in")

BaseComponent.register()
        </script>
    </head>
    <body onload="brython({debug: 10})">
        <ui-popup id="popup1">TEST POPUP (Click anywhere on this page to close me.)</ui-popup>
        <div id="show_popup">Show popup (Click me!)</div>
        <script type="text/python">
from browser import window, console, document

document["show_popup"].bind("click", document["popup1"].open)
        </script>
    </body>
</html>