From 8df6e7ba91ef2fde23cb0de6ddbc2eca42a85da3 Mon Sep 17 00:00:00 2001 From: Lucio D'Alessandro Date: Sun, 29 Mar 2020 23:59:15 +0100 Subject: [PATCH] First commit --- src/reagent_datatable/table.cljs | 436 +++++++++++----------- src/reagent_datatable/table_utils.cljs | 481 ++++++++++++++++--------- 2 files changed, 527 insertions(+), 390 deletions(-) diff --git a/src/reagent_datatable/table.cljs b/src/reagent_datatable/table.cljs index eee8f09..a1312b8 100644 --- a/src/reagent_datatable/table.cljs +++ b/src/reagent_datatable/table.cljs @@ -1,280 +1,278 @@ (ns reagent-datatable.table - (:require [reagent-datatable.table-utils :as utils] - [reagent.core :as r])) + (:require + [clojure.pprint :as pprint] + [reagent-datatable.table-utils :as utils] + [reagent.core :as r])) -(defn loading-table-body - "Renders a :tbody element containing a placeholder animation for use when a - tables content will be filled sometime after the page actually loads, likely - due to an ajax call" - [{:keys [rows cols]}] - (into [:tbody] - (for [row (range rows)] - ^{:key row} - (into [:tr.loading] - (for [col (range cols)] - ^{:key col} - [:td.td-loading-bar - [:span]]))))) +(defn pprint-str + [x] + (with-out-str (pprint/pprint x))) -(defn component-hide-show - [_component & [args]] - (let [!ref-toggle (atom nil) - !ref-box (atom nil) - active? (r/atom false) - ref-toggle (fn [el] (reset! !ref-toggle el)) - ref-box (fn [el] (reset! !ref-box el))] - (r/create-class - (let [handler (fn [e] - (let [^js node (.-target e)] - (cond - ;; don't close box if click happens on child-box - (.contains @!ref-box node) nil - ;; to toggle box - show/hide - (.contains @!ref-toggle node) (swap! active? not) - ;; always close child-box when clicking out - :else (reset! active? false))))] - {:component-did-mount - (fn [] - (js/document.addEventListener "mouseup" handler)) - :component-will-unmount - (fn [] - (js/document.removeEventListener "mouseup" handler)) - :reagent-render - (fn [component] - [component @active? ref-toggle ref-box args])})))) +(defn pprint-state + [x] + [:code + {:style {:text-align "left"}} + [:pre (pprint-str x)]]) (defn column-filter-select [table-atom column-key] - [:div.table-column__filter - [component-hide-show + [:div.column__filter + [utils/component-hide-show (fn [active? ref-toggle ref-box] - [:div.table-column__button-wrapper - [:button.button.table-column__button + [:div.column__button-wrapper + [:button.button.column__button {:ref ref-toggle} - [:i.table-column__button-icon.fa.fa-filter] + [:i.column__button-icon.fas.fa-filter] [:span "Select"] - [:i.table-column__button-icon.fa.fa-chevron-down]] - [:div.table-column__button-options - {:class (when active? "table-column__button-options--show") - :ref ref-box} - (doall - (for [value (utils/column-select-filter-options @table-atom column-key)] - ^{:key value} + [:i.column__button-icon.fas.fa-chevron-down]] + (into + [:div.column__button-options + {:class (when active? "column__button-options--show") + :ref ref-box}] + (map + (fn [[id value processed-value]] + ^{:key (str id value)} [:div.action__checkbox - {:on-click #(utils/column-select-filter-on-change table-atom value column-key)} - [:input.checkbox__input + {:on-click #(utils/column-select-filter-on-change table-atom column-key value processed-value)} + [:input {:type "checkbox" - :checked (utils/column-select-filter-value @table-atom column-key value) + :checked (utils/column-select-filter-value @table-atom column-key value processed-value) :value value :on-change #()}] - [:label.checkbox__custom - value]]))]])]]) + [:label processed-value]]) + (utils/column-select-filter-options @table-atom column-key)))])]]) (defn column-filter-input [table-atom column-key] - [:div.table-column__filter - [:input.input.input--side-icons.input--no-borders + [:div.column__filter + [:input.input.input--side-icons.input__no-borders {:value (utils/column-filter-value @table-atom column-key) :on-change #(utils/column-filter-on-change % table-atom column-key)}] [:span.input__icon.input__left-icon - [:i.fa.fa-filter]] + [:i.fas.fa-filter]] (when (not-empty (utils/column-filter-value @table-atom column-key)) [:span.input__icon.input__right-icon.input__icon--clickable {:on-click #(utils/column-filter-reset table-atom column-key)} - [:i.fa.fa-times]])]) + [:i.fas.fa-times]])]) (defn header-columns [table-atom] - (let [columns (utils/table-columns @table-atom)] + (let [dark (utils/dark-mode? @table-atom :columns) + columns (utils/table-columns @table-atom)] [:thead.table__head + {:class (when dark "table__head--dark")} (into [:tr] - (mapv + (map (fn [{:keys [column-key column-name]}] ^{:key column-key} [:th.table__cell.head__cell [:div.head__column-title {:on-click #(utils/column-sort table-atom column-key)} [:span column-name] - [:i.fa.fa-sort.column-title__sort-icon]] - (when (utils/column-filters? @table-atom column-key) - (if (utils/column-select-input? @table-atom column-key) - [column-filter-select table-atom column-key] - [column-filter-input table-atom column-key]))]) + [:i.fas.column-title__sort-icon + ;; sort table by column inc or dec order + {:class (utils/column-sort-icon @table-atom column-key)}]] + (case (utils/column-filter-type @table-atom column-key) + :input [column-filter-input table-atom column-key] + :select [column-filter-select table-atom column-key] + [:div.column__filter--disabled])]) columns))])) +(defn loading-table + [table-atom {:keys [rows cols]}] + (let [dark (utils/dark-mode? @table-atom :rows) + columns (:columns @table-atom)] + [:div.table__main + [:table.table + (if columns + [header-columns table-atom] + [:thead + {:class (when dark "thead-loading--dark")} + [:tr + (for [col (range cols)] + ^{:key col} + [:th [:span]])]]) + [:tbody.table__body + {:class (when dark "table__body--dark")} + (for [row (range rows)] + ^{:key row} + [:tr.loading + (for [col (range (or (count columns) cols))] + ^{:key col} + [:td.td-loading-bar + [:span.loading-bar__span]])])]]])) + (defn body-rows - [table rows] - (let [columns (utils/table-columns table) - link-row-fn (get-in table [:rows :link-row-fn])] - (if (:loading? table) - [loading-table-body {:rows 4 :cols (count columns)}] + [table-atom rows] + (let [columns (utils/table-columns @table-atom) + dark (utils/dark-mode? @table-atom :rows)] + (if (seq rows) [:tbody.table__body + {:class (when dark "table__body--dark")} (for [row rows] - ^{:key row} + ^{:key (:id row)} [:tr.table__row.body__row - (for [{:keys [column-key render-fn class] - :or {render-fn identity - class ""}} columns] - ^{:key (str row column-key)} + (for [{:keys [column-key render-fn]} columns] + ^{:key (str (:id row) column-key)} [:td.table__cell.body__cell - {:class class} - [:a - {:href (when link-row-fn (link-row-fn row))} - (render-fn (column-key row))]])])]))) + (if render-fn + (render-fn row column-key (column-key row)) + (column-key row))])])] + [:tbody.table__body.table__no-data + [:tr [:td.td__no-data + "No Match Found"]]]))) -(defn search-all +(defn filter-all [table-atom] - [:div.top__search-all - [:input.input.input--side-icons.input--no-borders - {:value (utils/search-all-value @table-atom) - :on-change #(utils/search-all-on-change % table-atom)}] + [:div.top__filter-all + [:input.input.input--side-icons.input__no-borders + {:value (utils/filter-all-value @table-atom) + :on-change #(utils/filter-all-on-change % table-atom)}] [:span.input__icon.input__left-icon - [:i.fa.fa-search]] - (when (not-empty (utils/search-all-value @table-atom)) + [:i.fas.fa-search]] + (when (not-empty (utils/filter-all-value @table-atom)) [:span.input__icon.input__right-icon.input__icon--clickable - {:on-click #(utils/search-all-reset table-atom)} - [:i.fa.fa-times]])]) + {:on-click #(utils/filter-all-reset table-atom)} + [:i.fas.fa-times]])]) (defn actions [table-atom] [:div.top__actions - [:div.action - [:i.fa.fa-refresh - {:on-click (:refresh-action @table-atom)}]] - [component-hide-show + [utils/component-hide-show (fn [active? ref-toggle ref-box] [:div.action - [:i.action__icon.fa.fa-th-large + [:i.action__icon.fas.fa-paint-brush {:ref ref-toggle}] (into [:div.action__options {:class (when active? "action__options--show") :ref ref-box} - [:div - {:style {:color "black"}} - "Hide Columns"]] - (for [{:keys [column-key column-name]} - (-> @table-atom :columns :data)] - ^{:key column-key} - [:div.action__checkbox - {:on-click #(utils/column-visibility-on-change table-atom column-key)} - [:input.checkbox__input - {:type "checkbox" - :checked (utils/column-visible? @table-atom column-key)}] - [:label.checkbox__custom - column-name]]))])]]) + [:div.action__title + "Dark Theme"]] + (for [[k s] [[:all "Toggle All"] + [:top "Top"] [:columns "Columns"] + [:rows "Rows"] [:navigation "Navigation"]]] + ^{:key k} + [:div.action__checkbox.switch__group + {:on-click + (if (= :all k) + #(utils/set-dark-mode-all table-atom) + #(utils/set-dark-mode table-atom k))} + [:div.onoffswitch + [:input + {:name "onoffswitch" + :class "onoffswitch-checkbox" + :id k + :checked (if (= :all k) + (utils/dark-mode-toggled-all? @table-atom) + (utils/dark-mode? @table-atom k)) + :on-change (if (= :all k) + #(utils/set-dark-mode-all table-atom) + #(utils/set-dark-mode table-atom k)) + :type "checkbox"}] + [:label.onoffswitch-label + {:for k} + [:span.onoffswitch-inner] + [:span.onoffswitch-switch]]] + [:label s]]))])] + [utils/component-hide-show + (fn [active? ref-toggle ref-box] + [:div.action + [:i.action__icon.fas.fa-th-large + {:ref ref-toggle}] + (into + [:div.action__options + {:class (when active? "action__options--show") + :ref ref-box} + [:div.action__title + "Show Columns"]] + (map + (fn [{:keys [column-key column-name]}] + ^{:key column-key} + [:div.action__checkbox.switch__group + {:on-click #(utils/column-visibility-on-change table-atom column-key)} + [:div.onoffswitch + [:input + {:name "onoffswitch" + :class "onoffswitch-checkbox" + :id column-key + :checked (utils/column-visible? @table-atom column-key) + :on-change #(utils/column-visibility-on-change table-atom column-key) + :type "checkbox"}] + [:label.onoffswitch-label + {:for column-key} + [:span.onoffswitch-inner] + [:span.onoffswitch-switch]]] + [:label column-name]]) + (-> @table-atom :columns)))])]]) (defn active-filters [table-atom] - (let [active-filters (utils/block-filter-values @table-atom)] - [:div.top__block-filters - (when (seq active-filters) - [:button.button--light.button.top__clear-filters - {:on-click #(utils/column-filter-reset-all table-atom)} - "RESET"]) - (for [[column-key value] active-filters] - ^{:key column-key} - [:button.button.button__active-filters - {:on-click #(utils/column-filter-reset table-atom column-key value)} - [:span value] - [:i.fa.fa-times-circle]])])) + [:div.top__block-filters + (when-let [filters (utils/block-filter-values @table-atom)] + [:<> + [:button.button--light.button.top__clear-filters + {:on-click #(utils/column-filter-reset-all table-atom)} + "RESET"] + (for [[column-key value select] filters] + ^{:key (str column-key value)} + [:button.button.button__active-filters + {:on-click + (if select + #(utils/column-select-filter-reset table-atom column-key (first value)) + #(utils/column-filter-reset table-atom column-key))} + [:span (if select (second value) value)] + [:i.fas.fa-times-circle]])])]) -(defn no-data-message - [rows] - (when (empty? rows) - [:div.table__no-data - [:div "Nothing to show"]])) +(defn pagination + [table-atom processed-rows] + (let [dark (utils/dark-mode? @table-atom :navigation)] + [:table.table__foot + {:class (when dark "table__foot--dark")} + [:tfoot + [:tr + [:td.foot__pagination + [:div.select.pagination__select + [:select + {:value (utils/pagination-rows-per-page @table-atom) + :on-change #(utils/pagination-rows-per-page-on-change % table-atom)} + [:option {:value "10"} (str "10" " rows")] + [:option {:value "50"} (str "50" " rows")] + [:option {:value "100"} (str "100" " rows")]]] + [:div.pagination__info (utils/pagination-current-and-total-pages @table-atom + processed-rows)] + [:div.pagination__arrow-group + [:div.pagination__arrow-nav + {:class (when (<= (utils/pagination-current-page @table-atom) 0) + "pagination__arrow-nav--disabled") + :on-click #(utils/pagination-dec-page table-atom)} + [:i.fas.fa-chevron-left]] + [:div.pagination__arrow-nav + {:class (when (utils/pagination-rows-exhausted? @table-atom + processed-rows) + "pagination__arrow-nav--disabled") + :on-click #(utils/pagination-inc-page table-atom + processed-rows)} + [:i.fas.fa-chevron-right]]]]]]])) (defn table - "Headers is a vector of columns, each column a map with the following keys: - :column-name - a string which will be displayed in the - header of each column - :column-key - This should match the key of the row - object you want this column to represent - :render-fn (optional) - a function which returns valid - hiccup syntax to be used for - each cell of the column - - data is a vector of rows, each row is a map which should contain keys - matching those in the columns data structure. Extra keys in - each row are ok,they will be ignored and not rendered - unless present in the columns - - options is an optional map with overrides to the default configuration of the table. - Example options are: - :filters - a list of column keys which should show a dropdown select filter rather - than a text search in that column - :loading? - a boolean which when true will cause the table to render a - loading animation - :refresh-action - A function that is called when the refresh icon is - clicked. By default clicking the refresh icon simply reloads - the page - :search-query - this value will be autofilled in the global search box, - useful for programatically setting the table to filter - based on some external value, such as query params in the URL - - Example value of headers: - - {:column-key :foo :column-name 'Foo'] - - Example value of data: - - [{:foo 'a value'} - {:foo 'another value' :bar 'this won't be rendered'}]}} " - ([headers data] (table headers data {})) - ([headers data {:keys [filters search-query link-row-fn] :as options}] - (let [table-atom (r/atom - (merge {:head {:search-all (or search-query "")} - :refresh-action #(js/window.location.reload) - :loading? false - :columns - {:hidden #{} - :filter-select (into {} - (for [filter filters] - [filter #{}])) - :column-filters (into {} - (for [filter filters] - [filter :select])) - :data headers} - :rows {:data data - :link-row-fn link-row-fn}} - options))] - (fn [] - (let [table @table-atom - [processed-rows paginated-rows] (utils/process-rows table-atom)] - [:div.table__wrapper - [:div.table__top - [:div.top__first-group - [search-all table-atom] - [actions table-atom]] - [active-filters table-atom]] - [:div.table__main - [no-data-message processed-rows] - [:table.table - [header-columns table-atom] - [body-rows table paginated-rows]]] - [:table.table__foot - [:tfoot - [:tr - [:td.foot__pagination - [:div.select.pagination__select - [:select - {:value (utils/pagination-rows-per-page table) - :on-change #(utils/pagination-rows-per-page-on-change % table-atom)} - [:option {:value "5"} (str "5" " rows")] - [:option {:value "15"} (str "15" " rows")] - [:option {:value "100"} (str "100" " rows")]]] - [:div.pagination__info - (utils/pagination-current-and-total-pages table processed-rows)] - [:div.pagination__arrow-group - [:div.pagination__arrow-nav - {:class (when (<= (utils/pagination-current-page table) 0) - "pagination__arrow-nav--disabled") - :on-click #(utils/pagination-dec-page table-atom)} - [:i.fa.fa-chevron-left]] - [:div.pagination__arrow-nav - {:class - (when (utils/pagination-rows-exhausted? table processed-rows) - "pagination__arrow-nav--disabled") - :on-click #(utils/pagination-inc-page table-atom processed-rows)} - [:i.fa.fa-chevron-right]]]]]]]]))))) + [table-atom] + (let [[processed-rows paginated-rows] (utils/process-rows @table-atom) + top-dark (utils/dark-mode? @table-atom :top) + rows-dark (utils/dark-mode? @table-atom :rows)] + [:div.table__wrapper + #_[pprint-state (dissoc @table-atom :rows)] + [:div.table__top + {:class (when top-dark "table__top--dark")} + [:div.top__first-group + [filter-all table-atom] + [actions table-atom]] + [active-filters table-atom]] + (if (utils/loading? @table-atom) + [loading-table table-atom {:rows 7 :cols 4}] + [:div.table__main + {:class (when rows-dark "table__main--dark")} + [:table.table + [header-columns table-atom] + [body-rows table-atom paginated-rows]]]) + [pagination table-atom processed-rows]])) diff --git a/src/reagent_datatable/table_utils.cljs b/src/reagent_datatable/table_utils.cljs index 45b18a5..0adeebc 100644 --- a/src/reagent_datatable/table_utils.cljs +++ b/src/reagent_datatable/table_utils.cljs @@ -1,183 +1,323 @@ (ns reagent-datatable.table-utils (:require - [medley.core :as medley] - [clojure.string :as str])) - -(defn process-string-for-filtering - "Removes excess whitespace and converts to lowercase." + [clojure.string :as s] + [reagent.core :as r])) + +(def example-data + {;; user provided data + :columns [{:column-key :status + :column-name "Status"} + {:column-key :scale-id + :column-name "ScaleID" + :render-fn (fn [row k v]) + :render-only #{:filter :sort :custom-one?}} + {:column-key :name + :column-name "Name"} + {:column-key :location + :column-name "Location"} + {:column-key :error + :column-name "Error"}] + :rows [{:id (random-uuid) + :status "ok" + :scale-id "ASD" + :name "luch" + :location "London" + :error "Overload"} + {:id (random-uuid) + :status "something" + :scale-id "ASD" + :name "luch" + :location "London" + :error "Overload"}] + :filters {:input #{:scale-id :name} + :select #{:status :error}} + ;; utils + :utils {:theme {:columns :dark + :navigation :dark + :rows :dark + :top :dark} + :toggle-all-themes true + :filter-all "value" + :filter-columns {:status #{["a" "a"] + ["b" [:i.fas.fa-user "component"]]} + :scale-id "id"} + :hidden {:status true + :name false} + :pagination {:rows-per-page 34 + :current-page 3} + :sort {:status :asc + ;; or :desc + }}}) + +(defn component-hide-show + [component & [args]] + (let [!ref-toggle (atom nil) + !ref-box (atom nil) + active? (r/atom false) + handler (fn [e] + (let [^js node (.-target e)] + (cond + ;; don't close box if click happens on child-box + (.contains @!ref-box node) nil + ;; to toggle box - show/hide + (.contains @!ref-toggle node) (swap! active? not) + ;; always close child-box when clicking out + :else (reset! active? false)))) + ref-toggle (fn [el] (reset! !ref-toggle el)) + ref-box (fn [el] (reset! !ref-box el))] + (r/create-class + {:component-did-mount + (fn [] + (js/document.addEventListener "mouseup" handler)) + :component-will-unmount + (fn [] + (js/document.removeEventListener "mouseup" handler)) + :reagent-render + (fn [component] + [component @active? ref-toggle ref-box args])}))) + +(defn process-string [s] (some-> s - str/trim not-empty - str/lower-case - (str/replace #"\s+" " "))) + s/trim + s/lower-case + (s/replace #"\s+" " "))) + +(defn dark-mode? + [table k] + (= :dark (get-in table [:utils :theme k]))) + +(defn set-dark-mode + [table-atom k] + (swap! table-atom update-in [:utils :theme k] + (fn [mode] + (if (= :dark mode) + :light + :dark)))) + +(defn dark-mode-toggled-all? + [table] + (get-in table [:utils :toggle-all-themes])) + +(defn set-dark-mode-all + [table-atom] + (swap! table-atom + #(-> % + (assoc-in [:utils :theme] + (let [m (fn [v] + {:top v + :columns v + :rows v + :navigation v})] + (if (-> % :utils :toggle-all-themes) + (m :light) + (m :dark)))) + (update-in [:utils :toggle-all-themes] not)))) (defn reset-pagination [table] - (assoc-in table [:pagination :current-page] 0)) + (assoc-in table [:utils :pagination :current-page] 0)) (defn column-sort [table-atom column-key] (swap! table-atom #(-> % reset-pagination - (update-in [:columns :sort] - (fn [[curr-key order-bool]] - (if (= curr-key column-key) - [curr-key (not order-bool)] - [column-key true])))))) + (update-in [:utils :sort] + (fn [m] + (let [curr-column-key (ffirst m) + curr-sort-val (first (vals m))] + (if (= curr-column-key column-key) + (update m column-key (fn [order] + (if (= :asc order) + :desc :asc))) + {column-key :asc}))))))) + +(defn column-sort-icon + [table column-key] + (let [sort (get-in table [:utils :sort])] + (case (get sort column-key) + :asc "fa-caret-down" + :desc "fa-caret-up" + "fa-caret-down"))) (defn column-sort-value [table] - (get-in table [:columns :sort])) + (-> table :utils :sort)) (defn column-filter-value [table column-key] - (get-in table [:columns :filter-input column-key])) - -(defn column-filter-values - [table] - (let [filter-input (->> (get-in table [:columns :filter-input]) - (medley/map-vals process-string-for-filtering) - (medley/remove-vals nil?) - (into {})) - select-input (medley/filter-vals seq (get-in table [:columns :filter-select]))] - (merge filter-input select-input))) + (-> table :utils :filter-columns column-key)) (defn column-filter-on-change [evt table-atom column-key] (swap! table-atom #(-> % reset-pagination - (assoc-in [:columns :filter-input column-key] + (assoc-in [:utils :filter-columns column-key] (-> evt .-target .-value))))) (defn column-filter-reset - ([table-atom column-key] - (swap! table-atom update-in [:columns :filter-input] dissoc column-key)) - ([table-atom column-key value] - (swap! table-atom #(-> % - (update-in [:columns :filter-input] dissoc column-key) - (update-in [:columns :filter-select column-key] disj value))))) - -(defn column-filters? - [table column-key] - (get-in table [:columns :column-filters?] column-key)) + [table-atom column-key] + (swap! table-atom update-in [:utils :filter-columns] dissoc column-key)) -(defn column-select-input? +;; use case statement for this in UI +(defn column-filter-type [table column-key] - (= :select (get-in table [:columns :column-filters column-key]))) + (let [input-filters (get-in table [:filters :input]) + select-filters (get-in table [:filters :select])] + (cond + (get input-filters column-key) :input + (get select-filters column-key) :select + :else nil))) + +(defn render-fn + [table column-key] + (some->> (:columns table) + (filter #(= column-key (:column-key %))) + first + :render-fn)) + +(defn process-cell-value + ([table row column-key value] + (process-cell-value table row column-key value true)) + ([table row column-key value allow?] + (let [render-fn (render-fn table column-key)] + (if (and allow? render-fn) + (render-fn row column-key value) + value)))) (defn column-select-filter-options [table column-key] - (->> (get-in table [:rows :data]) - (map (fn [row] - (get row column-key))) - (set))) + (let [processed-val #(process-cell-value table % column-key (column-key %))] + (->> (:rows table) + ;; to return only relevant k-v pair from row + (mapv (fn [row] + [column-key (column-key row) (processed-val row)])) + (group-by (juxt first second last)) + ;; to keep only [k raw-v processed-v] + (map first)))) (defn column-select-filter-on-change - [table-atom value column-key] + [table-atom column-key value processed-value] (swap! table-atom - (fn [table] - (let [curr-value (get-in table [:columns :filter-select column-key value])] - (if (seq curr-value) - (-> table - reset-pagination - (update-in [:columns :filter-select column-key] disj curr-value)) - (-> table - reset-pagination - (update-in [:columns :filter-select column-key] (fnil conj #{}) value))))))) + #(-> % + reset-pagination + (update-in [:utils :filter-columns column-key] + (fn [selected-values] + (let [val&processed [value processed-value]] + (if (get selected-values val&processed) + (disj selected-values val&processed) + (conj ((fnil conj #{}) selected-values) + val&processed)))))))) (defn column-select-filter-value - [table column-key value] - (get-in table [:columns :filter-select column-key value] false)) + [table column-key value processed-value] + (get-in table [:utils :filter-columns column-key [value processed-value] 0] false)) + +(defn column-select-filter-reset + [table-atom column-key value] + (swap! table-atom update-in [:utils :filter-columns column-key] + (fn [column-filters] + (->> column-filters + (remove (fn [v] + (and (vector? v) + (= (first v) value)))) + (into #{}))))) (defn column-filter-reset-all [table-atom] (swap! table-atom - (fn [table] - (-> table - reset-pagination - (update :columns dissoc :filter-select) - (update :columns dissoc :filter-input))))) + #(-> % + reset-pagination + (update :utils dissoc :filter-columns)))) -(defn search-all-value +(defn filter-all-value [table] - (get-in table [:head :search-all])) + (get-in table [:utils :filter-all] "")) -(defn search-all-on-change +(defn filter-all-on-change [evt table-atom] (swap! table-atom #(-> % reset-pagination - (assoc-in [:head :search-all] + (assoc-in [:utils :filter-all] (-> evt .-target .-value))))) -(defn search-all-reset +(defn filter-all-reset [table-atom] (swap! table-atom #(-> % reset-pagination - (update :head dissoc :search-all)))) - + (update :utils dissoc :filter-all)))) (defn block-filter-values - [{:keys [columns]}] - (concat - (remove (comp empty? second) (:filter-input columns)) - (->> columns - :filter-select - (medley/filter-vals seq) - ;; build a vector of filters with each filter taking the form of - ;; [:field-key "filter-term"] - (reduce (fn [col [k filter-set]] - (apply conj col - (for [filter filter-set] - [k filter]))) - nil) - (remove empty?)))) + "Collect only the active column filters for UI" + [table] + (->> (get-in table [:utils :filter-columns]) + (remove (comp empty? second)) + ;; from [:a "filter" :k #{"a" "b"}] + ;; to [[:a "filter"] [:k "a" :select] [:k "b" :select]] + (map (fn [[k v]] + (if (set? v) + (mapv #(vector k % :select) v) + (vector [k v])))) + (apply concat) + (not-empty))) (defn column-visible? [table column-key] - (contains? (get-in table [:columns :hidden]) column-key)) + (not (-> table :utils :hidden column-key))) (defn column-visibility-on-change [table-atom column-key] - (swap! table-atom #(-> % - reset-pagination - (update-in [:columns :filter-select] dissoc column-key) - (update-in [:columns :filter-input] dissoc column-key) - (update-in [:columns :hidden] - (if (column-visible? @table-atom column-key) disj conj) - column-key)))) + (swap! table-atom + #(-> % + reset-pagination + (update-in [:utils :filter-columns] dissoc column-key) + (update-in [:utils :hidden column-key] not)))) + +(defn- hidden-columns + "Transform hidden column keys from map to vec" + [table] + (->> (-> table :utils :hidden) + (filter second) + (map first) + (not-empty))) (defn table-columns [table] - (let [columns (get-in table [:columns :data]) - hidden (get-in table [:columns :hidden])] - (remove #(contains? hidden (:column-key %)) columns))) + (let [columns (:columns table) + hidden (-> table :utils :hidden)] + (remove #(get hidden (:column-key %)) columns))) + +(defn loading? + [table] + (empty? (:rows table))) (defn pagination-rows-per-page-on-change [evt table-atom] (swap! table-atom #(-> % - (assoc-in [:pagination :rows-per-page] + (assoc-in [:utils :pagination :rows-per-page] (js/parseInt (-> evt .-target .-value))) - (assoc-in [:pagination :current-page] 0)))) + (assoc-in [:utils :pagination :current-page] 0)))) (defn pagination-rows-per-page [table] - (or (get-in table [:pagination :rows-per-page]) 15)) + (get-in table [:utils :pagination :rows-per-page] 15)) (defn pagination-current-page [table] - (or (get-in table [:pagination :current-page]) 0)) + (get-in table [:utils :pagination :current-page] 0)) (defn pagination-current-and-total-pages [table processed-rows] (let [offset (pagination-current-page table) rows-per-page (pagination-rows-per-page table) - nth-rows-at-page (+ rows-per-page (* offset rows-per-page)) + nth-rows-at-page (+ rows-per-page + (* offset rows-per-page)) nth-rows (count processed-rows)] (str (inc (* offset rows-per-page)) "-" @@ -199,47 +339,35 @@ (defn pagination-inc-page [table-atom processed-rows] - (when-not (pagination-rows-exhausted? @table-atom processed-rows) - (swap! table-atom update-in [:pagination :current-page] + (when-not (pagination-rows-exhausted? @table-atom + processed-rows) + (swap! table-atom update-in [:utils :pagination :current-page] (fnil inc 0)))) (defn pagination-dec-page [table-atom] (when (> (pagination-current-page @table-atom) 0) - (swap! table-atom update-in [:pagination :current-page] + (swap! table-atom update-in [:utils :pagination :current-page] dec))) +(defn render-fn-allow? + [table column-key operation] + (let [column-map (first (filter #(= column-key (:column-key %)) + (:columns table))) + deny? (-> column-map :render-only operation)] + (not deny?))) (defn date? - "Returns true if the argument is a date, false otherwise." [d] (instance? js/Date d)) (defn date-as-sortable - "Returns something that can be used to order dates." [d] (.getTime d)) (defn compare-vals - "A comparator that works for the various types found in table structures. - This is a limited implementation that expects the arguments to be of - the same type. The :else case is to call compare, which will throw - if the arguments are not comparable to each other or give undefined - results otherwise. - Both arguments can be a vector, in which case they must be of equal - length and each element is compared in turn." [x y] (cond - (and (vector? x) - (vector? y) - (= (count x) (count y))) - (reduce #(let [r (compare (first %2) (second %2))] - (if (not= r 0) - (reduced r) - r)) - 0 - (map vector x y)) - (or (and (number? x) (number? y)) (and (string? x) (string? y)) (and (boolean? x) (boolean? y))) @@ -248,72 +376,87 @@ (and (date? x) (date? y)) (compare (date-as-sortable x) (date-as-sortable y)) - :else ;; hope for the best... are there any other possiblities? + :else (compare (str x) (str y)))) (defn resolve-sorting [table rows] - (if-let [[column-key order] (column-sort-value table)] - (sort - (fn [row1 row2] - (let [val1 (column-key row1) - val2 (column-key row2)] - (if order - (compare-vals val2 val1) - (compare-vals val1 val2)))) - rows) - ;; with no sorting return rows input + (if-let [m (column-sort-value table)] + (let [column-key (ffirst m) + order (get m column-key) + allow-sort? (render-fn-allow? table column-key :sort) + processed-val (fn [row] + (process-cell-value table row column-key + (column-key row) + allow-sort?))] + (sort + (fn [row1 row2] + (let [val1 (processed-val row1) + val2 (processed-val row2)] + + (if (= :desc order) + (compare-vals val2 val1) + (compare-vals val1 val2)))) + rows)) rows)) +(defn column-filters + [table] + (->> (get-in table [:utils :filter-columns]) + (remove (fn [k-v] (empty? (second k-v)))) + (map (fn [[k v]] [k (if (string? v) + (process-string v) + v)])) + (not-empty))) + (defn resolve-column-filtering [table rows] - (if-let [column-filters (column-filter-values table)] + (if-let [column-filters (column-filters table)] (filter - (fn [row-data-map] + (fn [row] (every? - (fn [[k v]] - (let [render-fn (or (:render-fn - (medley/find-first - #(= (:column-key %) k) - (get-in table [:columns :data]))) - identity) - row-v (render-fn (get row-data-map k))] - (if (string? v) - (some-> row-v - str/lower-case - (str/includes? v)) - ;; to filter when we have a select tag. - ;; the v values are in a set - (get v row-v)))) - column-filters)) rows) + (fn [[column-key filtering]] + (let [allow-filter? (render-fn-allow? table column-key :filter) + processed-val (str (process-cell-value table row column-key + (column-key row) + allow-filter?))] + (if (string? filtering) + (s/includes? (s/lower-case processed-val) filtering) + (get (->> filtering + (map + (comp + str (if allow-filter? second first))) + (into #{})) + processed-val)))) + column-filters)) + rows) rows)) -(defn resolve-search-all +(defn resolve-filter-all [table rows] - (if-let [search-value (process-string-for-filtering (search-all-value table))] + (if-let [filter-value (process-string + (filter-all-value table))] (filter - (fn [row-data] + (fn [row] (some - (fn [[k cell-data]] - (let [render-fn (or (:render-fn - (medley/find-first - #(= (:column-key %) k) - (get-in table [:columns :data]))) - identity) - cell (render-fn cell-data)] - (some-> cell - str - str/lower-case - (str/includes? search-value)))) - row-data)) rows) + (fn [[column-key cell-value]] + (let [allow-filter? (render-fn-allow? table column-key :filter) + processed-val (str (process-cell-value table row column-key + cell-value + allow-filter?))] + (s/includes? + (s/lower-case processed-val) + filter-value))) + (dissoc row :id))) + rows) rows)) (defn resolve-hidden-columns [table rows] - (if-let [hidden-columns (seq (get-in table [:columns :hidden]))] + (if-let [columns-to-hide (hidden-columns table)] (map - (fn [row-data] - (apply dissoc row-data hidden-columns)) + (fn [row] + (apply dissoc row columns-to-hide)) rows) rows)) @@ -326,15 +469,11 @@ current-page))])) (defn process-rows - [table-atom] - ;; all data transformation is performed here, on READ! - ;; swap! is not allowed in this function - (let [table @table-atom - rows (get-in table [:rows :data])] + [table] + (let [rows (-> table :rows)] (->> rows (resolve-hidden-columns table) (resolve-sorting table) (resolve-column-filtering table) - (resolve-search-all table) + (resolve-filter-all table) (resolve-pagination table)))) -