diff --git a/CHANGELOG.md b/CHANGELOG.md index 842ffd5b..18627030 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ > Committed but unreleased changes are put here, at the top. Older releases are detailed chronologically below. +## 2.18.0 (2024-02-02) + +#### Added + +- `simple-v-table` - `:on-export`, `:export-button-renderer` and `:show-export-button?` + ## 2.17.1 (2024-01-24) #### Fixed diff --git a/src/re_com/simple_v_table.cljs b/src/re_com/simple_v_table.cljs index 1e2a791e..3d85de19 100644 --- a/src/re_com/simple_v_table.cljs +++ b/src/re_com/simple_v_table.cljs @@ -4,9 +4,10 @@ [re-com.validate :refer [validate-args-macro]]) (:require [reagent.core :as reagent] + [re-com.buttons :refer [hyperlink row-button]] [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 ->v position-for-id item-for-id remove-id-item]] + [re-com.util :refer [px deref-or-value assoc-in-if-empty ->v position-for-id item-for-id remove-id-item clipboard-write! table->tsv]] [re-com.text :refer [label]] [re-com.validate :refer [vector-of-maps? vector-atom? parts?]] [re-com.v-table :as v-table])) @@ -204,12 +205,15 @@ (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."]} {:name :column-header-height :required false :default 31 :type "integer" :validate-fn number? :description [:span "px height of the column header section. Typically, equals " [:code ":row-height"] " * number-of-column-header-rows."]} - {:name :column-header-renderer :required false :type "cols parts sort-by-col -> hiccup" :validate-fn ifn? :description "You can provide a renderer function to override the inbuilt renderer for the columns headings"} + {:name :column-header-renderer :required false :type "cols parts sort-by-col -> hiccup" :validate-fn ifn? :description [:span "You can provide a renderer function to override the inbuilt renderer for the columns headings"]} + {:name :show-export-button? :required false :default false :type "boolean" :description [:span "When non-nil, adds a hiccup of " [:code ":export-button-render"] " to the component tree."]} + {:name :on-export :required false :type "columns, sorted-rows -> nil" :validate-fn ifn? :description "Called whenever the export button is clicked."} + {:name :export-button-renderer :required false :type "{:keys [columns rows on-export]} -> hiccup" :validate-fn ifn? :description [:span "Pass a component function to override the built-in export button. Declares a hiccup of your component in the " [:code ":top-right-renderer"] "position of the underlying " [:code "v-table"] "."]} {:name :max-width :required false :type "string" :validate-fn string? :description "standard CSS max-width setting of the entire table. Literally constrains the table to the given width so that if the table is wider than this it will add scrollbars. Ignored if value is larger than the combined width of all the columns and table padding."} {:name :max-rows :required false :type "integer" :validate-fn number? :description "The maximum number of rows to display in the table without scrolling. If not provided will take up all available vertical space."} {:name :row-height :required false :default 31 :type "integer" :validate-fn number? :description "px height of each row."} @@ -238,6 +242,13 @@ (remove zero?) (first)) 0))) +(defn clipboard-export-button [{:keys [columns rows on-export]}] + [row-button :src (at) + :md-icon-name "zmdi zmdi-copy" + :mouse-over-row? true + :tooltip (str "Copy " (count rows) " rows, " (count columns) " columns to clipboard.") + :on-click on-export]) + (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: @@ -254,6 +265,7 @@ (fn simple-v-table-render [& {:keys [model columns fixed-column-count fixed-column-border-color column-header-height column-header-renderer max-width max-rows row-height table-padding table-row-line-color on-click-row on-enter-row on-leave-row + show-export-button? on-export export-button-renderer striped? row-style cell-style class parts src debug-as] :or {column-header-height 31 @@ -262,7 +274,12 @@ table-padding 19 table-row-line-color "#EAEEF1" fixed-column-border-color "#BBBEC0" - column-header-renderer column-header-renderer} + column-header-renderer column-header-renderer + show-export-button? false + on-export (fn [{:keys [columns rows]}] (-> (remove (comp false? :export?) columns) + (table->tsv rows) + clipboard-write!)) + export-button-renderer clipboard-export-button} :as args}] (or (validate-args-macro simple-v-table-args-desc args) @@ -312,12 +329,20 @@ :row-content-width content-width :row-height row-height :max-row-viewport-height (when max-rows (* max-rows row-height)) - ;:max-width (px (or max-width (+ fixed-content-width content-width v-table/scrollbar-tot-thick))) ; :max-width handled by enclosing parent above + ;:max-width (px (or max-width (+ fixed-content-width content-width v-table/scrollbar-tot-thick))) ; :max-width handled by enclosing parent above - ;; ===== Corners (section 1) + ;; ===== Corners (section 1, 3) :top-left-renderer (partial column-header-renderer fixed-cols parts sort-by-column) ;; Used when there are fixed columns - - ;; ===== Styling + :top-right-renderer (when show-export-button? + #(let [rows (deref-or-value model) + columns (deref-or-value columns) + sort-by-column (deref-or-value sort-by-column)] + [export-button-renderer {:rows rows + :columns columns + :on-export (fn [_] (on-export {:columns columns + :rows (cond->> rows + sort-by-column (sort (multi-comparator (->v sort-by-column))))}))}])) + ;; ===== Styling :class class :parts (cond-> (-> ;; Remove the parts that are exclusive to simple-v-table, or v-table part diff --git a/src/re_com/util.cljs b/src/re_com/util.cljs index 026eff56..e38082f7 100644 --- a/src/re_com/util.cljs +++ b/src/re_com/util.cljs @@ -2,7 +2,8 @@ (:require [reagent.ratom :refer [RAtom Reaction RCursor Track Wrapper]] [goog.date.DateTime] - [goog.date.UtcDateTime])) + [goog.date.UtcDateTime] + [clojure.string :as str])) (defn fmap "Takes a function 'f' amd a map 'm'. Applies 'f' to each value in 'm' and returns. @@ -208,3 +209,17 @@ (.getMonth local-date-time) (.getDate local-date-time) 0 0 0 0))) + +(defn clipboard-write! [s] + ^js (-> js/navigator .-clipboard (.writeText s))) + +(defn table->tsv [columns rows] + (let [header-value-fn (some-fn :export-header-label :header-label (comp name :id)) + row-value-fn (some-fn :row-export-fn :row-label-fn :id) + row->cells (apply juxt (map row-value-fn columns)) + tsv-line #(str (str/join "\t" %) "\n")] + (->> rows + (map row->cells) + (cons (map header-value-fn columns)) + (map tsv-line) + (apply str)))) diff --git a/src/re_demo/simple_v_table.cljs b/src/re_demo/simple_v_table.cljs index db6d25f3..e99461eb 100644 --- a/src/re_demo/simple_v_table.cljs +++ b/src/re_demo/simple_v_table.cljs @@ -47,12 +47,12 @@ [:ul [:li "A table's dimensions will grow and shrink, to fit the space provided by its parent. When the parent imposes dimensions that are insufficient to show all of the table, scrollbars will appear."] [:li "Other times, we want a table to impose certain dimensions. Eg, it should always show 10 rows, and have no horizontal scrollbar, and we want the parent dimensions to change to accommodate."] - [:li "Width" + [:li [:strong "Width"] [:ul [:li "The full horizontal extent of the table is determined by the accumulated width of the columns"] [:li "If the width provided by the table's parent container is less than this extent, then horizontal scrollbars will appear for the unfixed columns"] [:li "Where you wish to be explicit about the table's viewable width, use the " [:code ":max-width"] " arg"]]] - [:li "Height" + [:li [:strong "Height"] [:ul [:li "The full vertical extent of the table is determined by the accumulated height of all the rows"] [:li "If the height provided by the table's parent container is less than this extent, then vertical scrollbars will appear"] @@ -60,9 +60,22 @@ [:li "Even if you are explicit via " [:code ":max-width"] " or " [:code ":max-rows"] ", the parent's dimensions will always dominate, if they are set"]] [title3 "Sorting"] [:ul - [:li "Items in " [:code ":columns"] " have an optional " [:code ":sort-by"] " key."] - [:li "If the value is " [:code "true"] ", clicking the column header will sort all the rows, using the result of the column's " [:code ":row-label-fn"] " as a sort key."] - [:li [:code ":sort-by"] " can also be map, with optional keys " [:code ":comp"] " and " [:code ":keyfn"] ", corresponding to the parameters of " [:code "clojure.core/sort-by"] "."]] + [:li [:strong "sort-by"] + [:ul + [:li "Items in " [:code ":columns"] " have an optional " [:code ":sort-by"] " key."] + [:li "If the value is " [:code "true"] ", clicking the column header will sort all the rows, using the result of the column's " [:code ":row-label-fn"] " as a sort key."] + [:li [:code ":sort-by"] " can also be map, with optional keys " [:code ":comp"] " and " [:code ":keyfn"] ", corresponding to the parameters of " [:code "clojure.core/sort-by"] "."]]] + [:li [:strong "hierarchical sort"] + [:ul + [:li "Shift-clicking a column header will conjoin that column into a hierarchical sort. A number will appear next to the sort icon, indicating its sorting precedence."] + [:li "For instance, click " [:strong "name"] ", then shift-click " [:strong "units"] ". The columns will appear as " [:strong "name ↓1 ... units ↓2."]] + [:li "Rows will sort by the column marked " [:strong "1"] ", except when any two rows compare equally, they will sort by " [:strong "2"] "."]]]] + [title3 "Export"] + [:ul + [:li "Pass " [:code ":show-export-button? true"] " to mount the export-button component into section 3 of the table (see " [:code ":top-right-renderer"] " in " [:code "v-table"] "."] + [:li "Clicking this button invokes " [:code ":on-export"] " with keyword arguments " [:code ":rows"] " and " [:code ":columns"] ". " [:code ":rows"] " is sorted."] + [:li "The default " [:code ":on-export"] " handler removes any columns declared with " [:code ":export? false"] ", serializes a TSV string and writes it to the clipboard."] + [:li "You can also pass your own component function as " [:code ":export-button-renderer"] ". It can accept keyword arguments " [:code ":rows, :columns, :on-export"] "."]] [p "The \"Sales Table Demo\" (to the right) allows you to experiment with these concepts."]]]) (defn dependencies diff --git a/src/re_demo/simple_v_table_sales.cljs b/src/re_demo/simple_v_table_sales.cljs index 59365096..0271ecb8 100644 --- a/src/re_demo/simple_v_table_sales.cljs +++ b/src/re_demo/simple_v_table_sales.cljs @@ -11,7 +11,7 @@ [re-demo.utils :refer [title3]] [re-com.util :refer [px]])) -;; 50 randomly sampled names from most popular baby names in 2019. +;; 50 randomly sampled names from most popular baby names in 2019. (def names ["Harris" "Jake" "Reece" "Aston" "Barry" "Oran" "Ritchie" "Crawford" "Raphael" "Clayton" "Johan" "Rhylen" "Caelin" "Calen" "Cassius" "Dakota" "Fabien" "Fraser-Lee" "Jonathin" "Khabat" "Lyotard" "Manpreet" "Mousa" "Rajvir" "Shadan" @@ -318,14 +318,18 @@ :src (at) :model sales-rows + ;; ==== Exporting + :show-export-button? true + ;; ===== Columns - :columns [{:id :id :header-label "Code" :row-label-fn :id :width 70 :align "center" :vertical-align "middle"} - {:id :region :header-label "Region" :row-label-fn :region :width 100 :align "left" :vertical-align "middle"} - {:id :name :header-label "Name" :row-label-fn :person :width 100 :align "left" :vertical-align "middle" :sort-by {}} - {:id :email :header-label "Email" :row-label-fn email-row-label-fn :width 200 :align "left" :vertical-align "middle"} - {:id :method :header-label "Method" :row-label-fn method-row-label-fn :width 100 :align "center" :vertical-align "middle"} - {:id :sales :header-label "Sales" :row-label-fn #(str "$" (:sales %)) :width 100 :align "right" :vertical-align "middle" :sort-by {:key-fn :sales}} - {:id :units :header-label "Units" :row-label-fn :units :width 100 :align "right" :vertical-align "middle" :sort-by true}] + :columns + [{:id :id :header-label "Code" :row-label-fn :id :width 70 :align "center" :vertical-align "middle" :export? false} + {:id :region :header-label "Region" :row-label-fn :region :width 100 :align "left" :vertical-align "middle"} + {:id :name :header-label "Name" :row-label-fn :person :width 100 :align "left" :vertical-align "middle" :sort-by {}} + {:id :email :header-label "Email" :row-label-fn email-row-label-fn :width 200 :align "left" :vertical-align "middle" :row-export-fn :email} + {:id :method :header-label "Method" :row-label-fn method-row-label-fn :width 100 :align "center" :vertical-align "middle" :row-export-fn (comp name :method)} + {:id :sales :header-label "Sales" :row-label-fn #(str "$" (:sales %)) :width 100 :align "right" :vertical-align "middle" :sort-by {:key-fn :sales}} + {:id :units :header-label "Units" :row-label-fn :units :width 100 :align "right" :vertical-align "middle" :sort-by true}] :fixed-column-count @fixed-column-count :fixed-column-border-color "#333"