|
8 | 8 | [re-com.util :as u :refer [deref-or-value position-for-id item-for-id ->v]]
|
9 | 9 | [re-com.box :refer [align-style flex-child-style v-box h-box box]]
|
10 | 10 | [re-com.validate :refer [vector-of-maps? css-style? html-attr? parts? number-or-string? log-warning
|
11 |
| - string-or-hiccup? position? position-options-list] :refer-macros [validate-args-macro]] |
| 11 | + string-or-hiccup? position? position-options-list part?] :refer-macros [validate-args-macro]] |
12 | 12 | [re-com.popover :refer [popover-tooltip]]
|
13 | 13 | [clojure.string :as string]
|
14 | 14 | [react :as react]
|
|
19 | 19 | ;; Inspiration: http://alxlit.name/bootstrap-chosen
|
20 | 20 | ;; Alternative: http://silviomoreto.github.io/bootstrap-select
|
21 | 21 |
|
22 |
| -(defn anchor-part [{:keys [label placeholder state theme]}] |
23 |
| - [:a (theme/props {:state state :part ::anchor} theme) |
24 |
| - (or label placeholder "Select an item")]) |
| 22 | +(def dropdown-parts-desc |
| 23 | + (when include-args-desc? |
| 24 | + [{:impl "[v-box]" |
| 25 | + :level 0 |
| 26 | + :name :wrapper |
| 27 | + :notes "Outer wrapper."} |
| 28 | + {:name :backdrop |
| 29 | + :impl "user-defined" |
| 30 | + :level 1 |
| 31 | + :notes "Transparent, clickable backdrop. Shown when the dropdown is open."} |
| 32 | + {:name :anchor-wrapper |
| 33 | + :impl "[box]" |
| 34 | + :level 1 |
| 35 | + :notes "Wraps the :anchor part. Opens or closes the dropdown when clicked."} |
| 36 | + {:name :anchor |
| 37 | + :impl "user-defined" |
| 38 | + :level 2 |
| 39 | + :notes "Displays the :label or :placeholder."} |
| 40 | + {:name :body-wrapper |
| 41 | + :impl "[box]" |
| 42 | + :level 1 |
| 43 | + :notes "Wraps the :body part. Provides intelligent positioning."} |
| 44 | + {:name :body |
| 45 | + :impl "user-defined" |
| 46 | + :level 2 |
| 47 | + :notes "Shown when the dropdown is open."}])) |
| 48 | + |
| 49 | +(def dropdown-parts |
| 50 | + (when include-args-desc? |
| 51 | + (-> (map :name dropdown-parts-desc) set))) |
| 52 | + |
| 53 | +(def dropdown-args-desc |
| 54 | + (when include-args-desc? |
| 55 | + [{:description "True when the dropdown is open." |
| 56 | + :name :model |
| 57 | + :required false |
| 58 | + :type "boolean | r/atom"} |
| 59 | + {:description |
| 60 | + "Called when the dropdown opens or closes." |
| 61 | + :name :on-change |
| 62 | + :required false |
| 63 | + :type "boolean -> nil" |
| 64 | + :validate-fn fn?} |
| 65 | + {:name :anchor |
| 66 | + :type "part" |
| 67 | + :validate-fn part? |
| 68 | + :required? false |
| 69 | + :description |
| 70 | + [:span "String, hiccup or function. When a function, acceps keyword args " |
| 71 | + [:code ":placholder"] ", " |
| 72 | + [:code ":label"] ", " |
| 73 | + [:code ":theme"] ", " |
| 74 | + [:code ":parts"] ", " |
| 75 | + [:code ":state"] " " |
| 76 | + " and " |
| 77 | + [:code ":transition!"] |
| 78 | + ". Returns either a string or hiccup, which shows within the clickable dropdown box."]} |
| 79 | + {:name :backdrop |
| 80 | + :required? false |
| 81 | + :type "part" |
| 82 | + :validate-fn part? |
| 83 | + :description (str "Displays when the dropdown is open. By default, renders a " |
| 84 | + "transparent overlay. Clicking this overlay closes the dropdown. " |
| 85 | + "When a function, :backdrop is passed the same keyword arguments " |
| 86 | + "as :anchor.")} |
| 87 | + {:name :body |
| 88 | + :required? false |
| 89 | + :type "part" |
| 90 | + :validate-fn part? |
| 91 | + :description (str "Displays when the dropdown is open. " |
| 92 | + "Appears either above or below the :anchor, " |
| 93 | + "depending on available screen-space. When a function, " |
| 94 | + ":body is passed the same keyword arguments as :anchor.")} |
| 95 | + {:name :disabled? |
| 96 | + :required false |
| 97 | + :type "boolean | r/atom"} |
| 98 | + {:default 0 |
| 99 | + :description "component's tabindex. A value of -1 removes from order" |
| 100 | + :name :tab-index |
| 101 | + :required false |
| 102 | + :type "integer | string" |
| 103 | + :validate-fn number-or-string?} |
| 104 | + {:description "height of the :anchor-wrapper part" |
| 105 | + :name :anchor-height |
| 106 | + :required false |
| 107 | + :type "integer | string" |
| 108 | + :validate-fn number-or-string?} |
| 109 | + {:description "height of the :body-wrapper part" |
| 110 | + :name :height |
| 111 | + :required false |
| 112 | + :type "integer | string" |
| 113 | + :validate-fn number-or-string?} |
| 114 | + {:description "min-height of the :body-wrapper part" |
| 115 | + :name :min-height |
| 116 | + :required false |
| 117 | + :type "integer | string" |
| 118 | + :validate-fn number-or-string?} |
| 119 | + {:description "max-height of the :body-wrapper part" |
| 120 | + :name :max-height |
| 121 | + :required false |
| 122 | + :type "integer | string" |
| 123 | + :validate-fn number-or-string?} |
| 124 | + {:description "width of the :anchor-wrapper and :body-wrapper parts" |
| 125 | + :name :width |
| 126 | + :required false |
| 127 | + :type "integer | string" |
| 128 | + :validate-fn number-or-string?} |
| 129 | + {:description "min-width of the :anchor-wrapper and :body-wrapper parts" |
| 130 | + :name :min-width |
| 131 | + :required false |
| 132 | + :type "integer | string" |
| 133 | + :validate-fn number-or-string?} |
| 134 | + {:description "max-width of the :anchor-wrapper and :body-wrapper parts" |
| 135 | + :name :max-width |
| 136 | + :required false |
| 137 | + :type "integer | string" |
| 138 | + :validate-fn number-or-string?} |
| 139 | + {:description (str "passed as a prop to the :anchor part. The default :anchor " |
| 140 | + "part will display :label inside a the clickable dropdown box.") |
| 141 | + :name :label |
| 142 | + :required false |
| 143 | + :type "string | hiccup"} |
| 144 | + {:default "\"Select an item\"" |
| 145 | + :description (str "passed as a prop to the :anchor part. The default :anchor part will " |
| 146 | + "show :placeholder in the clickable box if there is no :label.") |
| 147 | + :name :placeholder |
| 148 | + :required false |
| 149 | + :type "string | hiccup"} |
| 150 | + {:description "See Parts section below." |
| 151 | + :name :parts |
| 152 | + :required false |
| 153 | + :type "map" |
| 154 | + :validate-fn (parts? dropdown-parts)} |
| 155 | + {:name :theme |
| 156 | + :description "alpha"} |
| 157 | + {:name :main-theme |
| 158 | + :description "alpha"} |
| 159 | + {:name :theme-vars |
| 160 | + :description "alpha"} |
| 161 | + {:name :base-theme |
| 162 | + :description "alpha"}])) |
| 163 | + |
| 164 | +(defn anchor [{:keys [label placeholder state theme transition!]}] |
| 165 | + [:a (theme/props {:state state :part ::anchor :transition! transition!} theme) |
| 166 | + (or label placeholder)]) |
25 | 167 |
|
26 |
| -(defn backdrop-part [{:keys [state transition!]}] |
| 168 | +(defn backdrop [{:keys [state transition!]}] |
27 | 169 | (fn [{:keys [dropdown-open? state theme parts]}]
|
28 | 170 | [:div (theme/props {:transition! transition! :state state :part ::backdrop} theme)]))
|
29 | 171 |
|
|
58 | 200 | best-y (case v-pos :low a-h :high (- p-h))]
|
59 | 201 | [best-x best-y]))
|
60 | 202 |
|
61 |
| -(defn body-wrapper [{:keys [state parts theme anchor-ref popover-ref anchor-position]} & children] |
| 203 | +(defn body-wrapper [{:keys [state theme anchor-ref popover-ref anchor-position]} & children] |
62 | 204 | (let [set-popover-ref! #(reset! popover-ref %)
|
63 | 205 | optimize-position! #(reset! anchor-position (optimize-position! @anchor-ref @popover-ref))
|
64 | 206 | mounted! #(do
|
|
76 | 218 | (let [[left top] (or @anchor-position [0 0])]
|
77 | 219 | (into
|
78 | 220 | [:div#popover
|
79 |
| - (-> |
80 |
| - {:ref set-popover-ref! |
81 |
| - :style {:z-index 99999 |
82 |
| - :position "absolute" |
83 |
| - :top (str top "px") |
84 |
| - :left (str left "px") |
85 |
| - :opacity (if @anchor-position 1 0) |
86 |
| - :transition "opacity 0.2s"}} |
87 |
| - (theme/apply {:state state :part ::body-wrapper} theme))] |
| 221 | + (theme/apply {} |
| 222 | + {:state (merge state {:top top |
| 223 | + :left left |
| 224 | + :ref set-popover-ref!}) |
| 225 | + :part ::body-wrapper} |
| 226 | + theme)] |
88 | 227 | children)))})))
|
89 | 228 |
|
90 | 229 | (defn dropdown
|
|
94 | 233 | (let [[focused? anchor-ref popover-ref anchor-position] (repeatedly #(reagent/atom nil))
|
95 | 234 | anchor-ref! #(reset! anchor-ref %)
|
96 | 235 | transitionable (reagent/atom
|
97 |
| - (if @model :in :out))] |
| 236 | + (if (deref-or-value model) :in :out))] |
98 | 237 | (fn dropdown-render
|
99 | 238 | [& {:keys [disabled? on-change tab-index
|
100 |
| - width height min-width max-width min-height max-height anchor-height |
| 239 | + anchor-height |
| 240 | + model |
101 | 241 | label placeholder
|
102 | 242 | anchor backdrop body
|
103 |
| - parts style theme main-theme theme-vars base-theme] |
| 243 | + parts theme main-theme theme-vars base-theme |
| 244 | + width] |
| 245 | + :or {placeholder "Select an item"} |
104 | 246 | :as args}]
|
105 |
| - (let [theme {:variables theme-vars |
106 |
| - :base base-theme |
107 |
| - :main main-theme |
108 |
| - :user [theme (theme/parts parts)]} |
109 |
| - state {:openable (if @model :open :closed) |
110 |
| - :enable (if disabled? :disabled :enabled) |
111 |
| - :tab-index tab-index |
112 |
| - :focusable (if @focused? :focused :blurred) |
113 |
| - :transitionable @transitionable} |
114 |
| - open! (if on-change |
115 |
| - (handler-fn (on-change true)) |
116 |
| - (handler-fn (reset! model true))) |
117 |
| - close! (if on-change |
118 |
| - (handler-fn (on-change false)) |
119 |
| - (handler-fn (reset! model false))) |
120 |
| - transition! (fn [k] |
121 |
| - ((case k |
122 |
| - :toggle (if (-> state :openable (= :open)) open! close!) |
123 |
| - :open open! |
124 |
| - :close close! |
125 |
| - :focus #(reset! focused? true) |
126 |
| - :blur #(reset! focused? false) |
127 |
| - :enter #(js/setTimeout (fn [] (reset! transitionable :in)) 50) |
128 |
| - :exit #(js/setTimeout (fn [] (reset! transitionable :out)) 50)))) |
129 |
| - themed (fn [part props] (theme/apply props |
130 |
| - {:state state |
131 |
| - :part part |
132 |
| - :transition! transition!} |
133 |
| - theme)) |
134 |
| - part-props {:placeholder placeholder |
135 |
| - :transition! transition! |
136 |
| - :label label |
137 |
| - :theme theme |
138 |
| - :parts parts |
139 |
| - :state state}] |
140 |
| - [v-box |
141 |
| - (themed ::wrapper |
142 |
| - {:src (at) |
143 |
| - :style {:height anchor-height} |
144 |
| - :children |
145 |
| - [(when (= :open (:openable state)) |
146 |
| - [u/part backdrop part-props backdrop-part]) |
147 |
| - [box |
148 |
| - (themed ::anchor-wrapper |
149 |
| - {:src (at) |
150 |
| - :style {:padding "unset" |
151 |
| - :width "100%"} |
152 |
| - :attr {:ref anchor-ref! |
153 |
| - :on-click #(swap! model not)} |
154 |
| - :child [u/part anchor part-props anchor-part]})] |
155 |
| - (when (= :open (:openable state)) |
156 |
| - [body-wrapper {:anchor-ref anchor-ref |
157 |
| - :popover-ref popover-ref |
158 |
| - :anchor-position anchor-position |
159 |
| - :parts parts |
160 |
| - :state state |
161 |
| - :theme theme} |
162 |
| - [u/part body part-props]])]})])))) |
| 247 | + (or (validate-args-macro dropdown-args-desc args) |
| 248 | + (let [state {:openable (if (deref-or-value model) :open :closed) |
| 249 | + :enable (if disabled? :disabled :enabled) |
| 250 | + :tab-index tab-index |
| 251 | + :focusable (if (deref-or-value focused?) :focused :blurred) |
| 252 | + :transitionable @transitionable} |
| 253 | + open! (if on-change |
| 254 | + (handler-fn (on-change true)) |
| 255 | + #(reset! model true)) |
| 256 | + close! (if on-change |
| 257 | + (handler-fn (on-change false)) |
| 258 | + #(reset! model false)) |
| 259 | + transition! (fn [k] |
| 260 | + (case k |
| 261 | + :toggle (if (-> state :openable (= :open)) |
| 262 | + (close!) |
| 263 | + (open!)) |
| 264 | + :open (open!) |
| 265 | + :close (close!) |
| 266 | + :focus (reset! focused? true) |
| 267 | + :blur (reset! focused? false) |
| 268 | + :enter (js/setTimeout (fn [] (reset! transitionable :in)) 50) |
| 269 | + :exit (js/setTimeout (fn [] (reset! transitionable :out)) 50))) |
| 270 | + theme {:variables theme-vars |
| 271 | + :base base-theme |
| 272 | + :main main-theme |
| 273 | + :user [theme |
| 274 | + (theme/parts parts) |
| 275 | + (theme/<-props (merge args {:height anchor-height}) |
| 276 | + {:part ::anchor-wrapper |
| 277 | + :exclude [:max-height :min-height]}) |
| 278 | + (theme/<-props args |
| 279 | + {:part ::body-wrapper |
| 280 | + :include [:width :min-width |
| 281 | + :min-height :max-height]})]} |
| 282 | + themed (fn [part props & [special-theme]] |
| 283 | + (theme/apply props |
| 284 | + {:state state |
| 285 | + :part part |
| 286 | + :transition! transition!} |
| 287 | + (or special-theme theme))) |
| 288 | + part-props {:placeholder placeholder |
| 289 | + :transition! transition! |
| 290 | + :label label |
| 291 | + :theme theme |
| 292 | + :parts parts |
| 293 | + :state state}] |
| 294 | + [v-box |
| 295 | + (themed ::wrapper |
| 296 | + {:src (at) |
| 297 | + :children |
| 298 | + [(when (= :open (:openable state)) |
| 299 | + [u/part backdrop part-props re-com.dropdown/backdrop]) |
| 300 | + [box |
| 301 | + (themed ::anchor-wrapper |
| 302 | + {:src (at) |
| 303 | + :attr {:ref anchor-ref!} |
| 304 | + :child [u/part anchor part-props re-com.dropdown/anchor]})] |
| 305 | + (when (= :open (:openable state)) |
| 306 | + [body-wrapper {:anchor-ref anchor-ref |
| 307 | + :popover-ref popover-ref |
| 308 | + :anchor-position anchor-position |
| 309 | + :parts parts |
| 310 | + :state state |
| 311 | + :theme theme} |
| 312 | + [u/part body part-props]])]})]))))) |
163 | 313 |
|
164 | 314 | (defn- move-to-new-choice
|
165 | 315 | "In a vector of maps (where each map has an :id), return the id of the choice offset posititions away
|
|
0 commit comments