diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ba80935..1c72ed98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ > Committed but unreleased changes are put here, at the top. Older releases are detailed chronologically below. +## 2.21.30 (2024-09-13) + +#### Changed + +- `nested-grid` is now a flex item, taking up as much vertical and horizontal space as possible. + +#### Added + +- `nested-grid` - `:remove-empty-row-space?` and `:remove-empty-column-space?` props. + + ## 2.21.29 (2024-09-12) #### Fixed diff --git a/src/re_com/nested_grid.cljs b/src/re_com/nested_grid.cljs index 003ed735..606de921 100644 --- a/src/re_com/nested_grid.cljs +++ b/src/re_com/nested_grid.cljs @@ -142,6 +142,20 @@ :validate-fn boolean? :description [:span "When " [:code "true"] ", display a draggable resize button on row-header grid lines."]} + {:default true + :description + "If true removes whitespace between the last row and the horizontal scrollbar. Useful for tables without many rows where otherwise + there would be a big gap between the last row and the horizontal scrollbar at the bottom of the available space." + :name :remove-empty-row-space? + :required false + :type "boolean"} + {:default true + :description + "If true removes whitespace between the last column and the vertical scrollbar. Useful for tables without many columns where otherwise + there would be a big gap between the last column and the vertical scrollbar at the right of the available space." + :name :remove-empty-column-space? + :required false + :type "boolean"} {:name :max-height :required false :type "string" @@ -646,24 +660,27 @@ theme-cells? show-branch-paths? max-height max-width + remove-empty-row-space? remove-empty-column-space? column-width column-header-height row-header-width row-height show-export-button? on-export export-button on-export-cell on-export-column-header on-export-row-header show-zebra-stripes? show-selection-box? resize-columns? resize-rows?] - :or {column-header-height 25 - column-width 55 - row-header-width 80 - row-height 25 - show-export-button? true - show-branch-paths? false - show-selection-box? false - show-zebra-stripes? true - on-export-column-header header-label - on-export-row-header header-label - resize-columns? true - resize-rows? false - theme-cells? true}} + :or {column-header-height 25 + column-width 55 + row-header-width 80 + row-height 25 + remove-empty-row-space? true + remove-empty-column-space? true + show-export-button? true + show-branch-paths? false + show-selection-box? false + show-zebra-stripes? true + on-export-column-header header-label + on-export-row-header header-label + resize-columns? true + resize-rows? false + theme-cells? true}} (theme/top-level-part passed-in-props ::nested-grid) theme (theme/defaults props @@ -824,30 +841,30 @@ :right 0}} (u/part export-button (themed ::export-button - {:on-click #(let [column-headers (export-column-headers) - row-headers (export-row-headers) - spacers (export-spacers) - cells (export-cells) - header-rows (mapv into spacers column-headers) - main-rows (mapv into row-headers cells) - rows (concat header-rows main-rows)] - (on-export - {:column-headers column-headers - :row-headers row-headers - :spacers spacers - :cells cells - :header-rows header-rows - :main-rows main-rows - :rows rows - :default default-on-export}))}) + {:on-click #(let [column-headers (export-column-headers) + row-headers (export-row-headers) + spacers (export-spacers) + cells (export-cells) + header-rows (mapv into spacers column-headers) + main-rows (mapv into row-headers cells) + rows (concat header-rows main-rows)] + (on-export + {:column-headers column-headers + :row-headers row-headers + :spacers spacers + :cells cells + :header-rows header-rows + :main-rows main-rows + :rows rows + :default default-on-export}))}) :default default-export-button)]] cell-grid-container [:div (themed ::cell-grid-container - {:style {:max-height max-height - :max-width max-width - :display "grid" - :grid-template-columns (grid-template cell-grid-columns) - :grid-template-rows (grid-template cell-grid-rows)}})] + {:style {:max-height max-height + :max-width max-width + :display "grid" + :grid-template-columns (grid-template cell-grid-columns) + :grid-template-rows (grid-template cell-grid-rows)}})] column-header-cells (for [path column-paths :let [edge (cond-> #{} (start-branch? path column-paths) (conj :left) @@ -892,130 +909,134 @@ :drag drag :dimension :column :path path})])]) - row-header-cells (for [path row-paths - :let [edge (cond-> #{} - (start-branch? path row-paths) (conj :top) - (end-branch? path row-paths) (conj :bottom) - (= 1 (count path)) (conj :left) - (= (count path) row-depth) (conj :right)) - state {:edge edge - :row-path path - :path path - :header-spec (last path) - :show? show?} - props (merge {:theme (update theme :user-variables - conj (theme/with-state state)) - :selection? selection?} - state)]] - ^{:key [::row (or path (gensym))]} - [:div {:style {:grid-row-start (path->grid-line-name path) - :grid-row-end (str "span " (cond-> path - :do (header-cross-span showing-row-paths) - :do dec - (not show?) dec)) - :grid-column-start (count path) - :grid-column-end (str "span " (cond-> path - :do (header-main-span showing-row-paths) - (not show?) dec)) - :position "relative"}} - (u/part row-header-wrapper - (merge props {:children [(u/part row-header props :default row-header-part)]}) - :default row-header-wrapper-part) - (when (and resize-rows? show?) - [resize-button {:mouse-down-x mouse-down-x - :last-mouse-x last-mouse-x - :mouse-x mouse-x - :resize-handler resize-handler - :resize-columns? resize-columns? - :on-resize resize-column! - :drag drag - :dimension :row - :path path}])]) - header-spacer-cells (for [y (range column-depth) - x (range row-depth) - :let [props {:theme theme - :x x - :y y - :header-spacer header-spacer - :edge (cond-> #{} - (zero? y) (conj :top) - (zero? x) (conj :left) - (= y (dec column-depth)) (conj :bottom) - (= x (dec row-depth)) (conj :right))}]] - (u/part header-spacer-wrapper props :default header-spacer-wrapper-part)) - cells (if-not theme-cells? - (for [row-path showing-row-paths - column-path showing-column-paths - :let [value (when cell-value (cell-value {:column-path column-path :row-path row-path}))]] - [cell {:style {:grid-column (path->grid-line-name column-path) - :grid-row (path->grid-line-name row-path)} - :row-path row-path - :column-path column-path - :value value}]) - (for [row-path showing-row-paths - column-path showing-column-paths - :let [edge (cond-> #{} - (= column-path (first showing-column-paths)) (conj :left) - (= column-path (last showing-column-paths)) (conj :right) - (= row-path (first showing-row-paths)) (conj :top) - (= row-path (last showing-row-paths)) (conj :bottom) - (cell-section-left? column-path) (conj :column-section-left) - (cell-section-right? column-path) (conj :column-section-right)) - value (when cell-value (cell-value {:column-path column-path - :row-path row-path})) - state {:edge edge - :column-path column-path - :row-path row-path - :value value} - theme (update theme :user-variables - conj (theme/with-state state)) - props (merge {:cell cell - :theme theme} - state) - cell-props (merge {:value value - :theme theme} - state)]] - (u/part cell-wrapper - (merge props {:children [(u/part cell cell-props :default cell-part)]}) - :default cell-wrapper-part))) - zebra-stripes (for [i (filter even? (range 1 (inc (count row-paths))))] - ^{:key [::zebra-stripe i]} - [:div - (themed ::zebra-stripe - {:style - {:grid-column-start 1 - :grid-column-end "end" - :grid-row i - :background-color "#999" - :opacity 0.05 - :z-index 2 - :pointer-events "none"}})]) - box-selector [selection-part - {:drag drag - :grid-columns cell-grid-columns - :grid-rows cell-grid-rows - :selection? selection? - :mouse-x mouse-x - :mouse-y mouse-y - :mouse-down-x mouse-down-x - :mouse-down-y mouse-down-y - :selection-grid-spec selection-grid-spec}] + row-header-cells (for [path row-paths + :let [edge (cond-> #{} + (start-branch? path row-paths) (conj :top) + (end-branch? path row-paths) (conj :bottom) + (= 1 (count path)) (conj :left) + (= (count path) row-depth) (conj :right)) + state {:edge edge + :row-path path + :path path + :header-spec (last path) + :show? show?} + props (merge {:theme (update theme :user-variables + conj (theme/with-state state)) + :selection? selection?} + state)]] + ^{:key [::row (or path (gensym))]} + [:div {:style {:grid-row-start (path->grid-line-name path) + :grid-row-end (str "span " (cond-> path + :do (header-cross-span showing-row-paths) + :do dec + (not show?) dec)) + :grid-column-start (count path) + :grid-column-end (str "span " (cond-> path + :do (header-main-span showing-row-paths) + (not show?) dec)) + :position "relative"}} + (u/part row-header-wrapper + (merge props {:children [(u/part row-header props :default row-header-part)]}) + :default row-header-wrapper-part) + (when (and resize-rows? show?) + [resize-button {:mouse-down-x mouse-down-x + :last-mouse-x last-mouse-x + :mouse-x mouse-x + :resize-handler resize-handler + :resize-columns? resize-columns? + :on-resize resize-column! + :drag drag + :dimension :row + :path path}])]) + header-spacer-cells (for [y (range column-depth) + x (range row-depth) + :let [props {:theme theme + :x x + :y y + :header-spacer header-spacer + :edge (cond-> #{} + (zero? y) (conj :top) + (zero? x) (conj :left) + (= y (dec column-depth)) (conj :bottom) + (= x (dec row-depth)) (conj :right))}]] + (u/part header-spacer-wrapper props :default header-spacer-wrapper-part)) + cells (if-not theme-cells? + (for [row-path showing-row-paths + column-path showing-column-paths + :let [value (when cell-value (cell-value {:column-path column-path :row-path row-path}))]] + [cell {:style {:grid-column (path->grid-line-name column-path) + :grid-row (path->grid-line-name row-path)} + :row-path row-path + :column-path column-path + :value value}]) + (for [row-path showing-row-paths + column-path showing-column-paths + :let [edge (cond-> #{} + (= column-path (first showing-column-paths)) (conj :left) + (= column-path (last showing-column-paths)) (conj :right) + (= row-path (first showing-row-paths)) (conj :top) + (= row-path (last showing-row-paths)) (conj :bottom) + (cell-section-left? column-path) (conj :column-section-left) + (cell-section-right? column-path) (conj :column-section-right)) + value (when cell-value (cell-value {:column-path column-path + :row-path row-path})) + state {:edge edge + :column-path column-path + :row-path row-path + :value value} + theme (update theme :user-variables + conj (theme/with-state state)) + props (merge {:cell cell + :theme theme} + state) + cell-props (merge {:value value + :theme theme} + state)]] + (u/part cell-wrapper + (merge props {:children [(u/part cell cell-props :default cell-part)]}) + :default cell-wrapper-part))) + zebra-stripes (for [i (filter even? (range 1 (inc (count row-paths))))] + ^{:key [::zebra-stripe i]} + [:div + (themed ::zebra-stripe + {:style + {:grid-column-start 1 + :grid-column-end "end" + :grid-row i + :background-color "#999" + :opacity 0.05 + :z-index 2 + :pointer-events "none"}})]) + box-selector [selection-part + {:drag drag + :grid-columns cell-grid-columns + :grid-rows cell-grid-rows + :selection? selection? + :mouse-x mouse-x + :mouse-y mouse-y + :mouse-down-x mouse-down-x + :mouse-down-y mouse-down-y + :selection-grid-spec selection-grid-spec}] ;; FIXME This changes on different browsers - do we need to get it dynamically? ;; FIXME We should use :scrollbar-gutter (chrome>=94) - native-scrollbar-width 10 - native-width (apply + - native-scrollbar-width - showing-column-widths) - native-height (apply + - native-scrollbar-width - showing-row-heights)] - [:div - [:div {:style {:height (when show-export-button? 25)}} control-panel] + + native-width (+ u/scrollbar-tot-thick + (apply + showing-column-widths) + (apply + max-row-widths)) + native-height (+ u/scrollbar-thickness + (apply + max-column-heights) + (apply + showing-row-heights))] + [:<> + #_[:div {:style {:height (when show-export-button? 25)}} control-panel] [:div {:on-mouse-enter #(reset! hover? true) :on-mouse-leave #(reset! hover? false) :style - {:max-width max-width - :max-height max-height + {:max-width (or max-width (when remove-empty-column-space? + native-width)) + :max-height (or max-height (when remove-empty-row-space? + native-height)) + :flex 1 + :box-sizing :border-box :overflow :auto :display :grid :grid-template-columns (grid-template [(px (apply + max-row-widths)) @@ -1024,31 +1045,31 @@ [(px (apply + max-column-heights)) "1fr"]))}} (into [:div (themed ::header-spacer-grid-container - {:style {:display :grid - :box-sizing :border-box - :position :sticky - :top 0 - :left 0 - :z-index 3 - :grid-template-columns (grid-template max-row-widths) - :grid-template-rows (grid-template max-column-heights)}})] + {:style {:display :grid + :box-sizing :border-box + :position :sticky + :top 0 + :left 0 + :z-index 3 + :grid-template-columns (grid-template max-row-widths) + :grid-template-rows (grid-template max-column-heights)}})] header-spacer-cells) (into [:div (themed ::column-header-grid-container - {:style {:position :sticky - :top 0 - :z-index 2 - :display :grid - :width :fit-content - :grid-template-columns (grid-template cell-grid-columns) - :grid-template-rows (grid-template max-column-heights)}})] + {:style {:position :sticky + :top 0 + :z-index 2 + :display :grid + :width :fit-content + :grid-template-columns (grid-template cell-grid-columns) + :grid-template-rows (grid-template max-column-heights)}})] column-header-cells) (into [:div (themed ::row-header-grid-container - {:style {:position :sticky - :left 0 - :z-index 1 - :display "grid" - :grid-template-columns (grid-template max-row-widths) - :grid-template-rows (grid-template cell-grid-rows)}})] + {:style {:position :sticky + :left 0 + :z-index 1 + :display "grid" + :grid-template-columns (grid-template max-row-widths) + :grid-template-rows (grid-template cell-grid-rows)}})] row-header-cells) (-> cell-grid-container (into cells) @@ -1072,100 +1093,3 @@ :last-mouse-x last-mouse-x :last-mouse-y last-mouse-y :on-resize resize-handler}])])))) - -(def grid-width 640) -(def grid-height 260) -(def column-widths (repeat 8 80)) -(def columns (vec (interleave (repeatedly gensym) (repeat 8 80)))) - -(defn within [lower upper columns] - (let [a - (loop [sum 0 start nil end nil i 0 prefixes 0 [column & remainder] columns] - (cond - (empty? remainder) [start (or end (count columns))] - (not (number? column)) (recur sum start end (inc i) (inc prefixes) remainder) - (< (+ sum column) lower) (recur (+ sum column) start end (inc i) 0 remainder) - :else (- i prefixes)))] - a)) - -(within 200 4800 columns) -(within 2 4 [:a :b :c 2 :b :d :d :d :d :d :d :d 10]) - -(->> columns - (take 10) - (filter number?) - (apply +)) - -(def row-heights (repeat 13 20)) - -(def test-cells (map #(do ^{:key %} - [:div {:style {:overflow :hidden - :border-right "thin solid grey" - :border-bottom "thin solid grey"}} (str %)]) - (range 100))) - -(defn test-grid [] - (let [width (r/atom 0) - height (r/atom 0) - resize-observer (js/ResizeObserver. - (fn [entries] - (let [entry (aget entries 0) - content-rect (.-contentRect entry)] - (reset! width (.-width content-rect)) - (reset! height (.-height content-rect))))) - resize-observe! #(when % (.observe resize-observer %)) - scroll-left (r/atom 0) - scroll-top (r/atom 0) - vis (r/reaction @scroll-left)] - (fn [] - [:div {:ref resize-observe! - :style {:display :grid - :height "100%" - :min-height 25 - :max-height (+ grid-height 20) - :flex 1 - :grid-template-columns (str "200px minmax(0px, " grid-width "px)") - :grid-template-rows "20px 1fr"}} - [:div (str @vis)] - [scroll-container {:scroll-left scroll-left} - [:div {:style {:display :grid - :grid-template-columns (grid-template column-widths) - :background "orange"}} - (take 8 test-cells)]] - [scroll-container {:scroll-top scroll-top} - [:div {:style {:display :grid - :grid-template-rows (grid-template row-heights) - :background "orange"}} - (take 13 test-cells)]] - [:div {:style {:grid-template-columns (grid-template column-widths) - :grid-template-rows (grid-template row-heights) - :max-width grid-width - :overflow :auto - :max-height grid-height - :background "lightblue" - :display "grid"} - :on-scroll #(do (reset! scroll-top (.-scrollTop (.-target %))) - (reset! scroll-left (.-scrollLeft (.-target %))))} - test-cells]]))) - -(defn test-main [] - [box/v-box - :width "100%" - :height "100%" - :children - (into [[test-grid]] - (mapv - #(do [box/box :style {:background % - :opacity "0.1" - :max-height 200} - :size "1" - :child - [:div {:style {:min-height 100 - :width 50 - :background "white"}} - "XYZ" [:br] - "XYZ" [:br] - "XYZ" [:br] - "XYZ" [:br] - "XYZ"]]) - [#_"red" "green" #_"blue"]))]) diff --git a/src/re_com/simple_v_table.cljs b/src/re_com/simple_v_table.cljs index 8ebeab33..14025fa8 100644 --- a/src/re_com/simple_v_table.cljs +++ b/src/re_com/simple_v_table.cljs @@ -442,7 +442,7 @@ actual-table-width (+ fixed-content-width (when (pos? fixed-column-count) 1) ;; 1 border width (for fixed-col-border) content-width - v-table/scrollbar-tot-thick + u/scrollbar-tot-thick (* 2 table-padding) 2)] ;; 2 border widths [box @@ -476,7 +476,7 @@ :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 u/scrollbar-tot-thick))) ; :max-width handled by enclosing parent above ;; ===== Corners (section 1, 3) :top-left-renderer (fn [i row] [column-header-renderer {:columns fixed-cols :hover? header-hover? @@ -612,7 +612,7 @@ actual-table-width (+ fixed-content-width (when (pos? fixed-column-count) 1) ;; 1 border width (for fixed-col-border) content-width - v-table/scrollbar-tot-thick + u/scrollbar-tot-thick (* 2 table-padding) 2)] ;; 2 border widths [box @@ -641,7 +641,7 @@ :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 u/scrollbar-tot-thick))) ; :max-width handled by enclosing parent above ;; ===== Corners (section 1, 3) :top-left-renderer (fn [i row] [column-header-renderer {:columns fixed-cols :parts parts diff --git a/src/re_com/util.cljs b/src/re_com/util.cljs index 58cdaeca..11924f10 100644 --- a/src/re_com/util.cljs +++ b/src/re_com/util.cljs @@ -283,3 +283,8 @@ props) [:line {:x1 0 :y1 0 :x2 (<-px width) :y2 (<-px height)}] [:line {:x1 0 :y1 (<-px height) :x2 (<-px width) :y2 0}]]))) + +(def scrollbar-thickness 10) +(def scrollbar-margin 2) +(def scrollbar-tot-thick (+ scrollbar-thickness (* 2 scrollbar-margin))) + diff --git a/src/re_com/v_table.cljs b/src/re_com/v_table.cljs index aa950d6a..d4bc4833 100644 --- a/src/re_com/v_table.cljs +++ b/src/re_com/v_table.cljs @@ -14,10 +14,6 @@ ;; The public API for this component is called table (see last component in this file) -(def scrollbar-thickness 10) -(def scrollbar-margin 2) -(def scrollbar-tot-thick (+ scrollbar-thickness (* 2 scrollbar-margin))) - (def px (memoize util/px)) (defn show-row-data-on-alt-click @@ -524,7 +520,9 @@ {:name :max-row-viewport-height :required false :type "integer" :validate-fn number? :description [:span "The " [:b [:i "maximum"]] " px height of the row viewport area (section 5), excluding height of sections 4 and 6 (and horizontal scrollbar). If not specified, value determined by parent height and number of rows"]} {:name :scroll-rows-into-view :required false :type "atom containing map" :validate-fn map-atom? :description [:span "Scrolls the table to a particular row range. Must be an atom. The map contains the keys " [:code ":start-row"] " and " [:code ":end-row"] " (row indexes)."]} {:name :scroll-columns-into-view :required false :type "atom containing map" :validate-fn map-atom? :description [:span "Scrolls the table of a particular column range. Must be an atom. Map that contains the keys " [:code ":start-col"] " and " [:code ":end-col"] " in pixel units."]} - {:name :remove-empty-row-space? :required false :default true :type "boolean" :description "If true, removes whitespace between the last row and the horizontal scrollbar. Useful for tables without many rows where otherwise there would be a big gap between the last row and the horizontal scrollbar at the bottom of the available space."} + {:name :remove-empty-row-space? :required false :default true :type "boolean" :description + "If true, removes whitespace between the last row and the horizontal scrollbar. Useful for tables without many rows where otherwise + there would be a big gap between the last row and the horizontal scrollbar at the bottom of the available space."} {:name :class :required false :type "string" :validate-fn string? :description "CSS class names, space separated (these are applied to the table's outer container)"} {:name :parts :required false :type "map" :validate-fn (parts? v-table-parts) :description "See Parts section below."} {: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'."]} @@ -1136,7 +1134,7 @@ (or column-header-height 0) (or max-row-viewport-height (inc @content-rows-height)) ;; TODO: The inc prevents content scrollbar. Need to inc more if more than 1px borders specified (or column-footer-height 0) - scrollbar-tot-thick))} + util/scrollbar-tot-thick))} ;; TODO: Currently, scrolling a v-table with the mouse wheel also scrolls parent scrollbars (usually the one on the ) ;; The solution seems to be to use CSS overscroll-behavior @@ -1208,7 +1206,7 @@ [box/gap :src (at) - :size (px scrollbar-tot-thick)]]] + :size (px util/scrollbar-tot-thick)]]] ;; ========== MIDDLE SECTION (4, 5, 6) - column header/footer and content area @@ -1296,11 +1294,11 @@ :class (str "rc-v-table-h-scroll " (get-in parts [:h-scroll :class])) :type :horizontal :length @rl-row-viewport-width - :width scrollbar-thickness + :width util/scrollbar-thickness :content-length @content-rows-width :scroll-pos @scroll-x :on-change on-h-scroll-change - :style (merge {:margin (px-n scrollbar-margin 0)} + :style (merge {:margin (px-n util/scrollbar-margin 0)} (get-in parts [:h-scroll :style])) :attr (get-in parts [:h-scroll :attr])]]] @@ -1354,7 +1352,7 @@ [box/gap :src (at) - :size (px scrollbar-tot-thick)]]] + :size (px util/scrollbar-tot-thick)]]] ;; ========== Vertical scrollbar section @@ -1374,11 +1372,11 @@ :class (str "rc-v-table-v-scroll " (get-in parts [:v-scroll :class])) :type :vertical :length @rl-row-viewport-height - :width scrollbar-thickness + :width util/scrollbar-thickness :content-length @content-rows-height :scroll-pos @scroll-y :on-change on-v-scroll-change - :style (merge {:margin (px-n 0 scrollbar-margin)} + :style (merge {:margin (px-n 0 util/scrollbar-margin)} (get-in parts [:v-scroll :style])) :attr (get-in parts [:v-scroll :attr])]] [box/gap @@ -1386,7 +1384,7 @@ :size (px (or column-footer-height 0))] [box/gap :src (at) - :size (px scrollbar-tot-thick)]]] + :size (px util/scrollbar-tot-thick)]]] ;; ========== Debug section