Skip to content

Commit

Permalink
Improve simple-v-table sorting
Browse files Browse the repository at this point in the history
  • Loading branch information
kimo-k committed Jan 23, 2024
1 parent 521d10e commit 26ba255
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 76 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

> Committed but unreleased changes are put here, at the top. Older releases are detailed chronologically below.
## 2.17.0 (2024-01-23)

#### Changed

- `simple-v-table` - Shift-click multiple columns to sort hierarchically.

## 2.16.4 (2024-01-19)

#### Fixed
Expand Down
10 changes: 7 additions & 3 deletions run/resources/public/assets/css/re-com.css
Original file line number Diff line number Diff line change
Expand Up @@ -1428,6 +1428,10 @@ code {
simple-v-table
----------------------------------------------------------------------------------------*/

.rc-simple-v-table-wrapper .rc-v-table-column-headers:hover svg path {
fill: #ddd;
}
.rc-v-table-column-headers .rc-simple-v-table-column-header-sort-label {
opacity: 0;
}
dts
.rc-v-table-column-headers:hover .rc-simple-v-table-column-header-sort-label {
opacity: 0.25;
}
151 changes: 88 additions & 63 deletions src/re_com/simple_v_table.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,32 @@
[reagent.core :as reagent]
[re-com.config :refer [include-args-desc?]]
[re-com.box :refer [box h-box gap]]
[re-com.util :refer [px deref-or-value assoc-in-if-empty]]
[re-com.util :refer [px deref-or-value assoc-in-if-empty ->v position-for-id item-for-id remove-id-item]]
[re-com.text :refer [label]]
[re-com.validate :refer [vector-of-maps? vector-atom? parts?]]
[re-com.v-table :as v-table]))

(defn swap!-sort-by-column
[{:keys [key-fn order]} new-key-fn new-comp]
(if (= key-fn new-key-fn)
(if (= :asc order)
{:key-fn key-fn
:comp new-comp
:order :desc}
nil)
{:key-fn new-key-fn
:comp new-comp
:order :asc}))
(def default-sort-criterion {:keyfn :label :order :asc})

(defn update-sort-criteria
[criteria new-criterion]
(let [{:keys [id order] :as new-criterion} (merge default-sort-criterion new-criterion)
this? (comp #{id} :id)
this-criterion (item-for-id id criteria)
operation (cond
(nil? this-criterion) :add
(= order (:order this-criterion)) :flip
:else :drop)
flip #(update % :order {:asc :desc :desc :asc})]
(case operation
:flip (mapv #(cond-> % (this? %) flip) criteria)
:drop (remove-id-item id criteria)
:add (vec (conj criteria (merge default-sort-criterion new-criterion))))))

(defn sort-icon
[{:keys [size fill]
:or {size "16px"
fill "transparent"}}]
fill "black"}}]
[:svg {:width size
:height size
:viewBox "0 0 24 24"}
Expand All @@ -35,7 +41,7 @@
(defn arrow-down-icon
[{:keys [size fill]
:or {size "24px"
fill "transparent"}}]
fill "black"}}]
[:svg {:width size
:height size
:viewBox "0 0 24 24"}
Expand All @@ -45,7 +51,7 @@
(defn arrow-up-icon
[{:keys [size fill]
:or {size "24px"
fill "transparent"}}]
fill "black"}}]
[:svg {:width size
:height size
:viewBox "0 0 24 24"}
Expand All @@ -58,41 +64,57 @@
:center :center})

