(ns auto-ap.views.components.grid (:require [reagent.core :as r] [auto-ap.views.utils :refer [appearing]] [auto-ap.utils :refer [default-pagination-size]] [react :as react] [re-frame.core :as re-frame] [auto-ap.views.pages.data-page :as data-page])) (defonce grid-context (react/createContext "default")) (def Provider (.-Provider grid-context)) (def Consumer (.-Consumer grid-context)) (defn toggle-sort-by [params sort-key sort-name] (let [[found? sort] (reduce (fn [[found? sort] sort-item] (if (= sort-key (:sort-key sort-item)) [true (conj sort (update sort-item :asc not))] [found? (conj sort sort-item)])) [false []] (:sort params)) sort (if found? sort (conj sort {:sort-key sort-key :sort-name sort-name :asc true}))] (-> params (assoc :sort sort)))) (defn sort-icon [which sort] (let [sort-item (first (filter #(= which (:sort-key %)) sort))] (cond (and sort-item (:asc sort-item)) [:span.icon [:i.fa.fa-sort-up]] (and sort-item (not (:asc sort-item))) [:span.icon [:i.fa.fa-sort-down]] :else [:span.icon [:i.fa.fa-sort]]))) (defn bound [x y z] (cond (< z x) x (< y x) x (> y z) z :else y)) (defn paginator [{:keys [start per-page end count total on-change] :as g}] (let [per-page (or per-page default-pagination-size) max-buttons 5 buttons-before (Math/floor (/ max-buttons 2)) total-pages (Math/max 1 (Math/ceil (/ total per-page))) current-page (Math/floor (/ start per-page)) first-page-button (bound 0 (- current-page buttons-before) (- total-pages max-buttons)) all-buttons (into [] (for [x (range total-pages)] ^{:key x} [:li [:a.pagination-link {:class (when (= current-page x) "is-current") :on-click (fn [e] (on-change {:start (* x per-page) :per-page per-page}))} (inc x)]])) last-page-button (Math/min total-pages (+ max-buttons first-page-button)) extended-last-page-button (when (not= last-page-button total-pages) (list ^ {:key -1} [:li [:span.pagination-ellipsis "…"]] ^ {:key -2} (last all-buttons))) extended-first-page-button (when (not= first-page-button 0) (list ^{:key -1} (first all-buttons) ^{:key -2} [:li [:span.pagination-ellipsis "…"]]))] [:nav.pagination {:role "pagination"} [:ul.pagination-list extended-first-page-button (apply list (subvec all-buttons first-page-button last-page-button)) extended-last-page-button [:span.mx-4 (Math/min (inc start) total) "-" end "/" total] [:div.select [:select {:value per-page :on-change (fn [e] (on-change {:start 0 :per-page (js/parseInt (.. e -target -value ))}))} [:option {:value 20 } "20 per page" ] [:option {:value 50} "50 per page" ] [:option {:value 100} "100 per page" ] [:option {:value 200} "200 per page" ]]]]])) (defn sort-by-list [{:keys [sort on-change]}] [:div.field.is-grouped.is-grouped-multiline (for [{:keys [sort-key sort-name asc]} sort] ^{:key sort-key} [:div.control [:div.tags.has-addons [:div.tag.is-medium [:span.icon (if asc [:i.fa.fa-sort-up] [:i.fa.fa-sort-down])] [:span sort-name] ] [:a.tag.is-medium.is-delete {:on-click (fn [] (on-change {:sort (filter #(not= sort-key (:sort-key %)) sort)}))}]]])]) (defn controls [{:keys [start end count total per-page] :as para}] (let [children (r/children (r/current-component))] [:> Consumer {} (fn [consume] (let [{:strs [on-params-change params] :as consume} (js->clj consume)] (r/as-element (into [:div {:style {:margin-bottom "1rem"}} [:div.level (into [:div.level-left [:div.level-item [paginator {:start start :end end :count count :total total :per-page (:per-page params) :on-change on-params-change}]] [:div.level-item [sort-by-list {:sort (:sort params) :on-change on-params-change}]]] (mapv (fn [c] [:div.level-item c]) children))]]))))])) (defn table [{:keys [fullwidth class style]}] (into [:table.table.compact.grid {:class (cond-> [] fullwidth (conj "is-fullwidth") class (into class)) :style style}] (r/children (r/current-component)))) (defn header [] (into [:thead] (r/children (r/current-component)))) (defn header-cell [{:keys [style class]}] (into [:th {:style style :class class}] (r/children (r/current-component)))) (defn row [{:keys [class id checkable? entity] }] (let [children (r/children (r/current-component))] [:> Consumer {} (fn [consume] (let [on-check-changed (aget consume "on-check-changed") check-boxes? (aget consume "check-boxes?") checked (aget consume "checked")] (apply r/create-element "tr" #js {:className class} (when check-boxes? (r/as-element [:th {:style {:width "35px"}} [:input.checkbox (cond-> {:type "checkbox" :checked (if (get checked id) "checked" "") :on-change (fn [x e] (if id (let [checked (or checked #{})] ;; TODO only map once everything is moved over to data-page (if (map? checked) (if (get checked id) (on-check-changed (dissoc checked id)) (on-check-changed (assoc checked id entity))) (if (get checked id) (on-check-changed (disj checked id)) (on-check-changed (conj checked id)))))))} (boolean? checkable?) (assoc :disabled (not checkable?))) ]])) (map r/as-element children))))])) (defn button-cell [params] (apply r/create-element "td" #js {"style" #js {"overflow" "visible"}} (map r/as-element (r/children (r/current-component))))) (defn cell [params] (apply r/create-element "td" #js {:className (:class params)} (map r/as-element (r/children (r/current-component)))) ) (defn body [] (let [children (r/children (r/current-component))] [:> Consumer {} (fn [consume] (let [{:strs [column-count status check-boxes?]} (js->clj consume) column-count (cond-> column-count check-boxes? inc)] (r/as-element (cond (= :loading (:state status)) ^{:key "loading-body"} [:tbody.test (for [i (range default-pagination-size)] ^{:key i} [:tr (for [x (range column-count)] ^{:key x} [:td #_{:col-span column-count} [:div [:div.ph-item [:div.ph-row [:div.ph-col-12.big]]]]])])] (= :error (:state status)) ^{:key "error-body"} [:tbody [:tr [:td.has-text-centered {:col-span column-count} "An unexpected error has occured. " (-> status :error first :message) " Please try refreshing the page."]]] :else (into ^{:key "main-body"} [:tbody] children)))))])) (defn sortable-header-cell [{:keys [style class sort-key sort-name asc]}] (let [children (r/children (r/current-component))] [:> Consumer {} (fn [consume] (let [{:strs [on-params-change params] :as consume} (js->clj consume)] (r/as-element (conj (into [:th {:on-click (fn [e] (on-params-change (toggle-sort-by {:sort (:sort params)} sort-key sort-name))) :style (assoc style :cursor "pointer") :class class}] children) (sort-icon sort-key (:sort params))))))])) (defn grid [{:keys [on-params-change on-check-changed checked params status column-count check-boxes? data-page]}] (if data-page (let [page @(re-frame/subscribe [::data-page/page data-page])] (r/create-element Provider #js {:value #js {:on-params-change (fn [p] (re-frame/dispatch [::data-page/table-params-changed data-page p])) :on-check-changed (fn [new] (re-frame/dispatch [::data-page/toggle-check data-page new])) :check-boxes? check-boxes? :checked (:checked page) :params (:params page) :status (:status page) :column-count column-count}} (r/as-element (into [:<> ] (r/children (r/current-component)))))) (r/create-element Provider #js {:value #js {:on-params-change on-params-change :on-check-changed on-check-changed :check-boxes? check-boxes? :checked checked :params params :status status :column-count column-count}} (r/as-element (into [:<> ] (r/children (r/current-component))))))) (defn virtual-paginate [start per-page xs ] (take (or per-page default-pagination-size) (drop (or start 0) xs))) (defn virtual-paginate-controls [start per-page xs ] {:start (or start 0) :end (min (+ (or start 0) (or per-page default-pagination-size)) (count xs)) :total (count xs)})