// // Copyright (c) 2016, Brian Frank and Andy Frank // Licensed under the Academic Free License version 3.0 // // History: // 8 Mar 2016 Andy Frank Creation // using concurrent using dom using graphics ** ** DomListener monitors the DOM and invokes callbacks when modifications occur. ** ** DomListener works by registering a global ** [MutationObserver]`dom::MutationObserver` on the 'body' tag and collects ** all 'childList' events for his subtree. All mutation events are queued and ** processed on a [reqAnimationFrame]`dom::Win.reqAnimationFrame`. Registered ** nodes are held with weak references, and will be garbage collected when out ** of scope. ** @Js class DomListener { static DomListener cur() { r := Actor.locals["domkit.DomListener"] as DomListener if (r == null) Actor.locals["domkit.DomListener"] = r = DomListener() return r } ** Private ctor. private new make() { this.observer = MutationObserver() |recs| { checkMutations.addAll(recs) } this.observer.observe(Win.cur.doc.body, ["childList":true, "subtree":true]) reqCheck } ** Request callback when target node is mounted into document. Void onMount(Elem target, |Elem| f) { DomState state := map.get(target) ?: DomState() state.onMount = f map.set(target, state) } ** Request callback when target node is unmounted from document. Void onUnmount(Elem target, |Elem| f) { DomState state := map.get(target) ?: DomState() state.onUnmount = f map.set(target, state) } ** Request callback when target node size has changed. Void onResize(Elem target, |Elem| f) { DomState state := map.get(target) ?: DomState() state.onResize = f map.set(target, state) } ** Request check callback. private Void reqCheck() { Win.cur.reqAnimationFrame |->| { onCheck } } ** Callback to check elements. private Void onCheck() { try { // throttle checks nowTicks := Duration.nowTicks if (lastTicks != null && nowTicks-lastTicks < checkFreq) return this.lastTicks = nowTicks // debug // start := Duration.now // check mount/unmount checkMutations.each |r,i| { checkState.clear r.added.each |e| { findRegNodes(e, checkState) } checkState.each |e| { DomState s := map[e] s.fireMount(e) mounted[e.hash] = e } checkState.clear r.removed.each |e| { findRegNodes(e, checkState) } checkState.each |e| { DomState s := map[e] s.fireUnmount(e) mounted.remove(e.hash) } } // make sure we cleanup refs checkMutations.clear checkState.clear // check for resize events mounted.each |e| { DomState s := map[e] if (s.onResize != null) { s.newSize = e.size if (s.lastSize == null) s.lastSize = s.newSize if (s.lastSize != s.newSize) s.fireResize(e) s.lastSize = s.newSize } } // debug // dur := Duration.now - start // echo("# DomListener.onCheck [${dur.toMillis}ms]") } catch (Err err) { err.trace } finally { reqCheck } } ** Walk subtree to find all registered nodes. private Void findRegNodes(Elem elem, Elem[] list) { if (map.has(elem)) list.add(elem) elem.children.each |c| { findRegNodes(c, list) } } private Int checkFreq := 1sec.toNanos private Int? lastTicks private MutationObserver observer private WeakMap map := WeakMap() private Int:Elem mounted := [:] private MutationRec[] checkMutations := [,] private Elem[] checkState := [,] } ************************************************************************** ** DomState ************************************************************************** @Js internal class DomState { Func? onMount := null Func? onUnmount := null Func? onResize := null Size? lastSize Size? newSize Void fireMount(Elem elem) { if (mounted) return mounted = true unmounted = false try { onMount?.call(elem) } catch (Err err) { err.trace } } Void fireUnmount(Elem elem) { if (unmounted) return mounted = false unmounted = true try { onUnmount?.call(elem) } catch (Err err) { err.trace } } Void fireResize(Elem elem) { try { onResize?.call(elem) } catch (Err err) { err.trace } } private Bool mounted := false private Bool unmounted := true }