Skip to content

Commit 3b5edff

Browse files
committed
Move virtual scroll into ui subdir
1 parent 653d43d commit 3b5edff

File tree

3 files changed

+163
-5
lines changed

3 files changed

+163
-5
lines changed

README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,6 @@ Rather than returning the whole page on initial render and having two render pat
119119

120120
Router is a simple map, this means path parameters are not supported use query parameters or body instead. I've found over time that path parameters force you to adopt an arbitrary hierarchy that is often wrong (and place oriented programming). Removing them avoids this and means routing can be simplified to a map and have better performance than a more traditional adaptive radix tree router.
121121

122-
>📝 Note: The Hyperlith router is completely optional and you can swap it out for reitit if you want to support path params.
123-
124122
#### Reverse proxy
125123

126124
Hyperlith is designed to be deployed between a reverse proxy like caddy for handling HTTP2/3 (you want to be using HTTP2/3 with SSE).

examples/virtual_scroll/src/app/main.clj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
(:gen-class)
33
(:require [hyperlith.core :as h :refer [defaction defview]]
44
[hyperlith.extras.sqlite :as d]
5-
[hyperlith.extras.virtual-scroll :as vs]))
5+
[hyperlith.extras.ui.virtual-scroll :as vs]))
66

77
(def row-height 20)
88

