masua 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +7 -0
  2. package/index.ts +269 -0
  3. package/package.json +74 -0
  4. package/react.tsx +12 -0
package/README.md ADDED
@@ -0,0 +1,7 @@
1
+ <p align="center">
2
+ <img src="https://github.com/tobua/masua/raw/main/logo.png" alt="masua" width="300">
3
+ </p>
4
+
5
+ # masua
6
+
7
+ Simple masonry layout in TypeScript.
package/index.ts ADDED
@@ -0,0 +1,269 @@
1
+ interface Configuration {
2
+ sizes: number[]
3
+ columns: number[]
4
+ container: HTMLElement
5
+ count: number
6
+ width: number
7
+ removeListener: () => void
8
+ currentGutterX: number
9
+ currentGutterY: number
10
+ resizeTimeout: NodeJS.Timeout
11
+ baseWidth: number
12
+ gutterX: number
13
+ gutterY: number
14
+ gutter: number
15
+ minify: boolean
16
+ ultimateGutter: number
17
+ surroundingGutter: boolean
18
+ direction: 'ltr' | 'rtl'
19
+ wedge: boolean
20
+ }
21
+
22
+ function getCount(configuration: Configuration) {
23
+ if (configuration.surroundingGutter) {
24
+ return Math.floor(
25
+ (configuration.width - configuration.currentGutterX) /
26
+ (configuration.baseWidth + configuration.currentGutterX),
27
+ )
28
+ }
29
+
30
+ return Math.floor(
31
+ (configuration.width + configuration.currentGutterX) /
32
+ (configuration.baseWidth + configuration.currentGutterX),
33
+ )
34
+ }
35
+
36
+ function getLongest(configuration: Configuration) {
37
+ let longest = 0
38
+ for (let index = 0; index < configuration.count; index += 1) {
39
+ if (configuration.columns[index] > configuration.columns[longest]) {
40
+ longest = index
41
+ }
42
+ }
43
+ return longest
44
+ }
45
+
46
+ function getNextColumn(index: number, configuration: Configuration) {
47
+ return index % configuration.columns.length
48
+ }
49
+
50
+ function getShortest(configuration: Configuration) {
51
+ let shortest = 0
52
+ for (let index = 0; index < configuration.count; index += 1) {
53
+ if (configuration.columns[index] < configuration.columns[shortest]) {
54
+ shortest = index
55
+ }
56
+ }
57
+
58
+ return shortest
59
+ }
60
+
61
+ function reset(configuration: Configuration) {
62
+ configuration.sizes = []
63
+ configuration.columns = []
64
+ configuration.count = null
65
+ configuration.width = configuration.container.clientWidth
66
+ const minWidth = configuration.baseWidth
67
+ if (configuration.width < minWidth) {
68
+ configuration.width = minWidth
69
+ configuration.container.style.minWidth = `${minWidth}px`
70
+ }
71
+
72
+ if (getCount(configuration) === 1) {
73
+ // Set ultimate gutter when only one column is displayed
74
+ configuration.currentGutterX = configuration.ultimateGutter
75
+ // As gutters are reduced, two column may fit, forcing to 1
76
+ configuration.count = 1
77
+ } else if (configuration.width < configuration.baseWidth + 2 * configuration.currentGutterX) {
78
+ // Remove gutter when screen is to low
79
+ configuration.currentGutterX = 0
80
+ } else {
81
+ configuration.currentGutterX = configuration.gutterX
82
+ }
83
+ }
84
+
85
+ function computeWidth(configuration: Configuration) {
86
+ let width
87
+ if (configuration.surroundingGutter) {
88
+ width =
89
+ (configuration.width - configuration.currentGutterX) / configuration.count -
90
+ configuration.currentGutterX
91
+ } else {
92
+ width =
93
+ (configuration.width + configuration.currentGutterX) / configuration.count -
94
+ configuration.currentGutterX
95
+ }
96
+ width = Number.parseFloat(width.toFixed(2))
97
+
98
+ return width
99
+ }
100
+
101
+ function layout(configuration: Configuration) {
102
+ if (!configuration.container) {
103
+ console.error('Container not found')
104
+ return
105
+ }
106
+ reset(configuration)
107
+
108
+ // Computing columns count
109
+ if (configuration.count == null) {
110
+ configuration.count = getCount(configuration)
111
+ }
112
+ // Computing columns width
113
+ const colWidth = computeWidth(configuration)
114
+
115
+ for (let index = 0; index < configuration.count; index += 1) {
116
+ configuration.columns[index] = 0
117
+ }
118
+
119
+ // Saving children real heights
120
+ const { children } = configuration.container
121
+ for (let index = 0; index < children.length; index += 1) {
122
+ // Set colWidth before retrieving element height if content is proportional
123
+ const child = children[index] as HTMLElement
124
+ child.style.width = `${colWidth}px`
125
+ configuration.sizes[index] = child.clientHeight
126
+ }
127
+
128
+ let startX
129
+ if (configuration.direction === 'ltr') {
130
+ startX = configuration.surroundingGutter ? configuration.currentGutterX : 0
131
+ } else {
132
+ startX =
133
+ configuration.width - (configuration.surroundingGutter ? configuration.currentGutterX : 0)
134
+ }
135
+ if (configuration.count > configuration.sizes.length) {
136
+ // If more columns than children
137
+ const occupiedSpace =
138
+ configuration.sizes.length * (colWidth + configuration.currentGutterX) -
139
+ configuration.currentGutterX
140
+ if (configuration.wedge === false) {
141
+ if (configuration.direction === 'ltr') {
142
+ startX = (configuration.width - occupiedSpace) / 2
143
+ } else {
144
+ startX = configuration.width - (configuration.width - occupiedSpace) / 2
145
+ }
146
+ } else if (configuration.direction === 'ltr') {
147
+ //
148
+ } else {
149
+ startX = configuration.width - configuration.currentGutterX
150
+ }
151
+ }
152
+
153
+ // Computing position of children
154
+ for (let index = 0; index < children.length; index += 1) {
155
+ const nextColumn = configuration.minify
156
+ ? getShortest(configuration)
157
+ : getNextColumn(index, configuration)
158
+
159
+ let childrenGutter = 0
160
+ if (configuration.surroundingGutter || nextColumn !== configuration.columns.length) {
161
+ childrenGutter = configuration.currentGutterX
162
+ }
163
+ let x: number
164
+ if (configuration.direction === 'ltr') {
165
+ x = startX + (colWidth + childrenGutter) * nextColumn
166
+ } else {
167
+ x = startX - (colWidth + childrenGutter) * nextColumn - colWidth
168
+ }
169
+ const y = configuration.columns[nextColumn]
170
+ const child = children[index] as HTMLElement
171
+ child.style.transform = `translate3d(${Math.round(x)}px,${Math.round(y)}px,0)`
172
+
173
+ configuration.columns[nextColumn] +=
174
+ configuration.sizes[index] +
175
+ (configuration.count > 1 ? configuration.gutterY : configuration.ultimateGutter) // margin-bottom
176
+ }
177
+
178
+ configuration.container.style.height = `${configuration.columns[getLongest(configuration)] - configuration.currentGutterY}px`
179
+ }
180
+
181
+ function resizeThrottler(configuration: Configuration) {
182
+ // ignore resize events as long as an actualResizeHandler execution is in the queue
183
+ if (!configuration.resizeTimeout) {
184
+ configuration.resizeTimeout = setTimeout(() => {
185
+ configuration.resizeTimeout = null
186
+ // IOS Safari throw random resize event on scroll, call layout only if size has changed
187
+ if (configuration.container.clientWidth !== configuration.width) {
188
+ layout(configuration)
189
+ }
190
+ // The actualResizeHandler will execute at a rate of 30fps
191
+ }, 33)
192
+ }
193
+ }
194
+
195
+ function init(configuration: Configuration) {
196
+ // TODO what does this do?
197
+ // for (const i in configuration) {
198
+ // if (configuration[i] !== undefined) {
199
+ // this.conf[i] = conf[i]
200
+ // }
201
+ // }
202
+
203
+ if (configuration.gutterX == null || configuration.gutterY == null) {
204
+ // eslint-disable-next-line no-multi-assign
205
+ configuration.gutterX = configuration.gutterY = configuration.gutter
206
+ }
207
+ configuration.currentGutterX = configuration.gutterX
208
+ configuration.currentGutterY = configuration.gutterY
209
+
210
+ const onResize = resizeThrottler.bind(this, configuration)
211
+ window.addEventListener('resize', onResize)
212
+ configuration.removeListener = function removeListener() {
213
+ window.removeEventListener('resize', onResize)
214
+ if (configuration.resizeTimeout != null) {
215
+ window.clearTimeout(configuration.resizeTimeout)
216
+ configuration.resizeTimeout = null
217
+ }
218
+ }
219
+
220
+ layout(configuration)
221
+ }
222
+
223
+ function destroy(configuration: Configuration) {
224
+ if (typeof configuration.removeListener === 'function') {
225
+ configuration.removeListener()
226
+ }
227
+
228
+ const { children } = configuration.container
229
+ for (let index = 0; index < children.length; index += 1) {
230
+ const child = children[index] as HTMLElement
231
+ child.style.removeProperty('width')
232
+ child.style.removeProperty('transform')
233
+ }
234
+ configuration.container.style.removeProperty('height')
235
+ configuration.container.style.removeProperty('min-width')
236
+ }
237
+
238
+ export function grid(element: HTMLElement | string) {
239
+ if (!element && process.env.NODE_ENV !== 'production') {
240
+ throw new Error('masua: "element" parameter is missing or undefined.')
241
+ }
242
+
243
+ const configuration: Configuration = {
244
+ sizes: [],
245
+ columns: [],
246
+ container: typeof element === 'string' ? document.querySelector(element) : element,
247
+ count: null,
248
+ width: 0,
249
+ removeListener: null,
250
+ currentGutterX: null,
251
+ currentGutterY: null,
252
+ resizeTimeout: null,
253
+ baseWidth: 255,
254
+ gutterX: null,
255
+ gutterY: null,
256
+ gutter: 10,
257
+ minify: true,
258
+ ultimateGutter: 5,
259
+ surroundingGutter: true,
260
+ direction: 'ltr',
261
+ wedge: false,
262
+ }
263
+
264
+ init(configuration)
265
+
266
+ return {
267
+ destroy: () => destroy(configuration),
268
+ }
269
+ }
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "masua",
3
+ "description": "Simple masonry layout in TypeScript.",
4
+ "version": "0.9.0",
5
+ "repository": "github:tobua/masua",
6
+ "homepage": "https://tobua.github.io/masua",
7
+ "license": "MIT",
8
+ "author": "Matthias Giger",
9
+ "contributors": [
10
+ "Spope (minimasonry)"
11
+ ],
12
+ "scripts": {
13
+ "build": "padua build",
14
+ "start": "padua watch"
15
+ },
16
+ "padua": {
17
+ "tsconfig": {
18
+ "include": [
19
+ "index.ts",
20
+ "react.tsx"
21
+ ]
22
+ }
23
+ },
24
+ "devDependencies": {
25
+ "@types/react": "^18.2.63",
26
+ "padua": "latest"
27
+ },
28
+ "peerDependencies": {
29
+ "react": ">= 18"
30
+ },
31
+ "peerDependenciesMeta": {
32
+ "react": {
33
+ "optional": true
34
+ }
35
+ },
36
+ "trustedDependencies": [
37
+ "padua"
38
+ ],
39
+ "type": "module",
40
+ "sideEffects": false,
41
+ "main": "./index.ts",
42
+ "exports": {
43
+ ".": {
44
+ "default": "./index.ts"
45
+ },
46
+ "./react": {
47
+ "default": "./react.tsx"
48
+ }
49
+ },
50
+ "types": "./dist/index.d.ts",
51
+ "files": [
52
+ "index.ts",
53
+ "react.tsx"
54
+ ],
55
+ "keywords": [
56
+ "masonry",
57
+ "staggered",
58
+ "grid",
59
+ "pinterest",
60
+ "layout",
61
+ "react",
62
+ "typescript"
63
+ ],
64
+ "prettier": "padua/configuration/.prettierrc.json",
65
+ "eslintConfig": {
66
+ "extends": "./node_modules/padua/configuration/eslint.cjs"
67
+ },
68
+ "publishConfig": {
69
+ "provenance": false
70
+ },
71
+ "engines": {
72
+ "node": ">= 18"
73
+ }
74
+ }
package/react.tsx ADDED
@@ -0,0 +1,12 @@
1
+ import { useRef, useEffect } from 'react'
2
+ import { grid } from 'masua'
3
+
4
+ export function Grid({ ...props }: JSX.IntrinsicElements['div']) {
5
+ const gridRef = useRef(null)
6
+
7
+ useEffect(() => {
8
+ return grid(gridRef.current).destroy
9
+ }, [])
10
+
11
+ return <div ref={gridRef} {...props} />
12
+ }