diff --git a/README b/README
new file mode 100644
index 0000000..5db4817
--- /dev/null
+++ b/README
@@ -0,0 +1,11 @@
+# webfui
+
+Client Side Web Development Framework For ClojureScript
+
+## Usage
+
+Documentation is still pending (expect it by November 2012). At this time, your best guide is to run the code in the "webfui-examples" directory- Just run "lein deps;lein run" and fire up your Safari, Chrome, or iOS browser to "localhost:8080"
+
+## License
+
+Distributed under the Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php), the same as Clojure.
diff --git a/info.txt b/info.txt
new file mode 100644
index 0000000..25d1608
--- /dev/null
+++ b/info.txt
@@ -0,0 +1,66 @@
+*presentation notes
+
+What is the DOM?
+
+Problems with the DOM
+ - It changes
+ - Changing it is slow
+
+What is EDN?
+
+What is AJAX?
+
+How to build a web framework:
+ - Should do things the Clojure Way
+ - Code should be in the functional style
+ - The browser DOM should be EDN data stored in an atom
+
+DOM is not a complete representation of browser state
+ - Can't set button as pushed
+ - Can't set scrollbar position
+ - Can't set text selection
+
+Problem: AJAX requires side effects. Reframed problem: Our state atom no longer contains the whole world
+
+Original sin: Our state is _always_ corrupt in a program that uses AJAX.
+
+We have to contain the corruption (Can be done with FP)
+We have to repair the corruption (Requires imperative programming)
+
+"State Corruption" Design Pattern
+
+AJAX=Corruption of state
+
+Corruption is implicit- Make it explicit
+
+GET: State item has value of nil
+ or selection variable has id that points to nonexistent item
+
+POST: Create new item in state with id=nil
+
+PUT: Have "holding area" in state, set source of truth=nil. Move from holding area back to final area
+
+DELETE: Just delete item
+
+*todos
+item insert/deletion in delta system
+text selection
+full IK artwork
+"preview" in calculator
+
+*Hello World
+(ns my-app.core
+ (:use [webfui.framework :only [launch-app]]))
+
+(defn render-all []
+ "hello world!")
+
+(launch-app (atom nil) render-all)
+
+
+
+*timing
+~250 for mouseup
+~150 for webfui code
+~90 for parsing html
+~90 for delta detection
diff --git a/lib/clojure-1.3.0.jar b/lib/clojure-1.3.0.jar
new file mode 100644
index 0000000..370ef32
Binary files /dev/null and b/lib/clojure-1.3.0.jar differ
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..d0921d0
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,40 @@
+
+
+ 4.0.0
+ webfui
+ webfui
+ 0.2
+ webfui
+ Alpha release of Webfui- All Webfui source included in this repository release under Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
+ http://lisperati.com
+
+ src
+ test
+
+
+ resources
+
+
+
+
+ test-resources
+
+
+
+
+
+ central
+ http://repo1.maven.org/maven2
+
+
+ clojars
+ http://clojars.org/repo/
+
+
+
+
+
diff --git a/project.clj b/project.clj
new file mode 100644
index 0000000..cfda7ee
--- /dev/null
+++ b/project.clj
@@ -0,0 +1,3 @@
+(defproject webfui "0.2"
+ :description "Alpha release of Webfui- All Webfui source included in this repository release under Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)"
+ :url "http://lisperati.com")
\ No newline at end of file
diff --git a/src/webfui/core.cljs b/src/webfui/core.cljs
new file mode 100644
index 0000000..6ea09a6
--- /dev/null
+++ b/src/webfui/core.cljs
@@ -0,0 +1,63 @@
+(ns webfui.core
+ (:use [webfui.html :only [html parse-html parsed-html render-attribute-value html-delta]]
+ [webfui.plugin.core :only [active-plugins Plugin declare-events fix-dom]]
+ [cljs.reader :only [read-string]]))
+
+;; This file contains the core engine for webfui. You usually don't want to load this file directly, instead load webfui.dom or webfui.framework.
+
+(defn body []
+ (.-body js/document))
+
+(defn select-path-dom [node path]
+ (if-let [[cur & more] (seq path)]
+ (recur (.item (.-childNodes node) cur) more)
+ node))
+
+(defn dom-ready? []
+ (= (.-readyState js/document) "complete"))
+
+(defn dom-ready [fun]
+ (set! (.-onload js/window) fun))
+
+(defn parsed-html-watcher [key a old new]
+ (let [delta (html-delta (.-children old) (.-children new) [])]
+ (doseq [[typ path a b] delta]
+ (let [node (select-path-dom (body) path)]
+ (case typ
+ :att (if (= a :value)
+ (set! (.-value node) (str b))
+ (.setAttribute node (name a) (render-attribute-value a b)))
+ :rem-att (.removeAttribute node (name a))
+ :html (set! (.-innerHTML node) (apply str (map html a))))))
+ (doseq [plugin @active-plugins]
+ (fix-dom plugin))))
+
+(def parsed-html-atom (atom (parsed-html. :body {} nil)))
+
+(defn update-parsed-html-atom [new old]
+ (parsed-html. :body
+ {}
+ (if (or (seq? new) (list? new))
+ (parse-html new)
+ (parse-html (list new)))))
+
+(defn html-watcher [key a old new]
+ (swap! parsed-html-atom (partial update-parsed-html-atom new)))
+
+(def dom-watchers (atom {}))
+
+(defn core-add-dom-watch [id fun]
+ (swap! dom-watchers assoc id fun))
+
+(defn init-dom [html]
+ (let [b (body)]
+ (doseq [plugin @active-plugins]
+ (declare-events plugin (body) dom-watchers parsed-html-atom))
+ (add-watch html :dom-watcher html-watcher)
+ (add-watch parsed-html-atom :parsed-html-watcher parsed-html-watcher)
+ (swap! html identity)))
+
+(defn core-defdom [clj-dom]
+ (if (dom-ready?)
+ (init-dom clj-dom)
+ (dom-ready (partial init-dom clj-dom))))
diff --git a/src/webfui/dom.cljs b/src/webfui/dom.cljs
new file mode 100644
index 0000000..8b44e86
--- /dev/null
+++ b/src/webfui/dom.cljs
@@ -0,0 +1,11 @@
+(ns webfui.dom
+ (:use [webfui.core :only [core-add-dom-watch core-defdom]]
+ [webfui.plugin.core :only [register-plugin]]
+ [webfui.plugin.form-values :only [form-values]]))
+
+(def add-dom-watch core-add-dom-watch)
+
+(def defdom core-defdom)
+
+(register-plugin (form-values.))
+
diff --git a/src/webfui/dom_manipulation.cljs b/src/webfui/dom_manipulation.cljs
new file mode 100644
index 0000000..615c8d9
--- /dev/null
+++ b/src/webfui/dom_manipulation.cljs
@@ -0,0 +1,23 @@
+(ns webfui.dom-manipulation
+ (:use [webfui.html :only [unparse-html]]))
+
+;; This file is for DOM manipulation functions used by both the core of webfui as well as plugins.
+
+(defn path-dom [node]
+ (drop 3
+ (reverse ((fn f [node]
+ (lazy-seq (when node
+ (cons (dec (count (take-while identity (iterate #(.-previousSibling %) node)))) (f (.-parentNode node))))))
+ node))))
+
+(defn select-path-html [html path]
+ (if-let [[cur & more] (seq path)]
+ (recur (nth (.-children html) cur) more)
+ html))
+
+(defn resolve-target [parsed-html target]
+ (let [path (path-dom target)
+ parsed-element (select-path-html parsed-html path)]
+ (unparse-html parsed-element)))
+
+
diff --git a/src/webfui/framework.cljs b/src/webfui/framework.cljs
new file mode 100644
index 0000000..861960b
--- /dev/null
+++ b/src/webfui/framework.cljs
@@ -0,0 +1,51 @@
+(ns webfui.framework
+ (:use [webfui.dom :only [defdom add-dom-watch]]
+ [webfui.plugin.core :only [register-plugin]]
+ [webfui.plugin.mouse :only [mouse add-mouse-watch]]
+ [webfui.state-patches :only [patch]]
+ [webfui.utilities :only [get-attribute]]))
+
+(register-plugin (mouse.))
+
+(defn state-watcher [dom renderer key state old new]
+ (swap! dom (constantly (renderer new))))
+
+(declare cur-state)
+
+(defn launch-app [state renderer]
+ (let [dom (atom (renderer @state))]
+ (defdom dom)
+ (add-watch state :state-watcher (partial state-watcher dom renderer)))
+ (def cur-state state))
+
+(defn add-dom-watch-helper [id fun]
+ (add-dom-watch id
+ (fn [_ element-new]
+ (swap! cur-state
+ (fn [state]
+ (let [diff (fun state element-new)]
+ (patch state diff)))))))
+
+(def mouse-down-state (atom nil))
+
+(defn add-mouse-watch-helper [id fun optimization]
+ (add-mouse-watch id
+ (fn [element-old element-new points]
+ (swap! mouse-down-state
+ (fn [old]
+ (or old @cur-state)))
+ (reset! cur-state
+ (if (= optimization :incremental)
+ (let [mds @mouse-down-state
+ diff (fun mds element-old element-new (subvec points (max 0 (- (count points) 2))))
+ new-state (patch mds diff)]
+ (reset! mouse-down-state
+ (when (get-attribute element-old :active)
+ new-state))
+ new-state)
+ (let [mds @mouse-down-state
+ diff (fun mds element-old element-new points)]
+ (when-not (get-attribute element-old :active)
+ (reset! mouse-down-state nil))
+ (patch mds diff)))))))
+
diff --git a/src/webfui/framework/macros.clj b/src/webfui/framework/macros.clj
new file mode 100644
index 0000000..c4aa914
--- /dev/null
+++ b/src/webfui/framework/macros.clj
@@ -0,0 +1,11 @@
+;*CLJSBUILD-MACRO-FILE*;
+
+(ns webfui.framework.macros)
+
+(defmacro add-dom-watch [id vars & more]
+ `(webfui.framework/add-dom-watch-helper ~id (fn ~vars ~@more)))
+
+(defmacro add-mouse-watch [id vars & more]
+ (if (vector? id)
+ `(webfui.framework/add-mouse-watch-helper ~(first id) (fn ~vars ~@more) ~(second id))
+ `(webfui.framework/add-mouse-watch-helper ~id (fn ~vars ~@more) :full)))
\ No newline at end of file
diff --git a/src/webfui/html.cljs b/src/webfui/html.cljs
new file mode 100644
index 0000000..1f523c3
--- /dev/null
+++ b/src/webfui/html.cljs
@@ -0,0 +1,145 @@
+(ns webfui.html
+ (:use [clojure.set :only [union]]))
+
+(deftype parsed-tagname [tagname id classes])
+(deftype parsed-html [tagname attributes children])
+
+(defn parse-tagname [tagname]
+ (let [[_ name _ id classes] (re-matches #"^([^.^#]+)(#([^.]+))?(\..+)?" tagname)]
+ (parsed-tagname. (keyword name)
+ (when id
+ (keyword id))
+ (when classes
+ (map second (re-seq #"\.([^.]+)" classes))))))
+
+(let [cache (atom {})]
+ (defn parse-tagname-memoized [tagname]
+ (or (@cache tagname)
+ (let [val (parse-tagname tagname)]
+ (swap! cache assoc tagname val)
+ val))))
+
+(declare parse-html)
+
+(defn parse-element [element]
+ (let [[tagname & more] element
+ parsed (parse-tagname-memoized tagname)
+ classes (.-classes parsed)
+ id (.-id parsed)
+ tagname (.-tagname parsed)
+ attributes {}
+ attributes (if classes
+ (assoc attributes
+ :class
+ (apply str (interpose \ classes)))
+ attributes)
+ attributes (if id
+ (assoc attributes :id id)
+ attributes)
+ [a & b] more
+ [attributes children] (if (map? a)
+ [(merge attributes a) b]
+ [attributes more])]
+ (parsed-html. tagname attributes (parse-html children))))
+
+(defn merge-strings [lst]
+ (if-let [[x & more] lst]
+ (if-let [[y & more] more]
+ (if (and (string? x) (string? y))
+ (merge-strings (cons (str x y) more))
+ (cons x (merge-strings (cons y more))))
+ lst)
+ lst))
+
+(defn parse-html [html]
+ (mapcat (fn [x]
+ (cond (vector? x) [(parse-element x)]
+ (and (coll? x) (not (string? x))) (parse-html x)
+ :else [x]))
+ (merge-strings html)))
+
+(defn tag [tagname atts s]
+ (if (#{:br} tagname)
+ (str "<" (name tagname) atts ">")
+ (str "<" (name tagname) atts ">" (apply str s) "" (name tagname) ">")))
+
+(defn pixels [k]
+ (str (.toFixed k 3) "px"))
+
+(defn render-css [css]
+ (apply str
+ (interpose \;
+ (for [[k v] css]
+ (str (name k)
+ ":"
+ (cond (keyword? v) (name v)
+ (and (number? v) (#{:line-height :top :bottom :left :right :width :height} k)) (pixels v)
+ :else v))))))
+
+(defn render-attribute-value [key value]
+ (cond (keyword? value) (name value)
+ (= key :data) (print-str value)
+ (= key :style) (render-css value)
+ :else value))
+
+(defn render-attributes [atts]
+ (apply str
+ (for [[key value] atts]
+ (str " "
+ (name key)
+ "=\""
+ (render-attribute-value key value)
+ \"))))
+
+(defn html [content]
+ (cond (instance? parsed-html content) (let [tagname (.-tagname content)
+ attributes (.-attributes content)
+ children (.-children content)]
+ (if tagname
+ (tag tagname (when attributes
+ (render-attributes attributes))
+ (map html children))
+ ""))
+ :else (str content)))
+
+(defn html-delta [old-html new-html path]
+ (if (= (count old-html) (count new-html))
+ (let [pairs (map vector old-html new-html)
+ fixable (every? (fn [[old-child new-child]]
+ (if (and (instance? parsed-html old-child) (instance? parsed-html new-child))
+ (= (.-tagname old-child) (.-tagname new-child))
+ (= old-child new-child)))
+ pairs)]
+ (if fixable
+ (apply concat
+ (map-indexed (fn [i [old-element new-element]]
+ (when (instance? parsed-html old-element)
+ (let [old-tagname (.-tagname old-element)
+ old-attributes (.-attributes old-element)
+ old-children (.-children old-element)
+ new-tagname (.-tagname new-element)
+ new-attributes (.-attributes new-element)
+ new-children (.-children new-element)
+ path (conj path i)
+ att-delta (when (not= old-attributes new-attributes)
+ (mapcat (fn [key]
+ (let [old-val (old-attributes key)
+ new-val (new-attributes key)]
+ (cond (not new-val) [[:rem-att path key]]
+ (not= old-val new-val) [[:att path key new-val]]
+ :else [])))
+ (union (set (keys old-attributes)) (set (keys new-attributes)))))
+ child-delta (html-delta old-children new-children path)]
+ (concat att-delta child-delta))))
+ pairs))
+ [[:html path new-html]]))
+ [[:html path new-html]]))
+
+(defn unparse-html [html]
+ (if (or (string? html) (number? html))
+ html
+ (let [tagname (.-tagname html)
+ attributes (.-attributes html)
+ children (.-children html)]
+ (vec (concat [tagname attributes] (map unparse-html children))))))
+
diff --git a/src/webfui/macros.clj b/src/webfui/macros.clj
new file mode 100644
index 0000000..8779a0e
--- /dev/null
+++ b/src/webfui/macros.clj
@@ -0,0 +1,15 @@
+;*CLJSBUILD-MACRO-FILE*;
+
+(ns webfui.macros)
+
+(defmacro dbg [cur & more]
+ `(do (.log js/console (print-str '~cur "-->"))
+ (let [x# (~cur ~@more)]
+ (.log js/console (print-str '~cur "-->" (pr-str x#)))
+ x#)))
+
+(defmacro dbgv [& vars]
+ `(do ~@(map (fn [var]
+ `(do (.log js/console (print-str '~var "==>" (pr-str ~var)))
+ ~var))
+ vars)))
\ No newline at end of file
diff --git a/src/webfui/plugin/core.cljs b/src/webfui/plugin/core.cljs
new file mode 100644
index 0000000..9b797e9
--- /dev/null
+++ b/src/webfui/plugin/core.cljs
@@ -0,0 +1,10 @@
+(ns webfui.plugin.core)
+
+(def active-plugins (atom []))
+
+(defprotocol Plugin
+ (declare-events [this body dom-watchers parsed-html])
+ (fix-dom [this]))
+
+(defn register-plugin [plugin]
+ (swap! active-plugins conj plugin))
\ No newline at end of file
diff --git a/src/webfui/plugin/form_values.cljs b/src/webfui/plugin/form_values.cljs
new file mode 100644
index 0000000..e2fe523
--- /dev/null
+++ b/src/webfui/plugin/form_values.cljs
@@ -0,0 +1,25 @@
+(ns webfui.plugin.form-values
+ (:use [webfui.plugin.core :only [Plugin]]
+ [webfui.dom-manipulation :only [select-path-html parsed-html unparse-html path-dom resolve-target]]))
+
+(defn input [dom-watchers parsed-html event]
+ (let [target (.-target event)
+ [tagname attributes :as element] (resolve-target @parsed-html target)
+ event (@dom-watchers (keyword (:watch attributes)))]
+ (when (and event (contains? #{:input :textarea} tagname))
+ (let [value (.-value target)
+ new-element (update-in element
+ [1 :value]
+ (fn [old]
+ (set! (.-value target) old)
+ value))]
+ (event element new-element)))))
+
+(deftype form-values []
+ Plugin
+ (declare-events [this body dom-watchers parsed-html]
+ (.addEventListener body "input" (partial input dom-watchers parsed-html)))
+ (fix-dom [this]
+ nil))
+
+
diff --git a/src/webfui/plugin/mouse.cljs b/src/webfui/plugin/mouse.cljs
new file mode 100644
index 0000000..c3f5e79
--- /dev/null
+++ b/src/webfui/plugin/mouse.cljs
@@ -0,0 +1,121 @@
+(ns webfui.plugin.mouse
+ (:use [webfui.plugin.core :only [Plugin]]
+ [webfui.utilities :only [body]]
+ [webfui.dom-manipulation :only [resolve-target]]
+ [cljs.reader :only [read-string]]))
+
+(def mouse-watchers (atom {}))
+
+(declare mouse-down-element)
+
+(defn nodelist-to-seq [nl]
+ (let [result-seq (map #(.item nl %) (range (.-length nl)))]
+ result-seq))
+
+(defn offset [node]
+ (let [op (.-offsetParent node)]
+ (if op
+ (let [[x y] (offset op)]
+ [(+ x (.-offsetLeft node)) (+ y (.-offsetTop node))])
+ [0 0])))
+
+(defn all-elements-at-point [client-point]
+ ((fn f [element]
+ (let [[x y] client-point
+ chi (mapcat f (nodelist-to-seq (.-childNodes element)))]
+ (if (and (.-getBoundingClientRect element)
+ (let [rect (.getBoundingClientRect element)]
+ (and (<= (.-left rect) x) (<= (.-top rect) y) (> (.-right rect) x) (> (.-bottom rect) y))))
+ (cons element chi)
+ chi)))
+ (body)))
+
+(defn merge-data [acc lst]
+ (if-let [[k & more] lst]
+ (cond (not k) (recur acc more)
+ (not acc) (recur k more)
+ (and (map? k) (map? acc)) (recur (merge acc k) more)
+ :else (recur k more))
+ acc))
+
+(defn mouse-element [parsed-html ev]
+ (let [target (.-target ev)
+ typ (.-type ev)
+ point (cond (#{"touchstart"} typ) (let [touch (.item (.-touches ev) 0)]
+ [(.-clientX touch) (.-clientY touch)])
+ (#{"touchmove"} typ) (let [touch (.item (.-touches ev) 0)]
+ [(.-clientX touch) (.-clientY touch)])
+ (= "touchend" typ) (let [touch (.item (.-changedTouches ev) 0)]
+ [(.-clientX touch) (.-clientY touch)])
+ :else [(.-clientX ev) (.-clientY ev)])
+ elements (all-elements-at-point point)
+ data (merge-data nil
+ (for [element elements]
+ (when-let [s (.getAttribute element "data")]
+ (read-string s))))]
+ [(update-in (resolve-target @parsed-html target) [1] assoc :offset (offset target) :data data) [(.-pageX ev) (.-pageY ev)]]))
+
+(defn update-offset [element target]
+ (update-in element
+ [1]
+ (fn [attr]
+ (assoc attr :offset (offset target)
+ :data (if-let [data (.getAttribute target "data")]
+ (read-string data)
+ (:data attr))))))
+
+(defn mouse-event [element]
+ (@mouse-watchers (get-in element [1 :mouse])))
+
+(defn add-mouse-watch [id fun]
+ (swap! mouse-watchers assoc id fun))
+
+(defn mouse-down [parsed-html ev]
+ (.preventDefault ev)
+ (let [target (.-target ev)
+ [new-element point] (mouse-element parsed-html ev)
+ event (mouse-event new-element)]
+ (when event
+ (let [new-element (assoc-in new-element [1 :active] true)]
+ (def mouse-down-element new-element)
+ (def mouse-down-target target)
+ (def points [point])
+ (event new-element new-element points)))))
+
+(defn mouse-move [parsed-html ev]
+ (.preventDefault ev)
+ (when mouse-down-element
+ (let [target (.-target ev)
+ [new-element point] (mouse-element parsed-html ev)
+ event (mouse-event mouse-down-element)]
+ (def points (conj points point))
+ (event (update-offset mouse-down-element mouse-down-target) new-element points))))
+
+(defn mouse-up [parsed-html ev]
+ (let [target (.-target ev)
+ [new-element point] (mouse-element parsed-html ev)]
+ (when mouse-down-element
+ (let [event (mouse-event mouse-down-element)
+ first-element (update-in mouse-down-element [1] #(dissoc % :active))]
+ (event (update-offset first-element mouse-down-target) new-element points)
+ (def points nil)
+ (def mouse-down-element nil)
+ (def mouse-down-target nil)))))
+
+(deftype mouse []
+ Plugin
+ (declare-events [this body dom-watchers parsed-html]
+ (.setTimeout js/window
+ (fn []
+ (.scrollTo js/window 0 1))
+ 100)
+ (.addEventListener body "mousedown" (partial mouse-down parsed-html))
+ (.addEventListener body "mousemove" (partial mouse-move parsed-html))
+ (.addEventListener body "mouseup" (partial mouse-up parsed-html))
+ (.addEventListener js/window "touchstart" (partial mouse-down parsed-html))
+ (.addEventListener js/window "touchmove" (partial mouse-move parsed-html))
+ (.addEventListener js/window "touchend" (partial mouse-up parsed-html)))
+ (fix-dom [this]
+ nil))
+
+
diff --git a/src/webfui/plugin/scrolling.cljs b/src/webfui/plugin/scrolling.cljs
new file mode 100644
index 0000000..93eb322
--- /dev/null
+++ b/src/webfui/plugin/scrolling.cljs
@@ -0,0 +1,41 @@
+(ns webfui.plugin.scrolling
+ (:use [webfui.plugin.core :only [Plugin]]
+ [webfui.dom-manipulation :only [select-path-html parsed-html unparse-html path-dom resolve-target]]))
+
+(def dom-watchers-atom (atom nil))
+(def parsed-html-atom (atom nil))
+
+(defn scroll [event]
+ (let [target (.-target event)
+ [tagname attributes :as element] (resolve-target @@parsed-html-atom target)
+ scroll-top (.-scrollTop target)
+ scroll-left (.-scrollLeft target)
+ event (@@dom-watchers-atom (keyword (:watch attributes)))
+ new-element (update-in element [1] assoc :scroll-top scroll-top :scroll-left scroll-left)]
+ (event element new-element)))
+
+(defn check-for-scroll [event]
+ (let [target (.-target event)]
+ (.addEventListener target "scroll" scroll)))
+
+(defn xpath [s]
+ (let [res (.evaluate js/document s js/document nil (.-ORDERED_NODE_ITERATOR_TYPE js/XPathResult) nil)]
+ (doall (take-while identity (repeatedly (fn [] (.iterateNext res)))))))
+
+(deftype scrolling []
+ Plugin
+ (declare-events [this body dom-watchers parsed-html]
+ (reset! dom-watchers-atom dom-watchers)
+ (reset! parsed-html-atom parsed-html)
+ (.addEventListener body "mousedown" check-for-scroll)
+ (.addEventListener body "keydown" check-for-scroll))
+
+ (fix-dom [this]
+ (doseq [element (xpath "//*[@scroll-top]")]
+ (let [scroll-top (js/parseInt (.getAttribute element "scroll-top"))]
+ (when (> (js/Math.abs (- (.-scrollTop element) scroll-top)) 1)
+ (set! (.-scrollTop element) scroll-top)
+ (.addEventListener element "scroll" scroll)
+ (.removeAttribute element "scroll-top"))))))
+
+
diff --git a/src/webfui/state_patches.cljs b/src/webfui/state_patches.cljs
new file mode 100644
index 0000000..0ae97aa
--- /dev/null
+++ b/src/webfui/state_patches.cljs
@@ -0,0 +1,22 @@
+(ns webfui.state-patches
+ (:use [clojure.set :only [union]]))
+
+(defn patch [state diff]
+ (if diff
+ (cond (map? state) (into {}
+ (for [key (union (set (keys state)) (set (keys diff)))]
+ [key (let [val1 (state key)
+ val2 (diff key)]
+ (cond (and val1 val2) (patch val1 val2)
+ (contains? diff key) val2
+ :else val1))]))
+ (vector? state) (if (map? diff)
+ (vec (map-indexed (fn [index item]
+ (if-let [d (diff index)]
+ (patch item d)
+ item))
+ state))
+ diff)
+ :else diff)
+ state))
+
diff --git a/src/webfui/utilities.cljs b/src/webfui/utilities.cljs
new file mode 100644
index 0000000..76ec1cd
--- /dev/null
+++ b/src/webfui/utilities.cljs
@@ -0,0 +1,11 @@
+(ns webfui.utilities)
+
+(defn body []
+ (.-body js/document))
+
+(defn get-attribute [element key]
+ (get-in element [1 key]))
+
+(defn clicked [first-element last-element]
+ (and (= first-element last-element) (not (get-attribute first-element :active))))
+
diff --git a/test/webfui/test/core.clj b/test/webfui/test/core.clj
new file mode 100644
index 0000000..4f45a9e
--- /dev/null
+++ b/test/webfui/test/core.clj
@@ -0,0 +1,6 @@
+(ns webfui.test.core
+ (:use [webfui.core])
+ (:use [clojure.test]))
+
+(deftest replace-me ;; FIXME: write
+ (is false "No tests have been written."))
diff --git a/webfui-examples/.gitignore b/webfui-examples/.gitignore
new file mode 100644
index 0000000..b8c1b21
--- /dev/null
+++ b/webfui-examples/.gitignore
@@ -0,0 +1,5 @@
+pom.xml
+*jar
+/lib/
+/classes/
+.lein-deps-sum
diff --git a/webfui-examples/.lein-plugins/checksum b/webfui-examples/.lein-plugins/checksum
new file mode 100644
index 0000000..099bda8
--- /dev/null
+++ b/webfui-examples/.lein-plugins/checksum
@@ -0,0 +1 @@
+b0bea0f2cf6c59e422426393bfdfae292119cbec
\ No newline at end of file
diff --git a/webfui-examples/README.md b/webfui-examples/README.md
new file mode 100644
index 0000000..73745b9
--- /dev/null
+++ b/webfui-examples/README.md
@@ -0,0 +1,17 @@
+# webfui-examples
+
+A website written in noir.
+
+## Usage
+
+```bash
+lein deps
+lein run
+```
+
+## License
+
+Copyright (C) 2011 FIXME
+
+Distributed under the Eclipse Public License, the same as Clojure.
+
diff --git a/webfui-examples/project.clj b/webfui-examples/project.clj
new file mode 100644
index 0000000..29b05b5
--- /dev/null
+++ b/webfui-examples/project.clj
@@ -0,0 +1,42 @@
+(defproject webfui-examples "0.2"
+ :description "Examples for Webfui"
+ :source-path "src-clj"
+ :dependencies [[org.clojure/clojure "1.4.0"]
+ [webfui "0.2"]
+ [noir "1.3.0-beta10"]
+ [hiccup "1.0.1"]]
+ :main webfui-examples.server
+ :plugins [[lein-cljsbuild "0.2.7"]]
+ :cljsbuild {:builds {:add_two_numbers {:source-path "src-cljs/add_two_numbers"
+ :compiler {:output-to "resources/public/js/add_two_numbers.js"
+ :optimizations :whitespace
+ :pretty-print true}}
+ :add_two_numbers_low_level {:source-path "src-cljs/add_two_numbers_low_level"
+ :compiler {:output-to "resources/public/js/add_two_numbers_low_level.js"
+ :optimizations :whitespace
+ :pretty-print true}}
+ :calculator {:source-path "src-cljs/calculator"
+ :compiler {:output-to "resources/public/js/calculator.js"
+ :optimizations :whitespace
+ :pretty-print true}}
+ :calculator_many {:source-path "src-cljs/calculator_many"
+ :compiler {:output-to "resources/public/js/calculator_many.js"
+ :optimizations :advanced
+ :pretty-print false}}
+ :calculator_ajax {:source-path "src-cljs/calculator_ajax"
+ :compiler {:output-to "resources/public/js/calculator_ajax.js"
+ :optimizations :whitespace
+ :pretty-print true}}
+ :scrolling {:source-path "src-cljs/scrolling"
+ :compiler {:output-to "resources/public/js/scrolling.js"
+ :optimizations :whitespace
+ :pretty-print true}}
+ :mouse_tracking {:source-path "src-cljs/mouse_tracking"
+ :compiler {:output-to "resources/public/js/mouse_tracking.js"
+ :optimizations :whitespace
+ :pretty-print true}}
+ :inverse_kinematics {:source-path "src-cljs/inverse_kinematics"
+ :compiler {:output-to "resources/public/js/inverse_kinematics.js"
+ :optimizations :advanced
+ :pretty-print false}}}})
+
diff --git a/webfui-examples/resources/public/css/POCKC___.eot b/webfui-examples/resources/public/css/POCKC___.eot
new file mode 100644
index 0000000..a419478
Binary files /dev/null and b/webfui-examples/resources/public/css/POCKC___.eot differ
diff --git a/webfui-examples/resources/public/css/POCKC___.svg b/webfui-examples/resources/public/css/POCKC___.svg
new file mode 100644
index 0000000..bf79729
--- /dev/null
+++ b/webfui-examples/resources/public/css/POCKC___.svg
@@ -0,0 +1,603 @@
+
+
+
diff --git a/webfui-examples/resources/public/css/POCKC___.ttf b/webfui-examples/resources/public/css/POCKC___.ttf
new file mode 100644
index 0000000..7c61995
Binary files /dev/null and b/webfui-examples/resources/public/css/POCKC___.ttf differ
diff --git a/webfui-examples/resources/public/css/POCKC___.woff b/webfui-examples/resources/public/css/POCKC___.woff
new file mode 100644
index 0000000..76e67f1
Binary files /dev/null and b/webfui-examples/resources/public/css/POCKC___.woff differ
diff --git a/webfui-examples/resources/public/css/add_two_numbers.css b/webfui-examples/resources/public/css/add_two_numbers.css
new file mode 100644
index 0000000..876ddaf
--- /dev/null
+++ b/webfui-examples/resources/public/css/add_two_numbers.css
@@ -0,0 +1,9 @@
+body{background-color:dodgerblue;
+ text-align:center;
+ font-size:3em}
+div{padding:2em}
+input{font-size:1em;
+ background-color:lightblue;
+ width:5em}
+span{background-color:lightblue;
+ padding:0.1em}
diff --git a/webfui-examples/resources/public/css/add_two_numbers_low_level.css b/webfui-examples/resources/public/css/add_two_numbers_low_level.css
new file mode 100644
index 0000000..2c5e0ef
--- /dev/null
+++ b/webfui-examples/resources/public/css/add_two_numbers_low_level.css
@@ -0,0 +1,10 @@
+body{background-color:dodgerblue;
+ text-align:center;
+ font-family:sans-serif;
+ font-size:3em}
+div{padding:2em}
+input{font-size:1em;
+ background-color:lightblue;
+ width:5em}
+span{background-color:lightblue;
+ padding:0.1em}
diff --git a/webfui-examples/resources/public/css/calculator.css b/webfui-examples/resources/public/css/calculator.css
new file mode 100644
index 0000000..885df5b
--- /dev/null
+++ b/webfui-examples/resources/public/css/calculator.css
@@ -0,0 +1,43 @@
+@font-face {font-family:"Pocket Calculator";src:url("POCKC___.eot?") format("eot"),url("POCKC___.woff") format("woff"),url("POCKC___.ttf") format("truetype"),url("POCKC___.svg#PocketCalculator") format("svg");font-weight:normal;font-style:normal;}
+html, body, div, span, object,form,input,text,h1,h2,button,label,a,img{-moz-user-select:none;
+ -webkit-user-select:none;
+ -webkit-user-drag:none}
+textarea,input[type='text']{-webkit-user-select:text;
+ -moz-user-select:text}
+body{background-color:grey;
+ text-align:center;
+ font-weight:bold;
+ font-family:sans-serif;
+ font-size:3em}
+table{margin-top:1em;
+ background-color:black;
+ margin-left:auto;
+ margin-right:auto;
+ padding:0.2em;
+ -webkit-border-radius:0.5em;
+ border-radius:0.5em}
+div{text-align:center;
+ margin:0.2em;
+ width:2em;
+ padding-top:0.4em;
+ padding-bottom:0.35em;
+ -webkit-border-radius:0.3em;
+ border-radius:0.3em;
+ background-color:silver}
+#ms,#mr,#ac{background-color:crimson}
+#mult,#div,#plus,#minus,#period{background-color:tan}
+#eq{width:4.5em;background-color:orange}
+#display{background-color:turquoise;
+ width:6.5em;
+ -webkit-border-radius:0.2em;
+ border-radius:0.2em;
+ padding-top:0.1em;
+ padding-bottom:0.0em;
+ padding-right:0.35em;
+ font-family:"Pocket Calculator";
+ font-size:1.4em;
+ text-align:right}
+div{cursor:pointer}
+div:active{-webkit-transform:scale(0.9);
+ -moz-transform:scale(0.9);
+ opacity:0.7}
\ No newline at end of file
diff --git a/webfui-examples/resources/public/css/calculator_ajax.css b/webfui-examples/resources/public/css/calculator_ajax.css
new file mode 100644
index 0000000..885df5b
--- /dev/null
+++ b/webfui-examples/resources/public/css/calculator_ajax.css
@@ -0,0 +1,43 @@
+@font-face {font-family:"Pocket Calculator";src:url("POCKC___.eot?") format("eot"),url("POCKC___.woff") format("woff"),url("POCKC___.ttf") format("truetype"),url("POCKC___.svg#PocketCalculator") format("svg");font-weight:normal;font-style:normal;}
+html, body, div, span, object,form,input,text,h1,h2,button,label,a,img{-moz-user-select:none;
+ -webkit-user-select:none;
+ -webkit-user-drag:none}
+textarea,input[type='text']{-webkit-user-select:text;
+ -moz-user-select:text}
+body{background-color:grey;
+ text-align:center;
+ font-weight:bold;
+ font-family:sans-serif;
+ font-size:3em}
+table{margin-top:1em;
+ background-color:black;
+ margin-left:auto;
+ margin-right:auto;
+ padding:0.2em;
+ -webkit-border-radius:0.5em;
+ border-radius:0.5em}
+div{text-align:center;
+ margin:0.2em;
+ width:2em;
+ padding-top:0.4em;
+ padding-bottom:0.35em;
+ -webkit-border-radius:0.3em;
+ border-radius:0.3em;
+ background-color:silver}
+#ms,#mr,#ac{background-color:crimson}
+#mult,#div,#plus,#minus,#period{background-color:tan}
+#eq{width:4.5em;background-color:orange}
+#display{background-color:turquoise;
+ width:6.5em;
+ -webkit-border-radius:0.2em;
+ border-radius:0.2em;
+ padding-top:0.1em;
+ padding-bottom:0.0em;
+ padding-right:0.35em;
+ font-family:"Pocket Calculator";
+ font-size:1.4em;
+ text-align:right}
+div{cursor:pointer}
+div:active{-webkit-transform:scale(0.9);
+ -moz-transform:scale(0.9);
+ opacity:0.7}
\ No newline at end of file
diff --git a/webfui-examples/resources/public/css/calculator_many.css b/webfui-examples/resources/public/css/calculator_many.css
new file mode 100644
index 0000000..f1c1f16
--- /dev/null
+++ b/webfui-examples/resources/public/css/calculator_many.css
@@ -0,0 +1,43 @@
+@font-face {font-family:"Pocket Calculator";src:url("POCKC___.eot?") format("eot"),url("POCKC___.woff") format("woff"),url("POCKC___.ttf") format("truetype"),url("POCKC___.svg#PocketCalculator") format("svg");font-weight:normal;font-style:normal;}
+html, body, div, span, object,form,input,text,h1,h2,button,label,a,img{-moz-user-select:none;
+ -webkit-user-select:none;
+ -webkit-user-drag:none}
+textarea,input[type='text']{-webkit-user-select:text;
+ -moz-user-select:text}
+body{background-color:grey;
+ text-align:center;
+ font-weight:bold;
+ font-family:sans-serif;
+ font-size:0.4em}
+table.calc{margin-top:1em;
+ background-color:black;
+ margin-left:auto;
+ margin-right:auto;
+ padding:0.2em;
+ -webkit-border-radius:0.5em;
+ border-radius:0.5em}
+div{text-align:center;
+ margin:0.2em;
+ width:2em;
+ padding-top:0.4em;
+ padding-bottom:0.35em;
+ -webkit-border-radius:0.3em;
+ border-radius:0.3em;
+ background-color:silver}
+#ms,#mr,#ac{background-color:crimson}
+#mult,#div,#plus,#minus,#period{background-color:tan}
+#eq{width:4.5em;background-color:orange}
+#display{background-color:turquoise;
+ width:6.5em;
+ -webkit-border-radius:0.2em;
+ border-radius:0.2em;
+ padding-top:0.1em;
+ padding-bottom:0.0em;
+ padding-right:0.35em;
+ font-family:"Pocket Calculator";
+ font-size:1.4em;
+ text-align:right}
+div{cursor:pointer}
+div:active{-webkit-transform:scale(0.9);
+ -moz-transform:scale(0.9);
+ opacity:0.7}
\ No newline at end of file
diff --git a/webfui-examples/resources/public/css/inverse_kinematics.css b/webfui-examples/resources/public/css/inverse_kinematics.css
new file mode 100644
index 0000000..d209a26
--- /dev/null
+++ b/webfui-examples/resources/public/css/inverse_kinematics.css
@@ -0,0 +1,22 @@
+html, body, div, span, object,form,input,text,h1,h2,button,label,a,img{-moz-user-select:none;
+ -webkit-user-select:none;
+ -webkit-user-drag:none}
+body{margin:0;
+ padding:0;
+ position:absolute;
+ bottom:0px;
+ top:0px;
+ left:0px;
+ right:0px;
+ overflow:hidden}
+.node{opacity:0.5;
+ position:absolute;
+ color:white;
+ padding:5px;
+ font-weight:bold;
+ background-color:red}
+.destination{background-color:blue}
+.box{opacity:0.5;
+ position:absolute;
+ background-color:black;
+ -webkit-transform-origin:0% 0%}
\ No newline at end of file
diff --git a/webfui-examples/resources/public/css/mouse_tracking.css b/webfui-examples/resources/public/css/mouse_tracking.css
new file mode 100644
index 0000000..384da83
--- /dev/null
+++ b/webfui-examples/resources/public/css/mouse_tracking.css
@@ -0,0 +1,31 @@
+html, body, div, span, object,form,input,text,h1,h2,button,label,a,img{-moz-user-select:none;
+ -webkit-user-select:none;
+ -webkit-user-drag:none;
+}
+body{bottom:0px;
+ top:0px;
+ left:0px;
+ right:0px;
+ padding:0px;
+ margin:0px;
+ position:absolute;
+ font-size:3em;
+ overflow:hidden;
+ color:white}
+div.layer{
+background-color:blue;
+ position:absolute;
+top:0;
+left:0;
+}
+div{background-color:blue;
+ text-align:center;
+ position:absolute;
+ font-size:1em;
+ padding:0px;
+ margin:0px;
+ -webkit-border-radius:0.6em;
+ cursor:pointer}
+.dragged{position:absolute;
+ opacity:0.5;
+ background-color:black}
diff --git a/webfui-examples/resources/public/css/scrolling.css b/webfui-examples/resources/public/css/scrolling.css
new file mode 100644
index 0000000..e69de29
diff --git a/webfui-examples/resources/public/css/webfui_examples.css b/webfui-examples/resources/public/css/webfui_examples.css
new file mode 100644
index 0000000..876ddaf
--- /dev/null
+++ b/webfui-examples/resources/public/css/webfui_examples.css
@@ -0,0 +1,9 @@
+body{background-color:dodgerblue;
+ text-align:center;
+ font-size:3em}
+div{padding:2em}
+input{font-size:1em;
+ background-color:lightblue;
+ width:5em}
+span{background-color:lightblue;
+ padding:0.1em}
diff --git a/webfui-examples/resources/public/index.html b/webfui-examples/resources/public/index.html
new file mode 100644
index 0000000..31ba7f4
--- /dev/null
+++ b/webfui-examples/resources/public/index.html
@@ -0,0 +1,16 @@
+
+ Webfui Examples
+(Right now Chrome & Safari work 100%, Firefox will be ready shortly.)
+
+
+ - Add Two Numbers (low level version)
+ - Add Two Numbers (version with elegant code)
+ - Scrollbar Plugin Example
+ - Mouse Plugin Example
+ - Calculator
+ - Many Calculators
+ - Calculator with Serve-side Memory
+ - Inverse Kinematics
+
+
+
\ No newline at end of file
diff --git a/webfui-examples/resources/public/js/core.js b/webfui-examples/resources/public/js/core.js
new file mode 100644
index 0000000..0dff405
--- /dev/null
+++ b/webfui-examples/resources/public/js/core.js
@@ -0,0 +1,19235 @@
+var COMPILED = false;
+var goog = goog || {};
+goog.global = this;
+goog.DEBUG = true;
+goog.LOCALE = "en";
+goog.provide = function(name) {
+ if(!COMPILED) {
+ if(goog.isProvided_(name)) {
+ throw Error('Namespace "' + name + '" already declared.');
+ }
+ delete goog.implicitNamespaces_[name];
+ var namespace = name;
+ while(namespace = namespace.substring(0, namespace.lastIndexOf("."))) {
+ if(goog.getObjectByName(namespace)) {
+ break
+ }
+ goog.implicitNamespaces_[namespace] = true
+ }
+ }
+ goog.exportPath_(name)
+};
+goog.setTestOnly = function(opt_message) {
+ if(COMPILED && !goog.DEBUG) {
+ opt_message = opt_message || "";
+ throw Error("Importing test-only code into non-debug environment" + opt_message ? ": " + opt_message : ".");
+ }
+};
+if(!COMPILED) {
+ goog.isProvided_ = function(name) {
+ return!goog.implicitNamespaces_[name] && !!goog.getObjectByName(name)
+ };
+ goog.implicitNamespaces_ = {}
+}
+goog.exportPath_ = function(name, opt_object, opt_objectToExportTo) {
+ var parts = name.split(".");
+ var cur = opt_objectToExportTo || goog.global;
+ if(!(parts[0] in cur) && cur.execScript) {
+ cur.execScript("var " + parts[0])
+ }
+ for(var part;parts.length && (part = parts.shift());) {
+ if(!parts.length && goog.isDef(opt_object)) {
+ cur[part] = opt_object
+ }else {
+ if(cur[part]) {
+ cur = cur[part]
+ }else {
+ cur = cur[part] = {}
+ }
+ }
+ }
+};
+goog.getObjectByName = function(name, opt_obj) {
+ var parts = name.split(".");
+ var cur = opt_obj || goog.global;
+ for(var part;part = parts.shift();) {
+ if(goog.isDefAndNotNull(cur[part])) {
+ cur = cur[part]
+ }else {
+ return null
+ }
+ }
+ return cur
+};
+goog.globalize = function(obj, opt_global) {
+ var global = opt_global || goog.global;
+ for(var x in obj) {
+ global[x] = obj[x]
+ }
+};
+goog.addDependency = function(relPath, provides, requires) {
+ if(!COMPILED) {
+ var provide, require;
+ var path = relPath.replace(/\\/g, "/");
+ var deps = goog.dependencies_;
+ for(var i = 0;provide = provides[i];i++) {
+ deps.nameToPath[provide] = path;
+ if(!(path in deps.pathToNames)) {
+ deps.pathToNames[path] = {}
+ }
+ deps.pathToNames[path][provide] = true
+ }
+ for(var j = 0;require = requires[j];j++) {
+ if(!(path in deps.requires)) {
+ deps.requires[path] = {}
+ }
+ deps.requires[path][require] = true
+ }
+ }
+};
+goog.ENABLE_DEBUG_LOADER = true;
+goog.require = function(name) {
+ if(!COMPILED) {
+ if(goog.isProvided_(name)) {
+ return
+ }
+ if(goog.ENABLE_DEBUG_LOADER) {
+ var path = goog.getPathFromDeps_(name);
+ if(path) {
+ goog.included_[path] = true;
+ goog.writeScripts_();
+ return
+ }
+ }
+ var errorMessage = "goog.require could not find: " + name;
+ if(goog.global.console) {
+ goog.global.console["error"](errorMessage)
+ }
+ throw Error(errorMessage);
+ }
+};
+goog.basePath = "";
+goog.global.CLOSURE_BASE_PATH;
+goog.global.CLOSURE_NO_DEPS;
+goog.global.CLOSURE_IMPORT_SCRIPT;
+goog.nullFunction = function() {
+};
+goog.identityFunction = function(var_args) {
+ return arguments[0]
+};
+goog.abstractMethod = function() {
+ throw Error("unimplemented abstract method");
+};
+goog.addSingletonGetter = function(ctor) {
+ ctor.getInstance = function() {
+ return ctor.instance_ || (ctor.instance_ = new ctor)
+ }
+};
+if(!COMPILED && goog.ENABLE_DEBUG_LOADER) {
+ goog.included_ = {};
+ goog.dependencies_ = {pathToNames:{}, nameToPath:{}, requires:{}, visited:{}, written:{}};
+ goog.inHtmlDocument_ = function() {
+ var doc = goog.global.document;
+ return typeof doc != "undefined" && "write" in doc
+ };
+ goog.findBasePath_ = function() {
+ if(goog.global.CLOSURE_BASE_PATH) {
+ goog.basePath = goog.global.CLOSURE_BASE_PATH;
+ return
+ }else {
+ if(!goog.inHtmlDocument_()) {
+ return
+ }
+ }
+ var doc = goog.global.document;
+ var scripts = doc.getElementsByTagName("script");
+ for(var i = scripts.length - 1;i >= 0;--i) {
+ var src = scripts[i].src;
+ var qmark = src.lastIndexOf("?");
+ var l = qmark == -1 ? src.length : qmark;
+ if(src.substr(l - 7, 7) == "base.js") {
+ goog.basePath = src.substr(0, l - 7);
+ return
+ }
+ }
+ };
+ goog.importScript_ = function(src) {
+ var importScript = goog.global.CLOSURE_IMPORT_SCRIPT || goog.writeScriptTag_;
+ if(!goog.dependencies_.written[src] && importScript(src)) {
+ goog.dependencies_.written[src] = true
+ }
+ };
+ goog.writeScriptTag_ = function(src) {
+ if(goog.inHtmlDocument_()) {
+ var doc = goog.global.document;
+ doc.write('