From ebc399bfe14d191078a9c4c20ee7f5b07034b44d Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 20 Jul 2024 08:44:02 +0800 Subject: [PATCH 1/2] 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 8a25a2827..d3c9972aa 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 000000000..214caebba --- /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 d2e72aea3..6debf8517 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 000000000..d8c6db4b5 --- /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 17fa3c9ea..175f987e0 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 From bafe3bb20f2a2c7a3799f667b6e4b8d8c6f8df9c Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 20 Jul 2024 09:44:58 +0800 Subject: [PATCH 2/2] Refactor event binding logic in SUI core --- sui/core/build.go | 12 ++++++--- sui/core/event.go | 14 ++++++----- sui/core/injections.go | 43 +++++++++++++++++++-------------- sui/storages/local/page_test.go | 8 +++--- 4 files changed, 46 insertions(+), 31 deletions(-) diff --git a/sui/core/build.go b/sui/core/build.go index d3c9972aa..0009c90c4 100644 --- a/sui/core/build.go +++ b/sui/core/build.go @@ -54,6 +54,9 @@ func (page *Page) Build(ctx *BuildContext, option *BuildOption) (*goquery.Docume } doc.Find("body").SetAttr("s:ns", namespace) + // Bind the Page events + page.BindEvent(ctx, doc.Selection) + warnings, err := page.buildComponents(doc, ctx, option) if err != nil { return nil, ctx.warnings, err @@ -106,9 +109,6 @@ func (page *Page) Build(ctx *BuildContext, option *BuildOption) (*goquery.Docume 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 } @@ -282,6 +282,12 @@ func (page *Page) parseProps(from *goquery.Selection, to *goquery.Selection, ext for _, attr := range attrs { + // Copy Event + if strings.HasPrefix(attr.Key, "s:event") || strings.HasPrefix(attr.Key, "data:") || strings.HasPrefix(attr.Key, "json:") { + to.SetAttr(attr.Key, attr.Val) + continue + } + if strings.HasPrefix(attr.Key, "s:") || attr.Key == "is" || attr.Key == "parsed" { continue } diff --git a/sui/core/event.go b/sui/core/event.go index 214caebba..c959da487 100644 --- a/sui/core/event.go +++ b/sui/core/event.go @@ -18,9 +18,6 @@ func (page *Page) BindEvent(ctx *BuildContext, sel *goquery.Selection) { } func (page *Page) appendEventScript(ctx *BuildContext, sel *goquery.Selection) { - if page.parent != nil { - return - } if len(sel.Nodes) == 0 { return @@ -34,34 +31,39 @@ func (page *Page) appendEventScript(ctx *BuildContext, sel *goquery.Selection) { 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 + continue } + 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) + continue } + 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) + continue } } data := []string{} for name := range dataUnique { data = append(data, name) + sel.RemoveAttr(fmt.Sprintf("s:data-%s", name)) } json := []string{} for name := range jsonUnique { json = append(json, name) + sel.RemoveAttr(fmt.Sprintf("s:json-%s", name)) } dataRaw, _ := jsoniter.MarshalToString(data) diff --git a/sui/core/injections.go b/sui/core/injections.go index 6debf8517..14eb54479 100644 --- a/sui/core/injections.go +++ b/sui/core/injections.go @@ -7,6 +7,29 @@ const initScriptTmpl = ` var __sui_data = %s; } catch (e) { console.log('init data error:', e); } + + function __sui_event_handler(event, dataKeys, jsonKeys, elm, handler) { + const data = {}; + 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 != "") { + try { + data[key] = JSON.parse(value); + } catch (e) { + const message = e.message || e || "An error occurred"; + console.error(` + "`[SUI] Event Handler Error: ${message}`" + `, elm); + } + } + }) + + handler && handler(event, data, elm); + }; + document.addEventListener("DOMContentLoaded", function () { try { document.querySelectorAll("[s\\:ready]").forEach(function (element) { @@ -42,25 +65,9 @@ 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); + __sui_event_handler(event, dataKeys, jsonKeys, this, %s); }); ` @@ -77,5 +84,5 @@ func headInjectionScript(jsonRaw string) string { } func pageEventInjectScript(eventID, eventName, dataKeys, jsonKeys, handler string) string { - return fmt.Sprintf(pageEventScriptTmpl, eventID, eventName, dataKeys, jsonKeys, handler, handler) + return fmt.Sprintf(pageEventScriptTmpl, eventID, eventName, dataKeys, jsonKeys, handler) } diff --git a/sui/storages/local/page_test.go b/sui/storages/local/page_test.go index 62782835f..e632b40a4 100644 --- a/sui/storages/local/page_test.go +++ b/sui/storages/local/page_test.go @@ -66,13 +66,13 @@ func TestTemplatePageTree(t *testing.T) { assert.NotEmpty(t, pages) assert.NotEmpty(t, pages[1].Children) - if len(pages[1].Children) < 2 { + if len(pages[1].Children) < 3 { t.Fatalf("Pages error: %v", len(pages[1].Children)) } - assert.NotEmpty(t, pages[1].Children[0].Children) - if len(pages[1].Children[0].Children) < 2 { - t.Fatalf("Pages error: %v", len(pages[1].Children[0].Children)) + assert.NotEmpty(t, pages[2].Children[0].Children) + if len(pages[2].Children[0].Children) < 2 { + t.Fatalf("Pages error: %v", len(pages[2].Children[0].Children)) } }