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.
Files changed (4) hide show
  1. package/README.md +14 -1
  2. package/index.ts +105 -43
  3. package/package.json +12 -23
  4. 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: () => void
8
- currentGutterX: number | string
9
- currentGutterY: number | string
10
- resizeTimeout: NodeJS.Timeout
8
+ removeListener?: () => void
9
+ currentGutterX: number
10
+ currentGutterY: number
11
+ resizeTimeout?: ReturnType<typeof setTimeout>
11
12
  baseWidth: number
12
- gutterX: number | string
13
- gutterY: number | string
14
- gutter: number | string
13
+ gutterX: number
14
+ gutterY: number
15
+ gutter: number
15
16
  minify: boolean
16
- singleColumnGutter: string | number
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 = null
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 == null) {
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 = null
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(this, state)
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 = null
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 (let index = 0; index < children.length; index += 1) {
225
- const child = children[index] as HTMLElement
226
- child.style.removeProperty('width')
227
- child.style.removeProperty('transform')
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: typeof element === 'string' ? document.querySelector(element) : element,
242
- count: null,
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
- ...configuration,
255
- gutterX: configuration.gutterX || configuration.gutter || 10,
256
- gutterY: configuration.gutterY || configuration.gutter || 10,
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
- Object.assign(state, changes)
270
- state.gutterX = changes.gutterX || changes.gutter || state.gutterX
271
- state.gutterY = changes.gutterY || changes.gutter || state.gutterY
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
- changes.singleColumnGutter || changes.gutterY || changes.gutter || state.singleColumnGutter
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.10.3",
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
- "build": "padua build",
14
- "start": "padua watch"
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
- "@types/react": "^18.2.67",
26
- "padua": "^4.0.0"
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
- "index.ts",
52
- "react.tsx"
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 { useRef, useEffect, useMemo } from 'react'
2
- import { grid, type Configuration } from 'masua'
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>>(null)
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) return () => {}
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.destroy
47
- }, [children])
49
+ return instance.current?.destroy
50
+ }, [configurationProps, disabled])
48
51
 
49
52
  return (
50
53
  <div ref={gridRef} {...props}>