masua 0.9.0 → 0.10.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 +36 -1
- package/index.ts +109 -111
- package/package.json +5 -6
- package/react.tsx +42 -6
package/README.md
CHANGED
|
@@ -4,4 +4,39 @@
|
|
|
4
4
|
|
|
5
5
|
# masua
|
|
6
6
|
|
|
7
|
-
Simple masonry layout in TypeScript.
|
|
7
|
+
Simple masonry layout library in TypeScript. Initially forked from [minimasonry](https://github.com/Spope/MiniMasonry.js) by Spope.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { grid } from 'masua'
|
|
13
|
+
|
|
14
|
+
grid(document.querySelector('#my-grid'))
|
|
15
|
+
// With configuration options.
|
|
16
|
+
grid(document.querySelector('#my-custom-grid'), {
|
|
17
|
+
gutter: 20,
|
|
18
|
+
baseWidth: 300,
|
|
19
|
+
minify: false,
|
|
20
|
+
surroundingGutter: true,
|
|
21
|
+
ultimateGutter: 10,
|
|
22
|
+
direction: 'rtl',
|
|
23
|
+
wedge: true,
|
|
24
|
+
})
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## React
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { Grid } from 'masua'
|
|
31
|
+
|
|
32
|
+
const MyGrid = () => (
|
|
33
|
+
<Grid disabled={window.innerWidth < 501}>
|
|
34
|
+
<Box />
|
|
35
|
+
<Box size={3} />
|
|
36
|
+
<Box size={2} />
|
|
37
|
+
<Box />
|
|
38
|
+
<Box size={6} />
|
|
39
|
+
<Box size={4} />
|
|
40
|
+
</Grid>
|
|
41
|
+
)
|
|
42
|
+
```
|
package/index.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
interface
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
(
|
|
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(
|
|
44
|
+
function getLongest(state: State) {
|
|
37
45
|
let longest = 0
|
|
38
|
-
for (let index = 0; index <
|
|
39
|
-
if (
|
|
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,
|
|
47
|
-
return index %
|
|
54
|
+
function getNextColumn(index: number, state: State) {
|
|
55
|
+
return index % state.columns.length
|
|
48
56
|
}
|
|
49
57
|
|
|
50
|
-
function getShortest(
|
|
58
|
+
function getShortest(state: State) {
|
|
51
59
|
let shortest = 0
|
|
52
|
-
for (let index = 0; index <
|
|
53
|
-
if (
|
|
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(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const minWidth =
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
|
|
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(
|
|
80
|
+
if (getCount(state) === 1) {
|
|
73
81
|
// Set ultimate gutter when only one column is displayed
|
|
74
|
-
|
|
82
|
+
state.currentGutterX = state.ultimateGutter
|
|
75
83
|
// As gutters are reduced, two column may fit, forcing to 1
|
|
76
|
-
|
|
77
|
-
} else if (
|
|
84
|
+
state.count = 1
|
|
85
|
+
} else if (state.width < state.baseWidth + 2 * state.currentGutterX) {
|
|
78
86
|
// Remove gutter when screen is to low
|
|
79
|
-
|
|
87
|
+
state.currentGutterX = 0
|
|
80
88
|
} else {
|
|
81
|
-
|
|
89
|
+
state.currentGutterX = state.gutterX
|
|
82
90
|
}
|
|
83
91
|
}
|
|
84
92
|
|
|
85
|
-
function computeWidth(
|
|
93
|
+
function computeWidth(state: State) {
|
|
86
94
|
let width
|
|
87
|
-
if (
|
|
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(
|
|
102
|
-
if (!
|
|
105
|
+
function layout(state: State) {
|
|
106
|
+
if (!state.container) {
|
|
103
107
|
console.error('Container not found')
|
|
104
108
|
return
|
|
105
109
|
}
|
|
106
|
-
reset(
|
|
110
|
+
reset(state)
|
|
107
111
|
|
|
108
112
|
// Computing columns count
|
|
109
|
-
if (
|
|
110
|
-
|
|
113
|
+
if (state.count == null) {
|
|
114
|
+
state.count = getCount(state)
|
|
111
115
|
}
|
|
112
116
|
// Computing columns width
|
|
113
|
-
const colWidth = computeWidth(
|
|
117
|
+
const colWidth = computeWidth(state)
|
|
114
118
|
|
|
115
|
-
for (let index = 0; index <
|
|
116
|
-
|
|
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 } =
|
|
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
|
-
|
|
129
|
+
state.sizes[index] = child.clientHeight
|
|
126
130
|
}
|
|
127
131
|
|
|
128
132
|
let startX
|
|
129
|
-
if (
|
|
130
|
-
startX =
|
|
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 (
|
|
138
|
+
if (state.count > state.sizes.length) {
|
|
136
139
|
// If more columns than children
|
|
137
140
|
const occupiedSpace =
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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 =
|
|
146
|
+
startX = state.width - (state.width - occupiedSpace) / 2
|
|
145
147
|
}
|
|
146
|
-
} else if (
|
|
148
|
+
} else if (state.direction === 'ltr') {
|
|
147
149
|
//
|
|
148
150
|
} else {
|
|
149
|
-
startX =
|
|
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 =
|
|
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 (
|
|
161
|
-
childrenGutter =
|
|
160
|
+
if (state.surroundingGutter || nextColumn !== state.columns.length) {
|
|
161
|
+
childrenGutter = state.currentGutterX
|
|
162
162
|
}
|
|
163
163
|
let x: number
|
|
164
|
-
if (
|
|
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 =
|
|
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
|
-
|
|
174
|
-
|
|
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
|
-
|
|
177
|
+
state.container.style.height = `${state.columns[getLongest(state)] - state.currentGutterY}px`
|
|
179
178
|
}
|
|
180
179
|
|
|
181
|
-
function resizeThrottler(
|
|
180
|
+
function resizeThrottler(state: State) {
|
|
182
181
|
// ignore resize events as long as an actualResizeHandler execution is in the queue
|
|
183
|
-
if (!
|
|
184
|
-
|
|
185
|
-
|
|
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 (
|
|
188
|
-
layout(
|
|
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(
|
|
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
|
-
|
|
204
|
-
|
|
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,
|
|
205
|
+
const onResize = resizeThrottler.bind(this, state)
|
|
211
206
|
window.addEventListener('resize', onResize)
|
|
212
|
-
|
|
207
|
+
state.removeListener = function removeListener() {
|
|
213
208
|
window.removeEventListener('resize', onResize)
|
|
214
|
-
if (
|
|
215
|
-
window.clearTimeout(
|
|
216
|
-
|
|
209
|
+
if (state.resizeTimeout != null) {
|
|
210
|
+
window.clearTimeout(state.resizeTimeout)
|
|
211
|
+
state.resizeTimeout = null
|
|
217
212
|
}
|
|
218
213
|
}
|
|
219
214
|
|
|
220
|
-
layout(
|
|
215
|
+
layout(state)
|
|
221
216
|
}
|
|
222
217
|
|
|
223
|
-
function destroy(
|
|
224
|
-
if (typeof
|
|
225
|
-
|
|
218
|
+
function destroy(state: State) {
|
|
219
|
+
if (typeof state.removeListener === 'function') {
|
|
220
|
+
state.removeListener()
|
|
226
221
|
}
|
|
227
222
|
|
|
228
|
-
const { children } =
|
|
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
|
-
|
|
235
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
260
|
+
// TODO use calc if gutter is string.
|
|
261
|
+
|
|
262
|
+
init(state)
|
|
265
263
|
|
|
266
264
|
return {
|
|
267
|
-
destroy: () => destroy(
|
|
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.
|
|
3
|
+
"description": "Simple masonry layout library in TypeScript.",
|
|
4
|
+
"version": "0.10.1",
|
|
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.
|
|
26
|
-
"padua": "
|
|
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":
|
|
68
|
+
"provenance": true
|
|
70
69
|
},
|
|
71
70
|
"engines": {
|
|
72
71
|
"node": ">= 18"
|
package/react.tsx
CHANGED
|
@@ -1,12 +1,48 @@
|
|
|
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
|
-
|
|
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
|
+
children,
|
|
23
|
+
...props
|
|
24
|
+
}: JSX.IntrinsicElements['div'] & Partial<ReactConfiguration>) {
|
|
5
25
|
const gridRef = useRef(null)
|
|
26
|
+
const configurationProps = useMemo(
|
|
27
|
+
() =>
|
|
28
|
+
Object.entries(props).reduce((result, [key, value]) => {
|
|
29
|
+
if (configurationProperties.includes(key)) {
|
|
30
|
+
result[key] = value
|
|
31
|
+
delete props[key]
|
|
32
|
+
}
|
|
33
|
+
return result
|
|
34
|
+
}, {}),
|
|
35
|
+
[props],
|
|
36
|
+
)
|
|
6
37
|
|
|
7
38
|
useEffect(() => {
|
|
8
|
-
return
|
|
9
|
-
|
|
39
|
+
if (disabled) return () => {}
|
|
40
|
+
return grid(gridRef.current, configurationProps).destroy
|
|
41
|
+
}, [children])
|
|
10
42
|
|
|
11
|
-
return
|
|
43
|
+
return (
|
|
44
|
+
<div ref={gridRef} {...props}>
|
|
45
|
+
{children}
|
|
46
|
+
</div>
|
|
47
|
+
)
|
|
12
48
|
}
|