(defn column-header-item
[{:keys [id row-label-fn width height align header-label sort-by]} parts sort-by-column]
(let [{:keys [key-fn comp] :or {key-fn row-label-fn comp compare}} sort-by
{current-key-fn :key-fn order :order} @sort-by-column]
(let [on-click #(swap! sort-by-column swap!-sort-by-column key-fn comp)
justify (get align->justify (keyword align) :start)]
[h-box
:class (str "rc-simple-v-table-column-header-item " (get-in parts [:simple-column-header-item :class]))
:width (px width)
:justify justify
:align :center
:style (merge
{:padding "0px 12px"
:min-height "24px"
:height (px height)
:font-weight "bold"
:white-space "nowrap"
:overflow "hidden"
:text-overflow "ellipsis"}
(when sort-by
{:cursor "pointer"})
(get-in parts [:simple-column-header-item :style]))
:attr (merge
{}
(when sort-by
{:on-click on-click})
(get-in parts [:simple-column-header-item :attr]))
:children [header-label
(when sort-by
[:<>
[gap :size "16px"]
(if (not= current-key-fn key-fn)
[sort-icon]
(if (= order :desc)
[arrow-down-icon]
[arrow-up-icon]))])]])))
[& _]
(let [hover? (reagent/atom false)]
(fn [{:keys [id row-label-fn width height align header-label sort-by]} parts sort-by-column]
(let [sort-by (cond (true? sort-by) {} :else sort-by)
default-sort-by {:key-fn row-label-fn :comp compare :id id :order :asc}
ps (position-for-id id @sort-by-column)
{current-order :order} (item-for-id id @sort-by-column)
add-criteria! #(swap! sort-by-column update-sort-criteria (merge default-sort-by sort-by))
replace-criteria! #(reset! sort-by-column [(merge default-sort-by sort-by)])
on-click #(if (or (.-shiftKey %) (empty? (remove (clojure.core/comp #{id} :id) @sort-by-column)))
(add-criteria!)
(replace-criteria!))
justify (get align->justify (keyword align) :start)
multiple-columns-sorted? (> (count @sort-by-column) 1)]
[h-box
:class (str "rc-simple-v-table-column-header-item " (get-in parts [:simple-column-header-item :class]))
:width (px width)
:justify justify
:align :center
:style (merge
{:padding "0px 12px"
:min-height "24px"
:height (px height)
:font-weight "bold"
:white-space "nowrap"
:overflow "hidden"
:text-overflow "ellipsis"}
(when sort-by
{:cursor "pointer"})
(get-in parts [:simple-column-header-item :style]))
:attr (merge
{:on-mouse-enter #(reset! hover? true)
:on-mouse-leave #(reset! hover? false)}
(when sort-by
{:on-click on-click})
(get-in parts [:simple-column-header-item :attr]))
:children [header-label
(when sort-by
[h-box
:class (str "rc-simple-v-table-column-header-sort-label " (when current-order "rc-simple-v-table-column-header-sort-active"))
:min-width "35px"
:style (when current-order {:opacity 0.3})
:justify :center
:align :center
:children
[(case current-order
:asc [arrow-up-icon]
:desc [arrow-down-icon]
[sort-icon])
(when ps
[label :style {:visibility (when-not multiple-columns-sorted? "hidden")} :label (inc ps)])]])]]))))

