masua 0.9.0 → 0.10.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 +26 -1
  2. package/index.ts +109 -111
  3. package/package.json +5 -6
  4. package/react.tsx +35 -4
package/README.md CHANGED
@@ -4,4 +4,29 @@
4
4
 
5
5
  # masua
6
6
 
7
- Simple masonry layout in TypeScript.
7
+ Simple masonry layout library in TypeScript.
8
+
9
+ ## Usage
10
+
11
+ ```ts
12
+ import { grid } from 'masua'
13
+
14
+ grid(document.querySelector('#my-grid'))
15
+ ```
16
+
17
+ ## React
18
+
19
+ ```tsx
20
+ import { Grid } from 'masua'
21
+
22
+ const MyGrid = () => (
23
+ <Grid>
24
+ <Box />
25
+ <Box size={3} />
26
+ <Box size={2} />
27
+ <Box />
28
+ <Box size={6} />
29
+ <Box size={4} />
30
+ </Grid>
31
+ )
32
+ ```
package/index.ts CHANGED
@@ -1,17 +1,17 @@
1
- interface Configuration {
1
+ interface State {
2
2
  sizes: number[]
3
3
  columns: number[]
4
4
  container: HTMLElement
5
5
  count: number
6
6
  width: number
7
7
  removeListener: () => void
8
- currentGutterX: number
9
- currentGutterY: number
8
+ currentGutterX: number | string
9
+ currentGutterY: number | string
10
10
  resizeTimeout: NodeJS.Timeout
11
11
  baseWidth: number
12
- gutterX: number
13
- gutterY: number
14
- gutter: number
12
+ gutterX: number | string
13
+ gutterY: number | string
14
+ gutter: number | string
15
15
  minify: boolean
16
16
  ultimateGutter: number
17
17
  surroundingGutter: boolean
@@ -19,38 +19,46 @@ interface Configuration {
19
19
  wedge: boolean
20
20
  }
21
21
 
22
- function getCount(configuration: Configuration) {
23
- if (configuration.surroundingGutter) {
22
+ export interface Configuration {
23
+ baseWidth: number
24
+ gutter: number | string
25
+ gutterX: number | string
26
+ gutterY: number | string
27
+ minify: boolean
28
+ surroundingGutter: boolean
29
+ ultimateGutter: number
30
+ direction: 'ltr' | 'rtl'
31
+ wedge: boolean
32
+ }
33
+
34
+ function getCount(state: State) {
35
+ if (state.surroundingGutter) {
24
36
  return Math.floor(
25
- (configuration.width - configuration.currentGutterX) /
26
- (configuration.baseWidth + configuration.currentGutterX),
37
+ (state.width - state.currentGutterX) / (state.baseWidth + state.currentGutterX),
27
38
  )
28
39
  }
29
40
 
30
- return Math.floor(
31
- (configuration.width + configuration.currentGutterX) /
32
- (configuration.baseWidth + configuration.currentGutterX),
33
- )
41
+ return Math.floor((state.width + state.currentGutterX) / (state.baseWidth + state.currentGutterX))
34
42
  }
35
43
 
36
- function getLongest(configuration: Configuration) {
44
+ function getLongest(state: State) {
37
45
  let longest = 0
38
- for (let index = 0; index < configuration.count; index += 1) {
39
- if (configuration.columns[index] > configuration.columns[longest]) {
46
+ for (let index = 0; index < state.count; index += 1) {
47
+ if (state.columns[index] > state.columns[longest]) {
40
48
  longest = index
41
49
  }
42
50
  }
43
51
  return longest
44
52
  }
45
53
 
46
- function getNextColumn(index: number, configuration: Configuration) {
47
- return index % configuration.columns.length
54
+ function getNextColumn(index: number, state: State) {
55
+ return index % state.columns.length
48
56
  }
49
57
 
50
- function getShortest(configuration: Configuration) {
58
+ function getShortest(state: State) {
51
59
  let shortest = 0
52
- for (let index = 0; index < configuration.count; index += 1) {
53
- if (configuration.columns[index] < configuration.columns[shortest]) {
60
+ for (let index = 0; index < state.count; index += 1) {
61
+ if (state.columns[index] < state.columns[shortest]) {
54
62
  shortest = index
55
63
  }
56
64
  }
@@ -58,141 +66,132 @@ function getShortest(configuration: Configuration) {
58
66
  return shortest
59
67
  }
60
68
 
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`
69
+ function reset(state: State) {
70
+ state.sizes = []
71
+ state.columns = []
72
+ state.count = null
73
+ state.width = state.container.clientWidth
74
+ const minWidth = state.baseWidth
75
+ if (state.width < minWidth) {
76
+ state.width = minWidth
77
+ state.container.style.minWidth = `${minWidth}px`
70
78
  }
71
79
 
72
- if (getCount(configuration) === 1) {
80
+ if (getCount(state) === 1) {
73
81
  // Set ultimate gutter when only one column is displayed
74
- configuration.currentGutterX = configuration.ultimateGutter
82
+ state.currentGutterX = state.ultimateGutter
75
83
  // 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) {
84
+ state.count = 1
85
+ } else if (state.width < state.baseWidth + 2 * state.currentGutterX) {
78
86
  // Remove gutter when screen is to low
79
- configuration.currentGutterX = 0
87
+ state.currentGutterX = 0
80
88
  } else {
81
- configuration.currentGutterX = configuration.gutterX
89
+ state.currentGutterX = state.gutterX
82
90
  }
83
91
  }
84
92
 
85
- function computeWidth(configuration: Configuration) {
93
+ function computeWidth(state: State) {
86
94
  let width
87
- if (configuration.surroundingGutter) {
88
- width =
89
- (configuration.width - configuration.currentGutterX) / configuration.count -
90
- configuration.currentGutterX
95
+ if (state.surroundingGutter) {
96
+ width = (state.width - state.currentGutterX) / state.count - state.currentGutterX
91
97
  } else {
92
- width =
93
- (configuration.width + configuration.currentGutterX) / configuration.count -
94
- configuration.currentGutterX
98
+ width = (state.width + state.currentGutterX) / state.count - state.currentGutterX
95
99
  }
96
100
  width = Number.parseFloat(width.toFixed(2))
97
101
 
98
102
  return width
99
103
  }
100
104
 
101
- function layout(configuration: Configuration) {
102
- if (!configuration.container) {
105
+ function layout(state: State) {
106
+ if (!state.container) {
103
107
  console.error('Container not found')
104
108
  return
105
109
  }
106
- reset(configuration)
110
+ reset(state)
107
111
 
108
112
  // Computing columns count
109
- if (configuration.count == null) {
110
- configuration.count = getCount(configuration)
113
+ if (state.count == null) {
114
+ state.count = getCount(state)
111
115
  }
112
116
  // Computing columns width
113
- const colWidth = computeWidth(configuration)
117
+ const colWidth = computeWidth(state)
114
118
 
115
- for (let index = 0; index < configuration.count; index += 1) {
116
- configuration.columns[index] = 0
119
+ for (let index = 0; index < state.count; index += 1) {
120
+ state.columns[index] = 0
117
121
  }
118
122
 
119
123
  // Saving children real heights
120
- const { children } = configuration.container
124
+ const { children } = state.container
121
125
  for (let index = 0; index < children.length; index += 1) {
122
126
  // Set colWidth before retrieving element height if content is proportional
123
127
  const child = children[index] as HTMLElement
124
128
  child.style.width = `${colWidth}px`
125
- configuration.sizes[index] = child.clientHeight
129
+ state.sizes[index] = child.clientHeight
126
130
  }
127
131
 
128
132
  let startX
129
- if (configuration.direction === 'ltr') {
130
- startX = configuration.surroundingGutter ? configuration.currentGutterX : 0
133
+ if (state.direction === 'ltr') {
134
+ startX = state.surroundingGutter ? state.currentGutterX : 0
131
135
  } else {
132
- startX =
133
- configuration.width - (configuration.surroundingGutter ? configuration.currentGutterX : 0)
136
+ startX = state.width - (state.surroundingGutter ? state.currentGutterX : 0)
134
137
  }
135
- if (configuration.count > configuration.sizes.length) {
138
+ if (state.count > state.sizes.length) {
136
139
  // If more columns than children
137
140
  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
141
+ state.sizes.length * (colWidth + state.currentGutterX) - state.currentGutterX
142
+ if (state.wedge === false) {
143
+ if (state.direction === 'ltr') {
144
+ startX = (state.width - occupiedSpace) / 2
143
145
  } else {
144
- startX = configuration.width - (configuration.width - occupiedSpace) / 2
146
+ startX = state.width - (state.width - occupiedSpace) / 2
145
147
  }
146
- } else if (configuration.direction === 'ltr') {
148
+ } else if (state.direction === 'ltr') {
147
149
  //
148
150
  } else {
149
- startX = configuration.width - configuration.currentGutterX
151
+ startX = state.width - state.currentGutterX
150
152
  }
151
153
  }
152
154
 
153
155
  // Computing position of children
154
156
  for (let index = 0; index < children.length; index += 1) {
155
- const nextColumn = configuration.minify
156
- ? getShortest(configuration)
157
- : getNextColumn(index, configuration)
157
+ const nextColumn = state.minify ? getShortest(state) : getNextColumn(index, state)
158
158
 
159
159
  let childrenGutter = 0
160
- if (configuration.surroundingGutter || nextColumn !== configuration.columns.length) {
161
- childrenGutter = configuration.currentGutterX
160
+ if (state.surroundingGutter || nextColumn !== state.columns.length) {
161
+ childrenGutter = state.currentGutterX
162
162
  }
163
163
  let x: number
164
- if (configuration.direction === 'ltr') {
164
+ if (state.direction === 'ltr') {
165
165
  x = startX + (colWidth + childrenGutter) * nextColumn
166
166
  } else {
167
167
  x = startX - (colWidth + childrenGutter) * nextColumn - colWidth
168
168
  }
169
- const y = configuration.columns[nextColumn]
169
+ const y = state.columns[nextColumn]
170
170
  const child = children[index] as HTMLElement
171
171
  child.style.transform = `translate3d(${Math.round(x)}px,${Math.round(y)}px,0)`
172
172
 
173
- configuration.columns[nextColumn] +=
174
- configuration.sizes[index] +
175
- (configuration.count > 1 ? configuration.gutterY : configuration.ultimateGutter) // margin-bottom
173
+ state.columns[nextColumn] +=
174
+ state.sizes[index] + (state.count > 1 ? state.gutterY : state.ultimateGutter) // margin-bottom
176
175
  }
177
176
 
178
- configuration.container.style.height = `${configuration.columns[getLongest(configuration)] - configuration.currentGutterY}px`
177
+ state.container.style.height = `${state.columns[getLongest(state)] - state.currentGutterY}px`
179
178
  }
180
179
 
181
- function resizeThrottler(configuration: Configuration) {
180
+ function resizeThrottler(state: State) {
182
181
  // 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
182
+ if (!state.resizeTimeout) {
183
+ state.resizeTimeout = setTimeout(() => {
184
+ state.resizeTimeout = null
186
185
  // 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)
186
+ if (state.container.clientWidth !== state.width) {
187
+ layout(state)
189
188
  }
190
189
  // The actualResizeHandler will execute at a rate of 30fps
191
190
  }, 33)
192
191
  }
193
192
  }
194
193
 
195
- function init(configuration: Configuration) {
194
+ function init(state: State) {
196
195
  // TODO what does this do?
197
196
  // for (const i in configuration) {
198
197
  // if (configuration[i] !== undefined) {
@@ -200,47 +199,43 @@ function init(configuration: Configuration) {
200
199
  // }
201
200
  // }
202
201
 
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
202
+ state.currentGutterX = state.gutterX
203
+ state.currentGutterY = state.gutterY
209
204
 
210
- const onResize = resizeThrottler.bind(this, configuration)
205
+ const onResize = resizeThrottler.bind(this, state)
211
206
  window.addEventListener('resize', onResize)
212
- configuration.removeListener = function removeListener() {
207
+ state.removeListener = function removeListener() {
213
208
  window.removeEventListener('resize', onResize)
214
- if (configuration.resizeTimeout != null) {
215
- window.clearTimeout(configuration.resizeTimeout)
216
- configuration.resizeTimeout = null
209
+ if (state.resizeTimeout != null) {
210
+ window.clearTimeout(state.resizeTimeout)
211
+ state.resizeTimeout = null
217
212
  }
218
213
  }
219
214
 
220
- layout(configuration)
215
+ layout(state)
221
216
  }
222
217
 
223
- function destroy(configuration: Configuration) {
224
- if (typeof configuration.removeListener === 'function') {
225
- configuration.removeListener()
218
+ function destroy(state: State) {
219
+ if (typeof state.removeListener === 'function') {
220
+ state.removeListener()
226
221
  }
227
222
 
228
- const { children } = configuration.container
223
+ const { children } = state.container
229
224
  for (let index = 0; index < children.length; index += 1) {
230
225
  const child = children[index] as HTMLElement
231
226
  child.style.removeProperty('width')
232
227
  child.style.removeProperty('transform')
233
228
  }
234
- configuration.container.style.removeProperty('height')
235
- configuration.container.style.removeProperty('min-width')
229
+ state.container.style.removeProperty('height')
230
+ state.container.style.removeProperty('min-width')
236
231
  }
237
232
 
238
- export function grid(element: HTMLElement | string) {
233
+ export function grid(element: HTMLElement | string, configuration: Partial<Configuration> = {}) {
239
234
  if (!element && process.env.NODE_ENV !== 'production') {
240
235
  throw new Error('masua: "element" parameter is missing or undefined.')
241
236
  }
242
237
 
243
- const configuration: Configuration = {
238
+ const state: State = {
244
239
  sizes: [],
245
240
  columns: [],
246
241
  container: typeof element === 'string' ? document.querySelector(element) : element,
@@ -251,19 +246,22 @@ export function grid(element: HTMLElement | string) {
251
246
  currentGutterY: null,
252
247
  resizeTimeout: null,
253
248
  baseWidth: 255,
254
- gutterX: null,
255
- gutterY: null,
256
249
  gutter: 10,
257
250
  minify: true,
258
251
  ultimateGutter: 5,
259
- surroundingGutter: true,
252
+ surroundingGutter: false,
260
253
  direction: 'ltr',
261
254
  wedge: false,
255
+ ...configuration,
256
+ gutterX: configuration.gutterX || configuration.gutter || 10,
257
+ gutterY: configuration.gutterY || configuration.gutter || 10,
262
258
  }
263
259
 
264
- init(configuration)
260
+ // TODO use calc if gutter is string.
261
+
262
+ init(state)
265
263
 
266
264
  return {
267
- destroy: () => destroy(configuration),
265
+ destroy: () => destroy(state),
268
266
  }
269
267
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "masua",
3
- "description": "Simple masonry layout in TypeScript.",
4
- "version": "0.9.0",
3
+ "description": "Simple masonry layout library in TypeScript.",
4
+ "version": "0.10.0",
5
5
  "repository": "github:tobua/masua",
6
6
  "homepage": "https://tobua.github.io/masua",
7
7
  "license": "MIT",
@@ -22,8 +22,8 @@
22
22
  }
23
23
  },
24
24
  "devDependencies": {
25
- "@types/react": "^18.2.63",
26
- "padua": "latest"
25
+ "@types/react": "^18.2.64",
26
+ "padua": "^4.0.0"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "react": ">= 18"
@@ -47,7 +47,6 @@
47
47
  "default": "./react.tsx"
48
48
  }
49
49
  },
50
- "types": "./dist/index.d.ts",
51
50
  "files": [
52
51
  "index.ts",
53
52
  "react.tsx"
@@ -66,7 +65,7 @@
66
65
  "extends": "./node_modules/padua/configuration/eslint.cjs"
67
66
  },
68
67
  "publishConfig": {
69
- "provenance": false
68
+ "provenance": true
70
69
  },
71
70
  "engines": {
72
71
  "node": ">= 18"
package/react.tsx CHANGED
@@ -1,11 +1,42 @@
1
- import { useRef, useEffect } from 'react'
2
- import { grid } from 'masua'
1
+ import { useRef, useEffect, useMemo } from 'react'
2
+ import { grid, type Configuration } from 'masua'
3
3
 
4
- export function Grid({ ...props }: JSX.IntrinsicElements['div']) {
4
+ interface ReactConfiguration extends Configuration {
5
+ disabled: boolean
6
+ }
7
+
8
+ const configurationProperties = [
9
+ 'baseWidth',
10
+ 'gutter',
11
+ 'gutterX',
12
+ 'gutterY',
13
+ 'minify',
14
+ 'surroundingGutter',
15
+ 'ultimateGutter',
16
+ 'direction',
17
+ 'wedge',
18
+ ]
19
+
20
+ export function Grid({
21
+ disabled = false,
22
+ ...props
23
+ }: JSX.IntrinsicElements['div'] & Partial<ReactConfiguration>) {
5
24
  const gridRef = useRef(null)
25
+ const configurationProps = useMemo(
26
+ () =>
27
+ Object.entries(props).reduce((result, [key, value]) => {
28
+ if (configurationProperties.includes(key)) {
29
+ result[key] = value
30
+ delete props[key]
31
+ }
32
+ return result
33
+ }, {}),
34
+ [props],
35
+ )
6
36
 
7
37
  useEffect(() => {
8
- return grid(gridRef.current).destroy
38
+ if (disabled) return () => {}
39
+ return grid(gridRef.current, configurationProps).destroy
9
40
  }, [])
10
41
 
11
42
  return <div ref={gridRef} {...props} />