-
Notifications
You must be signed in to change notification settings - Fork 0
/
fitty_mod-1.0.js
336 lines (273 loc) · 10.4 KB
/
fitty_mod-1.0.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
export default ((w) => {
// no window, early exit
if (!w) return;
// node list to array helper method
const toArray = (nl) => [].slice.call(nl);
// states
const DrawState = {
IDLE: 0,
DIRTY_CONTENT: 1,
DIRTY_LAYOUT: 2,
DIRTY: 3
};
// all active fitty elements
let fitties = [];
// group all redraw calls till next frame, we cancel each frame request when a new one comes in. If no support for request animation frame, this is an empty function and supports for fitty stops.
let redrawFrame = null;
const requestRedraw =
"requestAnimationFrame" in w
? () => {
w.cancelAnimationFrame(redrawFrame);
redrawFrame = w.requestAnimationFrame(() =>
redraw(fitties.filter((f) => f.dirty && f.active))
);
}
: () => {};
// sets all fitties to dirty so they are redrawn on the next redraw loop, then calls redraw
const redrawAll = (type) => () => {
fitties.forEach((f) => (f.dirty = type));
requestRedraw();
};
// redraws fitties so they nicely fit their parent container
const redraw = (fitties) => {
// getting info from the DOM at this point should not trigger a reflow, let's gather as much intel as possible before triggering a reflow
// check if styles of all fitties have been computed
fitties
.filter((f) => !f.styleComputed)
.forEach((f) => {
f.styleComputed = computeStyle(f);
});
// restyle elements that require pre-styling, this triggers a reflow, please try to prevent by adding CSS rules (see docs)
fitties.filter(shouldPreStyle).forEach(applyStyle);
// we now determine which fitties should be redrawn
const fittiesToRedraw = fitties.filter(shouldRedraw);
// we calculate final styles for these fitties
fittiesToRedraw.forEach(calculateStyles);
// now we apply the calculated styles from our previous loop
fittiesToRedraw.forEach((f) => {
applyStyle(f);
markAsClean(f);
});
// now we dispatch events for all restyled fitties
fittiesToRedraw.forEach(dispatchFitEvent);
};
const markAsClean = (f) => (f.dirty = DrawState.IDLE);
const calculateStyles = (f) => {
// get available width from parent node
f.availableWidth = f.element.parentNode.clientWidth;
// get available height from parent node
f.availableHeight = f.element.parentNode.clientHeight;
// the width our target element uses
f.currentWidth = f.element.scrollWidth;
// the height our target element uses
f.currentHeight = f.element.scrollHeight;
// remember current font size
f.previousFontSize = f.currentFontSize;
// let's calculate the new font size taking into account the width and
// the height
f.currentFontSize = Math.min(
Math.min(
Math.max(
f.minSize,
(f.availableWidth / f.currentWidth) * f.previousFontSize
),
f.maxSize
),
Math.min(
Math.max(
f.minSize,
(f.availableHeight / f.currentHeight) * f.previousFontSize
),
f.maxSize
)
);
// if allows wrapping, only wrap when at minimum font size (otherwise would break container)
f.whiteSpace =
f.multiLine && f.currentFontSize === f.minSize ? "normal" : "nowrap";
};
// should always redraw if is not dirty layout, if is dirty layout, only redraw if size has changed
const shouldRedraw = (f) =>
f.dirty !== DrawState.DIRTY_LAYOUT ||
(f.dirty === DrawState.DIRTY_LAYOUT &&
(f.element.parentNode.clientWidth !== f.availableWidth ||
f.element.parentNode.clientHeight !== f.availableHeight));
// every fitty element is tested for invalid styles
const computeStyle = (f) => {
// get style properties
const style = w.getComputedStyle(f.element, null);
// get current font size in pixels (if we already calculated it, use the calculated version)
f.currentFontSize = parseFloat(style.getPropertyValue("font-size"));
// get display type and wrap mode
f.display = style.getPropertyValue("display");
f.whiteSpace = style.getPropertyValue("white-space");
};
// determines if this fitty requires initial styling, can be prevented by applying correct styles through CSS
const shouldPreStyle = (f) => {
let preStyle = false;
// if we already tested for prestyling we don't have to do it again
if (f.preStyleTestCompleted) return false;
// should have an inline style, if not, apply
if (!/inline-/.test(f.display)) {
preStyle = true;
f.display = "inline-block";
}
// to correctly calculate dimensions the element should have whiteSpace set to nowrap
if (f.whiteSpace !== "nowrap") {
preStyle = true;
f.whiteSpace = "nowrap";
}
// we don't have to do this twice
f.preStyleTestCompleted = true;
return preStyle;
};
// apply styles to single fitty
const applyStyle = (f) => {
f.element.style.whiteSpace = f.whiteSpace;
f.element.style.display = f.display;
f.element.style.fontSize = f.currentFontSize + "px";
};
// dispatch a fit event on a fitty
const dispatchFitEvent = (f) => {
f.element.dispatchEvent(
new CustomEvent("fit", {
detail: {
oldValue: f.previousFontSize,
newValue: f.currentFontSize,
scaleFactor: f.currentFontSize / f.previousFontSize
}
})
);
};
// fit method, marks the fitty as dirty and requests a redraw (this will also redraw any other fitty marked as dirty)
const fit = (f, type) => () => {
f.dirty = type;
if (!f.active) return;
requestRedraw();
};
const init = (f) => {
// save some of the original CSS properties before we change them
f.originalStyle = {
whiteSpace: f.element.style.whiteSpace,
display: f.element.style.display,
fontSize: f.element.style.fontSize
};
// should we observe DOM mutations
observeMutations(f);
// this is a new fitty so we need to validate if it's styles are in order
f.newbie = true;
// because it's a new fitty it should also be dirty, we want it to redraw on the first loop
f.dirty = true;
// we want to be able to update this fitty
fitties.push(f);
};
const destroy = (f) => () => {
// remove from fitties array
fitties = fitties.filter((_) => _.element !== f.element);
// stop observing DOM
if (f.observeMutations) f.observer.disconnect();
// reset the CSS properties we changes
f.element.style.whiteSpace = f.originalStyle.whiteSpace;
f.element.style.display = f.originalStyle.display;
f.element.style.fontSize = f.originalStyle.fontSize;
};
// add a new fitty, does not redraw said fitty
const subscribe = (f) => () => {
if (f.active) return;
f.active = true;
requestRedraw();
};
// remove an existing fitty
const unsubscribe = (f) => () => (f.active = false);
const observeMutations = (f) => {
// no observing?
if (!f.observeMutations) return;
// start observing mutations
f.observer = new MutationObserver(fit(f, DrawState.DIRTY_CONTENT));
// start observing
f.observer.observe(f.element, f.observeMutations);
};
// default mutation observer settings
const mutationObserverDefaultSetting = {
subtree: true,
childList: true,
characterData: true
};
// default fitty options
const defaultOptions = {
minSize: 16,
maxSize: 512,
multiLine: true,
observeMutations:
"MutationObserver" in w ? mutationObserverDefaultSetting : false
};
// array of elements in, fitty instances out
function fittyCreate(elements, options) {
// set options object
const fittyOptions = {
// expand default options
...defaultOptions,
// override with custom options
...options
};
// create fitties
const publicFitties = elements.map((element) => {
// create fitty instance
const f = {
// expand defaults
...fittyOptions,
// internal options for this fitty
element,
active: true
};
// initialise this fitty
init(f);
// expose API
return {
element,
fit: fit(f, DrawState.DIRTY),
unfreeze: subscribe(f),
freeze: unsubscribe(f),
unsubscribe: destroy(f)
};
});
// call redraw on newly initiated fitties
requestRedraw();
// expose fitties
return publicFitties;
}
// fitty creation function
function fitty(target, options = {}) {
// if target is a string
return typeof target === "string"
? // treat it as a querySelector
fittyCreate(toArray(document.querySelectorAll(target)), options)
: // create single fitty
fittyCreate([target], options)[0];
}
// handles viewport changes, redraws all fitties, but only does so after a timeout
let resizeDebounce = null;
const onWindowResized = () => {
w.clearTimeout(resizeDebounce);
resizeDebounce = w.setTimeout(
redrawAll(DrawState.DIRTY_LAYOUT),
fitty.observeWindowDelay
);
};
// define observe window property, so when we set it to true or false events are automatically added and removed
const events = ["resize", "orientationchange"];
Object.defineProperty(fitty, "observeWindow", {
set: (enabled) => {
const method = `${enabled ? "add" : "remove"}EventListener`;
events.forEach((e) => {
w[method](e, onWindowResized);
});
}
});
// fitty global properties (by setting observeWindow to true the events above get added)
fitty.observeWindow = true;
fitty.observeWindowDelay = 100;
// public fit all method, will force redraw no matter what
fitty.fitAll = redrawAll(DrawState.DIRTY);
// export our fitty function, we don't want to keep it to our selves
return fitty;
})(typeof window === "undefined" ? null : window);