├── .gitignore ├── Readme.md ├── build.boot ├── screenshot.png ├── src └── tutorial_cljs │ ├── core.cljs │ ├── editor.cljs │ ├── inline_eval.cljs │ ├── quil_complete.cljs │ ├── quil_docs.cljs │ ├── quil_sketch.cljs │ ├── repl.cljs │ └── text │ ├── cljs.cljs │ ├── quil.cljs │ ├── reagent.cljs │ └── webgl.cljs ├── static ├── index.html ├── js │ └── main.cljs.edn └── main.css └── vendor ├── clojure-parinfer.js ├── inlet.css └── inlet.js /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | pages/ 3 | dev/ 4 | build/ 5 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Interactive ClojureScript Tutorial Environment 2 | 3 | ## [Try it Out](https://jaredforsyth.com/tutorial-cljs) 4 | 5 | [![screenshot](./screenshot.png)](https://www.youtube.com/watch?v=DQSdW27Esto) 6 | [Screencast](https://www.youtube.com/watch?v=DQSdW27Esto) 7 | 8 | ## To Build 9 | 10 | Requires [boot](http://boot-clj.com). 11 | 12 | - start a dev server `boot dev` 13 | - build the project for hosting `boot doit` 14 | 15 | ## Powered By 16 | 17 | - [Clojurescript](https://github.com/clojure/clojurescript) 18 | - [Reepl](https://github.com/jaredly/reepl) 19 | - [Boot](http://boot-clj.com/) 20 | - [Reagent](http://reagent-project.github.io/) (which uses [React](https://facebook.github.io/react/)) 21 | - [Cljs-Devtools](https://github.com/binaryage/cljs-devtools/) 22 | - [Parinfer](https://shaunlebron.github.io/parinfer/) 23 | - [Replumb](https://github.com/ScalaConsultants/replumb/) 24 | - [Quil](http://quil.info) 25 | -------------------------------------------------------------------------------- /build.boot: -------------------------------------------------------------------------------- 1 | (set-env! 2 | :source-paths #{"src"} 3 | :resource-paths #{"html"} 4 | :dependencies 5 | '[ 6 | [adzerk/boot-cljs "1.7.228-1" :scope "test"] 7 | [adzerk/boot-cljs-repl "0.3.0" :scope "test"] 8 | [adzerk/boot-reload "0.4.4" :scope "test"] 9 | [pandeiro/boot-http "0.6.3" :scope "test"] 10 | [crisptrutski/boot-cljs-test "0.2.1" :scope "test"] 11 | [zilti/boot-typed "0.1.1" :scope "test"] 12 | [adzerk/bootlaces "0.1.13"] 13 | 14 | [replumb "0.1.5-SNAPSHOT"] 15 | [parinfer "0.2.3"] 16 | [reagent "0.5.1"] 17 | [re-frame "0.6.0"] 18 | [binaryage/devtools "0.4.1"] 19 | [cljsjs/codemirror "5.10.0-0"] 20 | [quil "2.3.0"] 21 | 22 | [reepl "1.0.1.1"] 23 | 24 | [ajchemist/boot-figwheel "0.5.0-0"] ;; latest release 25 | [com.cemerick/piggieback "0.2.1" :scope "test"] 26 | [figwheel-sidecar "0.5.0-2" :scope "test"] 27 | 28 | [weasel "0.7.0" :scope "test"] 29 | [org.clojure/tools.nrepl "0.2.12" :scope "test"] 30 | 31 | [org.clojure/tools.reader "1.0.0-alpha2"] 32 | [org.clojure/clojure "1.7.0"] 33 | [org.clojure/core.typed "0.3.18"] 34 | [org.clojure/clojurescript "1.7.228"] 35 | ]) 36 | 37 | (require 38 | '[adzerk.bootlaces :refer :all] 39 | '[adzerk.boot-cljs :refer [cljs]] 40 | '[adzerk.boot-cljs-repl :refer [cljs-repl start-repl]] 41 | '[adzerk.boot-reload :refer [reload]] 42 | '[crisptrutski.boot-cljs-test :refer [test-cljs]] 43 | '[pandeiro.boot-http :refer [serve]]) 44 | 45 | (require 'boot-figwheel) 46 | (refer 'boot-figwheel :rename '{cljs-repl fw-cljs-repl}) 47 | 48 | (def +version+ "1.0") 49 | (bootlaces! +version+) 50 | 51 | (task-options! 52 | pom {:project 'jaredly/tutorial-cljs 53 | :version +version+ 54 | :description "An interactive clojurescript tutorial" 55 | :url "https://github.com/jaredly/tutorial-cljs" 56 | :scm {:url "https://github.com/jaredly/tutorial-cljs"} 57 | :license {"ISC License" "https://opensource.org/licenses/ISC"}} 58 | figwheel {:build-ids ["dev"] 59 | :all-builds [{:id "dev" 60 | #_:compiler #_{:main 'tutorial-cljs.core 61 | :output-to "main.js"} 62 | :figwheel {:build-id "dev" 63 | :on-jsload "tutorial-cljs.core/main" 64 | :heads-up-display true 65 | :autoload true 66 | :debug false}}] 67 | :figwheel-options {:repl true 68 | :http-server-root "dev" 69 | :css-dirs ["dev"] 70 | :open-file-command "emacsclient"}}) 71 | 72 | (def foreign-libs 73 | [{:file "vendor/clojure-parinfer.js" 74 | :provides ["parinfer.codemirror.mode.clojure.clojure-parinfer"]}]) 75 | 76 | (defn add-jar-thing [package path] 77 | (let [rx (re-pattern path)] 78 | (comp 79 | (sift :add-jar 80 | {package rx}) 81 | (sift :move 82 | {rx (str "js/main.out/" path)})))) 83 | 84 | (defn cljs-and-stuff [optimizations] 85 | (comp 86 | (cljs :source-map true 87 | :compiler-options {:foreign-libs foreign-libs} 88 | :optimizations optimizations) 89 | (sift :add-jar 90 | {'cljsjs/codemirror 91 | #"cljsjs/codemirror/development/codemirror.css"}) 92 | (sift :move 93 | {#"cljsjs/codemirror/development/codemirror.css" 94 | "vendor/codemirror/codemirror.css"}) 95 | (add-jar-thing 96 | 'org.clojure/tools.reader 97 | "cljs/tools/reader/reader_types.clj") 98 | )) 99 | 100 | (deftask dev [] 101 | (set-env! :source-paths #{"src"}) 102 | (set-env! :resource-paths #{"static"}) 103 | (comp 104 | (serve :dir "dev" :port 3003) 105 | (watch) 106 | ;; (speak) 107 | (reload :on-jsload 'tutorial-cljs.core/main) 108 | (cljs-repl) 109 | (cljs-and-stuff :none) 110 | (target :dir #{"dev"}) 111 | )) 112 | 113 | (deftask devfw [] 114 | (set-env! :source-paths #(into % ["src"])) 115 | (comp (repl) (figwheel))) 116 | 117 | (deftask doit [] 118 | (set-env! :source-paths #{"src"}) 119 | (set-env! :resource-paths #{"static"}) 120 | (comp 121 | (cljs-and-stuff :simple) 122 | (target :dir #{"doit"}) 123 | )) 124 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredly/tutorial-cljs/897ab76c448552c50ec15223a55ceeff61f7f9be/screenshot.png -------------------------------------------------------------------------------- /src/tutorial_cljs/core.cljs: -------------------------------------------------------------------------------- 1 | (ns tutorial-cljs.core 2 | (:require [reepl.core :as reepl] 3 | [reepl.replumb :as reepl-replumb] 4 | [replumb.core :as replumb] 5 | 6 | [tutorial-cljs.repl :as repl] 7 | [tutorial-cljs.inline-eval :as inline-eval] 8 | [tutorial-cljs.editor :as editor] 9 | [tutorial-cljs.quil-sketch :as quil-sketch] 10 | 11 | [tutorial-cljs.text.cljs :as text-cljs] 12 | [tutorial-cljs.text.quil :as text-quil] 13 | [tutorial-cljs.text.webgl :as text-webgl] 14 | [tutorial-cljs.text.reagent :as text-reagent] 15 | 16 | [parinfer-codemirror.editor :as parinfer] 17 | 18 | [reepl.show-function :as show-function] 19 | [reepl.show-devtools :as show-devtools] 20 | [reepl.show-value :as show-value] 21 | 22 | [devtools.core :as devtools] 23 | 24 | [quil.core] 25 | [quil.middleware] 26 | 27 | [cljs.tools.reader :as reader] 28 | [cljsjs.codemirror] 29 | [cljs.js :as jsc] 30 | 31 | [reagent.core :as r] 32 | )) 33 | 34 | (def reagent-tag #{:div :span :button :a :table :li :ul :ol :tr :td :input :textarea}) 35 | 36 | (defn valid-reagent-start [val] 37 | (or (reagent-tag val) 38 | (= js/Function (type val)))) 39 | 40 | (defn reagent-shower [val] 41 | (when (and (vector? val) 42 | (valid-reagent-start (first val))) 43 | val)) 44 | 45 | (def tutorials 46 | {:cljs {:text text-cljs/text 47 | :title "ClojureScript" 48 | :prelude "(ns tutorial.cljs (:require [clojure.string :as str]))"} 49 | :quil {:text text-quil/text 50 | :title "Quil" 51 | :special-forms 52 | {'makesketch 53 | #(quil-sketch/handle-make-sketch 54 | (replumb.repl/current-ns) 55 | (reader/read-string %))} 56 | :prelude "(ns tutorial.quil (:require [quil.core :as q]))"} 57 | :reagent {:text text-reagent/text 58 | :title "Reagent" 59 | :showers [reagent-shower] 60 | :prelude "(ns tutorial.reagent (:require [reagent.core :as r]))"} 61 | #_:glsl #_{:text text-webgl/text 62 | :title "WebGL" 63 | :special-forms 64 | {'makegl 65 | identity} 66 | :prelude "(ns tutorial.webgl (:require [gamma.api :as g] [gamma.program :as p]))"} 67 | }) 68 | 69 | ;; TODO "rewind" state when a sketch is paused 70 | ;; TODO persist the text across reloads 71 | ;; TODO auto-pause when an error occurs 72 | ;; TODO pause via keyboard shortcut 73 | ;; TODO keyboard everything; allow "focus" the canvas 74 | ;; TODO control framerate w/ slider 75 | 76 | (defn main []) 77 | 78 | 79 | ;; TODO get reagent docs in here, and gamma 80 | (swap! jsc/*loaded* conj 81 | 'org.processingjs.Processing 82 | 'quil.core 83 | 'quil.middleware 84 | 'quil.util 85 | 'quil.sketch 86 | 'reagent.core 87 | ;; TODO make this real 88 | 'gamma.api 89 | 'gamma.program) 90 | 91 | (defonce -general-setup 92 | (do 93 | (devtools/install!) 94 | (parinfer/start-editor-sync!))) 95 | 96 | (defn maybe-fn-docs [fn] 97 | (let [doc (reepl-replumb/doc-from-sym fn)] 98 | (when (:forms doc) 99 | (with-out-str 100 | (reepl-replumb/print-doc doc))))) 101 | 102 | (defn keyname [name] 103 | (str "text-" name)) 104 | 105 | (defn get-saved [name] 106 | (aget js/localStorage (keyname name))) 107 | 108 | (defn save-text [name text] 109 | (aset js/localStorage (keyname name) text)) 110 | 111 | (def default-showers 112 | [show-devtools/show-devtools 113 | (partial show-function/show-fn-with-docs maybe-fn-docs)]) 114 | 115 | (defn go-to-tutorial [tutorial] 116 | (aset js/location "hash" (name tutorial))) 117 | 118 | (declare setup-tutorial) 119 | 120 | (defn go-from-hash [] 121 | (let [name (.slice (.-hash js/location) 1) 122 | kwd (when (not (empty? name)) 123 | (keyword name))] 124 | (setup-tutorial (if (and (tutorials kwd) kwd) kwd :quil)))) 125 | 126 | (.addEventListener js/window "hashchange" 127 | go-from-hash) 128 | 129 | (defn setup-tutorial [name] 130 | (js/console.log "setup" name) 131 | (let [{:keys [title text prelude special-forms showers] :as tutorial} (tutorials name)] 132 | 133 | (editor/render-text 134 | (or (get-saved name) 135 | text) 136 | (concat showers default-showers) 137 | special-forms 138 | (partial save-text name)) 139 | 140 | ;; # Render REPL 141 | ;; TODO pass in custom completers too 142 | (let [repl-el (js/document.getElementById "repl")] 143 | (r/render [repl/repl-view 144 | name 145 | (map #(assoc (second %) :name (first %)) tutorials) 146 | #(go-to-tutorial %) 147 | (concat showers default-showers) 148 | #(do 149 | (save-text name text) 150 | (setup-tutorial name))] repl-el)) 151 | 152 | (reepl-replumb/run-repl prelude 153 | repl/replumb-opts 154 | identity))) 155 | 156 | (go-from-hash) 157 | -------------------------------------------------------------------------------- /src/tutorial_cljs/editor.cljs: -------------------------------------------------------------------------------- 1 | (ns tutorial-cljs.editor 2 | (:require 3 | [reepl.core :as reepl] 4 | [tutorial-cljs.repl :as repl] 5 | [tutorial-cljs.inline-eval :as inline-eval] 6 | )) 7 | 8 | (defn render-text [initial-text showers special-forms on-change] 9 | (let [text-el (js/document.getElementById "text") 10 | complete-cmd (reepl/make-complete-cmd repl/auto-complete repl/complete-atom)] 11 | 12 | (aset text-el "innerHTML" "") 13 | (def text-mirror 14 | (js/CodeMirror. 15 | text-el 16 | #js {:lineNumbers true 17 | :matchBrackets true 18 | :cursorScrollMargin 5 19 | :value initial-text 20 | :keyMap (if (:vim @repl/settings) "vim" "default") 21 | :extraKeys #js {"Shift-Cmd-Enter" (fn [_] (inline-eval/hide-display)) 22 | "Shift-Ctrl-Enter" (fn [_] (inline-eval/hide-display)) 23 | "Ctrl-Enter" (fn [cm] 24 | (inline-eval/eval-current-form cm showers special-forms)) 25 | "Cmd-Enter" (fn [cm] 26 | (inline-eval/eval-current-form cm showers special-forms))} 27 | :autoCloseBrackets true 28 | :mode "clojure" 29 | #_(if (:parinfer @settings) 30 | "clojure-parinfer" 31 | "clojure")})) 32 | ;; TODO debounce? 33 | (.on text-mirror "change" #(on-change (.getValue text-mirror))) 34 | 35 | ;; TODO parinfer is way too slow for large files -- once it speeds up, it 36 | ;; would be awesome to have that here. 37 | #_(if (:parinfer @settings) 38 | (parinfer/parinferize! text-mirror 39 | (str "text" (swap! pi-count inc)) 40 | :indent-mode 41 | initial-text)) 42 | 43 | (.on text-mirror "keydown" 44 | (fn [inst evt] 45 | (case (.-keyCode evt) 46 | (17 18 91 93) ((complete-cmd :show-all)) 47 | 9 ((complete-cmd :cycle) (.-shiftKey evt) text-mirror evt) 48 | :none))) 49 | 50 | (.on text-mirror "keyup" 51 | (fn [inst evt] 52 | (reepl.code-mirror/complete-keyup complete-cmd (.-keyCode evt) inst))) 53 | 54 | (.addEventListener text-el "mouseup" 55 | (fn [evt] 56 | ((complete-cmd :set) (reepl.code-mirror/get-word-and-range text-mirror)))) 57 | )) 58 | 59 | -------------------------------------------------------------------------------- /src/tutorial_cljs/inline_eval.cljs: -------------------------------------------------------------------------------- 1 | (ns tutorial-cljs.inline-eval 2 | (:require 3 | [reagent.core :as r] 4 | [cljs.tools.reader :as reader] 5 | [reepl.replumb :as reepl-replumb] 6 | [tutorial-cljs.repl :as repl] 7 | [tutorial-cljs.quil-sketch :as quil-sketch] 8 | )) 9 | 10 | (def styles 11 | { 12 | :output-wrapper {:position :relative 13 | :top "-1.5em" 14 | :left "2em" 15 | :background-color :white 16 | :box-shadow "0 0 5px #aaa" 17 | ;; :opacity 0.9 18 | :z-index 10000 19 | :padding 5 20 | :border-radius 5} 21 | :error {:background-color :white 22 | :color :red} 23 | :error-cause {:margin-left 10} 24 | }) 25 | 26 | (defn to-pos [obj] 27 | {:line (.-line obj) 28 | :ch (.-ch obj)}) 29 | 30 | ;; TODO if you are at the start of a token, then return that one... 31 | (defn get-form-at-cursor 32 | "Returns [form-as-string end-pos]" 33 | [cm cursor] 34 | (let [token (.getTokenAt cm cursor)] 35 | (js/console.log "token" token) 36 | (case (.-type token) 37 | "bracket" (let [rr (.findMatchingBracket cm cursor) 38 | [from to] (if (.-forward rr) 39 | [(.-from rr) (.-to rr)] 40 | [(.-to rr) (.-from rr)]) 41 | to #js {:line (.-line to) 42 | :ch (inc (.-ch to))}] 43 | [(.getRange cm from to) to]) 44 | ("comment" nil) (if (or (= 0 (.-start token)) 45 | (= 0 (count (.-string token)))) 46 | (when-not (= 0 (.-line cursor)) 47 | (js/console.log token) 48 | (let [line (.getLine cm (dec (.-line cursor)))] 49 | (recur cm #js {:line (dec (.-line cursor)) 50 | :ch (count line)}))) 51 | (recur cm #js {:line (.-line cursor) 52 | :ch (.-start token)})) 53 | "string" [(str \" (.-string token)) #js {:line (.-line cursor) 54 | :ch (.-end token)}] 55 | [(.-string token) #js {:line (.-line cursor) 56 | :ch (.-end token)}]))) 57 | 58 | (defn get-active-form 59 | "Returns [form-as-string end-pos]" 60 | [cm] 61 | (let [selection (first (.listSelections cm)) 62 | start (to-pos (.-anchor selection)) 63 | end (to-pos (.-head selection))] 64 | (if (= start end) 65 | (get-form-at-cursor cm (.getCursor cm)) 66 | [(.getSelection cm) (clj->js end)]))) 67 | 68 | 69 | (def display-el (js/document.createElement "div")) 70 | 71 | (defn hide-display [] 72 | (when (.-parentNode display-el) 73 | (.removeChild (.-parentNode display-el) display-el))) 74 | 75 | (defn show-error [error] 76 | (let [cause (.-cause error)] 77 | [:div {:style (:error styles)} 78 | (.-message error) 79 | (when cause 80 | [:span {:style (:error-cause styles)} 81 | (.-message cause)])])) 82 | 83 | (defn show-output-view [showers success? value] 84 | [:div {:style (:output-wrapper styles)} 85 | (if-not success? 86 | [show-error value] 87 | [reepl.show-value/show-value 88 | value 89 | nil 90 | {:showers showers}])]) 91 | 92 | (defn show-output [cm showers pos success? value] 93 | ;; TODO display things cooler. using cljs-devtools, etc. 94 | ;; (js* "debugger;") 95 | (r/render (show-output-view showers success? value) display-el) 96 | (.addWidget cm pos display-el true)) 97 | 98 | (defn eval-current-form [cm showers special-forms] 99 | (when-let [[form pos] (get-active-form cm) 100 | ] 101 | (let [form (.trim form) 102 | is-list (= \( (first form)) 103 | first-item (and is-list 104 | (reader/read-string (.slice form 1))) 105 | special-handle (and special-forms (special-forms first-item))] 106 | (if special-handle 107 | (special-handle form) 108 | (reepl-replumb/run-repl 109 | form 110 | repl/replumb-opts 111 | (partial show-output cm showers pos)))))) 112 | -------------------------------------------------------------------------------- /src/tutorial_cljs/quil_complete.cljs: -------------------------------------------------------------------------------- 1 | (ns tutorial-cljs.quil-complete 2 | (:require 3 | [tutorial-cljs.quil-docs :as quil-docs] 4 | [reepl.replumb :as reepl-replumb] 5 | [replumb.repl] 6 | [replumb.ast] 7 | )) 8 | 9 | (def quil-names 10 | (sort (map :name quil-docs/docs))) 11 | 12 | (def quil-map 13 | (into {} (map #(-> [(:name %) (assoc % :type :normal :name (symbol 'quil.core (:name %)))]) quil-docs/docs))) 14 | 15 | (defn quil-prefix [] 16 | (let [nss (:requires (replumb.ast/namespace @replumb.repl/st (replumb.repl/current-ns))) 17 | qss (filter #(= 'quil.core (second %)) nss) 18 | name (first (first qss))] 19 | name)) 20 | 21 | (defn quil-complete [prefix text] 22 | (let [name (second (.split text "/"))] 23 | (->> quil-names 24 | (filter 25 | #(not (= -1 (.indexOf (str %) name)))) 26 | (map str) 27 | (sort (partial reepl-replumb/compare-completion name)) 28 | (map 29 | #(-> [(symbol prefix %) (str (symbol prefix %)) (str (symbol prefix %)) %])) 30 | ))) 31 | 32 | (defn quil-doc [text] 33 | (let [name (second (.split text "/"))] 34 | (with-out-str 35 | (reepl-replumb/print-doc (quil-map (symbol name)))))) 36 | -------------------------------------------------------------------------------- /src/tutorial_cljs/quil_sketch.cljs: -------------------------------------------------------------------------------- 1 | (ns tutorial-cljs.quil-sketch 2 | (:require org.processingjs.Processing 3 | [quil.core :as q] 4 | [quil.middleware :as m] 5 | [quil.sketch] 6 | [reagent.core :as r] 7 | )) 8 | 9 | (defn parse-make-sketch [form] 10 | {:post [(every? (comp not nil?) 11 | (map % [:setup :update :draw :size]))]} 12 | (try 13 | (let [name (second form) 14 | opts (apply hash-map (drop 2 form))] 15 | (assoc opts :name name)) 16 | (catch js/Error e 17 | (throw (js/Error. "Invalid makesketch form"))))) 18 | 19 | (defonce current-sketch (atom nil)) 20 | (defonce current-applet (atom nil)) 21 | 22 | (defonce applets (atom {})) 23 | 24 | (def styles 25 | { 26 | :container {:position :absolute 27 | :z-index 10000 28 | :box-shadow "0 0 5px #999" 29 | :border-radius 2 30 | :background-color :white 31 | } 32 | :handle {:cursor :pointer 33 | :display :flex 34 | :justify-content :center 35 | :align-items :center 36 | } 37 | :title {:flex 1 38 | :padding "5px 10px" 39 | :font-weight :bold} 40 | :close-button {:cursor :pointer 41 | :background-color :transparent 42 | :border :none 43 | :font-weight :bold 44 | :font-size "1.2em" 45 | :padding "6px 10px 8px" 46 | :line-height "10px" 47 | :color "#999"} 48 | :play-pause-button {:cursor :pointer 49 | :background-color :transparent 50 | :border :none 51 | :font-weight :bold 52 | :font-size "1em" 53 | :padding "6px 10px 8px" 54 | :line-height "10px" 55 | :color "black"} 56 | :pause-symbol {:border-width "0 4px 0 4px" 57 | :border-style "solid" 58 | :width 2 59 | :display "inline-block" 60 | :height 12 61 | :margin "0 2px 0px 2px" 62 | :border-color "#aaa"} 63 | }) 64 | 65 | (defn canvas [[width height] on-canvas] 66 | (r/create-class 67 | {:component-did-mount 68 | (fn [this] 69 | (on-canvas (r/dom-node this))) 70 | :reagent-render 71 | (fn [] 72 | [:canvas {:width width 73 | :tab-index 0 74 | :height height}])})) 75 | 76 | (defn mobile-sketch [name title size loop-atom pos-atom on-canvas on-close] 77 | (let [{:keys [left top]} @pos-atom 78 | state (r/atom {:top top 79 | :left left 80 | :moving false 81 | :dx 0 82 | :dy 0}) 83 | move-listener (fn [evt] 84 | (.preventDefault evt) 85 | (swap! state (fn [{:keys [dx dy] :as state}] 86 | (assoc state 87 | :top (- (.-clientY evt) dy) 88 | :left (- (.-clientX evt) dx))))) 89 | up-listener (fn up-listener [evt] 90 | (let [{:keys [top left]} @state] 91 | (swap! pos-atom assoc :top top :left left)) 92 | (.preventDefault evt) 93 | (.removeEventListener js/window "mouseup" up-listener) 94 | (.removeEventListener js/window "mousemove" move-listener) 95 | (swap! state assoc :moving false)) 96 | ] 97 | (fn [] 98 | (let [{:keys [top left moving]} @state] 99 | [:div 100 | {:style (merge (:container styles) 101 | {:top top 102 | :left left})} 103 | [canvas size on-canvas] 104 | [:div 105 | {:style (:handle styles) 106 | :on-mouse-down 107 | (fn [evt] 108 | (.preventDefault evt) 109 | (.addEventListener js/window "mouseup" up-listener) 110 | (.addEventListener js/window "mousemove" move-listener) 111 | (swap! state assoc 112 | :dx (- (.-clientX evt) left) 113 | :dy (- (.-clientY evt) top) 114 | :moving true)) 115 | } 116 | ;; [:span {:style (:name styles)} name] 117 | [:button {:style (:play-pause-button styles) 118 | :title "Play/Pause" 119 | :on-click #(swap! loop-atom not)} 120 | (if @loop-atom 121 | [:span {:style (:pause-symbol styles)}] 122 | #_"◼" 123 | "▶")] 124 | [:span {:style (:title styles)} title] 125 | [:button {:on-click on-close 126 | :title "Close" 127 | :style (:close-button styles)} 128 | "×"] 129 | ]])))) 130 | 131 | (defn get-in-ns [ns sym] 132 | (let [parts (vec (map munge (.split (str ns) ".")))] 133 | (reduce (fnil aget (js-obj)) 134 | js/window 135 | (conj parts (str sym))))) 136 | 137 | (defn get-fn [ns sym] 138 | (fn [state] 139 | (let [f (get-in-ns ns sym) 140 | res (when (and f (= js/Function (type f))) 141 | (f state))] 142 | (or res state)))) 143 | 144 | (defn handle-make-sketch [ns form] 145 | (js/console.log "make sketch" form) 146 | (let [opts (parse-make-sketch form) 147 | old-data (@applets (:name opts)) 148 | _ (js/console.log (:name opts) old-data @applets) 149 | [width height] (:size opts) 150 | node (or (:node old-data) 151 | (js/document.createElement "div")) 152 | pos-atom (or (:pos-atom old-data) 153 | (atom {:top 20 154 | :left (- (.-innerWidth js/window) width 20)})) 155 | loop-atom (or (:loop-atom old-data) 156 | (r/atom true)) 157 | _ (js/React.unmountComponentAtNode node) 158 | sketch (quil.sketch/make-sketch 159 | ;; TODO get actual values for title & size... 160 | {:title (:title opts) 161 | :setup (fn [] 162 | (let [new-applet (quil.sketch/current-applet) 163 | old-loop (.bind (.-loop new-applet) new-applet) 164 | old-no-loop (.bind (.-noLoop new-applet) new-applet) 165 | ] 166 | (aset new-applet "loop" #(do (reset! loop-atom true) 167 | (old-loop))) 168 | (aset new-applet "noLoop" #(do (reset! loop-atom false) 169 | (old-no-loop))) 170 | (swap! applets update (:name opts) 171 | (fn [old] 172 | (when old (.exit (:applet old))) 173 | {:node node 174 | :pos-atom pos-atom 175 | :loop-atom loop-atom 176 | :applet new-applet})) 177 | (add-watch loop-atom :wat 178 | (fn [_ _ _ new-val] 179 | (if new-val 180 | (old-loop) 181 | (old-no-loop)))) 182 | (reset! current-applet new-applet) 183 | ;; Allow q/* functions to be used from the REPL 184 | (js/setTimeout #(set! quil.sketch/*applet* new-applet) 5) 185 | ((get-fn ns (:setup opts))))) 186 | :draw (get-fn ns (:draw opts)) 187 | :update (get-fn ns (:update opts)) 188 | :middleware [m/fun-mode] 189 | :size (:size opts)})] 190 | (js/document.body.appendChild node) 191 | (r/render 192 | [mobile-sketch 193 | (str (:name opts)) 194 | (:title opts) 195 | (:size opts) 196 | loop-atom 197 | pos-atom 198 | #(js/Processing. % sketch) 199 | (fn [] 200 | (when-let [data (@applets (:name opts))] 201 | (.exit (:applet data)) 202 | (when-let [parent (.-parentNode (:node data))] 203 | (.removeChild parent (:node data))) 204 | (swap! applets dissoc (:name opts))))] 205 | node) 206 | (reset! current-sketch sketch))) 207 | -------------------------------------------------------------------------------- /src/tutorial_cljs/repl.cljs: -------------------------------------------------------------------------------- 1 | (ns tutorial-cljs.repl 2 | (:require [reepl.core :as reepl] 3 | [reepl.replumb :as reepl-replumb] 4 | [replumb.core :as replumb] 5 | 6 | [reepl.show-function :as show-function] 7 | [reepl.show-devtools :as show-devtools] 8 | [reepl.show-value :as show-value] 9 | 10 | [devtools.core :as devtools] 11 | 12 | [tutorial-cljs.quil-complete :as quil-complete] 13 | 14 | [parinfer-codemirror.editor :as parinfer] 15 | [parinfer.codemirror.mode.clojure.clojure-parinfer] 16 | [reagent.core :as r] 17 | )) 18 | 19 | (def styles 20 | {:container {:display :flex 21 | :flex-direction :column 22 | :flex 1 23 | :align-self :stretch 24 | :justify-content :center 25 | :align-items :center} 26 | :choose-container { 27 | :flex 1 28 | :flex-direction :row 29 | :align-items :center 30 | :margin-bottom 5 31 | :padding-left 10 32 | } 33 | :chooser-title {:font-size "1.5em" 34 | :margin-right 10} 35 | :chooser {:font-size "1.5em" 36 | :margin-right 5} 37 | :repl {:display :flex 38 | :flex 1 39 | :align-self :stretch 40 | :margin-left 10 41 | :background-color :white 42 | :border-radius 5} 43 | :bottom {:display :flex 44 | :align-self :stretch 45 | :margin-left 10 46 | :flex-direction :row 47 | :align-items :center 48 | :margin-bottom 5 49 | :color "#ddd"} 50 | :label {:margin "0 5px" 51 | :display :flex 52 | :flex-direction :row 53 | :align-items :center 54 | :font-size ".5em" 55 | :cursor :pointer} 56 | :checkbox {:margin-right 5 57 | :font-size ".5em"} 58 | 59 | :reset-button {:border :none 60 | :background-color :transparent 61 | :cursor :pointer 62 | :color "#faa" 63 | :margin-right 20 64 | :font-size ".8em" 65 | :padding "0 5px"} 66 | 67 | :link {:color "#aaa" 68 | :text-decoration :none 69 | :margin "2px 10px 0"} 70 | }) 71 | 72 | (def replumb-opts 73 | (merge (replumb/browser-options 74 | ["/js/main.out"] 75 | reepl-replumb/fetch-file!))) 76 | 77 | ;; Used to make the repl reload-tolerant 78 | (defonce repl-state 79 | (r/atom reepl/initial-state)) 80 | 81 | (defonce complete-atom 82 | (r/atom nil)) 83 | 84 | (defn maybe-fn-docs [fn] 85 | (let [doc (reepl-replumb/doc-from-sym fn)] 86 | (when (:forms doc) 87 | (with-out-str 88 | (reepl-replumb/print-doc doc))))) 89 | 90 | (def default-settings 91 | {:vim false 92 | :warning-as-error true 93 | :parinfer true}) 94 | 95 | (defn get-settings [] 96 | (let [val js/localStorage.reeplSettings] 97 | (if-not val 98 | default-settings 99 | (try 100 | (js->clj (js/JSON.parse val) :keywordize-keys true) 101 | (catch js/Error _ 102 | default-settings))))) 103 | 104 | (defn save-settings [settings] 105 | (let [str (js/JSON.stringify (clj->js settings))] 106 | (aset js/localStorage "reeplSettings" str))) 107 | 108 | (defonce 109 | settings (r/atom (get-settings))) 110 | 111 | (defonce pi-count (atom 0)) 112 | 113 | 114 | (defn checkbox [keyword title] 115 | [:label 116 | {:style (:label styles)} 117 | [:input {:type "checkbox" 118 | :checked (keyword @settings) 119 | :style (:checkbox styles) 120 | :on-change #(swap! settings update keyword not) 121 | }] 122 | title]) 123 | 124 | ;; TODO abstract out the quil-complete stuff 125 | (defn auto-complete [sym] 126 | (let [text (str sym) 127 | quil (quil-complete/quil-prefix)] 128 | (if (and quil (= 0 (.indexOf text (str quil "/")))) 129 | (quil-complete/quil-complete quil text) 130 | (reepl-replumb/process-apropos sym)))) 131 | 132 | (defn get-docs [sym] 133 | (let [text (str sym) 134 | quil (quil-complete/quil-prefix)] 135 | (if (and quil (= 0 (.indexOf text (str quil "/")))) 136 | (quil-complete/quil-doc text) 137 | (reepl-replumb/process-doc sym)))) 138 | 139 | (defn repl-view [current-tutorial tutorials on-tutorial showers reset-text] 140 | [:div 141 | {:style (:container styles)} 142 | [:div {:style (styles :bottom)} 143 | [:div {:style (styles :choose-container)} 144 | [:span {:style (:chooser-title styles)} 145 | "Tutorial"] 146 | (into 147 | 148 | [:select {:style (:chooser styles) 149 | :value (name current-tutorial) 150 | :on-change #(on-tutorial (keyword (.-target.value %)))}] 151 | (map #(-> [:option {:value (name (:name %))} 152 | (:title %)]) tutorials)) 153 | 154 | [:button {:style (:reset-button styles) 155 | :on-click reset-text} 156 | "Revert Text"]] 157 | [checkbox :vim "Vim"] 158 | [checkbox :parinfer "Parinfer"] 159 | [checkbox :warning-as-error "Warning as error"] 160 | [:a {:href "https://github.com/jaredly/tutorial-cljs" 161 | :target :_blank 162 | :style (:link styles)} 163 | "Github"] 164 | ] 165 | [:div {:style (:repl styles)} 166 | [reepl/repl 167 | :execute #(reepl-replumb/run-repl %1 168 | (merge replumb-opts 169 | {:warning-as-error (:warning-as-error @settings)}) %2) 170 | :complete-word auto-complete 171 | :get-docs get-docs 172 | :state repl-state 173 | :complete-atom complete-atom 174 | :show-value-opts 175 | {:showers showers} 176 | :js-cm-opts {:mode "clojure-parinfer" 177 | :keyMap (if (:vim @settings) "vim" "default") 178 | :showCursorWhenSelecting true} 179 | :on-cm-init #(parinfer/parinferize! % (swap! pi-count inc) 180 | :indent-mode (.getValue %))]]]) 181 | 182 | (add-watch settings :settings #(do 183 | ;; (render-text) 184 | (save-settings %4))) 185 | -------------------------------------------------------------------------------- /src/tutorial_cljs/text/cljs.cljs: -------------------------------------------------------------------------------- 1 | (ns tutorial-cljs.text.cljs) 2 | 3 | (def text 4 | ";; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 | ;; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 | ;; +++++++++++++++ +++++++++++++++ 7 | ;; An Interactive Introduction to ClojureScript 8 | ;; +++++++++++++++ +++++++++++++++ 9 | ;; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 10 | ;; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 11 | 12 | ;; Adapted from David Nolen's tutorial for Light Table users 13 | ;; https://github.com/swannodette/lt-cljs-tutorial/blob/master/lt-cljs-tutorial.cljs 14 | 15 | ;; Using this Tutorial 16 | ;; ============================================================================ 17 | 18 | ;; # Evaluating forms 19 | ;; 20 | ;; You can evaluate forms in this tutorial by putting your cursor at the end of 21 | ;; the form and pressing \"Cmd-Enter\". The output will be displayed to the 22 | ;; right of your cursor. You can dismiss the output view (if it's in your way) 23 | ;; with \"Cmd-Shift-Enter\". 24 | ;; 25 | ;; Try evaluating this: 26 | 27 | (+ 1 2) ; <- put your cursor right after the closing ) and press Cmd-Enter 28 | 29 | ;; ^ You can also put your cursor on the following line and press Cmd-Enter 30 | 31 | ;; Ok, that was cool, but how about some data that's more complicated? 32 | 33 | {:some 10 34 | :other 20 35 | :list (range 10)} ; evaluate this, and you'll be able to interact with the result 36 | 37 | ;; # Documentation + auto-complete 38 | ;; 39 | ;; If you click `range` in that code above, the documentation for the range 40 | ;; function will appear in the bottom-right corner of this page. You can type 41 | ;; into this document, and documentation + auto-complete suggestions will 42 | ;; appear. Press Tab (and Shift-Tab) to cycle through the suggestions. 43 | ;; 44 | ;; Go ahead, put your cursor at the end of `map`, and see what other functions 45 | ;; have `map` in the name. 46 | 47 | map 48 | 49 | ;; # The REPL 50 | ;; 51 | ;; The right hand pane is a REPL where you can type in clojurescript code and 52 | ;; see the results. It will show you documentation + auto-complete suggestions 53 | ;; as well. 54 | 55 | 56 | ;; Basics 57 | ;; ============================================================================ 58 | 59 | ;; Declaring a namespaces 60 | ;; ---------------------------------------------------------------------------- 61 | 62 | ;; ClojureScript supports modularity via namespaces. They allow you to group 63 | ;; logical definitions together. 64 | 65 | (ns lt-cljs-tutorial.main 66 | (:require [clojure.string :as str])) 67 | 68 | ;; NOTE: We've evaluated this namespace definition for you already, so if you 69 | ;; type ::hello in the REPL, you'll see `:lt-cljs-tutorial.main/hello' 70 | 71 | ;; :require is how you can import functionality from a different namespace into 72 | ;; the current one. Here we are requiring `clojure.string` and giving it an 73 | ;; alias. We could write the following: 74 | 75 | (clojure.string/blank? \"\") 76 | 77 | ;; But that's really verbose compared to: 78 | 79 | (str/blank? \"\") 80 | 81 | 82 | ;; Comments 83 | ;; ---------------------------------------------------------------------------- 84 | 85 | ;; There are three ways to create comments in ClojureScript. The first way is 86 | ;; by preceding a line with a semi-colon, just like the lines you are reading 87 | ;; now. 88 | 89 | ;; The second way is by preceding a form with `#_`. This causes ClojureScript 90 | ;; to skip the evaluation of only the form immediately following, without 91 | ;; affecting the evaluation of the surrounding forms. 92 | 93 | ;; Try to reveal the secret message below: 94 | 95 | (str \"The secret word is \" #_(str/reverse \"tpircSerujolC\")) 96 | 97 | ;; Finally, you can also create a comment using the `comment` macro. One common 98 | ;; technique is to use the `comment` macro to include code to be evaluated in a 99 | ;; REPL, but which you do not normally want to be included in the compiled 100 | ;; source. 101 | 102 | ;; For example, try placing your cursor after the last `)` below and type 103 | ;; Command-ENTER: 104 | 105 | (comment 106 | 107 | (str/upper-case \"This is only a test...\") 108 | 109 | ) 110 | 111 | ;; The `comment` macro makes the whole form return `nil`. Now go back and 112 | ;; place your cursor after the middle line, then type Command-ENTER. In this way 113 | ;; you can include code samples or quick tests in-line with the rest of 114 | ;; your code. 115 | 116 | 117 | ;; Definitions 118 | ;; ---------------------------------------------------------------------------- 119 | 120 | ;; Once you have a namespace, you can start creating top level definitions in 121 | ;; that namespace. 122 | 123 | ;; You can define a top level with `def`. 124 | 125 | (def x 1) 126 | 127 | x 128 | 129 | ;; You can also refer to top level definitions by fully qualifying them. 130 | 131 | lt-cljs-tutorial.main/x 132 | 133 | ;; This means top levels can never be shadowed by locals and function 134 | ;; parameters. 135 | 136 | (let [x 2] 137 | lt-cljs-tutorial.main/x) 138 | 139 | ;; One way to define a function is like this. 140 | 141 | (def y (fn [] 1)) 142 | 143 | (y) 144 | 145 | ;; Defining functions in ClojureScript is common enough that `defn` sugar is 146 | ;; provided and idiomatic. 147 | 148 | (defn z [] 1) 149 | 150 | (z) 151 | 152 | 153 | ;; Literal data types 154 | ;; ---------------------------------------------------------------------------- 155 | 156 | ;; ClojureScript comes out of the box with the usual useful data literals. 157 | 158 | ;; Booleans 159 | 160 | (def a-boolean true) 161 | 162 | ;; Strings 163 | 164 | (def a-string \"Hello!\") 165 | 166 | ;; Regular Expressions 167 | 168 | (def a-regexp #\"\\d{3}-?\\d{3}-?\\d{4}\") 169 | 170 | ;; Numbers 171 | 172 | (def a-number 1) 173 | 174 | 175 | ;; Function literals 176 | ;; ---------------------------------------------------------------------------- 177 | 178 | ;; ClojureScript also supports a shorthand function literal which is useful 179 | ;; You can use the % and %N placeholders to represent function arguments. 180 | 181 | ;; You should not abuse the function literal notation as it degrades readability 182 | ;; outside of simple cases. It is nice for simple functional cases such as 183 | ;; the following. You could map over a ClojureScript vector like this: 184 | 185 | (map (fn [n] (* n 2)) [1 2 3 4 5]) 186 | 187 | ;; Or you can save typing a few characters like this: 188 | 189 | (map #(* % 2) [1 2 3 4 5]) 190 | 191 | 192 | ;; JavaScript data type literals 193 | ;; ---------------------------------------------------------------------------- 194 | 195 | ;; You can construct a JavaScript array with the `array` function. 196 | 197 | (def an-array (array 1 2 3)) 198 | 199 | ;; But ClojureScript also supports JavaScript data literals via the `#js` 200 | ;; reader literal. 201 | 202 | (def another-array #js [1 2 3]) 203 | 204 | ;; Similarly, you can create simple JavaScript objects with `js-obj`. 205 | 206 | (def an-object (js-obj \"foo\" \"bar\")) 207 | 208 | ;; But again you can save a few characters with `#js`. 209 | 210 | (def another-object #js {\"foo\" \"bar\"}) 211 | 212 | ;; It's important to note that `#js` is shallow, the contents of `#js` will be 213 | ;; ClojureScript data unless preceded by `#js`. 214 | 215 | ;; This is a mutable JavaScript object with an immutable ClojureScript vector 216 | ;; inside. 217 | 218 | (def shallow #js {\"foo\" [1 2 3]}) 219 | 220 | 221 | ;; Constructing a type 222 | ;; ---------------------------------------------------------------------------- 223 | 224 | ;; Of course some JavaScript data types you will want to create with a 225 | ;; constructor. 226 | 227 | ;; (js/Date.) is equivalent to new Date(). 228 | 229 | (def a-date (js/Date.)) 230 | 231 | (def another-date #inst \"2014-01-15\") 232 | 233 | ;; Note the above returns an `#inst` data literal. 234 | 235 | (def another-regexp (js/RegExp. \"\\\\d{3}-?\\\\d{3}-?\\\\d{4}\")) 236 | 237 | ;; Handy 238 | 239 | ;; NOTE: js/Foo is how you refer to global JavaScript entities of any kind. 240 | 241 | js/Date 242 | 243 | js/RegExp 244 | 245 | js/requestAnimationFrame 246 | 247 | ;; If you're curious about other JavaScript interop jump to the bottom of this 248 | ;; tutorial. 249 | 250 | 251 | ;; ClojureScript data types 252 | ;; ============================================================================ 253 | 254 | ;; Unless there is a good reason, you should generally write your ClojureScript 255 | ;; programs with ClojureScript data types. They have many advantages over 256 | ;; JavaScript data types - they present a uniform API and they are immutable. 257 | 258 | ;; Vectors 259 | ;; ---------------------------------------------------------------------------- 260 | 261 | ;; Instead of arrays, ClojureScript programmers use persistent vectors. They are 262 | ;; like arrays - they support efficient random access, efficient update 263 | ;; and efficient addition to the end. 264 | 265 | (def a-vector [1 2 3 4 5]) 266 | 267 | ;; We can get the length of a vector in constant time via `count`. 268 | 269 | (count a-vector) 270 | 271 | ;; We can add an element to the end. 272 | 273 | (def another-vector (conj a-vector 6)) 274 | 275 | ;; Note this does not mutate the array! `a-vector` will be left 276 | ;; unchanged. 277 | 278 | a-vector 279 | 280 | another-vector 281 | 282 | ;; Hallelujah! Here is where some ClojureScript magic 283 | ;; happens. `another-vector` appears to be a completely new vector 284 | ;; compared to `a-vector`. But it is not really so. Internally, the new 285 | ;; vector efficiently shares the `a-vector` structure. In this way, you 286 | ;; get the benefits of immutability without paying in performance. 287 | 288 | ;; We can access any element in a vector with `nth`. The following 289 | ;; will return the second element. 290 | 291 | (nth a-vector 1) 292 | 293 | (nth [\"foo\" \"bar\" \"baz\"] 1) 294 | 295 | ;; Or with `get`... 296 | 297 | (get a-vector 0) 298 | 299 | ;; ...which allows you to return an alternate value when the index is 300 | ;; out-of bounds. 301 | 302 | (get a-vector -1 :out-of-bounds) 303 | (get a-vector (count a-vector) :out-of-bounds) 304 | 305 | ;; Surprisingly, vectors can be treated as functions. This is actually 306 | ;; a very useful property for associative data structures to have as 307 | ;; we'll see below with sets. 308 | 309 | (a-vector 1) 310 | 311 | ([\"foo\" \"bar\" \"baz\"] 1) 312 | 313 | 314 | ;; Maps 315 | ;; ---------------------------------------------------------------------------- 316 | 317 | ;; Along with vectors, maps are the most common data type in ClojureScript. 318 | ;; Map usage is analogous to the usage of Object in JavaScript, but 319 | ;; ClojureScript maps are immutable and considerably more flexible. 320 | 321 | ;; Let's define a simple map. Note `:foo` is a ClojureScript keyword. 322 | ;; ClojureScript programmers prefer to use keywords for keys instead 323 | ;; of strings. They are more distinguishable from the rest of the 324 | ;; code, more efficient than plain strings, and they can be used in 325 | ;; function position (i.e. first position after the open parens), as 326 | ;; we'll see in a moment. 327 | 328 | (def a-map {:foo \"bar\" :baz \"woz\"}) 329 | 330 | ;; We can get the number of key-value pairs in constant time. 331 | 332 | (count a-map) 333 | 334 | ;; We can access a particular value for a key with `get`. 335 | 336 | (get a-map :foo) 337 | 338 | ;; and return an alternative value when the key is not present 339 | 340 | (get a-map :bar :not-found) 341 | 342 | ;; We can add a new key-value pair with `assoc`. 343 | 344 | (def another-map (assoc a-map :noz \"goz\")) 345 | 346 | ;; Again a-map is unchanged! Same magic as before for vectors 347 | 348 | a-map 349 | 350 | another-map 351 | 352 | ;; We can remove a key-value pair with `dissoc`. 353 | 354 | (dissoc a-map :foo) 355 | 356 | ;; Again a-map is unchanged! 357 | 358 | a-map 359 | 360 | ;; Like vectors, maps can act like functions. 361 | 362 | (a-map :foo) 363 | 364 | ;; However ClojureScript keywords themselves can act like functions and the 365 | ;; following is more idiomatic. 366 | 367 | (:foo a-map) 368 | 369 | ;; We can check if a map contains a key, with `contains?`. 370 | 371 | (contains? a-map :foo) 372 | 373 | ;; We can get all the keys in a map with `keys`. 374 | 375 | (keys a-map) 376 | 377 | ;; And all of the values with `vals`. 378 | 379 | (vals a-map) 380 | 381 | ;; We can put a lot of things in a map, even other maps 382 | (def a-nested-map {:customer-id 1e6 383 | :preferences {:nickname \"Bob\" 384 | :avatar \"http://en.gravatar.com/userimage/0/0.jpg\"} 385 | :services {:alerts {:daily true}}}) 386 | 387 | ;; and navigate its keys to get the nested value you're interested in 388 | 389 | (get-in a-nested-map [:preferences :nickname]) 390 | (get-in a-nested-map [:services :alerts :daily]) 391 | 392 | ;; or just find a top level key-value pair (i.e. MapEntry) by key 393 | 394 | (find a-nested-map :customer-id) 395 | (find a-nested-map :services) 396 | 397 | ;; There are many cool ways to create maps. 398 | 399 | (zipmap [:foo :bar :baz] [1 2 3]) 400 | 401 | (hash-map :foo 1 :bar 2 :baz 3) 402 | 403 | (apply hash-map [:foo 1 :bar 2 :baz 3]) 404 | 405 | (into {} [[:foo 1] [:bar 2] [:baz 3]]) 406 | 407 | ;; Unlike JavaScript objects, ClojureScript maps support complex keys. 408 | 409 | (def complex-map {[1 2] :one-two [3 4] :three-four}) 410 | 411 | (get complex-map [3 4]) 412 | 413 | 414 | ;; Keyword digression 415 | ;; ---------------------------------------------------------------------------- 416 | 417 | ;; Let's take a moment to digress about keywords as they are so ubiquitous 418 | ;; in ClojureScript code. 419 | 420 | (identity :foo) 421 | 422 | ;; If you add an additional preceding colon you'll get a namespaced keyword. 423 | 424 | (identity ::foo) 425 | 426 | ;; What good is this for? It allows you to put data into collections without 427 | ;; fear of namespace clashes without the tedium of manual namespacing them 428 | ;; in your source. 429 | 430 | (identity {:user/foo ::foo}) 431 | 432 | 433 | ;; Sets 434 | ;; ---------------------------------------------------------------------------- 435 | 436 | ;; ClojureScript also supports sets. 437 | 438 | (def a-set #{:cat :dog :bird}) 439 | 440 | ;; `:cat` is already in `a-set`, so it will be unchanged. 441 | 442 | (conj a-set :cat) 443 | 444 | ;; But `:zebra` isn't. 445 | 446 | (conj a-set :zebra) 447 | 448 | ;; If you haven't guessed already, `conj` is a \"polymorphic\" function that adds 449 | ;; an item to a collection. This is some of the uniformity we alluded to 450 | ;; earlier. 451 | 452 | ;; `contains?` works on sets just like it does on maps. 453 | 454 | (contains? a-set :cat) 455 | 456 | ;; Like vectors and maps, sets can also act as functions. If the argument 457 | ;; exists in the set it will be returned, otherwise the set will return nil. 458 | 459 | (#{:cat :dog :bird} :cat) 460 | 461 | ;; This is powerful when combined with conditionals. 462 | 463 | (defn check [x] 464 | (if (#{:cat :dog :bird} x) 465 | :valid 466 | :invalid)) 467 | 468 | (check :cat) 469 | (check :zebra) 470 | 471 | 472 | ;; Lists 473 | ;; ---------------------------------------------------------------------------- 474 | 475 | ;; A less common ClojureScript data structure is lists. This may be 476 | ;; surprising as ClojureScript is a Lisp, but maps, vectors and sets 477 | ;; are the 'go-to' data structures for most applications. Still, lists are sometimes 478 | ;; useful—especially when dealing with code (i.e. code is data). 479 | 480 | (def a-list '(:foo :bar :baz)) 481 | 482 | ;; `conj` is \"polymorphic\" on lists as well, and it's smart enough to 483 | ;; add the new item in the most efficient way on the basis of the 484 | ;; collection type. 485 | (conj a-list :front) 486 | 487 | ;; and lists are immutable as well 488 | 489 | a-list 490 | 491 | ;; You can get the first element of a list 492 | 493 | (first a-list) 494 | 495 | ;; or the tail of a list 496 | 497 | (rest a-list) 498 | 499 | ;; which allows you to easly verify how ClojureScript shares data 500 | ;; structure instead of inefficiently copying data for supporting 501 | ;; immutability. 502 | 503 | (def another-list (conj a-list :front)) 504 | 505 | another-list 506 | 507 | a-list 508 | 509 | (identical? (rest another-list) a-list) 510 | 511 | ;; `identical?` checks whether two things are represented by the same 512 | ;; thing in memory. 513 | 514 | 515 | ;; Equality 516 | ;; ============================================================================ 517 | 518 | ;; ClojureScript has a much simpler notion of equality than what is present 519 | ;; in JavaScript. In ClojureScript equality is always deep equality. 520 | 521 | (= {:one 1 :two \"2\"} {:one 1 :two \"2\"}) 522 | 523 | ;; Maps are not ordered. 524 | 525 | (= {:one 1 :two \"2\"} {:two \"2\" :one 1}) 526 | 527 | ;; For sequential collections, equality just works. 528 | 529 | (= [1 2 3] '(1 2 3)) 530 | 531 | ;; Again, it is possible to check whether two things are represented 532 | ;; by the same thing in memory with `identical?`. 533 | 534 | (def my-vec [1 2 3]) 535 | (def your-vec [1 2 3]) 536 | 537 | (identical? my-vec your-vec) 538 | 539 | 540 | ;; Control 541 | ;; ============================================================================ 542 | 543 | ;; In order to write useful programs, we need to be able to express 544 | ;; control flow. ClojureScript provides the usual control constructs, 545 | ;; however truth-y and false-y values are not the same as in 546 | ;; JavaScript so it's worth reviewing. 547 | 548 | ;; if 549 | ;; ---------------------------------------------------------------------------- 550 | 551 | ;; 0 is not a false-y value. 552 | 553 | (if 0 554 | \"Zero is not false-y\" 555 | \"Yuck\") 556 | 557 | ;; Nor is the empty string. 558 | 559 | (if \"\" 560 | \"An empty string is not false-y\" 561 | \"Yuck\") 562 | 563 | ;; the empty vector 564 | 565 | (if [] 566 | \"An empty vector is not false-y\" 567 | \"Yuck\") 568 | 569 | ;; the empty list 570 | 571 | (if () 572 | \"An empty list is not false-y\" 573 | \"Yuck\") 574 | 575 | ;; the empty map 576 | 577 | (if {} 578 | \"An empty map is not false-y\" 579 | \"Yuck\") 580 | 581 | ;; the empty set 582 | 583 | (if #{} 584 | \"An empty set is not false-y\" 585 | \"Yuck\") 586 | 587 | ;; and even the empty regexp 588 | 589 | (if #\"\" 590 | \"An empty regexp is not false-y\" 591 | \"Yuck\") 592 | 593 | ;; The only false-y values in ClojureScript are `nil` and `false`. `undefined` 594 | ;; is not really a valid ClojureScript value and is generally coerced to `nil`. 595 | 596 | 597 | ;; cond 598 | ;; ---------------------------------------------------------------------------- 599 | 600 | ;; Nesting `if` tends to be noisy and hard to read so ClojureScript 601 | ;; provides a `cond` macro to deal with this. 602 | 603 | (cond 604 | nil \"Not going to return this\" 605 | false \"Nope not going to return this either\" 606 | :else \"Default case\") 607 | 608 | 609 | ;; loop/recur 610 | ;; ---------------------------------------------------------------------------- 611 | 612 | ;; The most primitive looping construct in ClojureScript is `loop`/`recur`. 613 | ;; Like `let`, `loop` establishes bindings and allows you to set their initial values. 614 | ;; Like `let`, you may have a sequence of forms for the body. In tail 615 | ;; positions, you may write a `recur` statement that will set the bindings for 616 | ;; the next iteration of the `loop`. Using `loop`/`recur` is usually considered bad 617 | ;; style if a reasonable functional solution via `map`/`filter`/`reduce` or a list 618 | ;; comprehension is possible. 619 | 620 | ;; While you might write this in JavaScript: 621 | ;; 622 | ;; var ret = []; 623 | ;; for(var i = 0; i < 10; i++) ret.push(i) 624 | ;; 625 | ;; In ClojureScript you would write `loop`/`recur` like so: 626 | 627 | (loop [i 0 ret []] 628 | (if (< i 10) 629 | (recur (inc i) (conj ret i)) 630 | ret)) 631 | 632 | ;; Again avoid `loop`/`recur` unless you really need it. The loop above would 633 | ;; be better expressed as the following: 634 | 635 | (into [] (range 10)) 636 | 637 | 638 | ;; Moar functions 639 | ;; ============================================================================ 640 | 641 | ;; Functions are the essence of any significant ClojureScript program, so 642 | ;; we will dive into features that are unique to ClojureScript functions that 643 | ;; might be unfamiliar. 644 | 645 | ;; Here is a simple function that takes two arguments and adds them. 646 | 647 | (defn foo1 [a b] 648 | (+ a b)) 649 | 650 | (foo1 1 2) 651 | 652 | ;; Functions can have multiple arities. 653 | 654 | (defn foo2 655 | ([a b] (+ a b)) 656 | ([a b c] (* a b c))) 657 | 658 | (foo2 3 4) 659 | (foo2 3 4 5) 660 | 661 | ;; Multiple arities can be used to supply default values. 662 | 663 | (defn defaults 664 | ([x] (defaults x :default)) 665 | ([x y] [x y])) 666 | 667 | (defaults :explicit) 668 | (defaults :explicit1 :explicit2) 669 | 670 | ;; Functions support rest arguments. 671 | 672 | (defn foo3 [a b & d] 673 | [a b d]) 674 | 675 | (foo3 1 2) 676 | (foo3 1 2 3 4) 677 | 678 | ;; You can apply functions. 679 | 680 | (apply + [1 2 3 4 5]) 681 | 682 | 683 | ;; multimethods 684 | ;; ---------------------------------------------------------------------------- 685 | 686 | ;; Often when you need some polymorphism, and performance isn't an issue, 687 | ;; multimethods will suffice. Multimethods are functions that allow open 688 | ;; extension, but instead of limiting dispatch to type, dispatch is controlled 689 | ;; by whatever value the dispatch fn originally supplied to `defmulti` returns. 690 | 691 | ;; Here is the simplest multimethod you can write. It simply dispatches on 692 | ;; the value received. 693 | 694 | (defmulti simple-multi identity) 695 | 696 | ;; Now we can define methods for particular values. 697 | 698 | (defmethod simple-multi 1 699 | [value] \"Dispatched on 1\") 700 | 701 | (simple-multi 1) 702 | 703 | (defmethod simple-multi \"foo\" 704 | [value] \"Dispatched on foo\") 705 | 706 | (simple-multi \"foo\") 707 | 708 | ;; However we haven't defined a case for \"bar\" 709 | ; (Highlight and evaluate the `simple-multi` form below) 710 | (comment 711 | (simple-multi \"bar\") 712 | ) 713 | 714 | 715 | ;; Here is a function that takes a list. It dispatches on the first element 716 | ;; of the list! 717 | ;; Note that this example uses destructuring, which is covered later. 718 | 719 | (defmulti parse (fn [[f & r :as form]] f)) 720 | 721 | (defmethod parse 'if 722 | [form] {:op :if}) 723 | 724 | (defmethod parse 'let 725 | [form] {:op :let}) 726 | 727 | (parse '(if a b c)) 728 | (parse '(let [x 1] x)) 729 | 730 | 731 | ;; Scoping 732 | ;; ============================================================================ 733 | 734 | ;; Unlike JavaScript, there is no hoisting in ClojureScript. ClojureScript 735 | ;; has lexical scoping. 736 | 737 | (def some-x 1) 738 | 739 | (let [some-x 2] 740 | some-x) 741 | 742 | some-x 743 | 744 | ;; Closures 745 | ;; ---------------------------------------------------------------------------- 746 | 747 | ;; Could a language with such a name miss closures? Surely it can't. You 748 | ;; may be already familiar with them in JavaScript, even if it's a 749 | ;; variable scoped language. 750 | 751 | (let [a 1e3] 752 | (defn foo [] 753 | (* a a)) 754 | (defn bar [] 755 | (+ (foo) a))) 756 | 757 | ;; Above we defined `foo` and `bar` functions inside the scope of a 758 | ;; `let` form and they both know about `a` (i.e. they close over `a`) 759 | ;; Note, even if defined inside a `let`, `foo` and `bar` are available 760 | ;; in the outer scope. This is because all `def` expressions are always 761 | ;; top level. See the footnote at the end of this section. 762 | 763 | 764 | (foo) 765 | (bar) 766 | 767 | ;; And Nobody else. 768 | 769 | (comment 770 | (defn baz [] 771 | (type a)) 772 | (baz) 773 | ) 774 | 775 | ;; That's why some people say that closures are the poor man's objects. 776 | ;; They encapsulate the information as well. 777 | 778 | ;; But in ClojureScript, functions' parameters and let bindings' locals 779 | ;; are not mutable! That goes for loop locals, too! 780 | 781 | (let [fns (loop [i 0 ret []] 782 | (if (< i 10) 783 | (recur (inc i) (conj ret (fn [] i))) 784 | ret))] 785 | (map #(%) fns)) 786 | 787 | ;; In JavaScript you would see a list of ten 9s. In ClojureScript we 788 | ;; see the expected numbers from 0 to 9. 789 | 790 | ;; FOOTNOTE: 791 | ;; 792 | ;; `def` expressions (including `defn`) are always top level. People familiar 793 | ;; with Scheme or other Lisps often mistakenly write the following in Clojure: 794 | 795 | (defn not-scheme [] 796 | (defn no-no-no [])) 797 | 798 | ;; This is almost always incorrect. If you need to write a local function just 799 | ;; do it with a let binding. 800 | 801 | (defn outer-fn [] 802 | (let [inner-fn (fn [])])) 803 | 804 | 805 | ;; Destructuring 806 | ;; ============================================================================ 807 | 808 | ;; In any serious ClojureScript program, there will be significant amounts of 809 | ;; data manipulation. Again, we will see that ClojureScript's uniformity 810 | ;; pays off. 811 | 812 | ;; In ClojureScript anywhere bindings are allowed (like `let` or function 813 | ;; parameters), destructuring is allowed. This is similar to the destructuring 814 | ;; proposed for ES6, but the system provided in ClojureScript benefits from 815 | ;; all the collections supporting uniform access. 816 | 817 | 818 | ;; Sequence destructuring 819 | ;; ---------------------------------------------------------------------------- 820 | 821 | ;; Destructuring sequential types is particularly useful. 822 | 823 | (let [[f & r] '(1 2 3)] 824 | f) 825 | 826 | (let [[f & r] '(1 2 3)] 827 | r) 828 | 829 | (let [[r g b] [255 255 150]] 830 | g) 831 | 832 | ;; _ is just a convention for saying that you are not interested in the 833 | ;; item at the corresponding position. It has no other special meaning. 834 | ;; Here we're only interested in the third local variable named `b`. 835 | 836 | (let [[_ _ b] [255 255 150]] 837 | b) 838 | 839 | ;; destructuring function arguments works just as well. Here we are 840 | ;; only interested in the second argument `g`. 841 | 842 | (defn green [[_ g _]] g) 843 | 844 | (green [255 255 150]) 845 | 846 | 847 | ;; Map destructuring 848 | ;; ---------------------------------------------------------------------------- 849 | 850 | ;; Map destructuring is also useful. Here we destructure the value for the 851 | ;; `:foo` key and bind it to a local `f`, and the value for `:baz` key 852 | ;; and bind it to a local `b`. 853 | 854 | (let [{f :foo b :baz} {:foo \"bar\" :baz \"woz\"}] 855 | [f b]) 856 | 857 | ;; If we don't want to rename, we can just use `:keys`. 858 | 859 | (let [{:keys [first last]} {:first \"Bob\" :last \"Smith\"}] 860 | [first last]) 861 | 862 | ; We can also destructure a nested map 863 | 864 | (let [{:keys [first last] {:keys [addr1 addr2]} :address} {:first \"Bob\" :last \"Smith\" :address {:addr1 \"123\" :addr2 \"Main street\"}}] 865 | [first last addr1 addr2]) 866 | 867 | ; Similar to :keys for keyword, :strs and :syms directives are available for matching string and symbol :keys 868 | 869 | (let [{:strs [first last]} {\"first\" \"Bob\" \"last\" \"Smith\"}] 870 | [first last]) 871 | 872 | (let [{:syms [first last]} {'first \"Bob\" 'last \"Smith\"}] 873 | [first last]) 874 | 875 | ;; The above map destructuring form is very useful when you need to 876 | ;; define a function with optional, non positional and defaulted 877 | ;; arguments. 878 | 879 | (defn magic [& {:keys [k g h] 880 | :or {k 1 881 | g 2 882 | h 3}}] 883 | (hash-map :k k 884 | :g g 885 | :h h)) 886 | 887 | (magic) 888 | (magic :k 10) 889 | (magic :g 100) 890 | (magic :h 1000) 891 | (magic :k 10 :g 100 :h 1000) 892 | (magic :h 1000 :k 10 :g 100) 893 | 894 | ;; Sequences 895 | ;; ============================================================================ 896 | 897 | ;; We said that ClojureScript data structures are to be preferred as they 898 | ;; provide a uniform interface. All ClojureScript collections satisfy 899 | ;; the ISeqable protocol, which means iteration is uniform 900 | ;; (i.e. polymorphic) for all collection types. 901 | 902 | 903 | ;; Map / Filter / Reduce 904 | ;; ---------------------------------------------------------------------------- 905 | 906 | ;; ClojureScript supports the same bells and whistles out of the box that you may 907 | ;; be familiar with from other functional programming languages or JavaScript 908 | ;; libraries such as Underscore.js 909 | 910 | (map inc [0 1 2 3 4 5 6 7 8 9]) 911 | 912 | (filter even? (range 10)) 913 | 914 | (remove odd? (range 10)) 915 | 916 | ;; ClojureScript's `map` and `filter` operations are lazy. You can stack up 917 | ;; operations without getting too concerned about multiple traversals. 918 | 919 | (map #(* % %) (filter even? (range 20))) 920 | 921 | (reduce + (range 100)) 922 | 923 | 924 | ;; List comprehensions 925 | ;; ---------------------------------------------------------------------------- 926 | 927 | ;; ClojureScript supports the list comprehensions you might know from various 928 | ;; languages. List comprehensions are sometimes more natural or more readable 929 | ;; than a chain of `map` and `filter` operations. 930 | 931 | (for [x (range 1 10) 932 | y (range 1 10)] 933 | [x y]) 934 | 935 | (for [x (range 1 10) 936 | y (range 1 10) 937 | :when (and (zero? (rem x y)) 938 | (even? (quot x y)))] 939 | [x y]) 940 | 941 | (for [x (range 1 10) 942 | y (range 1 10) 943 | :let [prod (* x y)]] 944 | [x y prod]) 945 | 946 | 947 | ;; Seqable collections 948 | ;; ---------------------------------------------------------------------------- 949 | 950 | ;; Most ClojureScript collections can be coerced into sequences. 951 | 952 | (seq {:foo \"bar\" :baz \"woz\"}) 953 | (seq #{:cat :dog :bird}) 954 | (seq [1 2 3 4 5]) 955 | (seq '(1 2 3 4 5)) 956 | 957 | ;; Many ClojureScript functions will call `seq` on their arguments in order to 958 | ;; provide the expected behavior. The following demonstrates that you can 959 | ;; uniformly iterate over all the ClojureScript collections! 960 | 961 | (first {:foo \"bar\" :baz \"woz\"}) 962 | (rest {:foo \"bar\" :baz \"woz\"}) 963 | 964 | (first #{:cat :dog :bird}) 965 | (rest #{:cat :dog :bird}) 966 | 967 | (first [1 2 3 4 5]) 968 | (rest [1 2 3 4 5]) 969 | 970 | (first '(1 2 3 4 5)) 971 | (rest '(1 2 3 4 5)) 972 | 973 | 974 | ;; Metadata 975 | ;; ============================================================================ 976 | 977 | ;; All of the ClojureScript standard collections support metadata. Metadata 978 | ;; is a useful way to annotate data without affecting equality. The 979 | ;; ClojureScript compiler uses this language feature to great effect. 980 | 981 | ;; You can add metadata to a ClojureScript collection with `with-meta`. The 982 | ;; metadata must be a map. 983 | 984 | (def plain-data [0 1 2 3 4 5 6 7 8 9]) 985 | 986 | (def decorated-data (with-meta plain-data {:url \"http://lighttable.com\"})) 987 | 988 | ;; Metadata has no effect on equality. 989 | 990 | (= plain-data decorated-data) 991 | 992 | ;; You can access metadata with `meta`. 993 | 994 | (meta decorated-data) 995 | 996 | 997 | ;; Error Handling 998 | ;; ============================================================================ 999 | 1000 | ;; Error handling in ClojureScript is relatively straightforward and more or 1001 | ;; less similar to what is offered in JavaScript. 1002 | 1003 | ;; You can construct an error like this. 1004 | 1005 | (js/Error. \"Oops\") 1006 | 1007 | ;; You can throw an error like this. 1008 | ;; (Highlight and evaluate the `throw` form below) 1009 | 1010 | (comment 1011 | (throw (js/Error. \"Oops\")) 1012 | ) 1013 | 1014 | ;; You can catch an error like this. 1015 | 1016 | (try 1017 | (throw (js/Error. \"Oops\")) 1018 | (catch js/Error e 1019 | e)) 1020 | 1021 | ;; JavaScript unfortunately allows you to throw anything. You can handle 1022 | ;; this in ClojureScript with the following. 1023 | 1024 | (try 1025 | (throw (js/Error. \"Oops\")) 1026 | (catch :default e 1027 | e)) 1028 | 1029 | ;; Catches are optional. You can also use multiple forms to handle different types of errors. 1030 | 1031 | (try 1032 | (throw (js/Error. \"Oops\")) 1033 | (catch js/Error e 1034 | e) 1035 | (catch Error e 1036 | e) 1037 | (finally 1038 | \"Cleanup here\")) 1039 | 1040 | 1041 | ;; Mutation 1042 | ;; ============================================================================ 1043 | 1044 | ;; Atoms 1045 | ;; ---------------------------------------------------------------------------- 1046 | 1047 | ;; A little bit of mutability goes a long way. ClojureScript does not offer 1048 | ;; any traditional mutable data structures, however it does support identities 1049 | ;; that can evolve over time via `atom`. 1050 | 1051 | (def x (atom 1)) 1052 | 1053 | ;; You can dereference the value of an atom with `@`. 1054 | 1055 | @x 1056 | 1057 | ;; This is equivalent to calling `deref`. 1058 | 1059 | (deref x) 1060 | 1061 | ;; If you want to change the value of an atom you can use `reset!` which returns 1062 | ;; the new value. It's idiomatic to add the bang char `!` at the end of function 1063 | ;; names mutating objects. 1064 | 1065 | (reset! x 2) 1066 | 1067 | x 1068 | 1069 | @x 1070 | 1071 | ;; swap! 1072 | ;; ------------------------------------------------------------------------------ 1073 | 1074 | ;; If you want to change the value of an atom on the basis of its current value, 1075 | ;; you can use `swap!`. In its simplest form, `swap!` accepts as a first argument 1076 | ;; the atom itself and as a second argument an updating function of one argument 1077 | ;; which will be instantiated with the current value of the atom. `swap!` returns 1078 | ;; the new value of the atom. 1079 | 1080 | (swap! x inc) 1081 | 1082 | x 1083 | 1084 | @x 1085 | 1086 | ;; If your updating function needs extra arguments to calculate the new value, you 1087 | ;; have to pass them as extra arguments to `swap!` after the updating function 1088 | ;; itself. 1089 | 1090 | (swap! x (fn [old extra-arg] 1091 | (+ old extra-arg)) 39) 1092 | 1093 | x 1094 | 1095 | @x 1096 | 1097 | ;; As usual when anonymous functions are simple enough, it's idiomatic to use 1098 | ;; the condensed form. 1099 | 1100 | (swap! x #(- %1 %2) 42) 1101 | 1102 | x 1103 | 1104 | @x 1105 | 1106 | ;; Note that the updating function has to be free of side-effects because a 1107 | ;; waiting writer could call it more than once in a spin loop. 1108 | 1109 | 1110 | ;; set! 1111 | ;; ---------------------------------------------------------------------------- 1112 | 1113 | ;; Sometimes you need to mutate existing JavaScript objects. For this you 1114 | ;; have `set!`. 1115 | 1116 | (def c (.createElement js/document \"canvas\")) 1117 | (def ctxt (.getContext c \"2d\")) 1118 | 1119 | ;; We can use property access with `set!` to change the fill color of a 1120 | ;; a canvas rendering context. 1121 | 1122 | (set! (.-fillColor ctxt) \"#ffffff\") 1123 | 1124 | 1125 | ;; The ClojureScript Standard Library 1126 | ;; ============================================================================ 1127 | 1128 | ;; The ClojureScript standard library largely mirrors the Clojure standard 1129 | ;; library with the exception of functionality that assumes a multithreaded 1130 | ;; environment, first class namespaces, and Java numerics. 1131 | 1132 | ;; Here are some highlights and patterns that newcomers to ClojureScript might 1133 | ;; find useful. Remember you can type Control-Shift-D at anytime to bring up 1134 | ;; the documentation panel to see what any of these function do. 1135 | 1136 | (apply str (interpose \", \" [\"Bob\" \"Mary\" \"George\"])) 1137 | 1138 | ((juxt :first :last) {:first \"Bob\" :last \"Smith\"}) 1139 | 1140 | (def people [{:first \"John\" :last \"McCarthy\"} 1141 | {:first \"Alan\" :last \"Kay\"} 1142 | {:first \"Joseph\" :last \"Licklider\"} 1143 | {:first \"Robin\" :last \"Milner\"}]) 1144 | 1145 | (map :first people) 1146 | 1147 | (take 5 (repeat \"red\")) 1148 | 1149 | (take 5 (repeat \"blue\")) 1150 | 1151 | (take 5 (interleave (repeat \"red\") (repeat \"blue\"))) 1152 | 1153 | (take 10 (cycle [\"red\" \"white\" \"blue\"])) 1154 | 1155 | (partition 2 [:a 1 :b 2 :c 3 :d 4 :e 5]) 1156 | 1157 | (partition 2 1 [:a 1 :b 2 :c 3 :d 4 :e 5]) 1158 | 1159 | (take-while #(< % 5) (range 10)) 1160 | 1161 | (drop-while #(< % 5) (range 10)) 1162 | 1163 | 1164 | ;; Protocols 1165 | ;; ============================================================================ 1166 | 1167 | ;; The ClojureScript language is constructed on a rich set of protocols. The 1168 | ;; same uniformity provided by ClojureScript collections can be extended to 1169 | ;; your own types or even types that you do not control! 1170 | 1171 | ;; A lot of the uniform power we saw early was because the ClojureScript 1172 | ;; collections are implemented in terms of protocols. Collections can be 1173 | ;; coerced into sequences because they implement ISeqable. You can use `get` 1174 | ;; on vectors and maps because they implement ILookup. 1175 | 1176 | (get {:foo \"bar\"} :foo) 1177 | (get [:cat :bird :dog] 1) 1178 | 1179 | ;; Map destructuring actually desugars into `get` calls. That means if you extend 1180 | ;; your type to ILookup it will also support map destructuring! 1181 | 1182 | 1183 | ;; extend-type 1184 | ;; ---------------------------------------------------------------------------- 1185 | 1186 | ;; ClojureScript supports custom extension to types that avoid many of the 1187 | ;; pitfalls that you encounter in other languages. For example imagine we have 1188 | ;; some awesome polymorphic functionality in mind. 1189 | 1190 | (defprotocol MyProtocol (awesome [this])) 1191 | 1192 | ;; It's idiomatic to name the first argument of a protocol's functions 1193 | ;; as `this` which reminds you that it is the argument used by 1194 | ;; ClojureScript to dispatch the right function implementation on the 1195 | ;; basis of the type of the value of `this` 1196 | 1197 | ;; Now imagine we want JavaScript strings to participate. We can do this 1198 | ;; simply. 1199 | 1200 | (extend-type string 1201 | MyProtocol 1202 | (awesome [this] (vector this \"Totally awesome!\"))) 1203 | 1204 | (awesome \"Is this awesome?\") 1205 | 1206 | 1207 | ;; extend-protocol 1208 | ;; ---------------------------------------------------------------------------- 1209 | 1210 | ;; Sometimes you want to extend several types to a protocol at once. You can 1211 | ;; use extend-protocol for this. extend-protocol simply desugars into multiple 1212 | ;; extend-type forms. 1213 | 1214 | ;; As said while learning about `let` special form, when we're not 1215 | ;; interested in the value of an argument it's idiomatic to use the 1216 | ;; underscore as a placeholder like above. 1217 | 1218 | (extend-protocol MyProtocol 1219 | js/Date 1220 | (awesome [_] \"Having an awesome time!\") 1221 | number 1222 | (awesome [_] \"I'm an awesome number!\")) 1223 | 1224 | (awesome #inst \"2014\") 1225 | (awesome 5) 1226 | 1227 | 1228 | ;; reify 1229 | ;; ---------------------------------------------------------------------------- 1230 | 1231 | ;; Sometimes it's useful to make an anonymous type which implements various 1232 | ;; protocols. 1233 | 1234 | ;; For example say we want a JavaScript object to support ILookup. Now we don't 1235 | ;; want to blindly `extend-type object`, that would pollute the behavior of plain 1236 | ;; JavaScript objects for everyone. 1237 | 1238 | ;; Instead we can provide a helper function that takes an object and returns 1239 | ;; something that provides this functionality. 1240 | 1241 | (defn ->lookup [obj] 1242 | (reify 1243 | ILookup 1244 | (-lookup [this k] 1245 | (-lookup this k nil)) 1246 | (-lookup [this k not-found] 1247 | (let [k (name k)] 1248 | (if (.hasOwnProperty obj k) 1249 | (aget obj k) 1250 | not-found))))) 1251 | 1252 | ;; We can then selectively make JavaScript objects work with `get`. 1253 | 1254 | (get (->lookup #js {\"foo\" \"bar\"}) :foo) 1255 | 1256 | ;; But this also means we get destructuring on JavaScript objects. 1257 | 1258 | (def some-object #js {\"foo\" \"bar\" \"baz\" \"woz\"}) 1259 | 1260 | (let [{:keys [foo baz]} (->lookup some-object)] 1261 | [foo baz]) 1262 | 1263 | 1264 | ;; specify 1265 | ;; ---------------------------------------------------------------------------- 1266 | 1267 | ;; TODO fill this out 1268 | 1269 | ;; Macros 1270 | ;; ============================================================================ 1271 | 1272 | 1273 | ;; Types & Records 1274 | ;; ============================================================================ 1275 | 1276 | ;; deftype 1277 | ;; ---------------------------------------------------------------------------- 1278 | 1279 | ;; Sometimes a map will simply not suffice, in these cases you will want to 1280 | ;; make your own custom type. 1281 | 1282 | (deftype Foo [a b]) 1283 | 1284 | ;; It's idiomatic to use CamelCase to name a `deftype`. You can instantiate a 1285 | ;; deftype instance using the same constructor pattern we've already discussed. 1286 | 1287 | (Foo. 1 2) 1288 | 1289 | ;; You can access properties of a deftype instance using property access 1290 | ;; syntax. 1291 | 1292 | (.-a (Foo. 1 2)) 1293 | 1294 | ;; You can implement protocol methods on a deftype. Note that the first 1295 | ;; argument to any deftype or defrecord method is the instance itself. 1296 | ;; The dash in `-count` has no special meaning. It's just a convention for 1297 | ;; the core ClojureScript protocols. You need not adopt it. 1298 | 1299 | (deftype Foo [a b] 1300 | ICounted 1301 | (-count [this] 2)) 1302 | 1303 | (count (Foo. 1 2)) 1304 | 1305 | ;; Sometimes it's useful to implement methods directly on the deftype. 1306 | 1307 | (deftype Foo [a b] 1308 | Object 1309 | (toString [this] (str a \", \" b))) 1310 | 1311 | (.toString (Foo. 1 2)) 1312 | 1313 | ;; deftype fields are immutable unless specified. The following will not compile. 1314 | ;; (To prove it to yourself, highlight and evaluate the `deftype` form below.) 1315 | 1316 | (comment 1317 | 1318 | (deftype Foo [a ^:mutable b] 1319 | Object 1320 | (setA [this val] (set! a val))) 1321 | 1322 | ) 1323 | 1324 | ;; The following will compile. 1325 | 1326 | (deftype Foo [a ^:mutable b] 1327 | Object 1328 | (setB [this val] (set! b val))) 1329 | 1330 | 1331 | ;; defrecord 1332 | ;; ---------------------------------------------------------------------------- 1333 | 1334 | ;; `deftype` doesn't provide much out of the box. Often what you want to do is 1335 | ;; have a domain object that acts more or less like a map. This is what 1336 | ;; `defrecord` is for. 1337 | 1338 | ;; Like `deftype`, it's idiomatic to use CamelCase to name a `defrecord`. 1339 | 1340 | (defrecord Person [first last]) 1341 | 1342 | ;; You can construct an instance in the usual way. 1343 | 1344 | (Person. \"Bob\" \"Smith\") 1345 | 1346 | ;; Or you can use the provided constructors. 1347 | 1348 | (->Person \"Bob\" \"Smith\") 1349 | 1350 | (map->Person {:first \"Bob\" :last \"Smith\"}) 1351 | 1352 | ;; It's considered idiomatic (and recommended) to define a factory function 1353 | ;; which returns the created instance of a defrecord/deftype. It's idiomatic to use 1354 | ;; dash-case for factories names. 1355 | 1356 | (defn person [first last] 1357 | (->Person first last)) 1358 | 1359 | ;; records work like maps 1360 | 1361 | (seq (person \"Bob\" \"Smith\")) 1362 | 1363 | (:first (person \"Bob\" \"Smith\")) 1364 | 1365 | (keys (person \"Bob\" \"Smith\")) 1366 | 1367 | (vals (person \"Bob\" \"Smith\")) 1368 | 1369 | ;; both deftype and defrecord are open to dynamic extensions (i.e. open class) 1370 | 1371 | (keys (assoc (person \"Bob\" \"Smith\") :age 18)) 1372 | 1373 | 1374 | ;; Records & Protocols 1375 | ;; ---------------------------------------------------------------------------- 1376 | 1377 | ;; You can extend a defrecord to satisfy a protocol as you do with deftype. 1378 | 1379 | (extend-type Person 1380 | MyProtocol 1381 | (awesome [this] 1382 | (str (:last this) \", \" (:first this)))) 1383 | 1384 | (awesome (person \"Bob\" \"Smith\")) 1385 | 1386 | (satisfies? MyProtocol (person \"Bob\" \"Smith\")) 1387 | 1388 | ;; Or you can extend a protocol on a defrecord. 1389 | 1390 | (extend-protocol MyProtocol 1391 | Person 1392 | (awesome [this] 1393 | (str (:last this) \", \" (:first this)))) 1394 | 1395 | (awesome (person \"Bob\" \"Smith\")) 1396 | 1397 | (satisfies? MyProtocol (person \"Bob\" \"Smith\")) 1398 | 1399 | ;; If you need a more sophisticated form of polymorphism consider multimethods. 1400 | 1401 | ;; If you mix types/records with protocols you are modeling your problem with an 1402 | ;; object oriented approach, which is sometimes useful. 1403 | 1404 | ;; Note ClojureScript does not offer a direct form of inheritance. Instead, 1405 | ;; reuse/extension by composition is encouraged. It's best to avoid 1406 | ;; deftype/defrecord and model your problem with plain maps. You can easily 1407 | ;; switch to records later on down the line. 1408 | 1409 | (defrecord Contact [person email]) 1410 | 1411 | ;; Even if it's not required, remember to define a factory function to create 1412 | ;; instances of the new Contact record type by internally calling the factory 1413 | ;; function for the Person record type. 1414 | 1415 | (defn contact [first last email] 1416 | (->Contact (person first last) email)) 1417 | 1418 | (contact \"Bob\" \"Smith\" \"bob.smith@acme.com\") 1419 | 1420 | ;; And extend the protocol on defrecord as well. 1421 | 1422 | (extend-protocol MyProtocol 1423 | Contact 1424 | (awesome [this] 1425 | (str (awesome (:person this)) \", \" (:email this)))) 1426 | 1427 | (awesome (contact \"Bob\" \"Smith\" \"bob.smith@acme.com\")) 1428 | 1429 | ;; To change the value of a nested key you use 'assoc-in', like with maps. 1430 | 1431 | (assoc-in (contact \"Bob\" \"Smith\" \"bob.smith@acme.com\") 1432 | [:person :first] \"Robert\") 1433 | 1434 | ;; If you need to use the previous value of a nested field for calculating the 1435 | ;; new one, you can use 'update-in', like with maps. 1436 | 1437 | (update-in (contact \"Bob\" \"Smith\" \"bob.smith@acme.com\") 1438 | [:person :first] #(str/replace %1 #\"Bob\" %2) \"Robert\") 1439 | 1440 | ;; As said, the main difference with the majority of OO languages is that your 1441 | ;; instances of deftypes/defrecords are immutable. 1442 | 1443 | (def bob (contact \"Bob\" \"Smith\" \"bob.smith@acme.com\")) 1444 | 1445 | (update-in bob [:person :first] #(str/replace %1 #\"Bob\" %2) \"Robert\") 1446 | 1447 | (get-in bob [:person :first]) 1448 | 1449 | 1450 | ;; JavaScript Interop 1451 | ;; ============================================================================ 1452 | 1453 | ;; Property Access 1454 | ;; ---------------------------------------------------------------------------- 1455 | 1456 | (def a-date (js/Date.)) 1457 | 1458 | ;; You can access properties with the `.-` property access syntax. 1459 | 1460 | (.-getSeconds a-date) 1461 | 1462 | 1463 | ;; Method Calls 1464 | ;; ---------------------------------------------------------------------------- 1465 | 1466 | ;; Methods can be invoked with the `.` syntax. 1467 | 1468 | (.getSeconds a-date) 1469 | 1470 | ;; The above desugars into the following. 1471 | 1472 | (. a-date (getSeconds)) 1473 | 1474 | ;; For example, you can write a `console.log` call like so. 1475 | 1476 | (. js/console (log \"Interop!\")) 1477 | 1478 | 1479 | ;; Primitive Array Operations 1480 | ;; ---------------------------------------------------------------------------- 1481 | 1482 | ;; When writing performance sensitive code, sometimes dealing with mutable 1483 | ;; arrays is unavoidable. ClojureScript provides a variety of functions for 1484 | ;; creating and manipulating JavaScript arrays. 1485 | 1486 | ;; You can make an array of specific size with `make-array` 1487 | 1488 | (make-array 32) 1489 | 1490 | ;; You can access an element of an array with `aget`. 1491 | 1492 | (aget #js [\"one\" \"two\" \"three\"] 1) 1493 | 1494 | ;; You can access nested arrays with `aget`. 1495 | 1496 | (aget #js [#js [\"one\" \"two\" \"three\"]] 0 1) 1497 | 1498 | ;; You can set the contents of an array with aset. 1499 | 1500 | (def yucky-stuff #js [1 2 3]) 1501 | 1502 | (aset yucky-stuff 1 4) 1503 | 1504 | yucky-stuff 1505 | ") 1506 | -------------------------------------------------------------------------------- /src/tutorial_cljs/text/quil.cljs: -------------------------------------------------------------------------------- 1 | (ns tutorial-cljs.text.quil) 2 | 3 | (def text 4 | ";; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 | ;; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 | ;; +++++++++++++++ ++++++++++++++++ 7 | ;; An Interactive Introduction to Quil 8 | ;; +++++++++++++++ ++++++++++++++++ 9 | ;; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 10 | ;; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 11 | 12 | ;; ============================================================================ 13 | ;; Using this Tutorial 14 | ;; ============================================================================ 15 | 16 | ;; # Evaluating forms 17 | ;; 18 | ;; You can evaluate forms in this tutorial by putting your cursor at the end of 19 | ;; the form and pressing \"Cmd-Enter\". The output will be displayed to the 20 | ;; right of your cursor. You can dismiss the output view (if it's in your way) 21 | ;; with \"Cmd-Shift-Enter\". 22 | ;; 23 | ;; Try evaluating this: 24 | 25 | (+ 1 2) ; <- put your cursor right after the closing ) and press Cmd-Enter 26 | 27 | ;; ^ You can also put your cursor on the following line and press Cmd-Enter 28 | 29 | ;; Ok, that was cool, but how about some data that's more complicated? 30 | {:some 10 31 | :other 20 32 | :list (range 10)} ; evaluate this, and you'll be able to interact with the result 33 | 34 | ;; # Documentation + auto-complete 35 | ;; 36 | ;; If you click `range` in that code above, the documentation for the range 37 | ;; function will appear in the bottom-right corner of this page. You can type 38 | ;; into this document, and documentation + auto-complete suggestions will 39 | ;; appear. Press Tab (and Shift-Tab) to cycle through the suggestions. 40 | ;; 41 | ;; Go ahead, put your cursor at the end of `map`, and see what other functions 42 | ;; have `map` in the name. 43 | 44 | map 45 | 46 | ;; # The REPL 47 | ;; 48 | ;; The right hand pane is a REPL where you can type in clojurescript code and 49 | ;; see the results. It will show you documentation + auto-complete suggestions 50 | ;; as well. 51 | 52 | 53 | ;; ============================================================================ 54 | ;; The Setup 55 | ;; ============================================================================ 56 | 57 | ;; We've already evaluated this for you, so you're already in the tutorial.quil ns 58 | (ns tutorial.quil 59 | (:require [quil.core :as q])) 60 | 61 | ;; We start with a fairly simple setup function, no state, and empty update & 62 | ;; draw functions so we can experiment at the REPL 63 | 64 | (defn setup [] 65 | (q/smooth) 66 | (q/frame-rate 10) 67 | (q/background 255) 68 | {}) 69 | 70 | (defn q-update [state] 71 | state) 72 | 73 | (defn draw [state] 74 | ) 75 | 76 | ;; In normal quil, this `makesketch` would be `q/defsketch`, but we're doing some 77 | ;; magic so that it will work in this tutorial enviornment. The API is the same, 78 | ;; although we automatically enable the `fun-mode` middleware for you. 79 | (makesketch example 80 | :title \"My Sketch\" 81 | :setup setup 82 | :update q-update 83 | :draw draw 84 | :size [323 200]) 85 | 86 | ;; When you evaluate this (Cmd-Enter on the line below it), a new quil sketch 87 | ;; will open. If you evaluate it again, the sketch will reload (setup will run 88 | ;; again, etc.). If you change `example` to some other name, or create a new 89 | ;; `(makesketch)` form with a different name, another sketch will open. The most 90 | ;; recent sketch you've created will also be available to the REPL -- if you 91 | ;; execute `q/rect` in the REPL or in this page, it will be evaluated on the most 92 | ;; recently created sketch. 93 | 94 | 95 | ;; ============================================================================ 96 | ;; Drawing 97 | ;; ============================================================================ 98 | ;; Once you've made the sketch, we can start drawing on it. 99 | 100 | ;; # Solids & fill 101 | 102 | (q/rect 20 20 200 200) 103 | 104 | ;; You can change the fill 105 | (q/fill 200) ; one argument (0-255) means gray 106 | 107 | (q/rect 20 20 200 200) ; now evalute again, and see that the color has changed 108 | 109 | (q/fill 0 255 100) ; three arguments (0-255) mean red, green, blue 110 | 111 | (q/rect 20 20 200 200) 112 | 113 | (q/ellipse 100 100 60 30) ; unlike the rect, the ellipse is *centered* around the first two args 114 | 115 | ;; # Lines & Stroke 116 | 117 | (q/line 0 0 200 100) 118 | 119 | (q/stroke 255 0 100) ; you can also pass one arg for gray 120 | (q/stroke-weight 20) 121 | 122 | (q/line 0 0 200 100) ;; now try that line ^ again 123 | 124 | ;; You can clear the canvas at any time with this 125 | (q/background 255) ; 255 = white 126 | 127 | ;; Now lets use a clojurescript loop to do something more fun 128 | (dorun 129 | (for [angle (range 0 360 10)] 130 | (let [x (q/cos (q/radians angle)) 131 | y (q/sin (q/radians angle))] 132 | (q/stroke (/ angle 2)) 133 | (q/line 100 100 (+ 100 (* x 100)) (+ 100 (* y 100)))))) 134 | 135 | ;; ============================================================================ 136 | ;; Animating 137 | ;; ============================================================================ 138 | ;; Here we only have to change the draw function, and the 139 | ;; sketch automatically updates to show the changes. 140 | 141 | (defn draw [state] 142 | (q/stroke (q/random 255)) 143 | ;; (q/stroke (q/random 255) (q/random 255) (q/random 255)) 144 | (q/stroke-weight (q/random 10)) 145 | (q/fill (q/random 255)) 146 | 147 | (let [diam (q/random 100) 148 | x (q/random (q/width)) 149 | y (q/random (q/height))] 150 | (q/ellipse x y diam diam))) 151 | ;; (put your cursor here and hit Cmd-Enter) 152 | 153 | ;; Now try taking that `(dorun)` form from above and putting it in the draw 154 | ;; function here; then you'll always have that wheel of lines on top of the 155 | ;; circles that are drawn. 156 | 157 | ;; There's a `q/stroke` call in that draw function that's commented out -- 158 | ;; remove the `;;' to make your sketch more colorful. 159 | ;; Can you make the fill colorful as well? 160 | 161 | 162 | ;; TODO more to come :) 163 | ") 164 | 165 | -------------------------------------------------------------------------------- /src/tutorial_cljs/text/reagent.cljs: -------------------------------------------------------------------------------- 1 | (ns tutorial-cljs.text.reagent) 2 | 3 | (def text 4 | ";; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 | ;; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 | ;; +++++++++++++++ ++++++++++++++++ 7 | ;; An Interactive Introduction to Reagent 8 | ;; +++++++++++++++ ++++++++++++++++ 9 | ;; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 10 | ;; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 11 | 12 | ;; ============================================================================ 13 | ;; Using this Tutorial 14 | ;; ============================================================================ 15 | 16 | ;; # Evaluating forms 17 | ;; 18 | ;; You can evaluate forms in this tutorial by putting your cursor at the end of 19 | ;; the form and pressing \"Cmd-Enter\". The output will be displayed to the 20 | ;; right of your cursor. You can dismiss the output view (if it's in your way) 21 | ;; with \"Cmd-Shift-Enter\". 22 | ;; 23 | ;; Try evaluating this: 24 | 25 | (+ 1 2) ; <- put your cursor right after the closing ) and press Cmd-Enter 26 | 27 | ;; ^ You can also put your cursor on the following line and press Cmd-Enter 28 | 29 | ;; Ok, that was cool, but how about some data that's more complicated? 30 | {:some 10 31 | :other 20 32 | :list (range 10)} ; evaluate this, and you'll be able to interact with the result 33 | 34 | ;; # Documentation + auto-complete 35 | ;; 36 | ;; If you click `range` in that code above, the documentation for the range 37 | ;; function will appear in the bottom-right corner of this page. You can type 38 | ;; into this document, and documentation + auto-complete suggestions will 39 | ;; appear. Press Tab (and Shift-Tab) to cycle through the suggestions. 40 | ;; 41 | ;; Go ahead, put your cursor at the end of `map`, and see what other functions 42 | ;; have `map` in the name. 43 | 44 | map 45 | 46 | ;; # The REPL 47 | ;; 48 | ;; The right hand pane is a REPL where you can type in clojurescript code and 49 | ;; see the results. It will show you documentation + auto-complete suggestions 50 | ;; as well. 51 | 52 | 53 | ;; ============================================================================ 54 | ;; Basic forms 55 | ;; ============================================================================ 56 | 57 | ;; We've already evaluated this for you, so you're already in the tutorial.reagent ns 58 | (ns tutorial.reagent 59 | (:require [reagent.core :as r])) 60 | 61 | [:span \"Hello\"] 62 | 63 | [:div \"one\" \"two\"] 64 | 65 | [:span {:style {:color :red}} \"Red text\"] 66 | 67 | [:button {:on-click #(js/alert \"clicked!\")} \"Click for a message\"] 68 | 69 | ;; ============================================================================ 70 | ;; Components 71 | ;; ============================================================================ 72 | 73 | (defn my-component [name] 74 | [:span (str \"Hello \" name)]) 75 | 76 | [my-component \"Julie\"] 77 | 78 | 79 | 80 | 81 | 82 | ;; ============================================================================ 83 | ;; Components 84 | ;; ============================================================================ 85 | 86 | 87 | (defn my-stateful-component [initial-count] 88 | (let [count (r/atom initial-count)] 89 | (fn [] 90 | [:button {:on-click #(swap! count inc)} 91 | (str \"Clicked \" @count \" times\")]))) 92 | 93 | [my-stateful-component 10] 94 | 95 | 96 | ;; TODO more to come :) 97 | ") 98 | 99 | (def unready 100 | " 101 | 102 | ;; ============================================================================ 103 | ;; Render Windows 104 | ;; ============================================================================ 105 | 106 | ;; TODO do we even need this??? 107 | ;; Basically: how do I make this be devcards? 108 | 109 | 110 | ;; In normal reagent, this would be `r/render`, but we're doing some magic so that 111 | ;; it will work in this tutorial enviornment. 112 | (makerender 113 | :example 114 | \"My Awesome Button\" 115 | [:button nil \"Click and despair\" ]) 116 | 117 | ;; When you evaluate this (Cmd-Enter on the line below it), a new reagent render 118 | ;; will open. If you evaluate it again, the render will reload. 119 | ;; If you change `:example` to some other name, or create a new 120 | ;; `(makerender)` form with a different name, another render will open. 121 | 122 | ") 123 | -------------------------------------------------------------------------------- /src/tutorial_cljs/text/webgl.cljs: -------------------------------------------------------------------------------- 1 | (ns tutorial-cljs.text.webgl) 2 | 3 | (def text 4 | ";; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 | ;; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 | ;; +++++++++++++++ +++++++++++++++ 7 | ;; An Interactive Introduction to Gamma (WebGL) 8 | ;; +++++++++++++++ +++++++++++++++ 9 | ;; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 10 | ;; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 11 | 12 | ;; ============================================================================ 13 | ;; Using this Tutorial 14 | ;; ============================================================================ 15 | 16 | ;; # Evaluating forms 17 | ;; 18 | ;; You can evaluate forms in this tutorial by putting your cursor at the end of 19 | ;; the form and pressing \"Cmd-Enter\". The output will be displayed to the 20 | ;; right of your cursor. You can dismiss the output view (if it's in your way) 21 | ;; with \"Cmd-Shift-Enter\". 22 | ;; 23 | ;; Try evaluating this: 24 | 25 | (+ 1 2) ; <- put your cursor right after the closing ) and press Cmd-Enter 26 | 27 | ;; ^ You can also put your cursor on the following line and press Cmd-Enter 28 | 29 | ;; Ok, that was cool, but how about some data that's more complicated? 30 | {:some 10 31 | :other 20 32 | :list (range 10)} ; evaluate this, and you'll be able to interact with the result 33 | 34 | ;; # Documentation + auto-complete 35 | ;; 36 | ;; If you click `range` in that code above, the documentation for the range 37 | ;; function will appear in the bottom-right corner of this page. You can type 38 | ;; into this document, and documentation + auto-complete suggestions will 39 | ;; appear. Press Tab (and Shift-Tab) to cycle through the suggestions. 40 | ;; 41 | ;; Go ahead, put your cursor at the end of `map`, and see what other functions 42 | ;; have `map` in the name. 43 | 44 | map 45 | 46 | ;; # The REPL 47 | ;; 48 | ;; The right hand pane is a REPL where you can type in clojurescript code and 49 | ;; see the results. It will show you documentation + auto-complete suggestions 50 | ;; as well. 51 | 52 | 53 | ;; ============================================================================ 54 | ;; Basic forms 55 | ;; ============================================================================ 56 | 57 | ") 58 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | ClojureScript Tutorial 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /static/js/main.cljs.edn: -------------------------------------------------------------------------------- 1 | {:require [tutorial-cljs.core] 2 | :compiler-options {:asset-path "js/main.out"}} 3 | -------------------------------------------------------------------------------- /static/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #444; 3 | font-family: sans-serif; 4 | display: flex; 5 | flex-direction: row; 6 | 7 | margin: 0; 8 | padding: 8px; 9 | position: fixed; 10 | top: 0; 11 | left: 0; 12 | right: 0; 13 | bottom: 0; 14 | } 15 | 16 | #text { 17 | flex: 1; 18 | } 19 | 20 | #repl { 21 | flex: 1; 22 | } 23 | 24 | h1, h4 { 25 | margin: 0 0 20px 0; 26 | color: white; 27 | text-shadow: 2px 1px 5px #888; 28 | } 29 | 30 | #button { 31 | padding: 10px 20px; 32 | font-size: 20px; 33 | margin: 5px 0; 34 | flex-shrink: 0; 35 | border-radius: 20px; 36 | background-color: white; 37 | font-weight: bold; 38 | border: none; 39 | } 40 | 41 | #wrapper { 42 | padding: 20px; 43 | display: flex; 44 | flex-direction: row; 45 | align-items: stretch; 46 | position: absolute; 47 | top: 0; 48 | bottom: 0; 49 | left: 0; 50 | right: 0; 51 | } 52 | 53 | #left { 54 | flex: 1; 55 | max-width: 800px; 56 | display: flex; 57 | flex-direction: column; 58 | } 59 | 60 | .text-container { 61 | flex: 1; 62 | display: flex; 63 | align-items: stretch; 64 | justify-content: stretch; 65 | flex-direction: column; 66 | min-height: 0; 67 | } 68 | 69 | #text { 70 | flex: 1; 71 | height: 100%; 72 | background-color: white; 73 | display: block; 74 | width: 100%; 75 | } 76 | 77 | #text > .CodeMirror { 78 | height: 100%!important; 79 | } 80 | 81 | #state-container { 82 | display: flex; 83 | flex-direction: row; 84 | } 85 | 86 | #state-left { 87 | flex: 1; 88 | flex-basis: fill; 89 | } 90 | 91 | #state-right { 92 | flex: 1; 93 | flex-basis: fill; 94 | } 95 | 96 | #state { 97 | height: 200px; 98 | } 99 | 100 | #state > .CodeMirror { 101 | height: 200px!important; 102 | } 103 | 104 | #current-state { 105 | height: 200px; 106 | white-space: pre; 107 | background-color: #555; 108 | font-family: monospace; 109 | color: white; 110 | overflow: auto; 111 | letter-spacing: 1px; 112 | line-height: 1.5; 113 | padding: 5px 10px; 114 | box-sizing: border-box; 115 | } 116 | 117 | #right { 118 | width: 400px; 119 | display: flex; 120 | flex-direction: column; 121 | margin-left: 20px; 122 | } 123 | 124 | #output { 125 | display: block; 126 | align-self: center; 127 | } 128 | 129 | #log { 130 | padding: 10px; 131 | background-color: #aaa; 132 | overflow: auto; 133 | font-family: monospace; 134 | white-space: pre-wrap; 135 | } 136 | 137 | #repl { 138 | flex: 1; 139 | display: flex; 140 | } 141 | 142 | #repl .CodeMirror { 143 | height: auto; 144 | } 145 | 146 | .small-title { 147 | color: white; 148 | font-weight: bold; 149 | } 150 | 151 | #container { 152 | display: flex; 153 | } 154 | 155 | #container .CodeMirror { 156 | height: auto; 157 | } -------------------------------------------------------------------------------- /vendor/clojure-parinfer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * To Parinfer developers, 3 | * 4 | * This is a syntax-highlighting mode for Clojure, copied from CodeMirror. 5 | * We modify it for Parinfer so that it dims the inferred parens at the end of a line. 6 | * (Search for Parinfer below for the relevant edit) 7 | * 8 | * For the purpose of extra-highlighting, we also modify it by tracking a previousToken 9 | * so we can highlight def'd symbols and symbols that are called to. Example: 10 | * 11 | * (def foo 123) (bar 123) 12 | * ^^^ ^^^ 13 | * |------------------|------------- highlighted as 'def' token type 14 | * 15 | * This Clojure mode also has logic for where to indent the cursor when pressing enter. 16 | * We do not modify this. 17 | * 18 | */ 19 | 20 | 21 | 22 | 23 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 24 | // Distributed under an MIT license: http://codemirror.net/LICENSE 25 | 26 | /** 27 | * Author: Hans Engel 28 | * Branched from CodeMirror's Scheme mode (by Koh Zi Han, based on implementation by Koh Zi Chun) 29 | */ 30 | 31 | (function(mod) { 32 | if (typeof exports == "object" && typeof module == "object") // CommonJS 33 | mod(require("../../lib/codemirror")); 34 | else if (typeof define == "function" && define.amd) // AMD 35 | define(["../../lib/codemirror"], mod); 36 | else // Plain browser env 37 | mod(CodeMirror); 38 | })(function(CodeMirror) { 39 | "use strict"; 40 | 41 | CodeMirror.defineMode("clojure-parinfer", function (options) { 42 | var BUILTIN = "builtin", COMMENT = "comment", STRING = "string", CHARACTER = "string-2", 43 | ATOM = "atom", NUMBER = "number", BRACKET = "bracket", KEYWORD = "keyword", VAR = "variable", 44 | SOL = "sol", EOL = "eol", MOL = "mol", // close-paren styles 45 | DEF = "def"; 46 | var INDENT_WORD_SKIP = options.indentUnit || 2; 47 | var NORMAL_INDENT_UNIT = options.indentUnit || 2; 48 | 49 | function makeKeywords(str) { 50 | var obj = {}, words = str.split(" "); 51 | for (var i = 0; i < words.length; ++i) obj[words[i]] = true; 52 | return obj; 53 | } 54 | 55 | var atoms = makeKeywords("true false nil"); 56 | 57 | var defs = makeKeywords( 58 | "defn defn- def defonce defmulti defmethod defmacro defstruct deftype ns"); 59 | 60 | var keywords = makeKeywords( 61 | "defn defn- def def- defonce defmulti defmethod defmacro defstruct deftype defprotocol defrecord defproject deftest slice defalias defhinted defmacro- defn-memo defnk defnk defonce- defunbound defunbound- defvar defvar- let letfn do case cond condp for loop recur when when-not when-let when-first if if-let if-not . .. -> ->> doto and or dosync doseq dotimes dorun doall load import unimport ns in-ns refer try catch finally throw with-open with-local-vars binding gen-class gen-and-load-class gen-and-save-class handler-case handle"); 62 | 63 | var builtins = makeKeywords( 64 | "* *' *1 *2 *3 *agent* *allow-unresolved-vars* *assert* *clojure-version* *command-line-args* *compile-files* *compile-path* *compiler-options* *data-readers* *e *err* *file* *flush-on-newline* *fn-loader* *in* *math-context* *ns* *out* *print-dup* *print-length* *print-level* *print-meta* *print-readably* *read-eval* *source-path* *unchecked-math* *use-context-classloader* *verbose-defrecords* *warn-on-reflection* + +' - -' -> ->> ->ArrayChunk ->Vec ->VecNode ->VecSeq -cache-protocol-fn -reset-methods .. / < <= = == > >= EMPTY-NODE accessor aclone add-classpath add-watch agent agent-error agent-errors aget alength alias all-ns alter alter-meta! alter-var-root amap ancestors and apply areduce array-map aset aset-boolean aset-byte aset-char aset-double aset-float aset-int aset-long aset-short assert assoc assoc! assoc-in associative? atom await await-for await1 bases bean bigdec bigint biginteger binding bit-and bit-and-not bit-clear bit-flip bit-not bit-or bit-set bit-shift-left bit-shift-right bit-test bit-xor boolean boolean-array booleans bound-fn bound-fn* bound? butlast byte byte-array bytes case cast char char-array char-escape-string char-name-string char? chars chunk chunk-append chunk-buffer chunk-cons chunk-first chunk-next chunk-rest chunked-seq? class class? clear-agent-errors clojure-version coll? comment commute comp comparator compare compare-and-set! compile complement concat cond condp conj conj! cons constantly construct-proxy contains? count counted? create-ns create-struct cycle dec dec' decimal? declare default-data-readers definline definterface defmacro defmethod defmulti defn defn- defonce defprotocol defrecord defstruct deftype delay delay? deliver denominator deref derive descendants destructure disj disj! dissoc dissoc! distinct distinct? doall dorun doseq dosync dotimes doto double double-array doubles drop drop-last drop-while empty empty? ensure enumeration-seq error-handler error-mode eval even? every-pred every? ex-data ex-info extend extend-protocol extend-type extenders extends? false? ffirst file-seq filter filterv find find-keyword find-ns find-protocol-impl find-protocol-method find-var first flatten float float-array float? floats flush fn fn? fnext fnil for force format frequencies future future-call future-cancel future-cancelled? future-done? future? gen-class gen-interface gensym get get-in get-method get-proxy-class get-thread-bindings get-validator group-by hash hash-combine hash-map hash-set identical? identity if-let if-not ifn? import in-ns inc inc' init-proxy instance? int int-array integer? interleave intern interpose into into-array ints io! isa? iterate iterator-seq juxt keep keep-indexed key keys keyword keyword? last lazy-cat lazy-seq let letfn line-seq list list* list? load load-file load-reader load-string loaded-libs locking long long-array longs loop macroexpand macroexpand-1 make-array make-hierarchy map map-indexed map? mapcat mapv max max-key memfn memoize merge merge-with meta method-sig methods min min-key mod munge name namespace namespace-munge neg? newline next nfirst nil? nnext not not-any? not-empty not-every? not= ns ns-aliases ns-imports ns-interns ns-map ns-name ns-publics ns-refers ns-resolve ns-unalias ns-unmap nth nthnext nthrest num number? numerator object-array odd? or parents partial partition partition-all partition-by pcalls peek persistent! pmap pop pop! pop-thread-bindings pos? pr pr-str prefer-method prefers primitives-classnames print print-ctor print-dup print-method print-simple print-str printf println println-str prn prn-str promise proxy proxy-call-with-super proxy-mappings proxy-name proxy-super push-thread-bindings pvalues quot rand rand-int rand-nth range ratio? rational? rationalize re-find re-groups re-matcher re-matches re-pattern re-seq read read-line read-string realized? reduce reduce-kv reductions ref ref-history-count ref-max-history ref-min-history ref-set refer refer-clojure reify release-pending-sends rem remove remove-all-methods remove-method remove-ns remove-watch repeat repeatedly replace replicate require reset! reset-meta! resolve rest restart-agent resultset-seq reverse reversible? rseq rsubseq satisfies? second select-keys send send-off seq seq? seque sequence sequential? set set-error-handler! set-error-mode! set-validator! set? short short-array shorts shuffle shutdown-agents slurp some some-fn sort sort-by sorted-map sorted-map-by sorted-set sorted-set-by sorted? special-symbol? spit split-at split-with str string? struct struct-map subs subseq subvec supers swap! symbol symbol? sync take take-last take-nth take-while test the-ns thread-bound? time to-array to-array-2d trampoline transient tree-seq true? type unchecked-add unchecked-add-int unchecked-byte unchecked-char unchecked-dec unchecked-dec-int unchecked-divide-int unchecked-double unchecked-float unchecked-inc unchecked-inc-int unchecked-int unchecked-long unchecked-multiply unchecked-multiply-int unchecked-negate unchecked-negate-int unchecked-remainder-int unchecked-short unchecked-subtract unchecked-subtract-int underive unquote unquote-splicing update-in update-proxy use val vals var-get var-set var? vary-meta vec vector vector-of vector? when when-first when-let when-not while with-bindings with-bindings* with-in-str with-loading-context with-local-vars with-meta with-open with-out-str with-precision with-redefs with-redefs-fn xml-seq zero? zipmap *default-data-reader-fn* as-> cond-> cond->> reduced reduced? send-via set-agent-send-executor! set-agent-send-off-executor! some-> some->>"); 65 | 66 | var indentKeys = makeKeywords( 67 | // Built-ins 68 | "ns fn def defn defmethod bound-fn if if-not case condp when while when-not when-first do future comment doto locking proxy with-open with-precision reify deftype defrecord defprotocol extend extend-protocol extend-type try catch " + 69 | 70 | // Binding forms 71 | "let letfn binding loop for doseq dotimes when-let if-let " + 72 | 73 | // Data structures 74 | "defstruct struct-map assoc " + 75 | 76 | // clojure.test 77 | "testing deftest " + 78 | 79 | // contrib 80 | "handler-case handle dotrace deftrace"); 81 | 82 | var tests = { 83 | digit: /\d/, 84 | digit_or_colon: /[\d:]/, 85 | hex: /[0-9a-f]/i, 86 | sign: /[+-]/, 87 | exponent: /e/i, 88 | keyword_char: /[^\s\(\[\;\)\]]/, 89 | symbol: /[\w*+!\-\._?:<>\/\xa1-\uffff]/ 90 | }; 91 | 92 | function stateStack(indent, type, prev) { // represents a state stack object 93 | this.indent = indent; 94 | this.type = type; 95 | this.prev = prev; 96 | } 97 | 98 | function pushStack(state, indent, type) { 99 | state.indentStack = new stateStack(indent, type, state.indentStack); 100 | } 101 | 102 | function popStack(state) { 103 | state.indentStack = state.indentStack.prev; 104 | } 105 | 106 | function isNumber(ch, stream){ 107 | // hex 108 | if ( ch === '0' && stream.eat(/x/i) ) { 109 | stream.eatWhile(tests.hex); 110 | return true; 111 | } 112 | 113 | // leading sign 114 | if ( ( ch == '+' || ch == '-' ) && ( tests.digit.test(stream.peek()) ) ) { 115 | stream.eat(tests.sign); 116 | ch = stream.next(); 117 | } 118 | 119 | if ( tests.digit.test(ch) ) { 120 | stream.eat(ch); 121 | stream.eatWhile(tests.digit); 122 | 123 | if ( '.' == stream.peek() ) { 124 | stream.eat('.'); 125 | stream.eatWhile(tests.digit); 126 | } 127 | 128 | if ( stream.eat(tests.exponent) ) { 129 | stream.eat(tests.sign); 130 | stream.eatWhile(tests.digit); 131 | } 132 | 133 | return true; 134 | } 135 | 136 | return false; 137 | } 138 | 139 | // Eat character that starts after backslash \ 140 | function eatCharacter(stream) { 141 | var first = stream.next(); 142 | // Read special literals: backspace, newline, space, return. 143 | // Just read all lowercase letters. 144 | if (first && first.match(/[a-z]/) && stream.match(/[a-z]+/, true)) { 145 | return; 146 | } 147 | // Read unicode character: \u1000 \uA0a1 148 | if (first === "u") { 149 | stream.match(/[0-9a-z]{4}/i, true); 150 | } 151 | } 152 | 153 | return { 154 | startState: function () { 155 | return { 156 | previousToken: null, 157 | indentStack: null, 158 | indentation: 0, 159 | mode: false, 160 | atStart: false 161 | }; 162 | }, 163 | 164 | token: function (stream, state) { 165 | 166 | if (stream.sol()) { 167 | state.atStart = (state.mode != "string"); 168 | } 169 | 170 | if (state.indentStack == null && stream.sol()) { 171 | // update indentation, but only if indentStack is empty 172 | state.indentation = stream.indentation(); 173 | } 174 | 175 | // skip spaces 176 | if (stream.eatSpace()) { 177 | return null; 178 | } 179 | var returnType = null; 180 | var previousToken = null; 181 | 182 | switch(state.mode){ 183 | case "string": // multi-line string parsing mode 184 | var next, escaped = false; 185 | while ((next = stream.next()) != null) { 186 | if (next == "\"" && !escaped) { 187 | 188 | state.mode = false; 189 | break; 190 | } 191 | escaped = !escaped && next == "\\"; 192 | } 193 | returnType = STRING; // continue on in string mode 194 | break; 195 | default: // default parsing mode 196 | var ch = stream.next(); 197 | 198 | if (ch == "\"") { 199 | state.mode = "string"; 200 | returnType = STRING; 201 | } else if (ch == "\\") { 202 | eatCharacter(stream); 203 | returnType = CHARACTER; 204 | } else if (ch == "'" && !( tests.digit_or_colon.test(stream.peek()) )) { 205 | returnType = ATOM; 206 | } else if (ch == ";") { // comment 207 | stream.skipToEnd(); // rest of the line is a comment 208 | returnType = COMMENT; 209 | } else if (isNumber(ch,stream)){ 210 | returnType = NUMBER; 211 | } else if (ch == "(" || ch == "[" || ch == "{" ) { 212 | if (ch == "(") { 213 | previousToken = "("; 214 | } 215 | 216 | var keyWord = '', indentTemp = stream.column(), letter; 217 | /** 218 | Either 219 | (indent-word .. 220 | (non-indent-word .. 221 | (;something else, bracket, etc. 222 | */ 223 | 224 | if (ch == "(") while ((letter = stream.eat(tests.keyword_char)) != null) { 225 | keyWord += letter; 226 | } 227 | 228 | if (keyWord.length > 0 && (indentKeys.propertyIsEnumerable(keyWord) || 229 | /^(?:def|with)/.test(keyWord))) { // indent-word 230 | pushStack(state, indentTemp + INDENT_WORD_SKIP, ch); 231 | } else { // non-indent word 232 | // we continue eating the spaces 233 | stream.eatSpace(); 234 | if (stream.eol() || stream.peek() == ";") { 235 | // nothing significant after 236 | // we restart indentation the user defined spaces after 237 | pushStack(state, indentTemp + NORMAL_INDENT_UNIT, ch); 238 | } else { 239 | pushStack(state, indentTemp + stream.current().length, ch); // else we match 240 | } 241 | } 242 | stream.backUp(stream.current().length - 1); // undo all the eating 243 | 244 | returnType = BRACKET; 245 | } else if (ch == ")" || ch == "]" || ch == "}") { 246 | returnType = BRACKET; 247 | 248 | // Parinfer: (style trailing delimiters) 249 | stream.eatWhile(/[\s,\]})]/); 250 | if (stream.eol() || stream.peek() == ";") { 251 | returnType += " " + EOL; 252 | } else if (state.atStart) { 253 | returnType += " " + SOL; 254 | } else { 255 | returnType += " " + MOL; 256 | } 257 | stream.backUp(stream.current().length - 1); 258 | 259 | if (state.indentStack != null && state.indentStack.type == (ch == ")" ? "(" : (ch == "]" ? "[" :"{"))) { 260 | popStack(state); 261 | } 262 | } else if ( ch == ":" ) { 263 | stream.eatWhile(tests.symbol); 264 | return ATOM; 265 | } else { 266 | stream.eatWhile(tests.symbol); 267 | 268 | if (keywords && keywords.propertyIsEnumerable(stream.current())) { 269 | returnType = KEYWORD; 270 | } else if (builtins && builtins.propertyIsEnumerable(stream.current())) { 271 | returnType = BUILTIN; 272 | } else if (atoms && atoms.propertyIsEnumerable(stream.current())) { 273 | returnType = ATOM; 274 | } else if (state.previousToken == "def" || state.previousToken == "(") { 275 | returnType = DEF; 276 | } else { 277 | returnType = VAR; 278 | } 279 | 280 | if (state.previousToken == "(" && defs && defs.propertyIsEnumerable(stream.current())) { 281 | previousToken = "def"; 282 | } 283 | } 284 | 285 | if (!(ch == ")" || ch == "]" || ch == "}")) { 286 | state.atStart = false; 287 | } 288 | } 289 | 290 | state.previousToken = previousToken; 291 | 292 | return returnType; 293 | }, 294 | 295 | indent: function (state) { 296 | if (state.indentStack == null) return state.indentation; 297 | return state.indentStack.indent; 298 | }, 299 | 300 | closeBrackets: {pairs: "()[]{}\"\""}, 301 | lineComment: ";;" 302 | }; 303 | }); 304 | 305 | CodeMirror.defineMIME("text/x-clojure", "clojure"); 306 | 307 | }); 308 | -------------------------------------------------------------------------------- /vendor/inlet.css: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================= 3 | * ========================================================= 4 | * ColorPicker 5 | * ========================================================= 6 | * ========================================================= 7 | * 8 | */ 9 | .inlet_clicker { 10 | z-index: 10; 11 | } 12 | .inlet_slider { 13 | opacity: 0.85; 14 | z-index: 10; 15 | width: 24%; 16 | display: block; 17 | border-radius: 3px; 18 | height: 14px; 19 | -webkit-box-shadow: inset 0px 0px 5px 0px rgba(4, 4, 4, 0.5); 20 | box-shadow: inset 0px 0px 5px 0px rgba(4, 4, 4, 0.5); 21 | background-color: #d6d6d6; 22 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d6d6d6), to(#ebebeb)); 23 | background-image: -webkit-linear-gradient(top, #d6d6d6, #ebebeb); 24 | background-image: -moz-linear-gradient(top, #d6d6d6, #ebebeb); 25 | background-image: -o-linear-gradient(top, #d6d6d6, #ebebeb); 26 | background-image: -ms-linear-gradient(top, #d6d6d6, #ebebeb); 27 | background-image: linear-gradient(top, #d6d6d6, #ebebeb); 28 | filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, StartColorStr='#d6d6d6', EndColorStr='#ebebeb'); 29 | } 30 | .inlet_slider:hover { 31 | opacity: 0.98; 32 | } 33 | .inlet_slider .range { 34 | width: 90%; 35 | margin-left: 5%; 36 | margin-top: 0px; 37 | } 38 | .inlet_slider input[type="range"] { 39 | -webkit-appearance: none; 40 | -moz-appearance: none; 41 | } 42 | @-moz-document url-prefix() { 43 | .inlet_slider input[type="range"] { 44 | position: absolute; 45 | } 46 | } 47 | .inlet_slider input::-moz-range-track { 48 | background: none; 49 | border: none; 50 | outline: none; 51 | } 52 | .inlet_slider input::-webkit-slider-thumb { 53 | cursor: col-resize; 54 | -webkit-appearance: none; 55 | -moz-apperance: none; 56 | width: 12px; 57 | height: 12px; 58 | margin-top: -13px; 59 | border-radius: 6px; 60 | border: 1px solid black; 61 | background-color: red; 62 | -webkit-box-shadow: 0px 0px 3px 0px rgba(4, 4, 4, 0.4); 63 | box-shadow: 0px 0px 3px 0px rgba(4, 4, 4, 0.4); 64 | background-color: #424242; 65 | background-image: -webkit-gradient(linear, left top, left bottom, from(#424242), to(#212121)); 66 | background-image: -webkit-linear-gradient(top, #424242, #212121); 67 | background-image: -moz-linear-gradient(top, #424242, #212121); 68 | background-image: -o-linear-gradient(top, #424242, #212121); 69 | background-image: -ms-linear-gradient(top, #424242, #212121); 70 | background-image: linear-gradient(top, #424242, #212121); 71 | filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, StartColorStr='#424242', EndColorStr='#212121'); 72 | } 73 | /* 74 | * ========================================================= 75 | * ========================================================= 76 | * ColorPicker 77 | * ========================================================= 78 | * ========================================================= 79 | * 80 | */ 81 | .ColorPicker { 82 | /* 83 | border: 1px solid rgba(0,0,0,0.5); 84 | border-radius: 6px; 85 | background: #0d0d0d; 86 | background: -webkit-gradient(linear, left top, left bottom, from(#333), color-stop(0.1, #111), to(#000000)); 87 | box-shadow: 2px 2px 5px 2px rgba(0,0,0,0.35); 88 | color:#AAA; 89 | */ 90 | text-shadow: 1px 1px 1px #000; 91 | color: #050505; 92 | cursor: default; 93 | display: block; 94 | font-family: 'arial', helvetica, sans-serif; 95 | font-size: 20px; 96 | padding: 7px 8px 20px; 97 | position: absolute; 98 | top: 100px; 99 | left: 700px; 100 | width: 229px; 101 | z-index: 100; 102 | border-radius: 3px; 103 | -webkit-box-shadow: inset 0px 0px 5px 0px rgba(4, 4, 4, 0.5); 104 | box-shadow: inset 0px 0px 5px 0px rgba(4, 4, 4, 0.5); 105 | background-color: rgba(214, 214, 215, 0.85); 106 | /* 107 | background-image: -webkit-gradient(linear, left top, left bottom, from(rgb(214, 214, 214)), to(rgb(235, 235, 235))); 108 | background-image: -webkit-linear-gradient(top, rgb(214, 214, 214), rgb(235, 235, 235)); 109 | background-image: -moz-linear-gradient(top, rgb(214, 214, 214), rgb(235, 235, 235)); 110 | background-image: -o-linear-gradient(top, rgb(214, 214, 214), rgb(235, 235, 235)); 111 | background-image: -ms-linear-gradient(top, rgb(214, 214, 214), rgb(235, 235, 235)); 112 | background-image: linear-gradient(top, rgb(214, 214, 214), rgb(235, 235, 235)); 113 | filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#d6d6d6', EndColorStr='#ebebeb'); 114 | */ 115 | } 116 | .ColorPicker br { 117 | clear: both; 118 | margin: 0; 119 | padding: 0; 120 | } 121 | .ColorPicker input.hexInput:hover, 122 | .ColorPicker input.hexInput:focus { 123 | color: #aa1212; 124 | } 125 | .ColorPicker input.hexInput { 126 | -webkit-transition-property: color; 127 | -webkit-transition-duration: .5s; 128 | background: none; 129 | border: 0; 130 | margin: 0; 131 | font-family: courier,monospace; 132 | font-size: 20px; 133 | position: relative; 134 | top: -2px; 135 | float: left; 136 | color: #050505; 137 | cursor: text; 138 | } 139 | .ColorPicker div.hexBox { 140 | border: 1px solid rgba(255, 255, 255, 0.5); 141 | border-radius: 2px; 142 | background: #FFF; 143 | float: left; 144 | font-size: 1px; 145 | height: 20px; 146 | margin: 0 5px 0 2px; 147 | width: 20px; 148 | cursor: pointer; 149 | } 150 | .ColorPicker div.hexBox div { 151 | width: inherit; 152 | height: inherit; 153 | } 154 | .ColorPicker div.hexClose { 155 | display: none; 156 | /* 157 | -webkit-transition-property: color, text-shadow; 158 | -webkit-transition-duration: .5s; 159 | position: relative; 160 | top: -1px; 161 | left: -1px; 162 | color:#FFF; 163 | cursor:pointer; 164 | float:right; 165 | padding: 0 5px; 166 | margin:0 4px 3px; 167 | user-select:none; 168 | -webkit-user-select: none; 169 | */ 170 | } 171 | .ColorPicker div.hexClose:hover { 172 | text-shadow: 0 0 20px #fff; 173 | color: #FF1100; 174 | } 175 | -------------------------------------------------------------------------------- /vendor/inlet.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.10.0 2 | ((function() { 3 | var HSLCircle, Picker, cssColorToRGB, fmod, hslToCSS, hslToRGB, hueToRGB, isValidCSSColor, map, normalizeColor, rgbToHSL, style, slice = [].slice; 4 | hueToRGB = function(m1, m2, h) { 5 | h = h < 0 ? h + 1 : h > 1 ? h - 1 : h; 6 | if (h * 6 < 1) { 7 | return m1 + (m2 - m1) * h * 6; 8 | } 9 | if (h * 2 < 1) { 10 | return m2; 11 | } 12 | if (h * 3 < 2) { 13 | return m1 + (m2 - m1) * (.66666 - h) * 6; 14 | } 15 | return m1; 16 | }; 17 | hslToRGB = function(h, s, l) { 18 | var m1, m2; 19 | m2 = l <= .5 ? l * (s + 1) : l + s - l * s; 20 | m1 = l * 2 - m2; 21 | return { 22 | r: hueToRGB(m1, m2, h + .33333), 23 | g: hueToRGB(m1, m2, h), 24 | b: hueToRGB(m1, m2, h - .33333) 25 | }; 26 | }; 27 | rgbToHSL = function(r, g, b) { 28 | var diff, h, l, max, min, s, sum; 29 | max = Math.max(r, g, b); 30 | min = Math.min(r, g, b); 31 | diff = max - min; 32 | sum = max + min; 33 | h = min === max ? 0 : r === max ? (60 * (g - b) / diff + 360) % 360 : g === max ? 60 * (b - r) / diff + 120 : 60 * (r - g) / diff + 240; 34 | l = sum / 2; 35 | s = l === 0 ? 0 : l === 1 ? 1 : l <= .5 ? diff / sum : diff / (2 - sum); 36 | return { 37 | h: h, 38 | s: s, 39 | l: l 40 | }; 41 | }; 42 | hslToCSS = function(h, s, l, a) { 43 | if (a != null) { 44 | return "hsla(" + fmod(Math.round(h * 180 / Math.PI), 360) + "," + Math.round(s * 100) + "%," + Math.round(l * 100) + "%," + a + ")"; 45 | } else { 46 | return "hsl(" + fmod(Math.round(h * 180 / Math.PI), 360) + "," + Math.round(s * 100) + "%," + Math.round(l * 100) + "%)"; 47 | } 48 | }; 49 | cssColorToRGB = function(cssColor) { 50 | var b, g, m, r, rgb, s; 51 | s = document.createElement("span"); 52 | document.body.appendChild(s); 53 | s.style.backgroundColor = cssColor; 54 | rgb = getComputedStyle(s).backgroundColor; 55 | document.body.removeChild(s); 56 | m = /^rgb\((\d+), (\d+), (\d+)\)$/.exec(rgb); 57 | if (!m) { 58 | m = /^rgba\((\d+), (\d+), (\d+), ([\d.]+)\)$/.exec(rgb); 59 | } 60 | r = parseInt(m[1]); 61 | g = parseInt(m[2]); 62 | b = parseInt(m[3]); 63 | if (m[4]) { 64 | return { 65 | r: r / 255, 66 | g: g / 255, 67 | b: b / 255, 68 | a: parseFloat(m[4]) 69 | }; 70 | } 71 | return { 72 | r: r / 255, 73 | g: g / 255, 74 | b: b / 255 75 | }; 76 | }; 77 | isValidCSSColor = function(cssColor) { 78 | var ret, s; 79 | s = document.createElement("span"); 80 | document.body.appendChild(s); 81 | s.style.backgroundColor = cssColor; 82 | ret = s.style.backgroundColor.length > 0; 83 | s.remove(); 84 | return ret; 85 | }; 86 | style = function(tag, styles) { 87 | var n, v; 88 | for (n in styles) { 89 | v = styles[n]; 90 | tag.style[n] = v; 91 | } 92 | return tag; 93 | }; 94 | fmod = function(x, m) { 95 | x = x % m; 96 | if (x < 0) { 97 | x += m; 98 | } 99 | return x; 100 | }; 101 | map = function(v, min, max) { 102 | return min + (max - min) * Math.min(1, Math.max(0, v)); 103 | }; 104 | HSLCircle = function() { 105 | function HSLCircle(radius1, width1, lightness) { 106 | var b, canvas, ctx, d, data, dx, dy, g, h, i, imgdata, j, r, radius, ref, ref1, ref2, s, width, x, y; 107 | this.radius = radius1; 108 | this.width = width1; 109 | this.lightness = lightness; 110 | radius = this.radius; 111 | width = this.width; 112 | canvas = this.canvas = document.createElement("canvas"); 113 | canvas.width = canvas.height = radius * 2; 114 | ctx = canvas.getContext("2d"); 115 | imgdata = ctx.createImageData(canvas.width, canvas.height); 116 | data = imgdata.data; 117 | for (y = i = 0, ref = canvas.height; 0 <= ref ? i < ref : i > ref; y = 0 <= ref ? ++i : --i) { 118 | for (x = j = 0, ref1 = canvas.width; 0 <= ref1 ? j < ref1 : j > ref1; x = 0 <= ref1 ? ++j : --j) { 119 | dy = y - radius; 120 | dx = x - radius; 121 | d = Math.sqrt(dy * dy + dx * dx); 122 | if (d > radius + 1.5) { 123 | continue; 124 | } 125 | d -= 10; 126 | s = Math.max(0, Math.min(1, d / (radius - width / 2 - 10))); 127 | h = Math.atan2(dy, dx) / (Math.PI * 2); 128 | ref2 = hslToRGB(h, s, this.lightness), r = ref2.r, g = ref2.g, b = ref2.b; 129 | data[(y * canvas.width + x) * 4 + 0] = r * 255; 130 | data[(y * canvas.width + x) * 4 + 1] = g * 255; 131 | data[(y * canvas.width + x) * 4 + 2] = b * 255; 132 | data[(y * canvas.width + x) * 4 + 3] = 255; 133 | } 134 | } 135 | ctx.putImageData(imgdata, 0, 0); 136 | } 137 | HSLCircle.prototype.drawHSLCircle = function(canvas, saturation) { 138 | var ctx, highlighted_r, radius, width; 139 | canvas.width = canvas.height = 2 * this.radius; 140 | ctx = canvas.getContext("2d"); 141 | width = this.width; 142 | radius = this.radius; 143 | highlighted_r = map(saturation, width, radius); 144 | ctx.save(); 145 | ctx.fillStyle = "rgba(0,0,0,0.3)"; 146 | ctx.beginPath(); 147 | ctx.arc(radius, radius, radius, 0, Math.PI * 2); 148 | ctx.fill(); 149 | ctx.fillStyle = "black"; 150 | ctx.beginPath(); 151 | ctx.arc(radius, radius, highlighted_r, 0, Math.PI * 2); 152 | ctx.arc(radius, radius, highlighted_r - width, 0, Math.PI * 2, true); 153 | ctx.fill(); 154 | ctx.globalCompositeOperation = "source-in"; 155 | ctx.drawImage(this.canvas, 0, 0); 156 | return ctx.restore(); 157 | }; 158 | return HSLCircle; 159 | }(); 160 | normalizeColor = function(color) { 161 | if (typeof color === "string") { 162 | color = cssColorToRGB(color); 163 | } 164 | if (color.r != null && color.g != null && color.b != null) { 165 | color = rgbToHSL(color.r, color.g, color.b); 166 | color.h = color.h * Math.PI / 180; 167 | } else if (color.h != null && color.s != null && color.l != null) { 168 | color.h = color.h * Math.PI / 180; 169 | } 170 | return color; 171 | }; 172 | Picker = function() { 173 | var attachEvents, makeCircle, makeColorPreview, makeKnob, makeLightnessSlider, makeRoot, radius, width; 174 | radius = 80; 175 | width = 25; 176 | function Picker(color) { 177 | this.color = normalizeColor(color); 178 | this.refColor = this.color; 179 | this.el = makeRoot(); 180 | this.circleContainer = this.el.appendChild(makeCircle.call(this)); 181 | this.lSlider = this.el.appendChild(makeLightnessSlider.call(this)); 182 | this.colorPreview = this.el.appendChild(makeColorPreview.call(this)); 183 | attachEvents.call(this); 184 | this.setLightness(this.color.l); 185 | } 186 | Picker.prototype.setHue = function(h) { 187 | var b, oR, r; 188 | this.color.h = h; 189 | r = map(this.color.s, width, radius) - width / 2; 190 | oR = radius - width / 2; 191 | style(this.hueKnob, { 192 | left: Math.round(oR + Math.cos(h) * r + 6 - 1) + "px", 193 | top: Math.round(oR + Math.sin(h) * r + 6 - 1) + "px" 194 | }); 195 | this.colorPreview.style.backgroundColor = this.lKnob.style.backgroundColor = this.hueKnob.style.backgroundColor = hslToCSS(this.color.h, this.color.s, this.color.l); 196 | b = hslToCSS(this.color.h, this.color.s, .5); 197 | this.lSlider.style.backgroundImage = "-webkit-linear-gradient(bottom, black, " + b + " 50%, white)"; 198 | this.lSlider.style.backgroundImage = "-moz-linear-gradient(bottom, black, " + b + " 50%, white)"; 199 | return this.emit("changed"); 200 | }; 201 | Picker.prototype.setSaturation = function(s) { 202 | this.color.s = s; 203 | this.circle.drawHSLCircle(this.circleCanvas, s); 204 | return this.setHue(this.color.h); 205 | }; 206 | Picker.prototype.setLightness = function(l) { 207 | this.color.l = l; 208 | this.circle = new HSLCircle(radius, width, l); 209 | this.lKnob.style.top = (1 - l) * this.lSlider._height - 11 + "px"; 210 | return this.setSaturation(this.color.s); 211 | }; 212 | Picker.prototype.setHSL = function(h, s, l) { 213 | this.color.h = fmod(h, 360) * Math.PI / 180; 214 | this.color.s = Math.max(0, Math.min(1, s)); 215 | l = Math.max(0, Math.min(1, l)); 216 | return this.setLightness(l); 217 | }; 218 | Picker.prototype.getHSL = function() { 219 | return { 220 | h: fmod(this.color.h * 180 / Math.PI, 360), 221 | s: this.color.s, 222 | l: this.color.l 223 | }; 224 | }; 225 | Picker.prototype.setRGB = function(r, g, b) { 226 | var h, l, ref, s; 227 | ref = rgbToHSL(r, g, b), h = ref.h, s = ref.s, l = ref.l; 228 | return this.setHSL(h, s, l); 229 | }; 230 | Picker.prototype.getRGB = function() { 231 | return hslToRGB(this.color.h / (Math.PI * 2), this.color.s, this.color.l); 232 | }; 233 | Picker.prototype.getCSS = function() { 234 | return hslToCSS(this.color.h, this.color.s, this.color.l); 235 | }; 236 | Picker.prototype.setCSS = function(css) { 237 | var b, g, r, ref; 238 | ref = cssColorToRGB(css), r = ref.r, g = ref.g, b = ref.b; 239 | return this.setRGB(r, g, b); 240 | }; 241 | Picker.prototype.on = function(e, l) { 242 | var base; 243 | if (this._listeners == null) { 244 | this._listeners = {}; 245 | } 246 | return ((base = this._listeners)[e] != null ? base[e] : base[e] = []).push(l); 247 | }; 248 | Picker.prototype.emit = function() { 249 | var args, e, i, l, len, ref, ref1, results; 250 | e = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; 251 | if (this._listeners) { 252 | ref1 = (ref = this._listeners[e]) != null ? ref : []; 253 | results = []; 254 | for (i = 0, len = ref1.length; i < len; i++) { 255 | l = ref1[i]; 256 | results.push(l.call.apply(l, [ this ].concat(slice.call(args)))); 257 | } 258 | return results; 259 | } 260 | }; 261 | Picker.prototype.removeListener = function(e, l) { 262 | var k; 263 | if (this._listeners[e]) { 264 | return this._listeners[e] = function() { 265 | var i, len, ref, results; 266 | ref = this._listeners[e]; 267 | results = []; 268 | for (i = 0, len = ref.length; i < len; i++) { 269 | k = ref[i]; 270 | if (k !== l) { 271 | results.push(k); 272 | } 273 | } 274 | return results; 275 | }.call(this); 276 | } 277 | }; 278 | attachEvents = function() { 279 | var c, updateCursor; 280 | this.lKnob.onmousedown = function(_this) { 281 | return function(e) { 282 | var move, up; 283 | document.documentElement.style.cursor = "pointer"; 284 | window.addEventListener("mousemove", move = function(e) { 285 | var r, y; 286 | r = _this.lSlider.getBoundingClientRect(); 287 | y = e.clientY - r.top; 288 | return _this.setLightness(Math.max(0, Math.min(1, 1 - y / _this.lSlider._height))); 289 | }); 290 | window.addEventListener("mouseup", up = function(e) { 291 | window.removeEventListener("mousemove", move); 292 | window.removeEventListener("mouseup", up); 293 | window.removeEventListener("blur", up); 294 | return document.documentElement.style.cursor = ""; 295 | }); 296 | window.addEventListener("blur", up); 297 | e.preventDefault(); 298 | return e.stopPropagation(); 299 | }; 300 | }(this); 301 | c = this.circleContainer; 302 | updateCursor = function(_this) { 303 | return function(e) { 304 | var d, dx, dy, r, t, x, y; 305 | x = e.layerX; 306 | y = e.layerY; 307 | dx = x - radius; 308 | dy = y - radius; 309 | d = Math.sqrt(dx * dx + dy * dy); 310 | t = Math.atan2(dy, dx); 311 | r = map(_this.color.s, width, radius); 312 | if (r - width < d && d < r) { 313 | if (-Math.PI / 8 < t && t < Math.PI / 8 || t >= 7 * Math.PI / 8 || t <= -7 * Math.PI / 8) { 314 | return c.style.cursor = "ew-resize"; 315 | } else if (Math.PI / 8 <= t && t < 3 * Math.PI / 8 || -7 * Math.PI / 8 < t && t <= -5 * Math.PI / 8) { 316 | return c.style.cursor = "nwse-resize"; 317 | } else if (3 * Math.PI / 8 <= t && t < 5 * Math.PI / 8 || -5 * Math.PI / 8 < t && t <= -3 * Math.PI / 8) { 318 | return c.style.cursor = "ns-resize"; 319 | } else if (5 * Math.PI / 8 <= t && t < 7 * Math.PI / 8 || -3 * Math.PI / 8 < t && t <= -Math.PI / 8) { 320 | return c.style.cursor = "nesw-resize"; 321 | } 322 | } else { 323 | return c.style.cursor = ""; 324 | } 325 | }; 326 | }(this); 327 | c.addEventListener("mouseover", function(e) { 328 | var move, out; 329 | updateCursor(e); 330 | c.addEventListener("mousemove", move = function(e) { 331 | return updateCursor(e); 332 | }); 333 | c.addEventListener("mouseout", out = function(e) { 334 | c.style.cursor = ""; 335 | c.removeEventListener("mousemove", move); 336 | c.removeEventListener("mouseout", out); 337 | return window.removeEventListener("blur", out); 338 | }); 339 | return window.addEventListener("blur", out); 340 | }); 341 | c.addEventListener("mousedown", function(_this) { 342 | return function(e) { 343 | var d, dx, dy, move, r, t, up, x, y; 344 | e.preventDefault(); 345 | x = e.layerX; 346 | y = e.layerY; 347 | dx = x - radius; 348 | dy = y - radius; 349 | d = Math.sqrt(dx * dx + dy * dy); 350 | t = Math.atan2(dy, dx); 351 | r = map(_this.color.s, width, radius); 352 | if (!(r - width < d && d < r)) { 353 | return; 354 | } 355 | document.documentElement.style.cursor = c.style.cursor; 356 | window.addEventListener("mousemove", move = function(e) { 357 | var cx, cy, s; 358 | r = _this.circleCanvas.getBoundingClientRect(); 359 | cx = r.left + r.width / 2; 360 | cy = r.top + r.height / 2; 361 | dx = e.clientX - cx; 362 | dy = e.clientY - cy; 363 | d = Math.sqrt(dx * dx + dy * dy); 364 | d -= 10; 365 | s = Math.max(0, Math.min(1, d / (radius - width / 2 - 10))); 366 | return _this.setSaturation(s); 367 | }); 368 | window.addEventListener("mouseup", up = function(e) { 369 | window.removeEventListener("mousemove", move); 370 | window.removeEventListener("mouseup", up); 371 | window.removeEventListener("blur", up); 372 | return document.documentElement.style.cursor = ""; 373 | }); 374 | return window.addEventListener("blur", up); 375 | }; 376 | }(this)); 377 | return this.hueKnob.onmousedown = function(_this) { 378 | return function(e) { 379 | var move, up; 380 | document.documentElement.style.cursor = "pointer"; 381 | window.addEventListener("mousemove", move = function(e) { 382 | var cx, cy, r; 383 | r = _this.circleCanvas.getBoundingClientRect(); 384 | cx = r.left + r.width / 2; 385 | cy = r.top + r.height / 2; 386 | return _this.setHue(Math.atan2(e.clientY - cy, e.clientX - cx)); 387 | }); 388 | window.addEventListener("mouseup", up = function(e) { 389 | window.removeEventListener("mousemove", move); 390 | window.removeEventListener("mouseup", up); 391 | window.removeEventListener("blur", up); 392 | return document.documentElement.style.cursor = ""; 393 | }); 394 | window.addEventListener("blur", up); 395 | e.preventDefault(); 396 | return e.stopPropagation(); 397 | }; 398 | }(this); 399 | }; 400 | makeRoot = function() { 401 | var div; 402 | div = document.createElement("div"); 403 | div.className = "picker"; 404 | style(div, { 405 | display: "inline-block", 406 | background: "hsl(0, 0%, 97%)", 407 | padding: "6px", 408 | borderRadius: "6px", 409 | boxShadow: "1px 1px 5px hsla(0, 0%, 39%, 0.2), hsla(0, 0%, 100%, 0.9) 0px 0px 1em 0.3em inset", 410 | border: "1px solid hsla(0, 0%, 59%, 0.2)", 411 | position: "absolute", 412 | backgroundImage: "-webkit-linear-gradient(left top, hsla(0, 0%, 0%, 0.05) 25%, transparent 25%, transparent 50%, hsla(0, 0%, 0%, 0.05) 50%, hsla(0, 0%, 0%, 0.05) 75%, transparent 75%, transparent)", 413 | backgroundSize: "40px 40px" 414 | }); 415 | style(div, { 416 | backgroundImage: "-moz-linear-gradient(left top, hsla(0, 0%, 0%, 0.05) 25%, transparent 25%, transparent 50%, hsla(0, 0%, 0%, 0.05) 50%, hsla(0, 0%, 0%, 0.05) 75%, transparent 75%, transparent)", 417 | zIndex: "1000" 418 | }); 419 | return div; 420 | }; 421 | makeCircle = function() { 422 | var circleContainer, k; 423 | circleContainer = document.createElement("div"); 424 | style(circleContainer, { 425 | display: "inline-block", 426 | width: radius * 2 + "px", 427 | height: radius * 2 + "px", 428 | borderRadius: radius + "px", 429 | boxShadow: "0px 0px 7px rgba(0,0,0,0.3)" 430 | }); 431 | circleContainer.appendChild(this.circleCanvas = document.createElement("canvas")); 432 | this.hueKnob = k = makeKnob(27); 433 | circleContainer.appendChild(k); 434 | return circleContainer; 435 | }; 436 | makeLightnessSlider = function() { 437 | var k, lSlider; 438 | lSlider = document.createElement("div"); 439 | style(lSlider, { 440 | display: "inline-block", 441 | width: "20px", 442 | height: radius * 2 - 22 + "px", 443 | marginLeft: "6px", 444 | borderRadius: "10px", 445 | boxShadow: "hsla(0, 100%, 100%, 0.1) 0 1px 2px 1px inset, hsla(0, 100%, 100%, 0.2) 0 1px inset, hsla(0, 0%, 0%, 0.4) 0 -1px 1px inset, hsla(0, 0%, 0%, 0.4) 0 1px 1px", 446 | position: "relative", 447 | top: "-11px" 448 | }); 449 | lSlider._height = radius * 2 - 22; 450 | this.lKnob = k = makeKnob(22); 451 | style(k, { 452 | left: "-1px" 453 | }); 454 | lSlider.appendChild(k); 455 | return lSlider; 456 | }; 457 | makeColorPreview = function() { 458 | var colorPreview, originalColor, originalColorTransparent; 459 | colorPreview = document.createElement("div"); 460 | originalColor = hslToCSS(this.refColor.h, this.refColor.s, this.refColor.l); 461 | originalColorTransparent = hslToCSS(this.refColor.h, this.refColor.s, this.refColor.l, 0); 462 | style(colorPreview, { 463 | boxShadow: "hsla(0, 0%, 0%, 0.5) 0 1px 5px, hsla(0, 100%, 100%, 0.4) 0 1px 1px inset, hsla(0, 0%, 0%, 0.3) 0 -1px 1px inset", 464 | height: "25px", 465 | marginTop: "6px", 466 | borderRadius: "3px", 467 | backgroundImage: "-webkit-linear-gradient(-20deg, " + originalColorTransparent + ", " + originalColorTransparent + " 69%, " + originalColor + " 70%, " + originalColor + ")" 468 | }); 469 | style(colorPreview, { 470 | backgroundImage: "-moz-linear-gradient(-20deg, " + originalColorTransparent + ", " + originalColorTransparent + " 69%, " + originalColor + " 70%, " + originalColor + ")" 471 | }); 472 | return colorPreview; 473 | }; 474 | makeKnob = function(size) { 475 | var el; 476 | el = document.createElement("div"); 477 | el.className = "knob"; 478 | style(el, { 479 | position: "absolute", 480 | width: size + "px", 481 | height: size + "px", 482 | backgroundColor: "red", 483 | borderRadius: Math.floor(size / 2) + "px", 484 | cursor: "pointer", 485 | backgroundImage: "-webkit-gradient(radial, 50% 0%, 0, 50% 0%, 15, color-stop(0%, rgba(255, 255, 255, 0.8)), color-stop(100%, rgba(255, 255, 255, 0.2)))", 486 | boxShadow: "white 0px 1px 1px inset, rgba(0, 0, 0, 0.4) 0px -1px 1px inset, rgba(0, 0, 0, 0.4) 0px 1px 4px 0px, rgba(0, 0, 0, 0.6) 0 0 2px" 487 | }); 488 | style(el, { 489 | backgroundImage: "radial-gradient(circle at center top, rgba(255,255,255,0.8), rgba(255, 255, 255, 0.2) 15px" 490 | }); 491 | return el; 492 | }; 493 | Picker.prototype.presentModal = function(x, y) { 494 | var modalFrame; 495 | style(this.el, { 496 | left: x + "px", 497 | top: y - 10 + "px", 498 | opacity: "0", 499 | webkitTransition: "0.15s", 500 | MozTransition: "0.15s" 501 | }); 502 | modalFrame = document.createElement("div"); 503 | modalFrame.style.position = "fixed"; 504 | modalFrame.style.top = modalFrame.style.left = modalFrame.style.bottom = modalFrame.style.right = "0"; 505 | modalFrame.style.zIndex = "999"; 506 | modalFrame.onclick = function(_this) { 507 | return function() { 508 | var end; 509 | document.body.removeChild(modalFrame); 510 | _this.el.style.top = y + 10 + "px"; 511 | _this.el.style.opacity = 0; 512 | end = function() { 513 | document.body.removeChild(_this.el); 514 | _this.el.removeEventListener("webkitTransitionEnd", end); 515 | return _this.el.removeEventListener("transitionend", end); 516 | }; 517 | _this.el.addEventListener("webkitTransitionEnd", end); 518 | _this.el.addEventListener("transitionend", end); 519 | return _this.emit("closed"); 520 | }; 521 | }(this); 522 | document.body.appendChild(modalFrame); 523 | document.body.appendChild(this.el); 524 | this.el.offsetHeight; 525 | this.el.style.opacity = "1"; 526 | this.el.style.top = y + "px"; 527 | return this; 528 | }; 529 | Picker.prototype.presentModalBeneath = function(el) { 530 | var elPos, x, y; 531 | elPos = el.getBoundingClientRect(); 532 | x = elPos.left + window.scrollX; 533 | y = elPos.bottom + window.scrollY + 4; 534 | return this.presentModal(x, y); 535 | }; 536 | return Picker; 537 | }(); 538 | window.thistle = { 539 | Picker: Picker, 540 | isValidCSSColor: isValidCSSColor 541 | }; 542 | })).call(this); 543 | 544 | if (typeof Color === "undefined") var Color = {}; 545 | 546 | if (typeof Color.Space === "undefined") Color.Space = {}; 547 | 548 | (function() { 549 | "use strict"; 550 | var useEval = false; 551 | var functions = {}; 552 | var shortcuts = { 553 | "HEX24>HSL": "HEX24>RGB>HSL", 554 | "HEX32>HSLA": "HEX32>RGBA>HSLA", 555 | "HEX24>CMYK": "HEX24>RGB>CMY>CMYK", 556 | "RGB>CMYK": "RGB>CMY>CMYK" 557 | }; 558 | var root = Color.Space = function(color, route) { 559 | if (shortcuts[route]) { 560 | route = shortcuts[route]; 561 | } 562 | var r = route.split(">"); 563 | if (typeof color === "object" && color[0] >= 0) { 564 | var type = r[0]; 565 | var tmp = {}; 566 | for (var i = 0; i < type.length; i++) { 567 | var str = type.substr(i, 1); 568 | tmp[str] = color[i]; 569 | } 570 | color = tmp; 571 | } 572 | if (functions[route]) { 573 | return functions[route](color); 574 | } 575 | var f = "color"; 576 | for (var pos = 1, key = r[0]; pos < r.length; pos++) { 577 | if (pos > 1) { 578 | key = key.substr(key.indexOf("_") + 1); 579 | } 580 | key += (pos === 0 ? "" : "_") + r[pos]; 581 | color = root[key](color); 582 | if (useEval) { 583 | f = "Color.Space." + key + "(" + f + ")"; 584 | } 585 | } 586 | if (useEval) { 587 | functions[route] = eval("(function(color) { return " + f + " })"); 588 | } 589 | return color; 590 | }; 591 | root.RGB_W3 = function(o) { 592 | return "rgb(" + (o.R >> 0) + "," + (o.G >> 0) + "," + (o.B >> 0) + ")"; 593 | }; 594 | root.RGBA_W3 = function(o) { 595 | var alpha = typeof o.A === "number" ? o.A / 255 : 1; 596 | return "rgba(" + (o.R >> 0) + "," + (o.G >> 0) + "," + (o.B >> 0) + "," + alpha + ")"; 597 | }; 598 | root.W3_RGB = function(o) { 599 | o = o.substr(4, o.length - 5).split(","); 600 | return { 601 | R: parseInt(o[0], 10), 602 | G: parseInt(o[1], 10), 603 | B: parseInt(o[2], 10) 604 | }; 605 | }; 606 | root.W3_RGBA = function(o) { 607 | o = o.substr(5, o.length - 6).split(","); 608 | return { 609 | R: parseInt(o[0], 10), 610 | G: parseInt(o[1], 10), 611 | B: parseInt(o[2], 10), 612 | A: parseFloat(o[3]) * 255 613 | }; 614 | }; 615 | root.HSL_W3 = function(o) { 616 | return "hsl(" + (o.H + .5 >> 0) + "," + (o.S + .5 >> 0) + "%," + (o.L + .5 >> 0) + "%)"; 617 | }; 618 | root.HSLA_W3 = function(o) { 619 | var alpha = typeof o.A === "number" ? o.A / 255 : 1; 620 | return "hsla(" + (o.H + .5 >> 0) + "," + (o.S + .5 >> 0) + "%," + (o.L + .5 >> 0) + "%," + alpha + ")"; 621 | }; 622 | root.W3_HSL = function(o) { 623 | var start = o.indexOf("(") + 1; 624 | var end = o.indexOf(")"); 625 | o = o.substr(start, end - start).split(","); 626 | return { 627 | H: parseInt(o[0], 10), 628 | S: parseInt(o[1], 10), 629 | L: parseInt(o[2], 10) 630 | }; 631 | }; 632 | root.W3_HSLA = function(o) { 633 | var start = o.indexOf("(") + 1; 634 | var end = o.indexOf(")"); 635 | o = o.substr(start, end - start).split(","); 636 | return { 637 | H: parseInt(o[0], 10), 638 | S: parseInt(o[1], 10), 639 | L: parseInt(o[2], 10), 640 | A: parseFloat(o[3], 10) * 255 641 | }; 642 | }; 643 | root.W3_HEX = root.W3_HEX24 = function(o) { 644 | if (o.substr(0, 1) === "#") o = o.substr(1); 645 | if (o.length === 3) o = o[0] + o[0] + o[1] + o[1] + o[2] + o[2]; 646 | return parseInt("0x" + o, 16); 647 | }; 648 | root.W3_HEX32 = function(o) { 649 | if (o.substr(0, 1) === "#") o = o.substr(1); 650 | if (o.length === 6) { 651 | return parseInt("0xFF" + o, 10); 652 | } else { 653 | return parseInt("0x" + o, 16); 654 | } 655 | }; 656 | root.HEX_W3 = root.HEX24_W3 = function(o, maxLength) { 657 | if (!maxLength) maxLength = 6; 658 | if (!o) o = 0; 659 | var n; 660 | var z = o.toString(16); 661 | n = z.length; 662 | while (n < maxLength) { 663 | z = "0" + z; 664 | n++; 665 | } 666 | n = z.length; 667 | while (n > maxLength) { 668 | z = z.substr(1); 669 | n--; 670 | } 671 | return "#" + z; 672 | }; 673 | root.HEX32_W3 = function(o) { 674 | return root.HEX_W3(o, 8); 675 | }; 676 | root.HEX_RGB = root.HEX24_RGB = function(o) { 677 | return { 678 | R: o >> 16, 679 | G: o >> 8 & 255, 680 | B: o & 255 681 | }; 682 | }; 683 | root.HEX32_RGBA = function(o) { 684 | return { 685 | R: o >>> 16 & 255, 686 | G: o >>> 8 & 255, 687 | B: o & 255, 688 | A: o >>> 24 689 | }; 690 | }; 691 | root.RGBA_HEX32 = function(o) { 692 | return (o.A << 24 | o.R << 16 | o.G << 8 | o.B) >>> 0; 693 | }; 694 | root.RGB_HEX24 = root.RGB_HEX = function(o) { 695 | if (o.R < 0) o.R = 0; 696 | if (o.G < 0) o.G = 0; 697 | if (o.B < 0) o.B = 0; 698 | if (o.R > 255) o.R = 255; 699 | if (o.G > 255) o.G = 255; 700 | if (o.B > 255) o.B = 255; 701 | return o.R << 16 | o.G << 8 | o.B; 702 | }; 703 | root.RGB_CMY = function(o) { 704 | return { 705 | C: 1 - o.R / 255, 706 | M: 1 - o.G / 255, 707 | Y: 1 - o.B / 255 708 | }; 709 | }; 710 | root.RGBA_HSLA = root.RGB_HSL = function(o) { 711 | var _R = o.R / 255, _G = o.G / 255, _B = o.B / 255, min = Math.min(_R, _G, _B), max = Math.max(_R, _G, _B), D = max - min, H, S, L = (max + min) / 2; 712 | if (D === 0) { 713 | H = 0; 714 | S = 0; 715 | } else { 716 | if (L < .5) S = D / (max + min); else S = D / (2 - max - min); 717 | var DR = ((max - _R) / 6 + D / 2) / D; 718 | var DG = ((max - _G) / 6 + D / 2) / D; 719 | var DB = ((max - _B) / 6 + D / 2) / D; 720 | if (_R === max) H = DB - DG; else if (_G === max) H = 1 / 3 + DR - DB; else if (_B === max) H = 2 / 3 + DG - DR; 721 | if (H < 0) H += 1; 722 | if (H > 1) H -= 1; 723 | } 724 | return { 725 | H: H * 360, 726 | S: S * 100, 727 | L: L * 100, 728 | A: o.A 729 | }; 730 | }; 731 | root.RGBA_HSVA = root.RGB_HSV = function(o) { 732 | var _R = o.R / 255, _G = o.G / 255, _B = o.B / 255, min = Math.min(_R, _G, _B), max = Math.max(_R, _G, _B), D = max - min, H, S, V = max; 733 | if (D === 0) { 734 | H = 0; 735 | S = 0; 736 | } else { 737 | S = D / max; 738 | var DR = ((max - _R) / 6 + D / 2) / D; 739 | var DG = ((max - _G) / 6 + D / 2) / D; 740 | var DB = ((max - _B) / 6 + D / 2) / D; 741 | if (_R === max) H = DB - DG; else if (_G === max) H = 1 / 3 + DR - DB; else if (_B === max) H = 2 / 3 + DG - DR; 742 | if (H < 0) H += 1; 743 | if (H > 1) H -= 1; 744 | } 745 | return { 746 | H: H * 360, 747 | S: S * 100, 748 | V: V * 100, 749 | A: o.A 750 | }; 751 | }; 752 | root.CMY_RGB = function(o) { 753 | return { 754 | R: Math.max(0, (1 - o.C) * 255), 755 | G: Math.max(0, (1 - o.M) * 255), 756 | B: Math.max(0, (1 - o.Y) * 255) 757 | }; 758 | }; 759 | root.CMY_CMYK = function(o) { 760 | var C = o.C; 761 | var M = o.M; 762 | var Y = o.Y; 763 | var K = Math.min(Y, Math.min(M, Math.min(C, 1))); 764 | C = Math.round((C - K) / (1 - K) * 100); 765 | M = Math.round((M - K) / (1 - K) * 100); 766 | Y = Math.round((Y - K) / (1 - K) * 100); 767 | K = Math.round(K * 100); 768 | return { 769 | C: C, 770 | M: M, 771 | Y: Y, 772 | K: K 773 | }; 774 | }; 775 | root.CMYK_CMY = function(o) { 776 | return { 777 | C: o.C * (1 - o.K) + o.K, 778 | M: o.M * (1 - o.K) + o.K, 779 | Y: o.Y * (1 - o.K) + o.K 780 | }; 781 | }; 782 | root.HSLA_RGBA = root.HSL_RGB = function(o) { 783 | var H = o.H / 360; 784 | var S = o.S / 100; 785 | var L = o.L / 100; 786 | var R, G, B; 787 | var temp1, temp2, temp3; 788 | if (S === 0) { 789 | R = G = B = L; 790 | } else { 791 | if (L < .5) temp2 = L * (1 + S); else temp2 = L + S - S * L; 792 | temp1 = 2 * L - temp2; 793 | temp3 = H + 1 / 3; 794 | if (temp3 < 0) temp3 += 1; 795 | if (temp3 > 1) temp3 -= 1; 796 | if (6 * temp3 < 1) R = temp1 + (temp2 - temp1) * 6 * temp3; else if (2 * temp3 < 1) R = temp2; else if (3 * temp3 < 2) R = temp1 + (temp2 - temp1) * (2 / 3 - temp3) * 6; else R = temp1; 797 | temp3 = H; 798 | if (temp3 < 0) temp3 += 1; 799 | if (temp3 > 1) temp3 -= 1; 800 | if (6 * temp3 < 1) G = temp1 + (temp2 - temp1) * 6 * temp3; else if (2 * temp3 < 1) G = temp2; else if (3 * temp3 < 2) G = temp1 + (temp2 - temp1) * (2 / 3 - temp3) * 6; else G = temp1; 801 | temp3 = H - 1 / 3; 802 | if (temp3 < 0) temp3 += 1; 803 | if (temp3 > 1) temp3 -= 1; 804 | if (6 * temp3 < 1) B = temp1 + (temp2 - temp1) * 6 * temp3; else if (2 * temp3 < 1) B = temp2; else if (3 * temp3 < 2) B = temp1 + (temp2 - temp1) * (2 / 3 - temp3) * 6; else B = temp1; 805 | } 806 | return { 807 | R: R * 255, 808 | G: G * 255, 809 | B: B * 255, 810 | A: o.A 811 | }; 812 | }; 813 | root.HSVA_RGBA = root.HSV_RGB = function(o) { 814 | var H = o.H / 360; 815 | var S = o.S / 100; 816 | var V = o.V / 100; 817 | var R, G, B, D, A, C; 818 | if (S === 0) { 819 | R = G = B = Math.round(V * 255); 820 | } else { 821 | if (H >= 1) H = 0; 822 | H = 6 * H; 823 | D = H - Math.floor(H); 824 | A = Math.round(255 * V * (1 - S)); 825 | B = Math.round(255 * V * (1 - S * D)); 826 | C = Math.round(255 * V * (1 - S * (1 - D))); 827 | V = Math.round(255 * V); 828 | switch (Math.floor(H)) { 829 | case 0: 830 | R = V; 831 | G = C; 832 | B = A; 833 | break; 834 | case 1: 835 | R = B; 836 | G = V; 837 | B = A; 838 | break; 839 | case 2: 840 | R = A; 841 | G = V; 842 | B = C; 843 | break; 844 | case 3: 845 | R = A; 846 | G = B; 847 | B = V; 848 | break; 849 | case 4: 850 | R = C; 851 | G = A; 852 | B = V; 853 | break; 854 | case 5: 855 | R = V; 856 | G = A; 857 | B = B; 858 | break; 859 | } 860 | } 861 | return { 862 | R: R, 863 | G: G, 864 | B: B, 865 | A: o.A 866 | }; 867 | }; 868 | })(); 869 | 870 | Inlet = function() { 871 | function inlet(ed, options) { 872 | var editor = ed; 873 | var slider; 874 | var picker; 875 | var clicker; 876 | if (!options) options = {}; 877 | if (!options.picker) options.picker = {}; 878 | if (!options.slider) options.slider = {}; 879 | if (!options.clicker) options.clicker = {}; 880 | var container = options.container || document.body; 881 | var topOffset = options.picker.topOffset || 220; 882 | var bottomOffset = options.picker.bottomOffset || 16; 883 | var topBoundary = options.picker.topBoundary || 250; 884 | var leftOffset = options.picker.leftOffset || 75; 885 | var yOffset = options.slider.yOffset || 15; 886 | var xOffset = options.slider.xOffset || 0; 887 | var sliderWidth = options.slider.width; 888 | var horizontalMode = options.horizontalMode || "page"; 889 | var fixedContainer = options.fixedContainer; 890 | var sliderCB = options.slider.callback || function(active) {}; 891 | var pickerCB = options.picker.callback || function(active) {}; 892 | var clickerCB = options.clicker.callback || function(active) {}; 893 | var wrapper = editor.getWrapperElement(); 894 | wrapper.addEventListener("mouseup", onClick); 895 | document.body.addEventListener("mouseup", windowOnClick); 896 | editor.setOption("onKeyEvent", onKeyDown); 897 | var clickerDiv = document.createElement("div"); 898 | clickerDiv.className = "inlet_clicker"; 899 | clickerDiv.style.visibility = "hidden"; 900 | clickerDiv.style.position = "absolute"; 901 | container.appendChild(clickerDiv); 902 | var clicker = document.createElement("input"); 903 | clicker.className = "checkbox"; 904 | clicker.setAttribute("type", "checkbox"); 905 | clicker.addEventListener("change", onClicker); 906 | clickerDiv.appendChild(clicker); 907 | function onClicker(event) { 908 | var value = String(clicker.checked); 909 | var cursor = editor.getCursor(true); 910 | var boolean = getMatch(cursor, "boolean"); 911 | if (!boolean) return; 912 | var start = { 913 | line: cursor.line, 914 | ch: boolean.start 915 | }; 916 | var end = { 917 | line: cursor.line, 918 | ch: boolean.end 919 | }; 920 | editor.replaceRange(value, start, end); 921 | } 922 | var sliderDiv = document.createElement("div"); 923 | sliderDiv.className = "inlet_slider"; 924 | sliderDiv.style.visibility = "hidden"; 925 | if (sliderWidth) { 926 | sliderDiv.style.width = sliderWidth; 927 | } 928 | if (fixedContainer) { 929 | sliderDiv.style.position = "fixed"; 930 | } else { 931 | sliderDiv.style.position = "absolute"; 932 | } 933 | sliderDiv.style.top = 0; 934 | container.appendChild(sliderDiv); 935 | var slider = document.createElement("input"); 936 | slider.className = "range"; 937 | slider.setAttribute("type", "range"); 938 | slider.addEventListener("input", onSlide); 939 | slider.addEventListener("change", onSlide); 940 | var isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1; 941 | if (!isFirefox) slider.addEventListener("mouseup", onSlideMouseUp); 942 | sliderDiv.appendChild(slider); 943 | function onSlide(event) { 944 | var value = String(slider.value); 945 | var cursor = editor.getCursor(true); 946 | var number = getMatch(cursor, "number"); 947 | if (!number) return; 948 | var start = { 949 | line: cursor.line, 950 | ch: number.start 951 | }; 952 | var end = { 953 | line: cursor.line, 954 | ch: number.end 955 | }; 956 | editor.dragging = true; 957 | editor.replaceRange(value, start, end); 958 | } 959 | function onSlideMouseUp(event) { 960 | slider.value = 0; 961 | var cursor = editor.getCursor(true); 962 | var number = getMatch(cursor, "number"); 963 | if (!number) return; 964 | var value = parseFloat(number.string); 965 | var sliderRange = getSliderRange(value); 966 | slider.setAttribute("value", value); 967 | slider.setAttribute("step", sliderRange.step); 968 | slider.setAttribute("min", sliderRange.min); 969 | slider.setAttribute("max", sliderRange.max); 970 | slider.value = value; 971 | editor.dragging = false; 972 | } 973 | var clickTarget; 974 | function windowOnClick(evt) { 975 | if (evt.target === clickTarget || evt.target === sliderDiv || evt.target === slider || evt.target === clickerDiv || evt.target === clicker) return; 976 | sliderDiv.style.visibility = "hidden"; 977 | clickerDiv.style.visibility = "hidden"; 978 | } 979 | var LEFT = 37; 980 | var UP = 38; 981 | var RIGHT = 39; 982 | var DOWN = 40; 983 | function onKeyDown() { 984 | if (arguments.length == 1) { 985 | event = arguments[0]; 986 | } else { 987 | event = arguments[1]; 988 | } 989 | if (event.keyCode == LEFT || event.keyCode == DOWN) { 990 | if (sliderDiv.style.visibility === "visible") { 991 | slider.stepDown(1); 992 | onSlide(); 993 | return true; 994 | } else if (event.altKey) { 995 | onClick(); 996 | } else {} 997 | } else if (event.keyCode == RIGHT || event.keyCode == UP) { 998 | if (sliderDiv.style.visibility === "visible") { 999 | slider.stepUp(1); 1000 | onSlide(); 1001 | return true; 1002 | } else if (event.altKey) { 1003 | onClick(); 1004 | } else {} 1005 | } else { 1006 | sliderDiv.style.visibility = "hidden"; 1007 | } 1008 | } 1009 | var pickerCallback = function(color, type) { 1010 | var cursor = editor.getCursor(); 1011 | if (!type) return; 1012 | var match = getMatch(cursor, type); 1013 | var start = { 1014 | line: cursor.line, 1015 | ch: match.start 1016 | }; 1017 | var end = { 1018 | line: cursor.line, 1019 | ch: match.end 1020 | }; 1021 | editor.picking = true; 1022 | editor.replaceRange(color, start, end); 1023 | setTimeout(function() { 1024 | editor.picking = false; 1025 | }, 100); 1026 | }; 1027 | picker = new thistle.Picker("#ffffff"); 1028 | function onClick(ev) { 1029 | if (editor.somethingSelected()) { 1030 | return; 1031 | } 1032 | clickTarget = ev.target; 1033 | var cursor = editor.getCursor(true); 1034 | var token = editor.getTokenAt(cursor); 1035 | cursorOffset = editor.cursorCoords(true, "page"); 1036 | var leftBase = editor.cursorCoords(true, horizontalMode).left; 1037 | var numberMatch = getMatch(cursor, "number"); 1038 | var hslMatch = getMatch(cursor, "hsl"); 1039 | var hexMatch = getMatch(cursor, "hex"); 1040 | var rgbMatch = getMatch(cursor, "rgb"); 1041 | var booleanMatch = getMatch(cursor, "boolean"); 1042 | var pickerTop = cursorOffset.top - topOffset; 1043 | if (cursorOffset.top < topBoundary) { 1044 | pickerTop = cursorOffset.top + bottomOffset; 1045 | } 1046 | var pickerLeft = leftBase - leftOffset; 1047 | sliderDiv.style.visibility = "hidden"; 1048 | clickerDiv.style.visibility = "hidden"; 1049 | if (hexMatch) { 1050 | var color = hexMatch.string; 1051 | picker = new thistle.Picker(color); 1052 | picker.setCSS(color); 1053 | picker.presentModal(pickerLeft, pickerTop); 1054 | picker.on("changed", function() { 1055 | picked = picker.getCSS(); 1056 | picked = Color.Space(picked, "W3>HSL>RGB>HEX24>W3"); 1057 | pickerCallback(picked, "hex"); 1058 | }); 1059 | } else if (hslMatch) { 1060 | var color = hslMatch.string; 1061 | picker = new thistle.Picker(color); 1062 | picker.setCSS(color); 1063 | picker.presentModal(pickerLeft, pickerTop); 1064 | picker.on("changed", function() { 1065 | picked = picker.getCSS(); 1066 | pickerCallback(picked, "hsl"); 1067 | }); 1068 | } else if (rgbMatch) { 1069 | var color = rgbMatch.string; 1070 | picker = new thistle.Picker(color); 1071 | picker.setCSS(color); 1072 | picker.presentModal(pickerLeft, pickerTop); 1073 | picker.on("changed", function() { 1074 | picked = picker.getCSS(); 1075 | picked = Color.Space(picked, "W3>HSL>RGB>W3"); 1076 | pickerCallback(picked, "rgb"); 1077 | }); 1078 | } else if (numberMatch) { 1079 | slider.value = 0; 1080 | var value = parseFloat(numberMatch.string); 1081 | var sliderRange = getSliderRange(value); 1082 | slider.setAttribute("value", value); 1083 | slider.setAttribute("step", sliderRange.step); 1084 | slider.setAttribute("min", sliderRange.min); 1085 | slider.setAttribute("max", sliderRange.max); 1086 | slider.value = value; 1087 | var sliderTop = cursorOffset.top - yOffset; 1088 | var sliderStyle = window.getComputedStyle(sliderDiv); 1089 | var sliderWidth = getPixels(sliderStyle.width); 1090 | var sliderLeft = leftBase - sliderWidth / 2 + xOffset; 1091 | sliderDiv.style.top = sliderTop - 10 + "px"; 1092 | sliderDiv.style.left = sliderLeft + "px"; 1093 | sliderDiv.style.visibility = "visible"; 1094 | } else if (booleanMatch) { 1095 | var clickerTop = cursorOffset.top - yOffset; 1096 | var clickerStyle = window.getComputedStyle(clickerDiv); 1097 | var clickerWidth = getPixels(clickerStyle.width); 1098 | var clickerLeft = leftBase - clickerWidth / 2 + xOffset; 1099 | var value = JSON.parse(booleanMatch.string); 1100 | if (value) { 1101 | clickerDiv.removeChild(clicker); 1102 | clicker = document.createElement("input"); 1103 | clicker.className = "checkbox"; 1104 | clicker.setAttribute("type", "checkbox"); 1105 | clicker.setAttribute("checked", "checked"); 1106 | clicker.addEventListener("change", onClicker); 1107 | clickerDiv.appendChild(clicker); 1108 | } else { 1109 | clickerDiv.removeChild(clicker); 1110 | clicker = document.createElement("input"); 1111 | clicker.className = "checkbox"; 1112 | clicker.setAttribute("type", "checkbox"); 1113 | clicker.addEventListener("change", onClicker); 1114 | clickerDiv.appendChild(clicker); 1115 | } 1116 | clickerDiv.style.top = clickerTop - 3 + "px"; 1117 | clickerDiv.style.left = clickerLeft + "px"; 1118 | clickerDiv.style.visibility = "visible"; 1119 | } else {} 1120 | } 1121 | function getSliderRange(value) { 1122 | var range, step, sliderMin, sliderMax; 1123 | if (value === 0) { 1124 | range = [ -100, 100 ]; 1125 | } else { 1126 | range = [ -value * 3, value * 5 ]; 1127 | } 1128 | if (range[0] < range[1]) { 1129 | min = range[0]; 1130 | max = range[1]; 1131 | } else { 1132 | min = range[1]; 1133 | max = range[0]; 1134 | } 1135 | if (max - min > 20) { 1136 | step = 1; 1137 | } else { 1138 | step = (max - min) / 200; 1139 | } 1140 | return { 1141 | min: min, 1142 | max: max, 1143 | step: step 1144 | }; 1145 | } 1146 | function getMatch(cursor, type) { 1147 | if (!type) return; 1148 | var re; 1149 | switch (type.toLowerCase()) { 1150 | case "boolean": 1151 | re = /true|false/g; 1152 | break; 1153 | case "hsl": 1154 | re = /hsla?\(\s*(\d{1,3})\s*,\s*(\d{1,3}\%)\s*,\s*(\d{1,3}\%)\s*(?:\s*,\s*(\d+(?:\.\d+)?)\s*)?\)/g; 1155 | break; 1156 | case "rgb": 1157 | re = /rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)/g; 1158 | break; 1159 | case "hex": 1160 | re = /#[a-fA-F0-9]{3,6}/g; 1161 | break; 1162 | case "number": 1163 | re = /[-]?\.?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g; 1164 | break; 1165 | default: 1166 | throw new Error("invalid match selection"); 1167 | return; 1168 | } 1169 | var line = editor.getLine(cursor.line); 1170 | var match = re.exec(line); 1171 | while (match) { 1172 | var val = match[0]; 1173 | var len = val.length; 1174 | var start = match.index; 1175 | var end = match.index + len; 1176 | if (cursor.ch >= start && cursor.ch <= end) { 1177 | match = null; 1178 | return { 1179 | start: start, 1180 | end: end, 1181 | string: val 1182 | }; 1183 | } 1184 | match = re.exec(line); 1185 | } 1186 | return; 1187 | } 1188 | } 1189 | function getPixels(style) { 1190 | var pix = 0; 1191 | if (style.length > 2) { 1192 | pix = parseFloat(style.slice(0, style.length - 2)); 1193 | } 1194 | if (!pix) pix = 0; 1195 | return pix; 1196 | } 1197 | function getOffset(el) { 1198 | var _x = 0; 1199 | var _y = 0; 1200 | while (el && !isNaN(el.offsetLeft) && !isNaN(el.offsetTop)) { 1201 | _x += el.offsetLeft - el.scrollLeft; 1202 | _y += el.offsetTop - el.scrollTop; 1203 | el = el.offsetParent; 1204 | } 1205 | return { 1206 | top: _y, 1207 | left: _x 1208 | }; 1209 | } 1210 | return inlet; 1211 | }(); --------------------------------------------------------------------------------