diff --git a/sui/core/build.go b/sui/core/build.go index 8a25a282..0009c90c 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 @@ -105,6 +108,7 @@ 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...) + return doc, ctx.warnings, err } @@ -278,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 new file mode 100644 index 00000000..c959da48 --- /dev/null +++ b/sui/core/event.go @@ -0,0 +1,85 @@ +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 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 + continue + } + + if strings.HasPrefix(attr.Key, "s:data-") { + name := strings.TrimPrefix(attr.Key, "s:data-") + dataUnique[name] = attr.Val + 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.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) + 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..14eb5447 100644 --- a/sui/core/injections.go +++ b/sui/core/injections.go @@ -7,15 +7,28 @@ 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); + + 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); + } } - element = element.parentElement; - } - return null; - } + }) + + handler && handler(event, data, elm); + }; document.addEventListener("DOMContentLoaded", function () { try { @@ -31,39 +44,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 +63,14 @@ const i118nScriptTmpl = ` } ` +const pageEventScriptTmpl = ` + document.querySelector("[s\\:event=%s]").addEventListener("%s", function (event) { + const dataKeys = %s; + const jsonKeys = %s; + __sui_event_handler(event, dataKeys, jsonKeys, this, %s); + }); +` + func bodyInjectionScript(jsonRaw string, debug bool) string { jsPrintData := "" if debug { @@ -94,3 +82,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) +} 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 diff --git a/sui/storages/local/page_test.go b/sui/storages/local/page_test.go index 62782835..e632b40a 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)) } }