Add fonts, autotable, autotable examples
This commit is contained in:
@@ -18,6 +18,9 @@ type nodeRT struct {
|
||||
dom js.Value
|
||||
jsFuncs map[string]js.Func
|
||||
refs map[string]*handlerRef
|
||||
// portal is the body-level container holding a TagPortal node's children. The
|
||||
// `dom` field is the hidden in-flow placeholder that marks its slot in the tree.
|
||||
portal js.Value
|
||||
}
|
||||
|
||||
type handlerRef struct{ fn func(vdom.Event) }
|
||||
@@ -32,7 +35,8 @@ func rt(n *vdom.VNode) *nodeRT {
|
||||
// clientEvent adapts a DOM event to vdom.Event.
|
||||
type clientEvent struct{ js js.Value }
|
||||
|
||||
func (e clientEvent) PreventDefault() { e.js.Call("preventDefault") }
|
||||
func (e clientEvent) PreventDefault() { e.js.Call("preventDefault") }
|
||||
func (e clientEvent) StopPropagation() { e.js.Call("stopPropagation") }
|
||||
|
||||
func (e clientEvent) Value() string {
|
||||
t := e.js.Get("target")
|
||||
@@ -46,6 +50,55 @@ func (e clientEvent) Value() string {
|
||||
return v.String()
|
||||
}
|
||||
|
||||
func (e clientEvent) Checked() bool {
|
||||
t := e.js.Get("target")
|
||||
if !t.Truthy() {
|
||||
return false
|
||||
}
|
||||
return t.Get("checked").Truthy()
|
||||
}
|
||||
|
||||
func (e clientEvent) Key() string {
|
||||
k := e.js.Get("key")
|
||||
if !k.Truthy() {
|
||||
return ""
|
||||
}
|
||||
return k.String()
|
||||
}
|
||||
|
||||
func (e clientEvent) ClientX() int { return coord(e.js, "clientX") }
|
||||
func (e clientEvent) ClientY() int { return coord(e.js, "clientY") }
|
||||
|
||||
func coord(ev js.Value, prop string) int {
|
||||
v := ev.Get(prop)
|
||||
if v.Type() != js.TypeNumber {
|
||||
return 0
|
||||
}
|
||||
return v.Int()
|
||||
}
|
||||
|
||||
func (e clientEvent) Target() any {
|
||||
t := e.js.Get("target")
|
||||
if !t.Truthy() {
|
||||
return nil
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (e clientEvent) SetData(format, data string) {
|
||||
if dt := e.js.Get("dataTransfer"); dt.Truthy() {
|
||||
dt.Call("setData", format, data)
|
||||
}
|
||||
}
|
||||
|
||||
func (e clientEvent) GetData(format string) string {
|
||||
dt := e.js.Get("dataTransfer")
|
||||
if !dt.Truthy() {
|
||||
return ""
|
||||
}
|
||||
return dt.Call("getData", format).String()
|
||||
}
|
||||
|
||||
func one(n *vdom.VNode) []*vdom.VNode {
|
||||
if n == nil {
|
||||
return nil
|
||||
@@ -55,14 +108,57 @@ func one(n *vdom.VNode) []*vdom.VNode {
|
||||
|
||||
// ---- fresh create + diff ----
|
||||
|
||||
func createDOM(n *vdom.VNode) js.Value {
|
||||
// SVG lives in its own XML namespace, and an element's namespace is fixed at
|
||||
// CREATION — you cannot fix it afterwards with an attribute.
|
||||
//
|
||||
// document.createElement("svg") does NOT make an SVG element; it makes an
|
||||
// HTMLUnknownElement that happens to be spelled "svg". It has no geometry, it
|
||||
// renders nothing, and its children are inert. Everything still *looks* right in the
|
||||
// DOM inspector, which is what makes this so easy to miss.
|
||||
//
|
||||
// The reason it only broke on NAVIGATION: a server-rendered page's icons come from
|
||||
// the HTML parser, which handles <svg> as foreign content and gets the namespace
|
||||
// right, and hydration merely adopts those nodes. Only elements the client CREATES —
|
||||
// i.e. everything rendered after the first paint — went through createElement and
|
||||
// came out inert. Hence: icons fine on load, gone as soon as you navigate.
|
||||
const svgNamespace = "http://www.w3.org/2000/svg"
|
||||
|
||||
// elementNS returns the namespace an element and its subtree belong to. Nested
|
||||
// <svg> switches into the SVG namespace; <foreignObject> switches back to HTML.
|
||||
func elementNS(tag, parentNS string) string {
|
||||
switch tag {
|
||||
case "svg":
|
||||
return svgNamespace
|
||||
case "foreignObject":
|
||||
return "" // HTML content inside SVG
|
||||
}
|
||||
return parentNS
|
||||
}
|
||||
|
||||
func createElement(tag, ns string) js.Value {
|
||||
if ns == "" {
|
||||
return document.Call("createElement", tag)
|
||||
}
|
||||
return document.Call("createElementNS", ns, tag)
|
||||
}
|
||||
|
||||
// createDOM builds a node. ns is the namespace inherited from the parent element:
|
||||
// "" for ordinary HTML, svgNamespace inside an <svg>.
|
||||
func createDOM(n *vdom.VNode, ns string) js.Value {
|
||||
if n.Tag == "" {
|
||||
d := document.Call("createTextNode", n.Text)
|
||||
rt(n).dom = d
|
||||
vdom.SetRefNode(n.Ref, d)
|
||||
return d
|
||||
}
|
||||
el := document.Call("createElement", n.Tag)
|
||||
if n.Tag == vdom.TagPortal {
|
||||
return createPortal(n)
|
||||
}
|
||||
|
||||
ns = elementNS(n.Tag, ns)
|
||||
el := createElement(n.Tag, ns)
|
||||
rt(n).dom = el
|
||||
vdom.SetRefNode(n.Ref, el)
|
||||
for k, v := range n.Attrs {
|
||||
el.Call("setAttribute", k, v)
|
||||
}
|
||||
@@ -77,12 +173,57 @@ func createDOM(n *vdom.VNode) js.Value {
|
||||
return el
|
||||
}
|
||||
for _, c := range n.Children {
|
||||
el.Call("appendChild", createDOM(c))
|
||||
el.Call("appendChild", createDOM(c, ns))
|
||||
}
|
||||
return el
|
||||
}
|
||||
|
||||
// createPortal builds the two halves of a TagPortal node: a hidden placeholder
|
||||
// that holds its slot in the tree (so sibling indexes — and hydration — still line
|
||||
// up), and a container appended to document.body that the children actually render
|
||||
// into. Returning the placeholder is what keeps the caller's appendChild correct.
|
||||
func createPortal(n *vdom.VNode) js.Value {
|
||||
r := rt(n)
|
||||
|
||||
placeholder := document.Call("createElement", "div")
|
||||
placeholder.Call("setAttribute", "data-portal", "")
|
||||
placeholder.Get("style").Call("setProperty", "display", "none")
|
||||
r.dom = placeholder
|
||||
|
||||
r.portal = newPortalContainer()
|
||||
vdom.SetRefNode(n.Ref, r.portal)
|
||||
for _, c := range n.Children {
|
||||
// The container is a plain <div> on document.body, so its children start in
|
||||
// the HTML namespace however deeply the portal was nested.
|
||||
r.portal.Call("appendChild", createDOM(c, ""))
|
||||
}
|
||||
return placeholder
|
||||
}
|
||||
|
||||
func newPortalContainer() js.Value {
|
||||
c := document.Call("createElement", "div")
|
||||
c.Call("setAttribute", "data-portal-container", "")
|
||||
document.Get("body").Call("appendChild", c)
|
||||
return c
|
||||
}
|
||||
|
||||
// nsOf reads an existing element's namespace, so anything created beneath it
|
||||
// inherits the right one. Derived from the live DOM rather than threaded through the
|
||||
// call stack: the parent already knows the answer, and it cannot go stale.
|
||||
func nsOf(el js.Value) string {
|
||||
if !el.Truthy() {
|
||||
return ""
|
||||
}
|
||||
uri := el.Get("namespaceURI")
|
||||
if uri.Truthy() && uri.String() == svgNamespace {
|
||||
return svgNamespace
|
||||
}
|
||||
return "" // HTML (or a text node / detached node)
|
||||
}
|
||||
|
||||
// patchChildren diffs a child list against `parent`'s children.
|
||||
func patchChildren(parent js.Value, old, next []*vdom.VNode) {
|
||||
ns := nsOf(parent) // read once: every child of this parent shares it
|
||||
n := max(len(old), len(next))
|
||||
for i := range n {
|
||||
var o, x *vdom.VNode
|
||||
@@ -92,21 +233,21 @@ func patchChildren(parent js.Value, old, next []*vdom.VNode) {
|
||||
if i < len(next) {
|
||||
x = next[i]
|
||||
}
|
||||
patch(parent, o, x)
|
||||
patch(parent, o, x, ns)
|
||||
}
|
||||
}
|
||||
|
||||
func patch(parent js.Value, o, x *vdom.VNode) {
|
||||
func patch(parent js.Value, o, x *vdom.VNode, ns string) {
|
||||
switch {
|
||||
case o == nil && x == nil:
|
||||
return
|
||||
case o == nil:
|
||||
parent.Call("appendChild", createDOM(x))
|
||||
parent.Call("appendChild", createDOM(x, ns))
|
||||
case x == nil:
|
||||
parent.Call("removeChild", rt(o).dom)
|
||||
release(o)
|
||||
case o.Tag != x.Tag:
|
||||
parent.Call("replaceChild", createDOM(x), rt(o).dom)
|
||||
parent.Call("replaceChild", createDOM(x, ns), rt(o).dom)
|
||||
release(o)
|
||||
default:
|
||||
x.Runtime = o.Runtime // adopt dom + listeners
|
||||
@@ -115,9 +256,18 @@ func patch(parent js.Value, o, x *vdom.VNode) {
|
||||
// o was a hydration hole (no adopted DOM node, e.g. a server/client
|
||||
// markup mismatch). Recreate this node fresh instead of calling into an
|
||||
// undefined DOM handle.
|
||||
parent.Call("appendChild", createDOM(x))
|
||||
parent.Call("appendChild", createDOM(x, ns))
|
||||
return
|
||||
}
|
||||
if x.Tag == vdom.TagPortal {
|
||||
// The placeholder stays put; the children diff against the body-level
|
||||
// container, not against `parent`.
|
||||
vdom.SetRefNode(x.Ref, rt(x).portal)
|
||||
patchChildren(rt(x).portal, o.Children, x.Children)
|
||||
return
|
||||
}
|
||||
// x is a fresh VNode each render, so re-point its ref at the adopted node.
|
||||
vdom.SetRefNode(x.Ref, dom)
|
||||
if x.Tag == "" {
|
||||
if x.Text != o.Text {
|
||||
dom.Set("nodeValue", x.Text)
|
||||
@@ -209,15 +359,43 @@ func addListener(n *vdom.VNode, name string, handler func(vdom.Event)) {
|
||||
|
||||
func release(n *vdom.VNode) {
|
||||
if n.Runtime != nil {
|
||||
for _, fn := range rt(n).jsFuncs {
|
||||
r := rt(n)
|
||||
for _, fn := range r.jsFuncs {
|
||||
fn.Release()
|
||||
}
|
||||
// A portal's children live at body level, so removing the placeholder from
|
||||
// the tree does not remove them — the container has to go explicitly, or the
|
||||
// panel outlives the component that opened it.
|
||||
if r.portal.Truthy() {
|
||||
if p := r.portal.Get("parentNode"); p.Truthy() {
|
||||
p.Call("removeChild", r.portal)
|
||||
}
|
||||
}
|
||||
detachRef(n, r)
|
||||
}
|
||||
for _, c := range n.Children {
|
||||
release(c)
|
||||
}
|
||||
}
|
||||
|
||||
// detachRef nils out the node's Ref — but only if the ref still points at the node
|
||||
// being released. On a tag change the reconciler creates the replacement *before*
|
||||
// releasing the old node (replaceChild needs both), and a component holds one Ref
|
||||
// across renders, so an unconditional detach here would nil the ref that createDOM
|
||||
// had just pointed at the new element.
|
||||
func detachRef(n *vdom.VNode, r *nodeRT) {
|
||||
if n.Ref == nil {
|
||||
return
|
||||
}
|
||||
cur, ok := n.Ref.Node().(js.Value)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if cur.Equal(r.dom) || (r.portal.Truthy() && cur.Equal(r.portal)) {
|
||||
vdom.SetRefNode(n.Ref, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- hydration: adopt server-rendered DOM instead of creating it ----
|
||||
|
||||
func hydrateNode(dom js.Value, n *vdom.VNode) {
|
||||
@@ -225,6 +403,18 @@ func hydrateNode(dom js.Value, n *vdom.VNode) {
|
||||
return // structural mismatch; leave a hole (a later re-render will fix)
|
||||
}
|
||||
rt(n).dom = dom
|
||||
vdom.SetRefNode(n.Ref, dom)
|
||||
if n.Tag == vdom.TagPortal {
|
||||
// SSR emitted the placeholder and nothing else (the server does not own
|
||||
// document.body), so adopt the placeholder but build the children fresh.
|
||||
r := rt(n)
|
||||
r.portal = newPortalContainer()
|
||||
vdom.SetRefNode(n.Ref, r.portal)
|
||||
for _, c := range n.Children {
|
||||
r.portal.Call("appendChild", createDOM(c, ""))
|
||||
}
|
||||
return
|
||||
}
|
||||
if n.Tag == "" {
|
||||
if dom.Get("nodeValue").String() != n.Text {
|
||||
dom.Set("nodeValue", n.Text)
|
||||
@@ -234,6 +424,22 @@ func hydrateNode(dom js.Value, n *vdom.VNode) {
|
||||
for name, h := range n.Events {
|
||||
addListener(n, name, h)
|
||||
}
|
||||
|
||||
// Reconcile attributes against the client's tree. Hydration ADOPTS the server's
|
||||
// DOM, so without this any disagreement between the server's HTML and the client's
|
||||
// render is baked in permanently: the DOM keeps the server's value, and no later
|
||||
// re-render fixes it either, because updateAttrs compares one client VNode against
|
||||
// the next — both of which agree with each other and disagree with the DOM.
|
||||
//
|
||||
// The client is the source of truth once it is running. It is also the only one of
|
||||
// the two that can be out of date in the other direction (a stale server binary,
|
||||
// an SSR cache), and a silently-wrong class is far worse than a redundant
|
||||
// setAttribute on first paint.
|
||||
for k, v := range n.Attrs {
|
||||
if dom.Call("getAttribute", k).String() != v {
|
||||
dom.Call("setAttribute", k, v)
|
||||
}
|
||||
}
|
||||
for k, v := range n.Props {
|
||||
dom.Set(k, v)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user