diff --git a/src/re_com/tree_select.cljs b/src/re_com/tree_select.cljs index 1b95ef36..d4324daa 100644 --- a/src/re_com/tree_select.cljs +++ b/src/re_com/tree_select.cljs @@ -7,6 +7,7 @@ [re-com.config :refer [include-args-desc?]] [re-com.util :refer [deref-or-value]] [re-com.box :refer [h-box v-box box gap]] + [re-com.checkbox :refer [checkbox]] [re-com.validate :as validate :refer [parts?]])) (def tree-select-parts-desc nil) @@ -14,7 +15,16 @@ (def tree-select-args-desc (when include-args-desc? [{:name :choices :required true :type "vector of maps | r/atom" :validate-fn validate/vector-of-maps? :description [:span "Each map represents a choice. Values corresponding to id, & label are extracted by the functions " [:code ":id-fn"] " & " [:code ":label-fn"] ". See below."]} - {:name :model :required true :type "a set of ids | r/atom" :description [:span "The set of the ids for currently selected choices. If nil or empty, see " [:code ":placeholder"] "."]}])) + {:name :model :required true :type "a set of ids | r/atom" :description [:span "The set of the ids for currently selected choices. If nil or empty, see " [:code ":placeholder"] "."]} + {:name :groups :required false :default "(reagent/atom nil)" :type "a set of paths | r/atom" :description [:span "The set of currently expanded group paths."]} + {:name :open-to :required false :type "keyword" :description [:span "How to expand groups when the component first mounts."]} + {:name :on-change :required true :type "set of ids -> nil" :validate-fn fn? :description [:span "This function is called whenever the selection changes. Called with one argument, the set of selected ids. See " [:code ":model"] "."]} + {:name :on-groups-change :required false :default "#(reset! groups %)" :type "set of ids -> nil" :validate-fn fn? :description [:span "This function is called whenever a group expands or collapses. Called with one argument, the set of expanded groups. See " [:code ":groups"] "."]} + {:name :disabled? :required false :default false :type "boolean" :description "if true, no user selection is allowed"} + {:name :label-fn :required false :default ":label" :type "map -> hiccup" :validate-fn ifn? :description [:span "A function which can turn a choice into a displayable label. Will be called for each element in " [:code ":choices"] ". Given one argument, a choice map, it returns a string or hiccup."]} + {:name :group-label-fn :required false :default "(comp name last)" :type "vector -> hiccup" :validate-fn ifn? :description [:span "A function which can turn a group vector into a displayable label. Will be called for each element in " [:code ":groups"] ". Given one argument, a group vector, it returns a string or hiccup."]} + {:name :group-renderer :required false :default "re-com.tree-select/group" :type [:code "{:keys [group label hide-show! toggle! open? checked? model disabled? showing? level]} -> hiccup"] :validate-fn ifn? :description "You can provide a renderer function to override the inbuilt renderer for group headers."} + {:name :choice-renderer :required false :default "re-com.tree-select/choice" :type [:code "{:keys [choice model label showing? disabled? toggle! checked? level]} -> hiccup"] :validate-fn ifn? :description "You can provide a renderer function to override the inbuilt renderer for group headers."}])) (def tree-select-dropdown-parts-desc nil) @@ -26,42 +36,34 @@ (when showing? [h-box :children - [[gap :size (str level "rem")] - [box - :attr {:on-click (when-not disabled? toggle!)} - :style {:cursor (if disabled? "default" "pointer")} - :child - (if checked? "☑" "☐")] - " " - label]])) + [[box + :style {:visibility "hidden"} + :child (apply str (repeat level "⯈"))] + [checkbox + :model checked? + :on-change toggle! + :label label + :disabled? disabled?]]])) (defn group [{:keys [label checked? toggle! hide-show! level showing? open? disabled?]}] (when showing? [h-box :class "chosen-container chosen-container-single chosen-container-active" - :style {:margin-left (str (dec level) "rem")} :children [[box - :attr {:on-click (when-not disabled? hide-show!)} + :style {:visibility "hidden"} + :child (apply str (repeat (dec level) "⯈"))] + [box + :attr {:on-click hide-show!} :style {:cursor "pointer"} :child (if open? "⯆" "⯈")] " " - #_[checkbox :src (at) - :model checked? - :on-change toggle! - :label label - :disabled? disabled? - :style {}] - [box - :attr {:on-click (when-not disabled? toggle!)} - :style {:cursor "default"} - :child - (case checked? :all "☑" :some "◽" "☐")] - " " - [:span - {:style {:overflow "none" - :word-wrap "break-word"}} - label]]])) + [checkbox + :attr {:ref #(when % (set! (.-indeterminate %) (= :some checked?)))} + :model checked? + :on-change toggle! + :label label + :disabled? disabled?]]])) (def group? (comp #{:group} :type)) @@ -85,65 +87,64 @@ ((fnil conj #{}) s k))) (defn tree-select - [& {:as props - :keys [model choices choice-renderer group-renderer groups open-to id-fn] - :or {open-to :chosen - groups (r/atom nil) + [& {:keys [model choices choice-renderer group-renderer groups on-groups-change open-to id-fn] + :or {groups (r/atom nil) id-fn :id + on-groups-change #(reset! groups %) choice-renderer choice group-renderer group}}] - (let [open-to (deref-or-value open-to)] - (println open-to) - (when-not (= :none open-to) - (reset! groups (case open-to - :all (infer-groups choices) - (:chosen nil) - (into #{} - (comp - (filter #(contains? (deref-or-value model) (id-fn %))) - (keep :group) - (mapcat ancestor-paths)) - choices))))) - (fn [& {:keys [choices group-label-fn disabled? min-width max-width on-change] - :or {group-label-fn {}}}] + (when-let [open-to (deref-or-value open-to)] + (on-groups-change (case open-to + :all (set (map :group (infer-groups choices))) + :none #{} + :chosen + (into #{} + (comp + (filter #(contains? (deref-or-value model) (id-fn %))) + (keep :group) + (mapcat ancestor-paths)) + choices)))) + (fn [& {:keys [choices group-label-fn disabled? min-width max-width on-change on-groups-change label-fn] + :or {on-groups-change #(reset! groups %)}}] (let [choices (deref-or-value choices) disabled? (deref-or-value disabled?) + label-fn (or label-fn :label) + group-label-fn (or group-label-fn (comp name last)) items (->> choices infer-groups (into choices) (sort-by (juxt (comp #(apply str %) :group) (complement group?)))) - item (fn [{:keys [group id] :as item}] + item (fn [{:keys [group id] :as item-props}] (let [group-v (as-v group)] - (if (group? item) + (if (group? item-props) [group-renderer (let [descendant? #(= group-v (vec (take (count group-v) (as-v (:group %))))) descendants (map :id (filter descendant? choices)) checked? (cond (every? (deref-or-value model) descendants) :all (some (deref-or-value model) descendants) :some)] - (merge item - {:label (or (group-label-fn (peek group-v)) (peek group-v)) - :hide-show! #(swap! groups toggle group-v) - :toggle! #(swap! model - (if (= :all checked?) set/difference (fnil into #{})) - descendants) - :open? (contains? @groups group-v) - :checked? checked? - :model model - :disabled? disabled? - :showing? (every? (set @groups) (rest (ancestor-paths group-v))) - :level (count group-v)}))] + {:group group-v + :label (group-label-fn group-v) + :hide-show! #(on-groups-change (toggle @groups group-v)) + :toggle! #(swap! model + (if (= :all checked?) set/difference (fnil into #{})) + descendants) + :open? (contains? @groups group-v) + :checked? checked? + :model model + :disabled? disabled? + :showing? (every? (set @groups) (rest (ancestor-paths group-v))) + :level (count group-v)})] [choice-renderer - (merge item - {:model model - :showing? (if-not group-v - true - (every? (set @groups) (ancestor-paths group-v))) - :disabled? disabled? - :toggle! (handler-fn (on-change (toggle @model id))) - :checked? (get @model id) - :level (inc (count group-v))})])))] - [:<> - [v-box - :min-width min-width - :max-width max-width - :children - (mapv item items)]]))) + {:choice item-props + :model model + :label (label-fn item-props) + :showing? (if-not group-v + true + (every? (set @groups) (ancestor-paths group-v))) + :disabled? disabled? + :toggle! (handler-fn (on-change (toggle @model id))) + :checked? (get @model id) + :level (inc (count group-v))}])))] + [v-box + :min-width min-width + :max-width max-width + :children (mapv item items)]))) diff --git a/src/re_demo/tree_select.cljs b/src/re_demo/tree_select.cljs index 9e9ecb58..6f396764 100644 --- a/src/re_demo/tree_select.cljs +++ b/src/re_demo/tree_select.cljs @@ -18,20 +18,14 @@ {:id :wellington :label "Wellington" :group [:oceania :new-zealand]} {:id :atlantis :label "atlantis"}]) -(def choices [{:id :bug :description "Something isn't working" :label "bug" :background-color "#fc2a29" :group [:x :national :metro]} - {:id :documentation :description "Improvements or additions to documentation" :label "documentation" :background-color "#0052cc"} - {:id :duplicate :description "This issue or pull request already exists" :label "duplicate" :background-color "#cccccc"} - {:id :enhancement :description "New feature or request" :label "enhancement" :background-color "#84b6eb"} - {:id :help :description "Extra attention is needed" :label "help" :background-color "#169819"} - {:id :invalid :description "This doesn't seem right" :label "invalid" :background-color "#e6e6e6"} - {:id :wontfix :description "This will not be worked on" :label "wontfix" :background-color "#eb6421"}]) - (defn demo [] (let [model (reagent/atom #{:sydney :auckland}) groups (reagent/atom nil) disabled? (reagent/atom false) open-to (reagent/atom :chosen) + label-fn (reagent/atom nil) + group-label-fn (reagent/atom nil) placeholder? (reagent/atom false) abbrev-fn? (reagent/atom false) abbrev-threshold? (reagent/atom false) @@ -39,25 +33,69 @@ min-width? (reagent/atom true) min-width (reagent/atom 200) max-width? (reagent/atom true) - max-width (reagent/atom 300)] + max-width (reagent/atom 300) + open-to-chosen (fn [] + [tree-select :src (at) + :min-width (when @min-width? (str @min-width "px")) + :max-width (when @max-width? (str @max-width "px")) + :disabled? disabled? + :label-fn @label-fn + :group-label-fn @group-label-fn + :open-to :chosen + :choices cities + :model model + :groups groups + :abbrev-fn (when @abbrev-fn? #(string/upper-case (first (:label %)))) + :abbrev-threshold (when @abbrev-threshold? abbrev-threshold) + :on-change #(reset! model %)]) + open-to-nil (fn [] + [tree-select :src (at) + :min-width (when @min-width? (str @min-width "px")) + :max-width (when @max-width? (str @max-width "px")) + :disabled? disabled? + :label-fn @label-fn + :group-label-fn @group-label-fn + :choices cities + :model model + :groups groups + :abbrev-fn (when @abbrev-fn? #(string/upper-case (first (:label %)))) + :abbrev-threshold (when @abbrev-threshold? abbrev-threshold) + :on-change #(reset! model %)]) + open-to-all (fn [] + [tree-select :src (at) + :min-width (when @min-width? (str @min-width "px")) + :max-width (when @max-width? (str @max-width "px")) + :disabled? disabled? + :open-to :all + :choices cities + :model model + :groups groups + :abbrev-fn (when @abbrev-fn? #(string/upper-case (first (:label %)))) + :abbrev-threshold (when @abbrev-threshold? abbrev-threshold) + :on-change #(reset! model %)]) + open-to-none (fn [] + [tree-select :src (at) + :min-width (when @min-width? (str @min-width "px")) + :max-width (when @max-width? (str @max-width "px")) + :disabled? disabled? + :open-to :none + :choices cities + :model model + :groups groups + :abbrev-fn (when @abbrev-fn? #(string/upper-case (first (:label %)))) + :abbrev-threshold (when @abbrev-threshold? abbrev-threshold) + :on-change #(reset! model %)])] (fn [] [v-box :src (at) :gap "11px" :width "450px" :align :start :children [[title2 "Demo"] - [tree-select :src (at) - :min-width (when @min-width? (str @min-width "px")) - :max-width (when @max-width? (str @max-width "px")) - :disabled? disabled? - :open-to @open-to - :placeholder (when @placeholder? "placeholder message") - :choices cities - :model model - :groups groups - :abbrev-fn (when @abbrev-fn? #(string/upper-case (first (:label %)))) - :abbrev-threshold (when @abbrev-threshold? abbrev-threshold) - :on-change #(reset! model %)] + [(case @open-to + nil open-to-nil + :chosen open-to-chosen + :all open-to-all + :none open-to-none)] [h-box :src (at) :height "45px" :gap "5px" @@ -93,16 +131,17 @@ :child [:code ":disabled?"]] :model disabled? :on-change #(reset! disabled? %)] - [checkbox :src (at) - :label [box :src (at) - :align :start - :child [:span "Supply the string \"placeholder message\" for the " [:code ":placeholder"] " parameter"]] - :model placeholder? - :on-change #(reset! placeholder? %)] [v-box :src (at) :children [[box :src (at) :align :start :child [:code ":open-to"]] + [radio-button :src (at) - :label [:span [:code ":chosen"] ", " [:code "nil"] ", ommitted - reveal every chosen item."] + :label [:span [:code "nil"] ", ommitted - use the intial value of " [:code "groups"] "."] + :value nil + :model @open-to + :on-change #(reset! open-to %) + :style {:margin-left "20px"}] + [radio-button :src (at) + :label [:span [:code ":chosen"] " - reveal every chosen item."] :value :chosen :model @open-to :on-change #(reset! open-to %) @@ -112,33 +151,31 @@ :value :all :model @open-to :on-change #(reset! open-to %) + :style {:margin-left "20px"}] + [radio-button :src (at) + :label [:span [:code ":none"] " - collapse all groups"] + :value :none + :model @open-to + :on-change #(reset! open-to %) :style {:margin-left "20px"}]]] [v-box :src (at) :gap "11px" :children [[checkbox :src (at) :label [box :src (at) :align :start - :child [:span "Supply an " [:code ":abbrev-fn"] " of " [:code "#(clojure.string/upper-case (first (:label %)))"]]] - :model abbrev-fn? - :on-change #(reset! abbrev-fn? %)] - (when @abbrev-fn? - [h-box :src (at) - :gap "5px" - :align :center - :children [[checkbox :src (at) - :label [box :src (at) - :align :start - :child [:span " and also supply an " [:code ":abbrev-threshold"] " of "]] - :model abbrev-threshold? - :on-change #(reset! abbrev-threshold? %)] - [slider - :model abbrev-threshold - :on-change #(reset! abbrev-threshold %) - :min 10 - :max 50 - :step 1 - :width "160px"] - [label :src (at) :label @abbrev-threshold]]])]] + :child [:span "Supply a " [:code ":label-fn"] + " of " + [:code "#(clojure.string/upper-case (:label %)))"]]] + :model label-fn + :on-change (fn [] (swap! label-fn (fn [x] (if x nil #(clojure.string/upper-case (:label %))))))] + [checkbox :src (at) + :label [box :src (at) + :align :start + :child [:span "Supply a " [:code ":group-label-fn"] + " of " + [:code "#(clojure.string/upper-case (name (last %))))"]]] + :model group-label-fn + :on-change (fn [] (swap! group-label-fn (fn [x] (if x nil #(clojure.string/upper-case (name (last %)))))))]]] [h-box :src (at) :align :center :children [[checkbox :src (at)