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.
- package/README.md +7 -0
- package/index.ts +269 -0
- package/package.json +74 -0
- package/react.tsx +12 -0
package/README.md
ADDED
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
|
+
}
|