masua 0.10.3 → 0.11.1
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.
- package/README.md +14 -1
- package/index.ts +105 -43
- package/package.json +12 -23
- package/react.tsx +15 -12
package/README.md
CHANGED
|
@@ -6,6 +6,19 @@
|
|
|
6
6
|
|
|
7
7
|
Simple masonry layout library in TypeScript. Initially forked from [minimasonry](https://github.com/Spope/MiniMasonry.js) by Spope.
|
|
8
8
|
|
|
9
|
+
- ❗ Check out the [interactive demo and documentation](https://tobua.github.io/masua)
|
|
10
|
+
- ⚛️ TypeScript and React support
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
⚠️ This plugin is published as TypeScript and JSX (for the React plugin) see [this post on 𝕏](https://twitter.com/matthiasgiger/status/1766443368567971946) for the reasoning. Make sure to add the necessary types listed below if they are missing in your project and align your [`tsconfig.json`](https://github.com/tobua/masua/blob/main/tsconfig.json) with the reference used for this project. React is only required when the `masua/react` export is used.
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
bun install masua
|
|
18
|
+
bun install @types/bun # or @types/node
|
|
19
|
+
bun install @types/react # for React export
|
|
20
|
+
```
|
|
21
|
+
|
|
9
22
|
## Usage
|
|
10
23
|
|
|
11
24
|
```ts
|
|
@@ -27,7 +40,7 @@ grid(document.querySelector('#my-custom-grid'), {
|
|
|
27
40
|
## React
|
|
28
41
|
|
|
29
42
|
```tsx
|
|
30
|
-
import { Grid } from 'masua'
|
|
43
|
+
import { Grid } from 'masua/react'
|
|
31
44
|
|
|
32
45
|
const MyGrid = () => (
|
|
33
46
|
<Grid disabled={window.innerWidth < 501}>
|
package/index.ts
CHANGED
|
@@ -2,18 +2,19 @@ interface State {
|
|
|
2
2
|
sizes: number[]
|
|
3
3
|
columns: number[]
|
|
4
4
|
container: HTMLElement
|
|
5
|
+
// Number of columns, only used for internal calculations.
|
|
5
6
|
count: number
|
|
6
7
|
width: number
|
|
7
|
-
removeListener
|
|
8
|
-
currentGutterX: number
|
|
9
|
-
currentGutterY: number
|
|
10
|
-
resizeTimeout
|
|
8
|
+
removeListener?: () => void
|
|
9
|
+
currentGutterX: number
|
|
10
|
+
currentGutterY: number
|
|
11
|
+
resizeTimeout?: ReturnType<typeof setTimeout>
|
|
11
12
|
baseWidth: number
|
|
12
|
-
gutterX: number
|
|
13
|
-
gutterY: number
|
|
14
|
-
gutter: number
|
|
13
|
+
gutterX: number
|
|
14
|
+
gutterY: number
|
|
15
|
+
gutter: number
|
|
15
16
|
minify: boolean
|
|
16
|
-
singleColumnGutter:
|
|
17
|
+
singleColumnGutter: number
|
|
17
18
|
surroundingGutter: boolean
|
|
18
19
|
direction: 'ltr' | 'rtl'
|
|
19
20
|
wedge: boolean
|
|
@@ -31,11 +32,18 @@ export interface Configuration {
|
|
|
31
32
|
wedge: boolean
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
interface NumberConfiguration extends Configuration {
|
|
36
|
+
gutter: number
|
|
37
|
+
gutterX: number
|
|
38
|
+
gutterY: number
|
|
39
|
+
singleColumnGutter: number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const log = (message: string) => console.log(`masua: ${message}.`)
|
|
43
|
+
|
|
34
44
|
function getCount(state: State) {
|
|
35
45
|
if (state.surroundingGutter) {
|
|
36
|
-
return Math.floor(
|
|
37
|
-
(state.width - state.currentGutterX) / (state.baseWidth + state.currentGutterX),
|
|
38
|
-
)
|
|
46
|
+
return Math.floor((state.width - state.currentGutterX) / (state.baseWidth + state.currentGutterX))
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
return Math.floor((state.width + state.currentGutterX) / (state.baseWidth + state.currentGutterX))
|
|
@@ -69,7 +77,7 @@ function getShortest(state: State) {
|
|
|
69
77
|
function reset(state: State) {
|
|
70
78
|
state.sizes = []
|
|
71
79
|
state.columns = []
|
|
72
|
-
state.count =
|
|
80
|
+
state.count = 0
|
|
73
81
|
state.width = state.container.clientWidth
|
|
74
82
|
const minWidth = state.baseWidth
|
|
75
83
|
if (state.width < minWidth) {
|
|
@@ -91,7 +99,7 @@ function reset(state: State) {
|
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
function computeWidth(state: State) {
|
|
94
|
-
let width
|
|
102
|
+
let width: number
|
|
95
103
|
if (state.surroundingGutter) {
|
|
96
104
|
width = (state.width - state.currentGutterX) / state.count - state.currentGutterX
|
|
97
105
|
} else {
|
|
@@ -102,6 +110,7 @@ function computeWidth(state: State) {
|
|
|
102
110
|
return width
|
|
103
111
|
}
|
|
104
112
|
|
|
113
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: TODO decrease complexity
|
|
105
114
|
function layout(state: State) {
|
|
106
115
|
if (!state.container) {
|
|
107
116
|
console.error('Container not found')
|
|
@@ -110,7 +119,7 @@ function layout(state: State) {
|
|
|
110
119
|
reset(state)
|
|
111
120
|
|
|
112
121
|
// Computing columns count
|
|
113
|
-
if (state.count
|
|
122
|
+
if (state.count === 0) {
|
|
114
123
|
state.count = getCount(state)
|
|
115
124
|
}
|
|
116
125
|
// Computing columns width
|
|
@@ -129,7 +138,7 @@ function layout(state: State) {
|
|
|
129
138
|
state.sizes[index] = child.clientHeight
|
|
130
139
|
}
|
|
131
140
|
|
|
132
|
-
let startX
|
|
141
|
+
let startX: number
|
|
133
142
|
if (state.direction === 'ltr') {
|
|
134
143
|
startX = state.surroundingGutter ? state.currentGutterX : 0
|
|
135
144
|
} else {
|
|
@@ -137,8 +146,7 @@ function layout(state: State) {
|
|
|
137
146
|
}
|
|
138
147
|
if (state.count > state.sizes.length) {
|
|
139
148
|
// If more columns than children
|
|
140
|
-
const occupiedSpace =
|
|
141
|
-
state.sizes.length * (colWidth + state.currentGutterX) - state.currentGutterX
|
|
149
|
+
const occupiedSpace = state.sizes.length * (colWidth + state.currentGutterX) - state.currentGutterX
|
|
142
150
|
if (state.wedge === false) {
|
|
143
151
|
if (state.direction === 'ltr') {
|
|
144
152
|
startX = (state.width - occupiedSpace) / 2
|
|
@@ -170,8 +178,7 @@ function layout(state: State) {
|
|
|
170
178
|
const child = children[index] as HTMLElement
|
|
171
179
|
child.style.transform = `translate3d(${Math.round(x)}px,${Math.round(y)}px,0)`
|
|
172
180
|
|
|
173
|
-
state.columns[nextColumn] +=
|
|
174
|
-
state.sizes[index] + (state.count > 1 ? state.gutterY : state.singleColumnGutter) // margin-bottom
|
|
181
|
+
state.columns[nextColumn] += state.sizes[index] + (state.count > 1 ? state.gutterY : state.singleColumnGutter) // margin-bottom
|
|
175
182
|
}
|
|
176
183
|
|
|
177
184
|
state.container.style.height = `${state.columns[getLongest(state)] - state.currentGutterY}px`
|
|
@@ -181,7 +188,7 @@ function resizeThrottler(state: State) {
|
|
|
181
188
|
// ignore resize events as long as an actualResizeHandler execution is in the queue
|
|
182
189
|
if (!state.resizeTimeout) {
|
|
183
190
|
state.resizeTimeout = setTimeout(() => {
|
|
184
|
-
state.resizeTimeout =
|
|
191
|
+
state.resizeTimeout = undefined
|
|
185
192
|
// IOS Safari throw random resize event on scroll, call layout only if size has changed
|
|
186
193
|
if (state.container.clientWidth !== state.width) {
|
|
187
194
|
layout(state)
|
|
@@ -202,13 +209,13 @@ function init(state: State) {
|
|
|
202
209
|
state.currentGutterX = state.gutterX
|
|
203
210
|
state.currentGutterY = state.gutterY
|
|
204
211
|
|
|
205
|
-
const onResize = resizeThrottler.bind(
|
|
212
|
+
const onResize = resizeThrottler.bind(null, state)
|
|
206
213
|
window.addEventListener('resize', onResize)
|
|
207
214
|
state.removeListener = function removeListener() {
|
|
208
215
|
window.removeEventListener('resize', onResize)
|
|
209
216
|
if (state.resizeTimeout != null) {
|
|
210
217
|
window.clearTimeout(state.resizeTimeout)
|
|
211
|
-
state.resizeTimeout =
|
|
218
|
+
state.resizeTimeout = undefined
|
|
212
219
|
}
|
|
213
220
|
}
|
|
214
221
|
|
|
@@ -221,56 +228,111 @@ function destroy(state: State) {
|
|
|
221
228
|
}
|
|
222
229
|
|
|
223
230
|
const { children } = state.container
|
|
224
|
-
for (
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
231
|
+
for (const child of Array.from(children)) {
|
|
232
|
+
const childElement = child as HTMLElement
|
|
233
|
+
childElement.style.removeProperty('width')
|
|
234
|
+
childElement.style.removeProperty('transform')
|
|
228
235
|
}
|
|
229
236
|
state.container.style.removeProperty('height')
|
|
230
237
|
state.container.style.removeProperty('min-width')
|
|
231
238
|
}
|
|
232
239
|
|
|
240
|
+
const sizeCache = new Map<string, number>()
|
|
241
|
+
|
|
242
|
+
function getSizeInPixels(size: string) {
|
|
243
|
+
const fromCache = sizeCache.get(size)
|
|
244
|
+
|
|
245
|
+
if (fromCache) {
|
|
246
|
+
return fromCache
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const tempElement = document.createElement('div')
|
|
250
|
+
tempElement.style.width = size
|
|
251
|
+
document.body.appendChild(tempElement)
|
|
252
|
+
const computedWidth = window.getComputedStyle(tempElement).width
|
|
253
|
+
document.body.removeChild(tempElement)
|
|
254
|
+
const pixels = Number.parseFloat(computedWidth)
|
|
255
|
+
sizeCache.set(size, pixels)
|
|
256
|
+
return pixels
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const sizeValues: (keyof Configuration)[] = ['gutter', 'gutterX', 'gutterY', 'singleColumnGutter']
|
|
260
|
+
|
|
261
|
+
function convertStringSizesToPixels(configuration: Partial<Configuration>): Partial<NumberConfiguration> {
|
|
262
|
+
for (const property of sizeValues) {
|
|
263
|
+
const value = configuration[property]
|
|
264
|
+
if (typeof value === 'string') {
|
|
265
|
+
configuration.gutter = getSizeInPixels(value)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return configuration as Partial<NumberConfiguration>
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function getContainer(element: HTMLElement | string) {
|
|
273
|
+
if (typeof element === 'string') {
|
|
274
|
+
const foundElementOnPage = document.querySelector(element) as HTMLElement
|
|
275
|
+
if (!foundElementOnPage) {
|
|
276
|
+
log(`element with selector ${element} not found on page`)
|
|
277
|
+
return false
|
|
278
|
+
}
|
|
279
|
+
return foundElementOnPage
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (element instanceof HTMLElement) {
|
|
283
|
+
return element
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
log('first argument invalid, must be HTMLElement or a string')
|
|
287
|
+
return false
|
|
288
|
+
}
|
|
289
|
+
|
|
233
290
|
export function grid(element: HTMLElement | string, configuration: Partial<Configuration> = {}) {
|
|
234
291
|
if (!element && process.env.NODE_ENV !== 'production') {
|
|
235
292
|
throw new Error('masua: "element" parameter is missing or undefined.')
|
|
236
293
|
}
|
|
237
294
|
|
|
295
|
+
const container = getContainer(element)
|
|
296
|
+
|
|
297
|
+
if (!container) {
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const numberConfiguration = convertStringSizesToPixels(configuration)
|
|
302
|
+
|
|
238
303
|
const state: State = {
|
|
239
304
|
sizes: [],
|
|
240
305
|
columns: [],
|
|
241
|
-
container
|
|
242
|
-
count:
|
|
306
|
+
container,
|
|
307
|
+
count: 0,
|
|
243
308
|
width: 0,
|
|
244
|
-
removeListener: null,
|
|
245
|
-
currentGutterX: null,
|
|
246
|
-
currentGutterY: null,
|
|
247
|
-
resizeTimeout: null,
|
|
248
309
|
baseWidth: 255,
|
|
249
310
|
gutter: 10,
|
|
250
311
|
minify: true,
|
|
251
312
|
surroundingGutter: false,
|
|
252
313
|
direction: 'ltr',
|
|
253
314
|
wedge: false,
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
315
|
+
currentGutterX: 0,
|
|
316
|
+
currentGutterY: 0,
|
|
317
|
+
...numberConfiguration,
|
|
318
|
+
gutterX: numberConfiguration.gutterX || numberConfiguration.gutter || 10,
|
|
319
|
+
gutterY: numberConfiguration.gutterY || numberConfiguration.gutter || 10,
|
|
257
320
|
// One column is theoretically an Y-gutter so that's preferred if available.
|
|
258
|
-
singleColumnGutter:
|
|
259
|
-
configuration.singleColumnGutter || configuration.gutterY || configuration.gutter || 10,
|
|
321
|
+
singleColumnGutter: numberConfiguration.singleColumnGutter || numberConfiguration.gutterY || numberConfiguration.gutter || 10,
|
|
260
322
|
}
|
|
261
323
|
|
|
262
|
-
// TODO use calc if gutter is string.
|
|
263
|
-
|
|
264
324
|
init(state)
|
|
265
325
|
|
|
266
326
|
return {
|
|
267
327
|
destroy: () => destroy(state),
|
|
268
328
|
update: (changes: Partial<Configuration> = {}) => {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
state
|
|
329
|
+
// TODO animate box and container location/height changes.
|
|
330
|
+
const numberChanges = convertStringSizesToPixels(changes)
|
|
331
|
+
Object.assign(state, numberChanges)
|
|
332
|
+
state.gutterX = numberChanges.gutterX || numberChanges.gutter || state.gutterX
|
|
333
|
+
state.gutterY = numberChanges.gutterY || numberChanges.gutter || state.gutterY
|
|
272
334
|
state.singleColumnGutter =
|
|
273
|
-
|
|
335
|
+
numberChanges.singleColumnGutter || numberChanges.gutterY || numberChanges.gutter || state.singleColumnGutter
|
|
274
336
|
layout(state)
|
|
275
337
|
},
|
|
276
338
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "masua",
|
|
3
3
|
"description": "Simple masonry layout library in TypeScript.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.11.1",
|
|
5
5
|
"repository": "github:tobua/masua",
|
|
6
6
|
"homepage": "https://tobua.github.io/masua",
|
|
7
7
|
"license": "MIT",
|
|
@@ -10,32 +10,25 @@
|
|
|
10
10
|
"Spope (minimasonry)"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
"padua": {
|
|
17
|
-
"tsconfig": {
|
|
18
|
-
"include": [
|
|
19
|
-
"index.ts",
|
|
20
|
-
"react.tsx"
|
|
21
|
-
]
|
|
22
|
-
}
|
|
13
|
+
"format": "bun biome format . --write",
|
|
14
|
+
"lint": "bun biome lint .",
|
|
15
|
+
"types": "tsc --noEmit"
|
|
23
16
|
},
|
|
24
17
|
"devDependencies": {
|
|
25
|
-
"@
|
|
26
|
-
"
|
|
18
|
+
"@biomejs/biome": "^1.6.4",
|
|
19
|
+
"@types/bun": "^1.0.12",
|
|
20
|
+
"@types/react": "^18.2.76",
|
|
21
|
+
"typescript": "^5.4.5"
|
|
27
22
|
},
|
|
28
23
|
"peerDependencies": {
|
|
29
|
-
"react": ">= 18"
|
|
24
|
+
"react": ">= 18",
|
|
25
|
+
"typescript": ">= 5"
|
|
30
26
|
},
|
|
31
27
|
"peerDependenciesMeta": {
|
|
32
28
|
"react": {
|
|
33
29
|
"optional": true
|
|
34
30
|
}
|
|
35
31
|
},
|
|
36
|
-
"trustedDependencies": [
|
|
37
|
-
"padua"
|
|
38
|
-
],
|
|
39
32
|
"type": "module",
|
|
40
33
|
"sideEffects": false,
|
|
41
34
|
"main": "./index.ts",
|
|
@@ -48,8 +41,8 @@
|
|
|
48
41
|
}
|
|
49
42
|
},
|
|
50
43
|
"files": [
|
|
51
|
-
"
|
|
52
|
-
"
|
|
44
|
+
"*.ts",
|
|
45
|
+
"*.tsx"
|
|
53
46
|
],
|
|
54
47
|
"keywords": [
|
|
55
48
|
"masonry",
|
|
@@ -60,10 +53,6 @@
|
|
|
60
53
|
"react",
|
|
61
54
|
"typescript"
|
|
62
55
|
],
|
|
63
|
-
"prettier": "padua/configuration/.prettierrc.json",
|
|
64
|
-
"eslintConfig": {
|
|
65
|
-
"extends": "./node_modules/padua/configuration/eslint.cjs"
|
|
66
|
-
},
|
|
67
56
|
"publishConfig": {
|
|
68
57
|
"provenance": true
|
|
69
58
|
},
|
package/react.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { type Configuration, grid } from 'masua'
|
|
2
|
+
import { type JSX, useEffect, useMemo, useRef } from 'react'
|
|
3
3
|
|
|
4
4
|
interface ReactConfiguration extends Configuration {
|
|
5
5
|
disabled: boolean
|
|
@@ -17,18 +17,15 @@ const configurationProperties = [
|
|
|
17
17
|
'wedge',
|
|
18
18
|
]
|
|
19
19
|
|
|
20
|
-
export function Grid({
|
|
21
|
-
disabled = false,
|
|
22
|
-
children,
|
|
23
|
-
...props
|
|
24
|
-
}: JSX.IntrinsicElements['div'] & Partial<ReactConfiguration>) {
|
|
20
|
+
export function Grid({ disabled = false, children, ...props }: JSX.IntrinsicElements['div'] & Partial<ReactConfiguration>) {
|
|
25
21
|
const gridRef = useRef(null)
|
|
26
|
-
const instance = useRef<ReturnType<typeof grid
|
|
22
|
+
const instance = useRef<ReturnType<typeof grid> | null>(null)
|
|
27
23
|
const configurationProps = useMemo(
|
|
28
24
|
() =>
|
|
29
|
-
Object.entries(props).reduce((result, [key, value]) => {
|
|
25
|
+
Object.entries(props).reduce((result: { [key: string]: string }, [key, value]) => {
|
|
30
26
|
if (configurationProperties.includes(key)) {
|
|
31
27
|
result[key] = value
|
|
28
|
+
// @ts-ignore
|
|
32
29
|
delete props[key]
|
|
33
30
|
}
|
|
34
31
|
return result
|
|
@@ -37,14 +34,20 @@ export function Grid({
|
|
|
37
34
|
)
|
|
38
35
|
|
|
39
36
|
useEffect(() => {
|
|
40
|
-
if (disabled)
|
|
37
|
+
if (disabled) {
|
|
38
|
+
return
|
|
39
|
+
}
|
|
41
40
|
if (instance.current) {
|
|
42
41
|
instance.current.update()
|
|
43
42
|
return instance.current.destroy
|
|
44
43
|
}
|
|
44
|
+
if (!gridRef.current) {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
45
48
|
instance.current = grid(gridRef.current, configurationProps)
|
|
46
|
-
return instance.current
|
|
47
|
-
}, [
|
|
49
|
+
return instance.current?.destroy
|
|
50
|
+
}, [configurationProps, disabled])
|
|
48
51
|
|
|
49
52
|
return (
|
|
50
53
|
<div ref={gridRef} {...props}>
|