UNPKG

6.97 kBJavaScriptView Raw
1import { window } from "./platform"
2import { find, equalRecords } from "./utils"
3
4
5
6/* Axes System
7
8This allows us to at-will work in a different orientation
9without having to manually keep track of knowing if we should be using
10x or y positions. */
11
12const axes = {
13 row: {},
14 column: {},
15}
16
17axes.row.main = {
18 start: "x",
19 end: "x2",
20 size: "w",
21}
22axes.row.cross = {
23 start: "y",
24 end: "y2",
25 size: "h",
26}
27axes.column.main = axes.row.cross
28axes.column.cross = axes.row.main
29
30
31
32const types = [
33 { name: "side", values: [ "start", "end" ]},
34 { name: "standing", values: [ "above", "right", "below", "left" ]},
35 { name: "flow", values: [ "column", "row" ]},
36]
37
38const validTypeValues = (
39 types.reduce(
40 (xs, { values }) => (xs.concat(values)),
41 []
42 )
43)
44
45const centerOfSize = (flow, axis, size) => (
46 size[axes[flow][axis].size] / 2
47)
48
49const centerOfBounds = (flow, axis, bounds) => (
50 bounds[axes[flow][axis].start] + (bounds[axes[flow][axis].size] / 2)
51)
52
53const centerOfBoundsFromBounds = (flow, axis, boundsTo, boundsFrom) => (
54 centerOfBounds(flow, axis, boundsTo) - boundsFrom[axes[flow][axis].start]
55)
56
57const place = (flow, axis, align, bounds, size) => {
58 const axisProps = axes[flow][axis]
59 return (
60 align === "center"
61 ? centerOfBounds(flow, axis, bounds) - centerOfSize(flow, axis, size)
62 : align === "end"
63 ? bounds[axisProps.end]
64 : align === "start"
65 /* DOM rendering unfolds leftward. Therefore if the slave is positioned before
66 the master then the slave`s position must in addition be pulled back
67 by its [the slave`s] own length. */
68 ? bounds[axisProps.start] - size[axisProps.size]
69 : null
70 )
71}
72
73
74
75/* Element Layout Queries */
76
77const El = {}
78
79El.calcBounds = (el) => {
80
81 if (el === window) {
82 return {
83 x: 0,
84 y: 0,
85 x2: el.innerWidth,
86 y2: el.innerHeight,
87 w: el.innerWidth,
88 h: el.innerHeight,
89 }
90 }
91
92 const b = el.getBoundingClientRect()
93
94 return {
95 x: b.left,
96 y: b.top,
97 x2: b.right,
98 y2: b.bottom,
99 w: b.right - b.left,
100 h: b.bottom - b.top,
101 }
102}
103
104El.calcSize = (el) => (
105 el === window ?
106 { w: el.innerWidth, h: el.innerHeight } :
107 { w: el.offsetWidth, h: el.offsetHeight }
108)
109
110El.calcScrollSize = (el) => (
111 el === window ?
112 {
113 w: el.scrollX || el.pageXOffset,
114 h: el.scrollY || el.pageYOffset,
115 } :
116 { w: el.scrollLeft, h: el.scrollTop }
117)
118
119
120
121/* Misc Utilities */
122
123const getPreferenceType = (preference) => (
124 types.reduce((found, type) => (
125 found ?
126 found :
127 type.values.indexOf(preference) !== -1 ?
128 type.name :
129 null
130 ), null)
131)
132
133
134
135/* Dimension Fit Checks */
136
137const fitWithinChecker = (dimension) => (domainSize, itemSize) => (
138 domainSize[dimension] >= itemSize[dimension]
139)
140
141const doesWidthFitWithin = fitWithinChecker("w")
142const doesHeightFitWithin = fitWithinChecker("h")
143
144const doesFitWithin = (domainSize, itemSize) => (
145 doesWidthFitWithin(domainSize, itemSize)
146 && doesHeightFitWithin(domainSize, itemSize)
147)
148
149
150
151/* Errors */
152
153const createPreferenceError = (givenValue) => (
154 new Error(
155 `The given layout placement of "${givenValue}" is not a valid choice. Valid choices are: ${validTypeValues.join(" | ")}.`
156 )
157)
158
159
160
161
162
163/* Algorithm for picking the best fitting zone for popover. The current technique will loop through all zones picking the last one that fits.
164In the case that none fit we should pick the least-not-fitting zone. */
165
166const pickZone = (opts, frameBounds, targetBounds, size) => {
167 const t = targetBounds
168 const f = frameBounds
169 const zones = [
170 { side: "start", standing: "above", flow: "column", order: -1, w: f.x2, h: t.y },
171 { side: "end", standing: "right", flow: "row", order: 1, w: (f.x2 - t.x2), h: f.y2 },
172 { side: "end", standing: "below", flow: "column", order: 1, w: f.x2, h: (f.y2 - t.y2) },
173 { side: "start", standing: "left", flow: "row", order: -1, w: t.x, h: f.y2 },
174 ]
175
176 /* Order the zones by the amount of popup that would be cut out if that zone is used.
177 The first one in the array is the one that cuts the least amount.
178
179 const area = size.w * size.h // Popup area is constant and it does not change the order
180 */
181 zones.forEach((z) => {
182 // TODO Update to satisfy linter
183 // eslint-disable-next-line no-param-reassign
184 z.cutOff = /* area */ - Math.max(0, Math.min(z.w,size.w)) * Math.max(0, Math.min(z.h,size.h))
185 })
186 zones.sort((a,b) => a.cutOff - b.cutOff)
187
188 const availZones = zones.filter((zone) => (
189 doesFitWithin(zone, size)
190 ))
191
192 /* If a place is required pick it from the available zones if possible. */
193
194 if (opts.place) {
195 const type = getPreferenceType(opts.place)
196 if (!type) throw createPreferenceError(opts.place)
197 const finder = (z) => z[type] === opts.place
198 return find(finder, availZones) || find(finder, zones)
199 }
200
201 /* If the preferred side is part of the available zones, use that otherwise
202 pick the largest available zone. If there are no available zones, pick the
203 largest zone. */
204
205 if (opts.preferPlace) {
206 const preferenceType = getPreferenceType(opts.preferPlace)
207 if (!preferenceType) throw createPreferenceError(opts.preferPlace)
208
209 // Try to fit first in zone where the pop up fit completely
210 const preferredAvailZones = availZones.filter((zone) => (
211 zone[preferenceType] === opts.preferPlace
212 ))
213 if (preferredAvailZones.length) return preferredAvailZones[0]
214
215 // If there are not areas where the pop up fit completely, it uses the preferred ones
216 // in order from the one the fit better
217 const preferredZones = zones.filter((zone) => (
218 zone[preferenceType] === opts.preferPlace
219 ))
220 if (!availZones.length && preferredZones.length) return preferredZones[0]
221 }
222
223 // Return a zone that fit completely or the one that fit the best
224 return availZones.length ? availZones[0] : zones[0]
225}
226
227
228
229/* TODO Document this. */
230
231const calcRelPos = (zone, masterBounds, slaveSize) => {
232 const { main, cross } = axes[zone.flow]
233 /* TODO: The slave is hard-coded to align cross-center with master. */
234 const crossAlign = "center"
235 const mainStart = place(zone.flow, "main", zone.side, masterBounds, slaveSize)
236 const mainSize = slaveSize[main.size]
237 const crossStart = place(zone.flow, "cross", crossAlign, masterBounds, slaveSize)
238 const crossSize = slaveSize[cross.size]
239
240 return {
241 [main.start]: mainStart,
242 mainLength: mainSize,
243 [main.end]: mainStart + mainSize,
244 [cross.start]: crossStart,
245 crossLength: crossSize,
246 [cross.end]: crossStart + crossSize,
247 }
248}
249
250
251
252export default {
253 El,
254 types,
255 validTypeValues,
256 calcRelPos,
257 place,
258 pickZone,
259 axes,
260 centerOfSize,
261 centerOfBounds,
262 centerOfBoundsFromBounds,
263 doesFitWithin,
264 equalCoords: equalRecords,
265}
266export {
267 El,
268 types,
269 validTypeValues,
270 calcRelPos,
271 place,
272 pickZone,
273 axes,
274 centerOfSize,
275 centerOfBounds,
276 centerOfBoundsFromBounds,
277 doesFitWithin,
278 equalRecords as equalCoords,
279}