From ebc399bfe14d191078a9c4c20ee7f5b07034b44d Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 20 Jul 2024 08:44:02 +0800 Subject: [PATCH] add event binding logic in SUI core --- sui/core/build.go | 4 ++ sui/core/event.go | 83 ++++++++++++++++++++++++++++++++++++++++++ sui/core/injections.go | 71 ++++++++++++++---------------------- sui/core/matcher.go | 74 +++++++++++++++++++++++++++++++++++++ sui/core/parser.go | 4 +- 5 files changed, 191 insertions(+), 45 deletions(-) create mode 100644 sui/core/event.go create mode 100644 sui/core/matcher.go diff --git a/sui/core/build.go b/sui/core/build.go index 8a25a282..d3c9972a 100644 --- a/sui/core/build.go +++ b/sui/core/build.go @@ -105,6 +105,10 @@ func (page *Page) Build(ctx *BuildContext, option *BuildOption) (*goquery.Docume // Append the scripts and styles ctx.scripts = append(ctx.scripts, scripts...) ctx.styles = append(ctx.styles, styles...) + + // Bind the events + page.BindEvent(ctx, doc.Selection) + return doc, ctx.warnings, err } diff --git a/sui/core/event.go b/sui/core/event.go new file mode 100644 index 00000000..214caebb --- /dev/null +++ b/sui/core/event.go @@ -0,0 +1,83 @@ +package core + +import ( + "fmt" + "strings" + + "github.com/PuerkitoBio/goquery" + jsoniter "github.com/json-iterator/go" + "golang.org/x/net/html" +) + +// BindEvent is a method that binds events to the page. +func (page *Page) BindEvent(ctx *BuildContext, sel *goquery.Selection) { + matcher := NewAttrPrefixMatcher(`s:on-`) + sel.FindMatcher(matcher).Each(func(i int, s *goquery.Selection) { + page.appendEventScript(ctx, s) + }) +} + +func (page *Page) appendEventScript(ctx *BuildContext, sel *goquery.Selection) { + if page.parent != nil { + return + } + + if len(sel.Nodes) == 0 { + return + } + + // Page events + events := map[string]string{} + dataUnique := map[string]string{} + jsonUnique := map[string]string{} + id := fmt.Sprintf("event-%d", ctx.sequence) + ctx.sequence++ + + for _, attr := range sel.Nodes[0].Attr { + if strings.HasPrefix(attr.Key, "s:on-") { + name := strings.TrimPrefix(attr.Key, "s:on-") + handler := attr.Val + events[name] = handler + } + if strings.HasPrefix(attr.Key, "s:data-") { + name := strings.TrimPrefix(attr.Key, "s:data-") + dataUnique[name] = attr.Val + sel.RemoveAttr(attr.Key) + sel.RemoveAttr(attr.Key) + sel.SetAttr(fmt.Sprintf("data:%s", name), attr.Val) + } + if strings.HasPrefix(attr.Key, "s:json-") { + name := strings.TrimPrefix(attr.Key, "s:json-") + jsonUnique[name] = attr.Val + sel.RemoveAttr(attr.Key) + sel.SetAttr(fmt.Sprintf("json:%s", name), attr.Val) + } + } + + data := []string{} + for name := range dataUnique { + data = append(data, name) + } + + json := []string{} + for name := range jsonUnique { + json = append(json, name) + } + + dataRaw, _ := jsoniter.MarshalToString(data) + jsonRaw, _ := jsoniter.MarshalToString(json) + + source := "" + for name, handler := range events { + source += pageEventInjectScript(id, name, dataRaw, jsonRaw, handler) + "\n" + sel.RemoveAttr(fmt.Sprintf("s:on-%s", name)) + } + + ctx.scripts = append(ctx.scripts, ScriptNode{ + Source: source, + Namespace: page.namespace, + Attrs: []html.Attribute{{Key: "event", Val: id}}, + }) + + sel.SetAttr("s:event", id) +} diff --git a/sui/core/injections.go b/sui/core/injections.go index d2e72aea..6debf851 100644 --- a/sui/core/injections.go +++ b/sui/core/injections.go @@ -7,16 +7,6 @@ const initScriptTmpl = ` var __sui_data = %s; } catch (e) { console.log('init data error:', e); } - function __sui_findParentWithAttribute(element, attributeName) { - while (element && element !== document) { - if (element.hasAttribute(attributeName)) { - return element.getAttribute(attributeName); - } - element = element.parentElement; - } - return null; - } - document.addEventListener("DOMContentLoaded", function () { try { document.querySelectorAll("[s\\:ready]").forEach(function (element) { @@ -31,39 +21,6 @@ const initScriptTmpl = ` } } }); - - document.querySelectorAll("[s\\:click]").forEach(function (element) { - const method = element.getAttribute("s:click"); - const cn = __sui_findParentWithAttribute(element, "s:cn"); - if (method && cn && typeof window[cn] === "function") { - const obj = new window[cn](); - if (typeof obj[method] === "function") { - element.addEventListener("click", function (event) { - try { - obj[method](element, event); - } catch (e) { - const message = e.message || e || "An error occurred"; - console.error(` + "`[SUI] ${cn}.${method} Error: ${message}`" + `); - } - }); - return - } - console.error(` + "`[SUI] ${cn}.${method} Error: Method not found`" + `); - return - } - - if (method && typeof window[method] === "function") { - element.addEventListener("click", function (event) { - try { - window[method](element, event); - } catch (e) { - const message = e.message || e || "An error occurred"; - console.error(` + "`[SUI] ${method} Error: ${message}`" + `); - } - }); - } - - }); } catch (e) {} }); %s @@ -83,6 +40,30 @@ const i118nScriptTmpl = ` } ` +const pageEventScriptTmpl = ` + document.querySelector("[s\\:event=%s]").addEventListener("%s", function (event) { + let data = {}; + const dataKeys = %s; + const jsonKeys = %s; + + const elm = this; + dataKeys.forEach(function (key) { + const value = elm.getAttribute("data:" + key); + data[key] = value; + }) + + jsonKeys.forEach(function (key) { + const value = elm.getAttribute("json:" + key); + data[key] = null; + if (value && value != "") { + data[key] = JSON.parse(value); + } + }) + + %s && %s(event, data, this); + }); +` + func bodyInjectionScript(jsonRaw string, debug bool) string { jsPrintData := "" if debug { @@ -94,3 +75,7 @@ func bodyInjectionScript(jsonRaw string, debug bool) string { func headInjectionScript(jsonRaw string) string { return fmt.Sprintf(``, jsonRaw) } + +func pageEventInjectScript(eventID, eventName, dataKeys, jsonKeys, handler string) string { + return fmt.Sprintf(pageEventScriptTmpl, eventID, eventName, dataKeys, jsonKeys, handler, handler) +} diff --git a/sui/core/matcher.go b/sui/core/matcher.go new file mode 100644 index 00000000..d8c6db4b --- /dev/null +++ b/sui/core/matcher.go @@ -0,0 +1,74 @@ +package core + +import ( + "regexp" + "strings" + + "golang.org/x/net/html" +) + +// AttrMatcher is a matcher that matches attribute keys +type AttrMatcher struct { + prefix string + re *regexp.Regexp +} + +// NewAttrPrefixMatcher creates a new attribute matcher that matches attribute keys with the given prefix +func NewAttrPrefixMatcher(prefix string) *AttrMatcher { + return &AttrMatcher{prefix: prefix} +} + +// NewAttrRegexpMatcher creates a new attribute matcher that matches attribute keys with the given regexp +func NewAttrRegexpMatcher(re *regexp.Regexp) *AttrMatcher { + return &AttrMatcher{re: re} +} + +// Match returns true if the node has an attribute key that matches the matcher +func (m *AttrMatcher) Match(n *html.Node) bool { + if m.re == nil { + return m.prefixMatch(n) + } + return m.regexpMatch(n) +} + +func (m *AttrMatcher) regexpMatch(n *html.Node) bool { + for _, attr := range n.Attr { + if m.re.MatchString(attr.Key) { + return true + } + } + return false +} + +func (m *AttrMatcher) prefixMatch(n *html.Node) bool { + for _, attr := range n.Attr { + if strings.HasPrefix(attr.Key, m.prefix) { + return true + } + } + return false +} + +// MatchAll returns all the nodes that have an attribute key that matches the matcher +func (m *AttrMatcher) MatchAll(n *html.Node) []*html.Node { + var nodes []*html.Node + for c := n.FirstChild; c != nil; c = c.NextSibling { + if m.Match(c) { + nodes = append(nodes, c) + } + nodes = append(nodes, m.MatchAll(c)...) + } + return nodes + +} + +// Filter returns all the nodes that have an attribute key that matches the matcher +func (m *AttrMatcher) Filter(ns []*html.Node) []*html.Node { + var nodes []*html.Node + for _, n := range ns { + if m.Match(n) { + nodes = append(nodes, n) + } + } + return nodes +} diff --git a/sui/core/parser.go b/sui/core/parser.go index 17fa3c9e..175f987e 100644 --- a/sui/core/parser.go +++ b/sui/core/parser.go @@ -67,14 +67,14 @@ var allowUsePropAttrs = map[string]bool{ "s:if": true, "s:elif": true, "s:for": true, - "s:click": true, + "s:event": true, } var keepAttrs = map[string]bool{ "s:ns": true, "s:cn": true, "s:ready": true, - "s:click": true, + "s:event": true, } // NewTemplateParser create a new template parser