Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add event binding logic in SUI core #698

Merged
merged 2 commits into from
Jul 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions sui/core/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}
Expand Down
85 changes: 85 additions & 0 deletions sui/core/event.go
Original file line number Diff line number Diff line change
@@ -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)
}
74 changes: 33 additions & 41 deletions sui/core/injections.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -94,3 +82,7 @@ func bodyInjectionScript(jsonRaw string, debug bool) string {
func headInjectionScript(jsonRaw string) string {
return fmt.Sprintf(`<script type="text/javascript">`+i118nScriptTmpl+`</script>`, jsonRaw)
}

func pageEventInjectScript(eventID, eventName, dataKeys, jsonKeys, handler string) string {
return fmt.Sprintf(pageEventScriptTmpl, eventID, eventName, dataKeys, jsonKeys, handler)
}
74 changes: 74 additions & 0 deletions sui/core/matcher.go
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 2 additions & 2 deletions sui/core/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions sui/storages/local/page_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}

Expand Down