add event binding logic in SUI core

This commit is contained in:
Max 2024-07-20 08:44:02 +08:00
parent 69e0070b0a
commit ebc399bfe1
5 changed files with 191 additions and 45 deletions

View file

@ -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
}

83
sui/core/event.go Normal file
View file

@ -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)
}

View file

@ -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(`<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, handler)
}

74
sui/core/matcher.go Normal file
View file

@ -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
}

View file

@ -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