Merge pull request #726 from trheyi/main
Add s:render feature for data update rendering
This commit is contained in:
commit
d9769af569
21 changed files with 1103 additions and 375 deletions
3
Makefile
3
Makefile
|
|
@ -130,7 +130,8 @@ bindata:
|
||||||
cp -r ui .tmp/data/public
|
cp -r ui .tmp/data/public
|
||||||
cp -r xgen .tmp/data/
|
cp -r xgen .tmp/data/
|
||||||
cp -r yao .tmp/data/
|
cp -r yao .tmp/data/
|
||||||
cp -r builder .tmp/data/
|
cp -r sui/libsui .tmp/data/
|
||||||
|
find .tmp/data -name ".DS_Store" -type f -delete
|
||||||
go-bindata -fs -pkg data -o data/bindata.go -prefix ".tmp/data/" .tmp/data/...
|
go-bindata -fs -pkg data -o data/bindata.go -prefix ".tmp/data/" .tmp/data/...
|
||||||
rm -rf .tmp/data
|
rm -rf .tmp/data
|
||||||
rm -rf .tmp/yao-init
|
rm -rf .tmp/yao-init
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
function foo() {
|
|
||||||
return "foo";
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
<div>Yao Builder</div>
|
|
||||||
233
data/bindata.go
233
data/bindata.go
File diff suppressed because one or more lines are too long
12
data/data.go
12
data/data.go
|
|
@ -46,18 +46,6 @@ func Setup() *assetfs.AssetFS {
|
||||||
panic("unreachable")
|
panic("unreachable")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Builder Builder ui
|
|
||||||
func Builder() *assetfs.AssetFS {
|
|
||||||
assetInfo := func(path string) (os.FileInfo, error) {
|
|
||||||
return os.Stat(path)
|
|
||||||
}
|
|
||||||
for k := range _bintree.Children {
|
|
||||||
k = "builder"
|
|
||||||
return &assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, AssetInfo: assetInfo, Prefix: k, Fallback: "index.html"}
|
|
||||||
}
|
|
||||||
panic("unreachable")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReplaceXGen bindata file
|
// ReplaceXGen bindata file
|
||||||
func ReplaceXGen(search, replace string) error {
|
func ReplaceXGen(search, replace string) error {
|
||||||
err := replaceXGenIndex(search, replace)
|
err := replaceXGenIndex(search, replace)
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,6 @@ var AppFileServer http.Handler
|
||||||
// XGenFileServerV1 XGen v1.0
|
// XGenFileServerV1 XGen v1.0
|
||||||
var XGenFileServerV1 http.Handler = http.FileServer(data.XgenV1())
|
var XGenFileServerV1 http.Handler = http.FileServer(data.XgenV1())
|
||||||
|
|
||||||
// BuilderFileServer Builder ui
|
|
||||||
var BuilderFileServer http.Handler = http.FileServer(data.Builder())
|
|
||||||
|
|
||||||
// AdminRoot cache
|
// AdminRoot cache
|
||||||
var AdminRoot = ""
|
var AdminRoot = ""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,26 @@ var dsl = []byte(`
|
||||||
"guard": "bearer-jwt",
|
"guard": "bearer-jwt",
|
||||||
"group": "__yao/sui/v1",
|
"group": "__yao/sui/v1",
|
||||||
"paths": [
|
"paths": [
|
||||||
|
{
|
||||||
|
"label": "Render",
|
||||||
|
"description": "Render the frontend page",
|
||||||
|
"path": "/render/*route",
|
||||||
|
"method": "POST",
|
||||||
|
"guard": "-",
|
||||||
|
"process": "sui.Render",
|
||||||
|
"in": [":context", "$param.route", ":payload"],
|
||||||
|
"out": { "status": 200, "type": "text/html; charset=utf-8" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Run",
|
||||||
|
"description": "Run the backend script, with Api prefix method",
|
||||||
|
"path": "/run/*route",
|
||||||
|
"guard": "-",
|
||||||
|
"method": "POST",
|
||||||
|
"process": "sui.Run",
|
||||||
|
"in": [":context", "$param.route", ":payload"],
|
||||||
|
"out": { "status": 200, "type": "application/json" }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "/:id/setting",
|
"path": "/:id/setting",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,9 @@ func init() {
|
||||||
process.RegisterGroup("sui", map[string]process.Handler{
|
process.RegisterGroup("sui", map[string]process.Handler{
|
||||||
"setting": Setting,
|
"setting": Setting,
|
||||||
|
|
||||||
|
"render": Render,
|
||||||
|
"run": Run,
|
||||||
|
|
||||||
"template.get": TemplateGet,
|
"template.get": TemplateGet,
|
||||||
"template.find": TemplateFind,
|
"template.find": TemplateFind,
|
||||||
"template.asset": TemplateAsset,
|
"template.asset": TemplateAsset,
|
||||||
|
|
|
||||||
111
sui/api/render.go
Normal file
111
sui/api/render.go
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/yaoapp/gou/process"
|
||||||
|
"github.com/yaoapp/yao/sui/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Render the frontend page
|
||||||
|
func Render(process *process.Process) interface{} {
|
||||||
|
process.ValidateArgNums(3)
|
||||||
|
ctx, ok := process.Args[0].(*gin.Context)
|
||||||
|
if !ok {
|
||||||
|
return "The context is required"
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Header("Content-Type", "text/html; charset=utf-8")
|
||||||
|
route := process.ArgsString(1)
|
||||||
|
payload := process.ArgsMap(2)
|
||||||
|
|
||||||
|
if route == "" {
|
||||||
|
return "The route is required"
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload["name"] == nil {
|
||||||
|
return "The render name is required"
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Request.URL.Path = route
|
||||||
|
r, _, err := NewRequestContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("<div class='text-danger'> %s </div>", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
var c *core.Cache = nil
|
||||||
|
if !r.Request.DisableCache() {
|
||||||
|
c = core.GetCache(r.File)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c == nil {
|
||||||
|
c, _, err = r.MakeCache()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("<div class='text-danger'> %s </div>", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c == nil {
|
||||||
|
return fmt.Sprintf("<div class='text-danger'> Cache not found </div>")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, ok := payload["data"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return fmt.Sprintf("<div class='text-danger'> Data not found </div>")
|
||||||
|
}
|
||||||
|
|
||||||
|
name, ok := payload["name"].(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Sprintf("<div class='text-danger'> Name not found </div>")
|
||||||
|
}
|
||||||
|
|
||||||
|
html, err := r.renderHTML(c, name, c.HTML, core.Data(data))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("<div class='text-danger'> %s </div>", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Request) renderHTML(c *core.Cache, name string, html string, data core.Data) (string, error) {
|
||||||
|
|
||||||
|
doc, err := core.NewDocument([]byte(html))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Document error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sel := doc.Find(fmt.Sprintf("[s\\:render='%s']", name))
|
||||||
|
if sel.Length() == 0 {
|
||||||
|
return "", fmt.Errorf("Render %s not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the page request data
|
||||||
|
option := core.ParserOption{
|
||||||
|
Theme: r.Request.Theme,
|
||||||
|
Locale: r.Request.Locale,
|
||||||
|
Debug: r.Request.DebugMode(),
|
||||||
|
DisableCache: r.Request.DisableCache(),
|
||||||
|
Route: r.Request.URL.Path,
|
||||||
|
Root: c.Root,
|
||||||
|
Script: c.Script,
|
||||||
|
Imports: c.Imports,
|
||||||
|
Request: r.Request,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the template
|
||||||
|
parser := core.NewTemplateParser(data, &option)
|
||||||
|
err = parser.RenderSelection(sel)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Parser error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sel.Find("[sui-hide]").Remove()
|
||||||
|
parser.Tidy(sel)
|
||||||
|
html, err = sel.Html()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Html error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return html, nil
|
||||||
|
}
|
||||||
8
sui/api/run.go
Normal file
8
sui/api/run.go
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import "github.com/yaoapp/gou/process"
|
||||||
|
|
||||||
|
// Run the backend script, with Api prefix method
|
||||||
|
func Run(process *process.Process) interface{} {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -71,6 +71,19 @@ func (page *Page) Build(ctx *BuildContext, option *BuildOption) (*goquery.Docume
|
||||||
ctx.warnings = append(ctx.warnings, warnings...)
|
ctx.warnings = append(ctx.warnings, warnings...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prepend the libsui.min.js
|
||||||
|
if !option.IgnoreLibSUI {
|
||||||
|
script := ScriptNode{
|
||||||
|
Parent: "head",
|
||||||
|
Attrs: []html.Attribute{
|
||||||
|
{Key: "src", Val: fmt.Sprintf("%s/libsui.min.js", option.AssetRoot)},
|
||||||
|
{Key: "type", Val: "text/javascript"},
|
||||||
|
{Key: "name", Val: "libsui"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ctx.scripts = append([]ScriptNode{script}, ctx.scripts...)
|
||||||
|
}
|
||||||
|
|
||||||
// Scripts
|
// Scripts
|
||||||
scripts, err := page.BuildScripts(ctx, option, "__page", namespace)
|
scripts, err := page.BuildScripts(ctx, option, "__page", namespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -58,13 +58,6 @@ func (page *Page) Compile(ctx *BuildContext, option *BuildOption) (string, []str
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SUI lib
|
|
||||||
lib, err := libsui(option.ScriptMinify)
|
|
||||||
if err != nil {
|
|
||||||
return "", warnings, err
|
|
||||||
}
|
|
||||||
head.AppendHtml("\n\n" + `<script name="sui" type="text/javascript">` + lib + `</script>` + "\n\n")
|
|
||||||
|
|
||||||
// Page Config
|
// Page Config
|
||||||
page.Config = page.GetConfig()
|
page.Config = page.GetConfig()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -94,10 +94,12 @@ func GetEventScript(sequence int, sel *goquery.Selection, ns string, cn string,
|
||||||
for name, handler := range events {
|
for name, handler := range events {
|
||||||
if ispage {
|
if ispage {
|
||||||
source += pageEventInjectScript(id, name, dataRaw, jsonRaw, handler) + "\n"
|
source += pageEventInjectScript(id, name, dataRaw, jsonRaw, handler) + "\n"
|
||||||
|
sel.SetAttr("s:event-cn", "__page")
|
||||||
} else {
|
} else {
|
||||||
source += compEventInjectScript(id, name, cn, dataRaw, jsonRaw, handler) + "\n"
|
source += compEventInjectScript(id, name, cn, dataRaw, jsonRaw, handler) + "\n"
|
||||||
|
sel.SetAttr("s:event-cn", cn)
|
||||||
}
|
}
|
||||||
sel.RemoveAttr(fmt.Sprintf("s:on-%s", name))
|
// sel.RemoveAttr(fmt.Sprintf("s:on-%s", name))
|
||||||
}
|
}
|
||||||
|
|
||||||
sel.SetAttr("s:event", id)
|
sel.SetAttr("s:event", id)
|
||||||
|
|
|
||||||
|
|
@ -5,226 +5,45 @@ import (
|
||||||
|
|
||||||
"github.com/evanw/esbuild/pkg/api"
|
"github.com/evanw/esbuild/pkg/api"
|
||||||
"github.com/yaoapp/gou/runtime/transform"
|
"github.com/yaoapp/gou/runtime/transform"
|
||||||
|
"github.com/yaoapp/yao/data"
|
||||||
)
|
)
|
||||||
|
|
||||||
var libsuicode = ""
|
var libsuicode = ""
|
||||||
|
|
||||||
func libsui(minify bool) (string, error) {
|
// LibSUI return the libsui code
|
||||||
if libsuicode != "" {
|
func LibSUI() ([]byte, []byte, error) {
|
||||||
return libsuicode, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
option := api.TransformOptions{Target: api.ES2015}
|
// Read source code from bindata
|
||||||
if minify {
|
index, err := data.Read("libsui/index.ts")
|
||||||
option.MinifyIdentifiers = true
|
|
||||||
option.MinifySyntax = true
|
|
||||||
option.MinifyWhitespace = true
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
libsuicode, err = transform.JavaScript(libsuisource, option)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("libsui error: %w", err)
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
return libsuicode, nil
|
|
||||||
|
utils, err := data.Read("libsui/utils.ts")
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
yao, err := data.Read("libsui/yao.ts")
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge the source code
|
||||||
|
source := fmt.Sprintf("%s\n%s\n%s", index, utils, yao)
|
||||||
|
|
||||||
|
// Build the source code
|
||||||
|
js, sm, err := transform.TypeScriptWithSourceMap(string(source), api.TransformOptions{
|
||||||
|
Target: api.ES2015,
|
||||||
|
MinifyIdentifiers: true,
|
||||||
|
MinifySyntax: true,
|
||||||
|
MinifyWhitespace: true,
|
||||||
|
Sourcefile: "libsui.ts",
|
||||||
|
})
|
||||||
|
|
||||||
|
return js, sm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const libsuisource = `
|
|
||||||
|
|
||||||
function $$(selector) {
|
|
||||||
elm = null;
|
|
||||||
if (typeof selector === "string" ){
|
|
||||||
elm = document.querySelector(selector);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selector instanceof HTMLElement) {
|
|
||||||
elm = selector;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (elm) {
|
|
||||||
cn = elm.getAttribute("s:cn");
|
|
||||||
if (cn != "" && typeof window[cn] === "function") {
|
|
||||||
const component = new window[cn](elm);
|
|
||||||
return new __sui_component(elm, component);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const $utils = {
|
|
||||||
|
|
||||||
RemoveClass: (element, className) => {
|
|
||||||
const classes = Array.isArray(className) ? className : className.split(" ");
|
|
||||||
classes.forEach((c) => {
|
|
||||||
const v = c.replace(/[\n\r\s]/g, "");
|
|
||||||
if (v === "") return;
|
|
||||||
element.classList.remove(v);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
AddClass: (element, className) => {
|
|
||||||
const classes = Array.isArray(className) ? className : className.split(" ");
|
|
||||||
classes.forEach((c) => {
|
|
||||||
const v = c.replace(/[\n\r\s]/g, "");
|
|
||||||
if (v === "") return;
|
|
||||||
element.classList.add(v);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
function __sui_component_root(elm, name) {
|
|
||||||
while (elm && elm.getAttribute("s:cn") !== name) {
|
|
||||||
elm = elm.parentElement;
|
|
||||||
}
|
|
||||||
return elm;
|
|
||||||
}
|
|
||||||
|
|
||||||
function __sui_state(component) {
|
|
||||||
this.handlers = component.watch || {};
|
|
||||||
this.Set = async function (key, value, target) {
|
|
||||||
const handler = this.handlers[key];
|
|
||||||
target = target || component.root;
|
|
||||||
if (handler && typeof handler === "function") {
|
|
||||||
const stateObj = {
|
|
||||||
target: target,
|
|
||||||
stopPropagation: function () {
|
|
||||||
target.setAttribute("state-propagation", "true");
|
|
||||||
},
|
|
||||||
}
|
|
||||||
await handler(value, stateObj);
|
|
||||||
const isStopPropagation = target ? target.getAttribute("state-propagation") === "true" : false;
|
|
||||||
if (isStopPropagation) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let parent = component.root.parentElement;
|
|
||||||
while (parent && !parent.getAttribute("s:cn")) {
|
|
||||||
parent = parent.parentElement;
|
|
||||||
}
|
|
||||||
if ( parent == document.body || parent == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Dispatch the state change custom event to parent component
|
|
||||||
const event = new CustomEvent("state:change", { detail: { key: key, value: value, target:component.root } });
|
|
||||||
parent.dispatchEvent(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function __sui_props(elm) {
|
|
||||||
this.Get = function (key) {
|
|
||||||
if (!elm || typeof elm.getAttribute !== "function") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const k = "prop:" + key;
|
|
||||||
const v = elm.getAttribute(k);
|
|
||||||
const json = elm.getAttribute("json-attr-prop:" + key) === "true";
|
|
||||||
if (json) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(v);
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.List = function () {
|
|
||||||
const props = {};
|
|
||||||
if (!elm || typeof elm.getAttribute !== "function") {
|
|
||||||
return props;
|
|
||||||
}
|
|
||||||
|
|
||||||
const attrs = elm.attributes;
|
|
||||||
for (let i = 0; i < attrs.length; i++) {
|
|
||||||
const attr = attrs[i];
|
|
||||||
if (attr.name.startsWith("prop:")) {
|
|
||||||
const k = attr.name.replace("prop:", "");
|
|
||||||
const json = elm.getAttribute("json-attr-prop:" + k) === "true";
|
|
||||||
if (json) {
|
|
||||||
try {
|
|
||||||
props[k] = JSON.parse(attr.value);
|
|
||||||
} catch (e) {
|
|
||||||
props[k] = null;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
props[k] = attr.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return props;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function __sui_component(elm, component) {
|
|
||||||
this.root = elm;
|
|
||||||
this.store = new __sui_store(elm);
|
|
||||||
this.props = new __sui_props(elm);
|
|
||||||
this.state = component ? new __sui_state(component) : {};
|
|
||||||
}
|
|
||||||
|
|
||||||
function __sui_event_handler(event, dataKeys, jsonKeys, target, root, handler) {
|
|
||||||
const data = {};
|
|
||||||
target = target || null;
|
|
||||||
if (target) {
|
|
||||||
dataKeys.forEach(function (key) {
|
|
||||||
const value = target.getAttribute("data:" + key);
|
|
||||||
data[key] = value;
|
|
||||||
})
|
|
||||||
jsonKeys.forEach(function (key) {
|
|
||||||
const value = target.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}`" + `, target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
handler && handler(event, data, {
|
|
||||||
rootElement: root,
|
|
||||||
targetElement: target
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function __sui_store(elm) {
|
|
||||||
elm = elm || document.body;
|
|
||||||
|
|
||||||
this.Get = function (key) {
|
|
||||||
return elm.getAttribute("data:" + key);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.Set = function (key, value) {
|
|
||||||
elm.setAttribute("data:" + key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.GetJSON = function (key) {
|
|
||||||
const value = elm.getAttribute("json:" + key);
|
|
||||||
if (value && value != "") {
|
|
||||||
try {
|
|
||||||
const res = JSON.parse(value);
|
|
||||||
return res;
|
|
||||||
} catch (e) {
|
|
||||||
const message = e.message || e || "An error occurred";
|
|
||||||
console.error(` + "`[SUI] Event Handler Error: ${message}`" + `, elm);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.SetJSON = function (key, value) {
|
|
||||||
elm.setAttribute("json:" + key, JSON.stringify(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.GetData = function () {
|
|
||||||
return this.GetJSON("__component_data") || {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const initScriptTmpl = `
|
const initScriptTmpl = `
|
||||||
try {
|
try {
|
||||||
var __sui_data = %s;
|
var __sui_data = %s;
|
||||||
|
|
@ -237,7 +56,7 @@ const initScriptTmpl = `
|
||||||
const cn = element.getAttribute("s:cn");
|
const cn = element.getAttribute("s:cn");
|
||||||
if (method && typeof window[cn] === "function") {
|
if (method && typeof window[cn] === "function") {
|
||||||
try {
|
try {
|
||||||
window[cn](element);
|
new window[cn](element);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const message = e.message || e || "An error occurred";
|
const message = e.message || e || "An error occurred";
|
||||||
console.error(` + "`[SUI] ${cn} Error: ${message}`" + `);
|
console.error(` + "`[SUI] ${cn} Error: ${message}`" + `);
|
||||||
|
|
@ -264,24 +83,30 @@ const i118nScriptTmpl = `
|
||||||
`
|
`
|
||||||
|
|
||||||
const pageEventScriptTmpl = `
|
const pageEventScriptTmpl = `
|
||||||
document.querySelector("[s\\:event=%s]").addEventListener("%s", function (event) {
|
if (document.querySelector("[s\\:event=%s]")) {
|
||||||
|
let elms = document.querySelectorAll("[s\\:event=%s]");
|
||||||
|
elms.forEach(function (element) {
|
||||||
|
element.addEventListener("%s", function (event) {
|
||||||
const dataKeys = %s;
|
const dataKeys = %s;
|
||||||
const jsonKeys = %s;
|
const jsonKeys = %s;
|
||||||
const root = document.body;
|
const root = document.body;
|
||||||
const target = this;
|
__sui_event_handler(event, dataKeys, jsonKeys, element, root, window.%s);
|
||||||
__sui_event_handler(event, dataKeys, jsonKeys, target, root, %s);
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const compEventScriptTmpl = `
|
const compEventScriptTmpl = `
|
||||||
if (document.querySelector("[s\\:event=%s]")) {
|
if (document.querySelector("[s\\:event=%s]")) {
|
||||||
document.querySelector("[s\\:event=%s]").addEventListener("%s", function (event) {
|
let elms = document.querySelectorAll("[s\\:event=%s]");
|
||||||
|
elms.forEach(function (element) {
|
||||||
|
element.addEventListener("%s", function (event) {
|
||||||
const dataKeys = %s;
|
const dataKeys = %s;
|
||||||
const jsonKeys = %s;
|
const jsonKeys = %s;
|
||||||
const root = __sui_component_root(this, "%s");
|
const root = __sui_component_root(element, "%s");
|
||||||
handler = new %s(root).%s;
|
handler = new %s(root).%s;
|
||||||
const target = event.target || null;
|
__sui_event_handler(event, dataKeys, jsonKeys, element, root, handler);
|
||||||
__sui_event_handler(event, dataKeys, jsonKeys, target, root, handler);
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
@ -333,7 +158,7 @@ func headInjectionScript(jsonRaw string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func pageEventInjectScript(eventID, eventName, dataKeys, jsonKeys, handler string) string {
|
func pageEventInjectScript(eventID, eventName, dataKeys, jsonKeys, handler string) string {
|
||||||
return fmt.Sprintf(pageEventScriptTmpl, eventID, eventName, dataKeys, jsonKeys, handler)
|
return fmt.Sprintf(pageEventScriptTmpl, eventID, eventID, eventName, dataKeys, jsonKeys, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func compEventInjectScript(eventID, eventName, component, dataKeys, jsonKeys, handler string) string {
|
func compEventInjectScript(eventID, eventName, component, dataKeys, jsonKeys, handler string) string {
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,8 @@ var allowUsePropAttrs = map[string]bool{
|
||||||
"s:elif": true,
|
"s:elif": true,
|
||||||
"s:for": true,
|
"s:for": true,
|
||||||
"s:event": true,
|
"s:event": true,
|
||||||
|
"s:event-cn": true,
|
||||||
|
"s:render": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
var keepAttrs = map[string]bool{
|
var keepAttrs = map[string]bool{
|
||||||
|
|
@ -79,6 +81,8 @@ var keepAttrs = map[string]bool{
|
||||||
"s:cn": true,
|
"s:cn": true,
|
||||||
"s:ready": true,
|
"s:ready": true,
|
||||||
"s:event": true,
|
"s:event": true,
|
||||||
|
"s:event-cn": true,
|
||||||
|
"s:render": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTemplateParser create a new template parser
|
// NewTemplateParser create a new template parser
|
||||||
|
|
@ -157,7 +161,7 @@ func (parser *TemplateParser) Render(html string) (string, error) {
|
||||||
if parser.option != nil && (parser.option.Request != nil || parser.option.Preview) {
|
if parser.option != nil && (parser.option.Request != nil || parser.option.Preview) {
|
||||||
// Remove the sui-hide attribute
|
// Remove the sui-hide attribute
|
||||||
doc.Find("[sui-hide]").Remove()
|
doc.Find("[sui-hide]").Remove()
|
||||||
parser.tidy(doc.Selection)
|
parser.Tidy(doc.Selection)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fmt.Println(doc.Html())
|
// fmt.Println(doc.Html())
|
||||||
|
|
@ -837,13 +841,13 @@ func (parser *TemplateParser) show(sel *goquery.Selection) {
|
||||||
// sel.SetAttr("style", style)
|
// sel.SetAttr("style", style)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (parser *TemplateParser) tidy(s *goquery.Selection) {
|
func (parser *TemplateParser) Tidy(s *goquery.Selection) {
|
||||||
|
|
||||||
s.Contents().Each(func(i int, child *goquery.Selection) {
|
s.Contents().Each(func(i int, child *goquery.Selection) {
|
||||||
|
|
||||||
node := child.Get(0)
|
node := child.Get(0)
|
||||||
if _, exist := child.Attr("s:jit"); node.Data == "slot" || exist {
|
if _, exist := child.Attr("s:jit"); node.Data == "slot" || exist {
|
||||||
parser.tidy(child)
|
parser.Tidy(child)
|
||||||
parser.removeWrapper(child)
|
parser.removeWrapper(child)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -861,7 +865,7 @@ func (parser *TemplateParser) tidy(s *goquery.Selection) {
|
||||||
// Remove the parsed attribute
|
// Remove the parsed attribute
|
||||||
attrs := []html.Attribute{}
|
attrs := []html.Attribute{}
|
||||||
for _, attr := range node.Attr {
|
for _, attr := range node.Attr {
|
||||||
if strings.HasPrefix(attr.Key, "s:") && !keepAttrs[attr.Key] {
|
if strings.HasPrefix(attr.Key, "s:") && !keepAttrs[attr.Key] && !strings.HasPrefix(attr.Key, "s:on-") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -872,7 +876,7 @@ func (parser *TemplateParser) tidy(s *goquery.Selection) {
|
||||||
}
|
}
|
||||||
|
|
||||||
node.Attr = attrs
|
node.Attr = attrs
|
||||||
parser.tidy(child)
|
parser.Tidy(child)
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -262,6 +262,7 @@ type BuildOption struct {
|
||||||
PublicRoot string `json:"public_root,omitempty"`
|
PublicRoot string `json:"public_root,omitempty"`
|
||||||
AssetRoot string `json:"asset_root,omitempty"`
|
AssetRoot string `json:"asset_root,omitempty"`
|
||||||
IgnoreAssetRoot bool `json:"ignore_asset_root,omitempty"`
|
IgnoreAssetRoot bool `json:"ignore_asset_root,omitempty"`
|
||||||
|
IgnoreLibSUI bool `json:"ignore_lib_sui,omitempty"`
|
||||||
IgnoreDocument bool `json:"ignore_document,omitempty"`
|
IgnoreDocument bool `json:"ignore_document,omitempty"`
|
||||||
JitMode bool `json:"jit_mode,omitempty"`
|
JitMode bool `json:"jit_mode,omitempty"`
|
||||||
WithWrapper bool `json:"with_wrapper,omitempty"`
|
WithWrapper bool `json:"with_wrapper,omitempty"`
|
||||||
|
|
|
||||||
340
sui/libsui/index.ts
Normal file
340
sui/libsui/index.ts
Normal file
|
|
@ -0,0 +1,340 @@
|
||||||
|
function $$(selector) {
|
||||||
|
let elm: HTMLElement | null = null;
|
||||||
|
if (typeof selector === "string") {
|
||||||
|
elm = document.querySelector(selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selector instanceof HTMLElement) {
|
||||||
|
elm = selector;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elm) {
|
||||||
|
const cn = elm.getAttribute("s:cn");
|
||||||
|
if (cn && cn != "" && typeof window[cn] === "function") {
|
||||||
|
const component = new window[cn](elm);
|
||||||
|
return new __sui_component(elm, component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function __sui_component_root(elm: Element, name: string) {
|
||||||
|
return elm.closest(`[s\\:cn=${name}]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function __sui_state(component) {
|
||||||
|
this.handlers = component.watch || {};
|
||||||
|
this.Set = async function (key, value, target) {
|
||||||
|
const handler = this.handlers[key];
|
||||||
|
target = target || component.root;
|
||||||
|
if (handler && typeof handler === "function") {
|
||||||
|
const stateObj = {
|
||||||
|
target: target,
|
||||||
|
stopPropagation: function () {
|
||||||
|
target.setAttribute("state-propagation", "true");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await handler(value, stateObj);
|
||||||
|
const isStopPropagation = target
|
||||||
|
? target.getAttribute("state-propagation") === "true"
|
||||||
|
: false;
|
||||||
|
if (isStopPropagation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parent = component.root.parentElement;
|
||||||
|
while (parent && !parent.getAttribute("s:cn")) {
|
||||||
|
parent = parent.parentElement;
|
||||||
|
}
|
||||||
|
if (parent == document.body || parent == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Dispatch the state change custom event to parent component
|
||||||
|
const event = new CustomEvent("state:change", {
|
||||||
|
detail: { key: key, value: value, target: component.root },
|
||||||
|
});
|
||||||
|
parent.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function __sui_props(elm) {
|
||||||
|
this.Get = function (key) {
|
||||||
|
if (!elm || typeof elm.getAttribute !== "function") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const k = "prop:" + key;
|
||||||
|
const v = elm.getAttribute(k);
|
||||||
|
const json = elm.getAttribute("json-attr-prop:" + key) === "true";
|
||||||
|
if (json) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(v);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.List = function () {
|
||||||
|
const props = {};
|
||||||
|
if (!elm || typeof elm.getAttribute !== "function") {
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attrs = elm.attributes;
|
||||||
|
for (let i = 0; i < attrs.length; i++) {
|
||||||
|
const attr = attrs[i];
|
||||||
|
if (attr.name.startsWith("prop:")) {
|
||||||
|
const k = attr.name.replace("prop:", "");
|
||||||
|
const json = elm.getAttribute("json-attr-prop:" + k) === "true";
|
||||||
|
if (json) {
|
||||||
|
try {
|
||||||
|
props[k] = JSON.parse(attr.value);
|
||||||
|
} catch (e) {
|
||||||
|
props[k] = null;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
props[k] = attr.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function __sui_component(elm, component) {
|
||||||
|
this.root = elm;
|
||||||
|
this.store = new __sui_store(elm);
|
||||||
|
this.props = new __sui_props(elm);
|
||||||
|
this.state = component ? new __sui_state(component) : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function __sui_event_handler(event, dataKeys, jsonKeys, target, root, handler) {
|
||||||
|
const data = {};
|
||||||
|
target = target || null;
|
||||||
|
if (target) {
|
||||||
|
dataKeys.forEach(function (key) {
|
||||||
|
const value = target.getAttribute("data:" + key);
|
||||||
|
data[key] = value;
|
||||||
|
});
|
||||||
|
jsonKeys.forEach(function (key) {
|
||||||
|
const value = target.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} `, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
handler &&
|
||||||
|
handler(event, data, {
|
||||||
|
rootElement: root,
|
||||||
|
targetElement: target,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function __sui_event_init(elm: Element) {
|
||||||
|
const eventElms = elm.querySelectorAll("[s\\:event]");
|
||||||
|
eventElms.forEach((eventElm) => {
|
||||||
|
const cn = eventElm.getAttribute("s:event-cn") || "";
|
||||||
|
|
||||||
|
// Data keys
|
||||||
|
const events: Record<string, string> = {};
|
||||||
|
const dataKeys: string[] = [];
|
||||||
|
const jsonKeys: string[] = [];
|
||||||
|
for (let i = 0; i < eventElm.attributes.length; i++) {
|
||||||
|
if (eventElm.attributes[i].name.startsWith("data:")) {
|
||||||
|
dataKeys.push(eventElm.attributes[i].name.replace("data:", ""));
|
||||||
|
}
|
||||||
|
if (eventElm.attributes[i].name.startsWith("json:")) {
|
||||||
|
jsonKeys.push(eventElm.attributes[i].name.replace("json:", ""));
|
||||||
|
}
|
||||||
|
if (eventElm.attributes[i].name.startsWith("s:on-")) {
|
||||||
|
const key = eventElm.attributes[i].name.replace("s:on-", "");
|
||||||
|
events[key] = eventElm.attributes[i].value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind the event
|
||||||
|
for (const name in events) {
|
||||||
|
const bind = events[name];
|
||||||
|
if (cn == "__page") {
|
||||||
|
const handler = window[bind];
|
||||||
|
const root = document.body;
|
||||||
|
const target = eventElm;
|
||||||
|
eventElm.addEventListener(name, (event) => {
|
||||||
|
__sui_event_handler(event, dataKeys, jsonKeys, target, root, handler);
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const comp = new window[cn](eventElm.closest(`[s\\:cn=${cn}]`));
|
||||||
|
const handler = comp[bind];
|
||||||
|
const root = comp.root;
|
||||||
|
const target = eventElm;
|
||||||
|
eventElm.addEventListener(name, (event) => {
|
||||||
|
__sui_event_handler(event, dataKeys, jsonKeys, target, root, handler);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function __sui_store(elm) {
|
||||||
|
elm = elm || document.body;
|
||||||
|
|
||||||
|
this.Get = function (key) {
|
||||||
|
return elm.getAttribute("data:" + key);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Set = function (key, value) {
|
||||||
|
elm.setAttribute("data:" + key, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.GetJSON = function (key) {
|
||||||
|
const value = elm.getAttribute("json:" + key);
|
||||||
|
if (value && value != "") {
|
||||||
|
try {
|
||||||
|
const res = JSON.parse(value);
|
||||||
|
return res;
|
||||||
|
} catch (e) {
|
||||||
|
const message = e.message || e || "An error occurred";
|
||||||
|
console.error(`[SUI] Event Handler Error: ${message}`, elm);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.SetJSON = function (key, value) {
|
||||||
|
elm.setAttribute("json:" + key, JSON.stringify(value));
|
||||||
|
};
|
||||||
|
|
||||||
|
this.GetData = function () {
|
||||||
|
return this.GetJSON("__component_data") || {};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SUI Render
|
||||||
|
* @param component
|
||||||
|
* @param name
|
||||||
|
*/
|
||||||
|
async function __sui_render(
|
||||||
|
component: Component | string,
|
||||||
|
name: string,
|
||||||
|
data: Record<string, any>,
|
||||||
|
option?: RenderOption
|
||||||
|
): Promise<string> {
|
||||||
|
const comp = (
|
||||||
|
typeof component === "object" ? component : $$(component)
|
||||||
|
) as Component;
|
||||||
|
|
||||||
|
if (comp == null) {
|
||||||
|
console.error(`[SUI] Component not found: ${component}`);
|
||||||
|
return Promise.reject("Component not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const elms = comp.root.querySelectorAll(`[s\\:render=${name}]`);
|
||||||
|
if (!elms.length) {
|
||||||
|
console.error(`[SUI] No element found with s:render=${name}`);
|
||||||
|
return Promise.reject("No element found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default options
|
||||||
|
option = option || {};
|
||||||
|
option.replace = option.replace === undefined ? true : option.replace;
|
||||||
|
option.showLoader =
|
||||||
|
option.showLoader === undefined ? false : option.showLoader;
|
||||||
|
option.withPageData =
|
||||||
|
option.withPageData === undefined ? false : option.withPageData;
|
||||||
|
|
||||||
|
// Prepare loader
|
||||||
|
let loader = `<span class="sui-render-loading">Loading...</span>`;
|
||||||
|
if (option.showLoader && option.replace) {
|
||||||
|
if (typeof option.showLoader === "string") {
|
||||||
|
loader = option.showLoader;
|
||||||
|
} else if (option.showLoader instanceof HTMLElement) {
|
||||||
|
loader = option.showLoader.outerHTML;
|
||||||
|
}
|
||||||
|
elms.forEach((elm) => (elm.innerHTML = loader));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare data
|
||||||
|
let _data = comp.store.GetData() || {};
|
||||||
|
if (option.withPageData) {
|
||||||
|
// @ts-ignore
|
||||||
|
_data = { ..._data, ...__sui_data };
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = window.location.pathname;
|
||||||
|
const url = `/api/__yao/sui/v1/render${route}`;
|
||||||
|
const payload = { name, data: { ..._data, ...data }, option };
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Cookie: document.cookie,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Native post request to the server
|
||||||
|
try {
|
||||||
|
const body = JSON.stringify(payload);
|
||||||
|
const response = await fetch(url, { method: "POST", headers, body: body });
|
||||||
|
const text = await response.text();
|
||||||
|
if (!option.replace) {
|
||||||
|
return Promise.resolve(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the response text to the elements
|
||||||
|
elms.forEach((elm) => {
|
||||||
|
elm.innerHTML = text;
|
||||||
|
__sui_event_init(elm);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.resolve(text);
|
||||||
|
} catch (e) {
|
||||||
|
//Set the error message
|
||||||
|
elms.forEach((elm) => {
|
||||||
|
elm.innerHTML = `<span class="sui-render-error">Failed to render</span>`;
|
||||||
|
console.error("Failed to render", e);
|
||||||
|
});
|
||||||
|
return Promise.reject("Failed to render");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Component = {
|
||||||
|
root: HTMLElement;
|
||||||
|
state: ComponentState;
|
||||||
|
store: ComponentStore;
|
||||||
|
watch?: Record<string, (value: any, state?: State) => void>;
|
||||||
|
Constants?: Record<string, any>;
|
||||||
|
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RenderOption = {
|
||||||
|
target?: HTMLElement; // default is same with s:render target
|
||||||
|
showLoader?: HTMLElement | string | boolean; // default is false
|
||||||
|
replace?: boolean; // default is true
|
||||||
|
withPageData?: boolean; // default is false
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ComponentState = {
|
||||||
|
Set: (key: string, value: any) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ComponentStore = {
|
||||||
|
Get: (key: string) => string;
|
||||||
|
Set: (key: string, value: any) => void;
|
||||||
|
GetJSON: (key: string) => any;
|
||||||
|
SetJSON: (key: string, value: any) => void;
|
||||||
|
GetData: () => Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type State = {
|
||||||
|
target: HTMLElement;
|
||||||
|
stopPropagation();
|
||||||
|
};
|
||||||
177
sui/libsui/utils.ts
Normal file
177
sui/libsui/utils.ts
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
function $Store(elm) {
|
||||||
|
if (!elm) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof elm === "string") {
|
||||||
|
elm = document.querySelectorAll(elm);
|
||||||
|
if (elm.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
elm = elm[0];
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
return new __sui_store(elm);
|
||||||
|
}
|
||||||
|
|
||||||
|
function $Query(selector: string): __Query {
|
||||||
|
return new __Query(selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
class __Query {
|
||||||
|
selector: string | Element = "";
|
||||||
|
elements: NodeListOf<Element> | null = null;
|
||||||
|
element: Element | null = null;
|
||||||
|
constructor(selector: string | Element) {
|
||||||
|
if (typeof selector === "string") {
|
||||||
|
this.selector = selector;
|
||||||
|
this.elements = document.querySelectorAll(selector);
|
||||||
|
if (this.elements.length > 0) {
|
||||||
|
this.element = this.elements[0];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.element = selector;
|
||||||
|
}
|
||||||
|
this.selector = selector;
|
||||||
|
}
|
||||||
|
|
||||||
|
elm(): Element | null {
|
||||||
|
return this.element;
|
||||||
|
}
|
||||||
|
|
||||||
|
elms(): NodeListOf<Element> | null {
|
||||||
|
return this.elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
$$() {
|
||||||
|
if (!this.element) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const root = this.element.closest("[s\\:cn]");
|
||||||
|
if (!root) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
return $$(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
each(callback: (element: __Query, index: number) => void) {
|
||||||
|
if (!this.elements) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.elements.forEach((element, index) => {
|
||||||
|
callback(new __Query(element), index);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
store() {
|
||||||
|
if (!this.element || typeof this.element.getAttribute !== "function") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
return new __sui_store(this.element);
|
||||||
|
}
|
||||||
|
|
||||||
|
attr(key) {
|
||||||
|
if (!this.element || typeof this.element.getAttribute !== "function") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.element.getAttribute(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
data(key) {
|
||||||
|
if (!this.element || typeof this.element.getAttribute !== "function") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.element.getAttribute("data:" + key);
|
||||||
|
}
|
||||||
|
|
||||||
|
json(key) {
|
||||||
|
if (!this.element || typeof this.element.getAttribute !== "function") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const v = this.element.getAttribute("json:" + key);
|
||||||
|
if (!v) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(v);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error parsing JSON for key ${key}: ${e}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasClass(className) {
|
||||||
|
return this.element?.classList.contains(className);
|
||||||
|
}
|
||||||
|
|
||||||
|
prop(key) {
|
||||||
|
if (!this.element || typeof this.element.getAttribute !== "function") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const k = "prop:" + key;
|
||||||
|
const v = this.element.getAttribute(k);
|
||||||
|
const json = this.element.getAttribute("json-attr-prop:" + key) === "true";
|
||||||
|
if (json && v) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(v);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error parsing JSON for prop ${key}: ${e}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeClass(className) {
|
||||||
|
const classes = Array.isArray(className) ? className : className.split(" ");
|
||||||
|
classes.forEach((c) => {
|
||||||
|
const v = c.replace(/[\n\r\s]/g, "");
|
||||||
|
if (v === "") return;
|
||||||
|
this.element?.classList.remove(v);
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
addClass(className) {
|
||||||
|
const classes = Array.isArray(className) ? className : className.split(" ");
|
||||||
|
classes.forEach((c) => {
|
||||||
|
const v = c.replace(/[\n\r\s]/g, "");
|
||||||
|
if (v === "") return;
|
||||||
|
this.element?.classList.add(v);
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
html(html?: string): __Query | string {
|
||||||
|
if (html === undefined) {
|
||||||
|
return this.element?.innerHTML || "";
|
||||||
|
}
|
||||||
|
if (this.element) {
|
||||||
|
this.element.innerHTML = html;
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function $Render(comp, option): __Render {
|
||||||
|
const r = new __Render(comp, option);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
class __Render {
|
||||||
|
comp = null;
|
||||||
|
option = null;
|
||||||
|
constructor(comp, option) {
|
||||||
|
this.comp = comp;
|
||||||
|
this.option = option;
|
||||||
|
}
|
||||||
|
async Render(name, data): Promise<string> {
|
||||||
|
// @ts-ignore
|
||||||
|
return __sui_render(this.comp, name, data, this.option);
|
||||||
|
}
|
||||||
|
}
|
||||||
182
sui/libsui/yao.ts
Normal file
182
sui/libsui/yao.ts
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
/**
|
||||||
|
* YAO Pure JavaScript SDK
|
||||||
|
* @author Max<max@iqka.com>
|
||||||
|
* @maintainer https://yaoapps.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Yao Object
|
||||||
|
* @param {*} host
|
||||||
|
*/
|
||||||
|
function Yao(host) {
|
||||||
|
this.host = `${
|
||||||
|
host || window.location.protocol + "//" + window.location.host
|
||||||
|
}/api`;
|
||||||
|
this.query = {};
|
||||||
|
new URLSearchParams(window.location.search).forEach((key, value) => {
|
||||||
|
this.query[key] = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get API
|
||||||
|
* @param {*} path
|
||||||
|
* @param {*} params
|
||||||
|
*/
|
||||||
|
Yao.prototype.Get = async function (path, params, headers) {
|
||||||
|
return this.Fetch("GET", path, params, null, headers);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post API
|
||||||
|
* @param {*} path
|
||||||
|
* @param {*} data
|
||||||
|
* @param {*} params
|
||||||
|
* @param {*} headers
|
||||||
|
*/
|
||||||
|
Yao.prototype.Post = async function (path, data, params, headers) {
|
||||||
|
return this.Fetch("POST", path, params, data, headers);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download API
|
||||||
|
* @param {*} path
|
||||||
|
* @param {*} params
|
||||||
|
*/
|
||||||
|
Yao.prototype.Download = async function (path, params, savefile, headers) {
|
||||||
|
try {
|
||||||
|
const blob = await this.Fetch("GET", path, params, null, headers, true);
|
||||||
|
|
||||||
|
var objectUrl = window.URL.createObjectURL(blob);
|
||||||
|
let anchor = document.createElement("a");
|
||||||
|
document.body.appendChild(anchor);
|
||||||
|
anchor.href = objectUrl;
|
||||||
|
anchor.download = savefile;
|
||||||
|
anchor.click();
|
||||||
|
window.URL.revokeObjectURL(objectUrl);
|
||||||
|
} catch (err) {
|
||||||
|
alert("成功创建导出任务!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch API
|
||||||
|
* @param {*} method
|
||||||
|
* @param {*} path
|
||||||
|
* @param {*} params
|
||||||
|
* @param {*} data
|
||||||
|
* @param {*} headers
|
||||||
|
*/
|
||||||
|
Yao.prototype.Fetch = async function (
|
||||||
|
method,
|
||||||
|
path,
|
||||||
|
params,
|
||||||
|
data,
|
||||||
|
headers,
|
||||||
|
isblob
|
||||||
|
) {
|
||||||
|
params = params || {};
|
||||||
|
headers = headers || {};
|
||||||
|
data = data || null;
|
||||||
|
var url = `${this.host}${path}`;
|
||||||
|
var queryString = this.Serialize(params);
|
||||||
|
if (queryString != "") {
|
||||||
|
url = url.includes("?") ? `${url}&${queryString}` : `${url}?${queryString}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = this.Token();
|
||||||
|
if (token != "") {
|
||||||
|
headers["authorization"] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!headers["Content-Type"]) {
|
||||||
|
headers["Content-Type"] = "application/json";
|
||||||
|
}
|
||||||
|
|
||||||
|
var options: any = {
|
||||||
|
method: method,
|
||||||
|
mode: "cors", // no-cors, *cors, same-origin
|
||||||
|
cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
|
||||||
|
credentials: "same-origin", // include, *same-origin, omit
|
||||||
|
headers: headers,
|
||||||
|
redirect: "follow", // manual, *follow, error
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data != null) {
|
||||||
|
options["body"] = JSON.stringify(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = await fetch(url, options);
|
||||||
|
const type = resp.headers.get("Content-Type") || "";
|
||||||
|
if (type.includes("application/json")) {
|
||||||
|
try {
|
||||||
|
const data = await resp.json();
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
return { code: resp.status, message: "empty return" };
|
||||||
|
}
|
||||||
|
} else if (isblob) {
|
||||||
|
return resp.blob();
|
||||||
|
} else if (type.includes("text/html") || type.includes("text/plain")) {
|
||||||
|
return resp.text();
|
||||||
|
}
|
||||||
|
return resp.text();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token API
|
||||||
|
* @param {*} path
|
||||||
|
* @param {*} params
|
||||||
|
*/
|
||||||
|
Yao.prototype.Token = function () {
|
||||||
|
var token = sessionStorage.getItem("token") || "";
|
||||||
|
if (token == "") {
|
||||||
|
return this.Cookie("__tk") || "";
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Cookie
|
||||||
|
* @param {*} cookieName
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
Yao.prototype.Cookie = function (cookieName) {
|
||||||
|
var name = cookieName + "=";
|
||||||
|
var decodedCookie = decodeURIComponent(document.cookie);
|
||||||
|
var cookieArray = decodedCookie.split(";");
|
||||||
|
|
||||||
|
for (var i = 0; i < cookieArray.length; i++) {
|
||||||
|
var cookie = cookieArray[i].trim();
|
||||||
|
if (cookie.indexOf(name) === 0) {
|
||||||
|
return cookie.substring(name.length, cookie.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
Yao.prototype.SetCookie = function (cookieName, cookieValue, expireDays) {
|
||||||
|
expireDays = expireDays || 30;
|
||||||
|
var d = new Date();
|
||||||
|
d.setTime(d.getTime() + expireDays * 24 * 60 * 60 * 1000);
|
||||||
|
var expires = "expires=" + d.toUTCString();
|
||||||
|
document.cookie = `${cookieName}=${cookieValue};${expires};path=/`;
|
||||||
|
};
|
||||||
|
|
||||||
|
Yao.prototype.DeleteCookie = function (cookieName) {
|
||||||
|
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize To Query String
|
||||||
|
* @param {*} obj
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
Yao.prototype.Serialize = function (obj) {
|
||||||
|
const str: string[] = [];
|
||||||
|
for (const p in obj)
|
||||||
|
if (obj.hasOwnProperty(p)) {
|
||||||
|
str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
|
||||||
|
}
|
||||||
|
return str.join("&");
|
||||||
|
};
|
||||||
|
|
@ -124,6 +124,12 @@ func (tmpl *Template) Build(option *core.BuildOption) ([]string, error) {
|
||||||
return warnings, err
|
return warnings, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add sui lib to the global
|
||||||
|
err = tmpl.UpdateJSSDK(option)
|
||||||
|
if err != nil {
|
||||||
|
return warnings, err
|
||||||
|
}
|
||||||
|
|
||||||
// Execute the build after hook
|
// Execute the build after hook
|
||||||
if option.ExecScripts {
|
if option.ExecScripts {
|
||||||
res := tmpl.ExecAfterBuildScripts()
|
res := tmpl.ExecAfterBuildScripts()
|
||||||
|
|
@ -200,6 +206,44 @@ func (tmpl *Template) SyncAssetFile(file string, option *core.BuildOption) error
|
||||||
return copy(sourceFile, targetFile)
|
return copy(sourceFile, targetFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateJSSDK update the js sdk
|
||||||
|
func (tmpl *Template) UpdateJSSDK(option *core.BuildOption) error {
|
||||||
|
|
||||||
|
jsCode, sourceMap, err := core.LibSUI()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// get source abs path
|
||||||
|
root, err := tmpl.local.DSL.PublicRoot(option.Data)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("SyncAssets: Get the public root error: %s. use %s", err.Error(), tmpl.local.DSL.Public.Root)
|
||||||
|
root = tmpl.local.DSL.Public.Root
|
||||||
|
}
|
||||||
|
|
||||||
|
targetRoot := filepath.Join(application.App.Root(), "public", root, "assets")
|
||||||
|
|
||||||
|
file := filepath.Join(targetRoot, "libsui.min.js")
|
||||||
|
mapFile := filepath.Join(targetRoot, "libsui.min.js.map")
|
||||||
|
|
||||||
|
// create the target directory
|
||||||
|
if exist, _ := os.Stat(targetRoot); exist == nil {
|
||||||
|
os.MkdirAll(targetRoot, os.ModePerm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the js sdk
|
||||||
|
// add source map url
|
||||||
|
jsCode = append(jsCode, []byte("\n//# sourceMappingURL=libsui.min.js.map")...)
|
||||||
|
err = os.WriteFile(file, jsCode, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the source map
|
||||||
|
err = os.WriteFile(mapFile, sourceMap, 0644)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (tmpl *Template) writeGlobalScript(data map[string]interface{}) error {
|
func (tmpl *Template) writeGlobalScript(data map[string]interface{}) error {
|
||||||
file, source, err := tmpl.backendScriptSource("__global.backend")
|
file, source, err := tmpl.backendScriptSource("__global.backend")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,10 @@ func TestPageEditorRender(t *testing.T) {
|
||||||
|
|
||||||
assert.NotEmpty(t, res.HTML)
|
assert.NotEmpty(t, res.HTML)
|
||||||
assert.NotEmpty(t, res.CSS)
|
assert.NotEmpty(t, res.CSS)
|
||||||
assert.NotEmpty(t, res.Scripts)
|
// assert.NotEmpty(t, res.Scripts)
|
||||||
assert.NotEmpty(t, res.Styles)
|
assert.NotEmpty(t, res.Styles)
|
||||||
assert.GreaterOrEqual(t, len(res.Styles), 1)
|
assert.GreaterOrEqual(t, len(res.Styles), 1)
|
||||||
assert.GreaterOrEqual(t, len(res.Scripts), 1)
|
// assert.GreaterOrEqual(t, len(res.Scripts), 1)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue