From 7eb0623ff80c12c1317151652fad48c17579eb72 Mon Sep 17 00:00:00 2001 From: Micah Date: Wed, 18 Dec 2013 09:19:24 -0600 Subject: [PATCH] improves autocomplete-field documents it adds example of it makes throttle configurable --- .gitignore | 1 + CHANGES.md | 4 + example/.gitignore | 12 ++ example/README.md | 1 + example/bin/specljs | 20 ++++ example/example.css | 30 +++++ example/example.html | 16 +++ example/project.clj | 27 +++++ example/spec/cljs/example/main_spec.cljs | 8 ++ .../src/cljs/example/autocomplete_demo.cljs | 32 +++++ example/src/cljs/example/main.cljs | 5 + project.clj | 2 +- .../filament/autocomplete_field_spec.cljs | 41 +++++-- src/cljs/com/eighthlight/filament/async.cljs | 16 +-- .../filament/autocomplete_field.cljs | 110 +++++++++++------- src/cljs/com/eighthlight/filament/util.cljs | 32 +++-- 16 files changed, 288 insertions(+), 69 deletions(-) create mode 100644 CHANGES.md create mode 100644 example/.gitignore create mode 100644 example/README.md create mode 100755 example/bin/specljs create mode 100644 example/example.css create mode 100644 example/example.html create mode 100644 example/project.clj create mode 100644 example/spec/cljs/example/main_spec.cljs create mode 100644 example/src/cljs/example/autocomplete_demo.cljs create mode 100644 example/src/cljs/example/main.cljs diff --git a/.gitignore b/.gitignore index 9c3cca0..aa46db4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /js /.idea pom.xml +pom.xml.asc *.jar *.class .lein-deps-sum diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..cb0a02a --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,4 @@ +# 1.1.2 + +* autocomplete field - improved key event handling +* autocomplete field does not throttle by default. Throttling is configurable. \ No newline at end of file diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..9850186 --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,12 @@ +/target +/lib +/classes +/checkouts +/js +pom.xml +*.jar +*.class +.lein-deps-sum +.lein-failures +.lein-plugins +.lein-repl-history diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..02dc8ac --- /dev/null +++ b/example/README.md @@ -0,0 +1 @@ +# example diff --git a/example/bin/specljs b/example/bin/specljs new file mode 100755 index 0000000..c2b7bd0 --- /dev/null +++ b/example/bin/specljs @@ -0,0 +1,20 @@ +#! /usr/bin/env phantomjs + +var fs = require("fs"); +var p = require('webpage').create(); +var sys = require('system'); + +p.onConsoleMessage = function (x) { + fs.write("/dev/stdout", x, "w"); +}; + +p.injectJs(phantom.args[0]); + +var result = p.evaluate(function () { + specljs.run.standard.armed = true; + return specljs.run.standard.run_specs( + cljs.core.keyword("color"), true + ); +}); + +phantom.exit(result); diff --git a/example/example.css b/example/example.css new file mode 100644 index 0000000..d5c9133 --- /dev/null +++ b/example/example.css @@ -0,0 +1,30 @@ +.autocomplete-dropdown { + background-color: white; + border: 1px solid #25a8e0; + border-top-width: 0; + margin: 1px 0 2px 0; + padding: 0; + position: absolute; + z-index: 2900; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; + -webkit-box-shadow: 0px 1px 5px rgba(64, 56, 41, 0.4); + -moz-box-shadow: 0px 1px 5px rgba(64, 56, 41, 0.4); + box-shadow: 0px 1px 5px rgba(64, 56, 41, 0.4); +} + +.autocomplete-dropdown li { + list-style: none; + width: 100%; + padding-left: 10px; +} + +.autocomplete-dropdown li:hover { + background-color: #ccc; +} + +.autocomplete-dropdown .highlighted { + background-color: #25a8e0; + color: white; +} \ No newline at end of file diff --git a/example/example.html b/example/example.html new file mode 100644 index 0000000..f7996da --- /dev/null +++ b/example/example.html @@ -0,0 +1,16 @@ + + + + + Filament Example + + + + +

Filament Examples