@@ -141,7 +141,7 @@
141141
[:link#css {:rel "stylesheet" :type "text/css" :href css}]
142142
[:main#morph.main
143143
[:div#foo1 {:style {:height :10vh}}
144-
[::vs/VirtualX#view-x
144+
[::vs/virtual-x#view-x
145145
{:v/item-size 100
146146
:v/max-rendered-items 1000
147147
:v/item-fn (partial col-builder db)
@@ -151,7 +151,7 @@
151151
:v/view-size (:width-1 tab-data)
152152
:v/scroll-pos (:x tab-data)}]]
153153
[:div#foo2 {:style {:height :90vh}}
154-
[::vs/VirtualY#view-y
154+
[::vs/virtual-y#view-y
155155
{:v/item-size 20
156156
:v/max-rendered-items 1000
157157
:v/item-fn (partial row-builder db)
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
(ns hyperlith.extras.ui.virtual-scroll
2+
(:require [hyperlith.core :as h]
3+
[clojure.math :as math]))
4+
5+
(defn resize-js [resize-handler-path]
6+
(format "@post(`%s?h=${el.clientHeight}&w=${el.clientWidth}`);"
7+
resize-handler-path))
8+
9+
(defn fetch-next-page-js
10+
[{:keys [fired-signal bottom top left right scroll-handler-path]}]
11+
(let [top (or top 0)
12+
bottom (or bottom 9007199254740991)
13+
left (or left 0)
14+
right (or right 9007199254740991)]
15+
(format
16+
"if (($%s !== -1) && (%s > el.scrollTop || %s < el.scrollTop || %s > el.scrollLeft || %s < el.scrollLeft))
17+
{$%s = -1; @post(`%s?x=${Math.floor(el.scrollLeft)}&y=${Math.floor(el.scrollTop)}`);}"
18+
fired-signal
19+
top
20+
bottom
21+
left
22+
right
23+
fired-signal
24+
scroll-handler-path)))
25+
26+
;; TODO: variable item height?
27+
;; TODO: send up initial device height on connect
28+
;; TODO: auto session/tab state with security/validation
29+
;; TODO: X scroll bar has max size on chrome. Normalise scroll?
30+
31+
(defn virtual-scroll-logic
32+
[{:v/keys [item-size max-rendered-items item-count-fn scroll-pos
33+
view-size item-fn buffer-items]}]
34+
(let [scroll-pos (or scroll-pos 0)
35+
view-size (or view-size 1000)
36+
max-rendered-items (or max-rendered-items 1000)
37+
max-size (* (int (/ max-rendered-items 2)) item-size)
38+
visible-items (int (/ (min view-size max-size) item-size))
39+
buffer-items (int (or buffer-items
40+
;; Default to number of items that
41+
;; fits in 4000px as user scroll speed
42+
;; not visible items determines how much
43+
;; you need to buffer
44+
(int (/ 4000 item-size))
45+
;; TODO: fix if this is larger than max
46+
;; render
47+
))
48+
rendered-items (+ (* 2 buffer-items) visible-items)
49+
offset-items (max (- (math/round (/ scroll-pos item-size))
50+
buffer-items)
51+
0)
52+
total-item-count (item-count-fn)
53+
remaining-items (- total-item-count offset-items)
54+
;; If a buffer item is one scroll will be triggered at 50%
55+
threshold-items (* 0.5 buffer-items)
56+
threshold-low (when (not= offset-items 0)
57+
(* (+ offset-items threshold-items) item-size))
58+
threshold-high (when (> remaining-items rendered-items)
59+
(* (- (+ offset-items rendered-items)
60+
visible-items threshold-items)
61+
item-size))
62+
translate (* offset-items item-size)
63+
grid-count (if (> remaining-items rendered-items)
64+
rendered-items
65+
remaining-items)]
66+
67+
{:threshold-low threshold-low
68+
:threshold-high threshold-high
69+
:fired-signal-val offset-items
70+
:translate (str translate "px")
71+
:max-size (str max-size "px")
72+
:item-grid-size (str (* grid-count item-size) "px")
73+
:item-grid (str "repeat(" grid-count "," item-size "px)")
74+
:size (str (* total-item-count item-size) "px")
75+
:item-fn (fn [] (item-fn offset-items rendered-items))}))
76+
77+
(defmethod h/html-resolve-alias ::virtual-x
78+
[_ {:keys [id]
79+
:v/keys [resize-handler-path scroll-handler-path]
80+
:as attrs} _]
81+
(let [{:keys [threshold-low threshold-high item-grid fired-signal-val
82+
translate max-size size item-fn item-grid-size]}
83+
(virtual-scroll-logic attrs)
84+
fired-signal (str id "fired")
85+
fetch-next-page? (fetch-next-page-js
86+
{:fired-signal fired-signal
87+
:left threshold-low
88+
:right threshold-high
89+
:scroll-handler-path scroll-handler-path})]
90+
(h/html
91+
[:div {;; make sure signal is initialised before data-on-load
92+
:data-signals (h/edn->json {fired-signal fired-signal-val})
93+
:style {:width :100%}}
94+
[:div (assoc attrs
95+
:data-on-resize__debounce.100ms__window
96+
(resize-js resize-handler-path)
97+
:data-on-load fetch-next-page?
98+
:data-on-scroll fetch-next-page?
99+
:style {:scroll-behavior :smooth
100+
:overscroll-behavior :contain
101+
:overflow-anchor :none
102+
:overflow-x :scroll
103+
:max-width max-size
104+
:width :100%})
105+
[:div
106+
{:id (str id "-virtual-table")
107+
:style {:pointer-events :none
108+
:width size}}
109+
[:div
110+
{:id (str id "-virtual-table-view")
111+
:style
112+
{;; if width isn't specified explicitly scroll bar will become chaos
113+
:width item-grid-size
114+
:display :grid
115+
:grid-template-columns item-grid
116+
:transform (str "translateX(" translate ")")}}
117+
(item-fn)]]]])))
118+
119+
(defmethod h/html-resolve-alias ::virtual-y
120+
[_ {:keys [id]
121+
:v/keys [resize-handler-path scroll-handler-path]
122+
:as attrs} _]
123+
(let [{:keys [threshold-low threshold-high item-grid fired-signal-val
124+
translate max-size size item-fn item-grid-size]}
125+
(virtual-scroll-logic attrs)
126+
fired-signal (str id "fired")
127+
fetch-next-page? (fetch-next-page-js
128+
{:fired-signal fired-signal
129+
:top threshold-low
130+
:bottom threshold-high
131+
:scroll-handler-path scroll-handler-path})]
132+
(h/html
133+
[:div {;; make sure signal is initialised before data-on-load
134+
:data-signals (h/edn->json {fired-signal fired-signal-val})
135+
:style {:height :100%}}
136+
[:div (assoc attrs
137+
:data-on-resize__debounce.100ms__window
138+
(resize-js resize-handler-path)
139+
:data-on-load fetch-next-page?
140+
:data-on-scroll fetch-next-page?
141+
:style {:scroll-behavior :smooth
142+
:overflow-anchor :none
143+
:overflow-y :scroll
144+
:max-height max-size
145+
:height :100%})
146+
[:div
147+
{:id (str id "-virtual-table")
148+
:style {:pointer-events :none
149+
:height size}}
150+
[:div
151+
{:id (str id "-virtual-table-view")
152+
:style
153+
{;; if height isn't specified explicitly scroll bar will become chaos
154+
:height item-grid-size
155+
:display :grid
156+
:grid-template-rows item-grid
157+
:transform (str "translateY(" translate ")")}}
158+
(item-fn)]]]])))
159+
160+

0 commit comments

Comments
 (0)