(defn column-header-renderer
":column-header-renderer AND :top-left-renderer - Render the table header"
Expand Down Expand Up @@ -182,7 +204,7 @@

(def simple-v-table-args-desc
(when include-args-desc?
[{:name :model :required true :type "r/atom containing vec of maps" :validate-fn vector-atom? :description "one element for each row in the table."}
[{:name :model :required true :type "r/atom containing vec of maps" #_#_:validate-fn vector-atom? :description "one element for each row in the table."}
{:name :columns :required true :type "vector of maps" :validate-fn vector-of-maps? :description [:span "one element for each column in the table. Must contain " [:code ":id"] "," [:code ":header-label"] "," [:code ":row-label-fn"] "," [:code ":width"] ", and " [:code ":height"] ". Optionally contains " [:code ":sort-by"] ", " [:code ":align"] " and " [:code ":vertical-align"] ". " [:code ":sort-by"] " can be " [:code "true"] " or a map optionally containing " [:code ":key-fn"] " and " [:code ":comp"] " ala " [:code "cljs.core/sort-by"] "."]}
{:name :fixed-column-count :required false :default 0 :type "integer" :validate-fn number? :description "the number of fixed (non-scrolling) columns on the left."}
{:name :fixed-column-border-color :required false :default "#BBBEC0" :type "string" :validate-fn string? :description [:span "The CSS color of the horizontal border between the fixed columns on the left, and the other columns on the right. " [:code ":fixed-column-count"] " must be > 0 to be visible."]}
Expand All @@ -204,6 +226,18 @@
{:name :src :required false :type "map" :validate-fn map? :description [:span "Used in dev builds to assist with debugging. Source code coordinates map containing keys" [:code ":file"] "and" [:code ":line"] ". See 'Debugging'."]}
{:name :debug-as :required false :type "map" :validate-fn map? :description [:span "Used in dev builds to assist with debugging, when one component is used implement another component, and we want the implementation component to masquerade as the original component in debug output, such as component stacks. A map optionally containing keys" [:code ":component"] "and" [:code ":args"] "."]}]))

(defn criteria-compare [a b {:keys [key-fn comp-fn order]
:or {key-fn :label order :asc comp-fn compare}}]
(cond-> (comp-fn (key-fn a) (key-fn b))
(= :desc order) -))

(defn multi-comparator [criteria]
(fn [a b]
(or (->> criteria
(map (partial criteria-compare a b))
(remove zero?)
(first)) 0)))

(defn simple-v-table
"Render a v-table and introduce the concept of columns (provide a spec for each).
Of the nine possible sections of v-table, this table only supports four:
Expand Down Expand Up @@ -244,16 +278,7 @@
content-width
v-table/scrollbar-tot-thick
(* 2 table-padding)
2) ;; 2 border widths
internal-model (reagent/track
(fn []
(if-let [{:keys [key-fn comp order] :or {comp compare}} @sort-by-column]
(do
(let [sorted (sort-by key-fn comp (deref-or-value model))]
(if (= order :desc)
(vec (reverse sorted))
(vec sorted))))
(deref-or-value model))))]
2)] ;; 2 border widths
[box
:src src
:debug-as (or debug-as (reflect-current-component))
Expand All @@ -273,8 +298,8 @@
:attr (get-in parts [:simple-wrapper :attr])
:child [v-table/v-table
:src (at)
:model internal-model

:model model
:sort-comp (multi-comparator (->v @sort-by-column))
;; ===== Column header (section 4)
:column-header-renderer (partial column-header-renderer content-cols parts sort-by-column)
:column-header-height column-header-height
Expand Down
5 changes: 5 additions & 0 deletions src/re_com/util.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@
[vect index item]
(apply merge (subvec vect 0 index) item (subvec vect index)))

(defn ->v [x] (cond (vector? x) x
(sequential? x) (vec x)
(nil? x) nil
:else [x]))

;; ----------------------------------------------------------------------------
;; Utilities for vectors of maps containing :id
;; ----------------------------------------------------------------------------
Expand Down
28 changes: 19 additions & 9 deletions src/re_com/v_table.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,8 @@
(when include-args-desc?
[{:name :model :required true :type "r/atom containing vec of maps" :validate-fn vector-atom? :description [:span "One element for each row displayed in the table. Typically, a vector of maps, but can be a seq of anything, with your functions like " [:code ":key-fn"] " extracting values."]}
{:name :key-fn :required false :default "nil" :type "map -> anything" :validate-fn ifn-or-nil? :description [:span "A function/keyword or nil. Given an element of " [:code ":model"] ", it should return its unique identifier which is used by Reagent as a unique id. If not specified or nil passed, the element's 0-based row-index will be used"]}
{:name :sort-comp :required false :default "nil" :type "map, map -> number" :validate-fn ifn-or-nil? :description [:span "Sorts " [:code ":model"] " using " [:code ":sort-comp"] "as a comparatison function. Can be combined with " [:code ":sort-keyfn"] "."]}
{:name :sort-keyfn :required false :default "nil" :type "map -> anything" :validate-fn ifn-or-nil? :description [:span "Sorts " [:code ":model"] " using " [:code ":sort-keyfn"] "as a sort-key function. Can be combined with " [:code ":sort-comp"] "."]}
{:name :virtual? :required false :default true :type "boolean" :description [:span "when true, only those rows that are visible are rendered to the DOM. Otherwise DOM will be generated for all rows, which might be prohibitive if there are a large number of rows."]}

{:name :row-height :required true :type "integer" :validate-fn number? :description "px height of each row, in sections 2, 5 and 8."}
Expand Down Expand Up @@ -828,11 +830,15 @@
top-row-index (reaction (int (/ @scroll-y row-height))) ;; The row number (zero-based) of the row currently rendered at the top of the table
bot-row-index (reaction (min (+ @top-row-index (dec @rows-per-viewport)) @m-size)) ;; The row number of the row currently rendered at the bottom of the table
virtual-scroll-y (reaction (mod @scroll-y row-height)) ;; Virtual version of scroll-y but this is a very small number (between 0 and the row-height)
row-sort-fn (reagent/atom nil)
sorted-model (reaction (if-let [row-sort-fn @row-sort-fn]
(vec (row-sort-fn @model))
@model))
virtual-rows (reaction (when (pos? @m-size)
(subvec @model
(subvec @sorted-model
(min @top-row-index @m-size)
(min (+ @top-row-index @rows-per-viewport 2) @m-size))))

rows (reaction (if virtual? @virtual-rows @sorted-model))
on-h-scroll-change #(reset! scroll-x %) ;; The on-change handler for the horizontal scrollbar
on-v-scroll-change #(reset! scroll-y %) ;; The on-change handler for the vertical scrollbar

Expand Down Expand Up @@ -1036,7 +1042,7 @@

:reagent-render
(fn v-table-render
[& {:keys [virtual? remove-empty-row-space? key-fn max-width
[& {:keys [virtual? remove-empty-row-space? key-fn sort-fn sort-comp sort-keyfn max-width
;; Section 1
top-left-renderer
;; Section 2
Expand All @@ -1060,15 +1066,19 @@
class parts src debug-as]
:or {virtual? true
remove-empty-row-space? true
key-fn nil}
key-fn nil
sort-fn nil}
:as args}]
(or
(validate-args-macro v-table-args-desc args)
(do
(reset! content-rows-width row-content-width)
(reset! content-rows-height (* @m-size row-height))

;; Scroll rows into view handling
(cond
(and sort-keyfn sort-comp) (reset! row-sort-fn (partial sort-by sort-keyfn sort-comp))
sort-keyfn (reset! row-sort-fn (partial sort-by sort-keyfn))
sort-comp (reset! row-sort-fn (partial sort sort-comp)))
;; Scroll rows into view handling
(when (not= (deref-or-value scroll-rows-into-view) @internal-scroll-rows-into-view)
;; TODO: Ideally allow non-atom nil but exception if it's not an atom when there's a value
(let [{:keys [start-row end-row]} (deref-or-value scroll-rows-into-view)
Expand Down Expand Up @@ -1159,7 +1169,7 @@
row-header-renderer
key-fn
@top-row-index
(if virtual? @virtual-rows @model) ;; rows
@rows ;; rows
(if virtual? @virtual-scroll-y @scroll-y) ;; scroll-y
;-----------------
row-header-selection-fn
Expand Down Expand Up @@ -1233,7 +1243,7 @@
row-renderer
key-fn
@top-row-index
(if virtual? @virtual-rows @model) ;; rows
@rows
@scroll-x
(if virtual? @virtual-scroll-y @scroll-y) ;; scroll-y
;-----------------
Expand Down Expand Up @@ -1312,7 +1322,7 @@
row-footer-renderer
key-fn
@top-row-index
(if virtual? @virtual-rows @model) ;; rows
@rows
(if virtual? @virtual-scroll-y @scroll-y) ;; scroll-y
;-----------------
row-viewport-height
Expand Down
Loading

0 comments on commit 26ba255

Please sign in to comment.