+ + + + diff --git a/example/project.clj b/example/project.clj new file mode 100644 index 0000000..b4ef52e --- /dev/null +++ b/example/project.clj @@ -0,0 +1,27 @@ +(defproject example "1.0.0" + :description "Example App using com.8thlight/Filament" + :license {:name "Eclipse Public License" + :url "http://www.eclipse.org/legal/epl-v10.html"} + + :dependencies [[com.8thlight/filament "1.1.2"] + [com.8thlight/hiccup "1.1.1"] + [org.clojure/clojure "1.5.1"]] + + :profiles {:dev {:dependencies [[org.clojure/clojurescript "0.0-2014"] + [speclj "2.9.1"] + [specljs "2.9.1"]]}} + :plugins [[speclj "2.9.1"] + [specljs "2.9.1"] + [lein-cljsbuild "1.0.0"]] + + :cljsbuild ~(let [run-specs ["bin/specljs" "js/example_dev.js"]] + {:builds {:dev {:source-paths ["src/cljs" "spec/cljs"] + :compiler {:output-to "js/example_dev.js" + :optimizations :whitespace + :pretty-print true} + :notify-command run-specs}} + + :test-commands {"test" run-specs}}) + + :source-paths ["src/clj" "src/cljs"] + :test-paths ["spec/clj"]) diff --git a/example/spec/cljs/example/main_spec.cljs b/example/spec/cljs/example/main_spec.cljs new file mode 100644 index 0000000..e845492 --- /dev/null +++ b/example/spec/cljs/example/main_spec.cljs @@ -0,0 +1,8 @@ +(ns example.main-spec + (:require-macros [specljs.core :refer [describe it should=]]) + (:require [specljs.core] + [example.main])) + +(describe "A ClojureScript test" + + ) diff --git a/example/src/cljs/example/autocomplete_demo.cljs b/example/src/cljs/example/autocomplete_demo.cljs new file mode 100644 index 0000000..f40c1a4 --- /dev/null +++ b/example/src/cljs/example/autocomplete_demo.cljs @@ -0,0 +1,32 @@ +(ns example.autocomplete-demo + (:require-macros [hiccup.core :as h]) + (:require [clojure.string :as string] + [com.eighthlight.filament.autocomplete-field :as autocomplete] + [domina :as dom] + [hiccup.core])) + +(def colors ["red" + "orange" + "yellow" + "green" + "blue" + "indigo" + "violet"]) + +(def dom [:form {:action "/fooey" :autocomplete "off"} + [:label {:for "autocomplete-field"} "Autocomplete:"] + [:input#autocomplete-field {:type "text"}]]) + +(defn autocomplete-on-select [[name value]] + (dom/set-value! (dom/by-id "autocomplete-field") value)) + +(defn autocomplete-on-search [q] + (autocomplete/show-dropdown (dom/by-id "autocomplete-field") + (map #(vector % %) + (filter #(re-find (re-pattern q) %) colors)))) + +(defn demo-autocomplete [] + (dom/append! (.-body js/document) (h/html dom)) + (let [field (dom/by-id "autocomplete-field")] + (autocomplete/arm-field field {:on-select autocomplete-on-select + :on-search autocomplete-on-search}))) diff --git a/example/src/cljs/example/main.cljs b/example/src/cljs/example/main.cljs new file mode 100644 index 0000000..6d4ed62 --- /dev/null +++ b/example/src/cljs/example/main.cljs @@ -0,0 +1,5 @@ +(ns example.main + (:require [example.autocomplete-demo :refer [demo-autocomplete]])) + +(defn ^:export init [] + (demo-autocomplete)) \ No newline at end of file diff --git a/project.clj b/project.clj index b48afa4..6d05dad 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject com.8thlight/filament "1.1.1" +(defproject com.8thlight/filament "1.1.2" :description "Rich client utilities" :url "http://github.com/8thlight/filament" :license {:name "Eclipse Public License" diff --git a/spec/cljs/com/eighthlight/filament/autocomplete_field_spec.cljs b/spec/cljs/com/eighthlight/filament/autocomplete_field_spec.cljs index bbd6c0e..cf9bbf4 100644 --- a/spec/cljs/com/eighthlight/filament/autocomplete_field_spec.cljs +++ b/spec/cljs/com/eighthlight/filament/autocomplete_field_spec.cljs @@ -1,6 +1,6 @@ (ns com.eighthlight.filament.autocomplete-field-spec (:require-macros [hiccups.core :as h] - [specljs.core :refer [describe context it should= should-contain with before around]]) + [specljs.core :refer [describe context it should= should-contain with before around should-invoke should-not-invoke]]) (:require [com.eighthlight.filament.async :as async] [com.eighthlight.filament.autocomplete-field :as ac] [com.eighthlight.filament.fx :as fx] @@ -18,15 +18,21 @@ (dom/set-html! (css/sel "body") (h/html [:input#testfield {:type "text" :name "testfield"}]))) (with input (dom/by-id "testfield")) (with trail (atom [])) - (around [it] - (with-redefs [async/handle-throttled-events event/listen!] - (it))) (it "arms a text field with search behavior" - (ac/arm-field @input {:on-search #(swap! @trail conj :armed)}) - (dom/set-value! @input "change") - (event/dispatch! @input :keyup {}) - (should= [:armed] @@trail)) + (should-not-invoke + async/handle-throttled-events {} + (ac/arm-field @input {:on-search #(swap! @trail conj :armed)}) + (dom/set-value! @input "change") + (event/dispatch! @input :keyup {}) + (should= [:armed] @@trail))) + + (it "arms a text field with throttled search behavior" + (should-invoke + async/handle-throttled-events {:with [:* :* :* 500]} + (ac/arm-field @input {:throttle 500 :on-search #(swap! @trail conj :armed)}) + (dom/set-value! @input "change") + (event/dispatch! @input :keyup {}))) (it "returns the value of the armed text field when searching" (ac/arm-field @input {:on-search #(swap! @trail conj %)}) @@ -34,12 +40,25 @@ (event/dispatch! @input :keyup {}) (should= ["Piper"] @@trail)) - (it "does invoke callback if text hasn't changed" + (it "DOESN'T invoke callback if text hasn't changed" (dom/set-value! @input "Piper") (ac/arm-field @input {:on-search #(swap! @trail conj %)}) (event/dispatch! @input :keyup {}) + (should= [] @@trail)) + + (it "DOES invoke callback if text hasn't changed but arrow is pressed" + (dom/set-value! @input "Piper") + (ac/arm-field @input {:on-search #(swap! @trail conj %)}) + (event/dispatch! @input :keyup {"keyCode" util/DOWN_ARROW}) (should= ["Piper"] @@trail)) + (it "DOESN'T invoke callback if text hasn't changed, arrow is pressed, but dropdown is open" + (dom/set-value! @input "Piper") + (ac/arm-field @input {:on-search #(swap! @trail conj %)}) + (ac/show-dropdown @input []) + (event/dispatch! @input :keyup {"keyCode" util/DOWN_ARROW}) + (should= [] @@trail)) + (context "Dropdown" (with dropdown (ac/show-dropdown @input [["one" "1"] ["two" "2"] ["three" "3"]])) @@ -87,6 +106,10 @@ (event/dispatch! @input :blur {}) (should= nil (ac/dropdown))) + (it "goes away on ECS pressed" + (event/dispatch! @input :keydown {"keyCode" util/ESC}) + (should= nil (ac/dropdown))) + ) ) diff --git a/src/cljs/com/eighthlight/filament/async.cljs b/src/cljs/com/eighthlight/filament/async.cljs index ba1ce6d..7b2ab35 100644 --- a/src/cljs/com/eighthlight/filament/async.cljs +++ b/src/cljs/com/eighthlight/filament/async.cljs @@ -36,11 +36,13 @@ (recur ::init last (pop cs)))))))) c)) -(defn handle-throttled-events [element event-type handler] - (let [event-channel (:chan (event-chan element event-type)) - throttled (throttle event-channel 1000)] - (go - (while true - (let [e (= next-index 0) @@ -64,7 +69,9 @@ (defn- handle-dropdown-navigation [text-field e] (cond (util/DOWN_ARROW? e) (highlight-next-option) - (util/UP_ARROW? e) (highlight-previous-option))) + (util/UP_ARROW? e) (highlight-previous-option) + (util/ENTER? e) (when (dropdown) (event/prevent-default e)) + (util/ESC? e) (when (dropdown) (close-dropdown)))) (defn- process-selection [input payload] (when input @@ -72,23 +79,17 @@ (close-dropdown) (on-select-action payload)))) -(defn select-highlighted-option [text-field] +(defn- select-highlighted-option [text-field] (when-let [selection (highlighted-option)] (process-selection text-field (dom/get-data selection :payload)))) (defn- handle-possible-selection [text-field e] - (when (util/ENTER? e) (select-highlighted-option text-field))) - -(defn arm-field [text-field callbacks] - (dom/set-data! text-field :on-select (:on-select callbacks)) - (event/listen! text-field :keydown (partial handle-dropdown-navigation text-field)) - (event/listen! text-field :keyup (partial handle-possible-selection text-field)) - (when-let [on-search (:on-search callbacks)] - (arm-search-callback text-field on-search)) - (event/listen! text-field :blur close-dropdown)) + (when (util/ENTER? e) + (event/prevent-default e) + (select-highlighted-option text-field))) -(defn attach-dropdown [input] - (let [dropdown (dom/single-node (h/html [:ul#autocomplete-dropdown.dropdown])) +(defn- attach-dropdown [input] + (let [dropdown (dom/single-node (h/html [:ul#autocomplete-dropdown.autocomplete-dropdown])) coords (goog.style/getPosition input) size (goog.style/getSize input)] (dom/append! (.-parentElement input) dropdown) @@ -97,11 +98,34 @@ (dom/set-style! dropdown "left" (str (.-x coords) "px")) dropdown)) -(defn hide-dropdown [] - (when-let [dropdown (dropdown)] - (dom/detach! dropdown))) +(defn arm-field + "Arms an input (text) with autocomplete behavior. + + callbacks/options: + :on-search - a fn that takes one parameter, the query or current text of the input. This fn typically performs + a search pased on the query and loads a dropdown (see show-dropdown), but it doesn't have to. + :on-select - a fn that take a dropdown payload ([ ]) and is called when a dropdown item + is selected. + :throttle - number of millisecond. Searches will not take place more frequently then the specified period. + By default, every change will trigger a search." + [text-field callbacks] + (dom/set-data! text-field :on-select (:on-select callbacks)) + (event/listen! text-field :keydown (partial handle-dropdown-navigation text-field)) + (event/listen! text-field :keyup (partial handle-possible-selection text-field)) + (when-let [on-search (:on-search callbacks)] + (arm-search-callback text-field on-search (:throttle callbacks))) + (event/listen! text-field :blur close-dropdown)) -(defn show-dropdown [input items] +(defn show-dropdown + "Open a dropdown menu attached to the bottom of the input. The second parameter must be a seq of pairs where the + first value is the display text and the second value is a payload for the programmer's use. + + CSS: Consider adding the following CSS to make dropdowns look good + .autocomplete-dropdown - to style the containing ul + .autocomplete-dropdown li - to style each list item in the dropdown + .autocomplete-dropdown li:hover - to style a list item when it's moused-over + .autocomplete-dropdown .highlighted - to style a list item highlighted via arrow keys" + [input items] (let [dropdown (or (dropdown) (attach-dropdown input))] (dom/destroy-children! dropdown) (doseq [[text data] items] @@ -110,3 +134,7 @@ (event/listen! item :mousedown #(process-selection input [text data])) (dom/append! dropdown item))) dropdown)) + +(def hide-dropdown + "Removes the dropdown menu form the dom." + close-dropdown) diff --git a/src/cljs/com/eighthlight/filament/util.cljs b/src/cljs/com/eighthlight/filament/util.cljs index 68c79e2..3b2bd5c 100644 --- a/src/cljs/com/eighthlight/filament/util.cljs +++ b/src/cljs/com/eighthlight/filament/util.cljs @@ -20,34 +20,44 @@ ; CLJS ONLY BELOW +(defn raw [e] + (try + (event/raw-event e) + (catch js/Object o + e))) + (def ENTER 13) -(defn ENTER? [e] (= ENTER (.-keyCode (event/raw-event e)))) +(defn ENTER? [e] (= ENTER (.-keyCode (raw e)))) (def ESC 27) -(defn ESC? [e] (= ESC (.-keyCode (event/raw-event e)))) +(defn ESC? [e] (= ESC (.-keyCode (raw e)))) (def SPACE 32) -(defn SPACE? [e] (= SPACE (.-keyCode (event/raw-event e)))) +(defn SPACE? [e] (= SPACE (.-keyCode (raw e)))) (def LEFT_ARROW 37) -(defn LEFT_ARROW? [e] (= LEFT_ARROW (.-keyCode (event/raw-event e)))) +(defn LEFT_ARROW? [e] (= LEFT_ARROW (.-keyCode (raw e)))) (def UP_ARROW 38) -(defn UP_ARROW? [e] (= UP_ARROW (.-keyCode (event/raw-event e)))) +(defn UP_ARROW? [e] (= UP_ARROW (.-keyCode (raw e)))) (def RIGHT_ARROW 39) -(defn RIGHT_ARROW? [e] (= RIGHT_ARROW (.-keyCode (event/raw-event e)))) +(defn RIGHT_ARROW? [e] (= RIGHT_ARROW (.-keyCode (raw e)))) (def DOWN_ARROW 40) -(defn DOWN_ARROW? [e] (= DOWN_ARROW (.-keyCode (event/raw-event e)))) +(defn DOWN_ARROW? [e] (= DOWN_ARROW (.-keyCode (raw e)))) + +(defn ARROW? [e] + (let [code (.-keyCode (raw e))] + (and (>= code LEFT_ARROW) (<= code DOWN_ARROW)))) (def not-blank? (complement string/blank?)) (defn override-click! [nodes action] (event/listen! nodes - :click (fn [e] - (event/prevent-default e) - (action e)))) + :click (fn [e] + (event/prevent-default e) + (action e)))) (defn element-id [element] (dom/attr element :id)) @@ -65,7 +75,7 @@ default-error text)] (dom/set-html! (css/sel "#flash-container") - (h/html [:div.flash [:h2.error message]])))) + (h/html [:div.flash [:h2.error message]])))) (defn clear-flash [] (dom/set-html! (css/sel "#flash-container") ""))