/*eslint no-console: "off"*/ /* Copyright (c) 2017-2021 Rik Schennink - All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ 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 ? (options = { sync: false }) => { w.cancelAnimationFrame(redrawFrame); const redrawFn = () => redraw(fitties.filter((f) => f.dirty && f.active)); if (options.sync) { return redrawFn(); } redrawFrame = w.requestAnimationFrame(redrawFn); } : () => {}; // sets all fitties to dirty so they are redrawn on the next redraw loop, then calls redraw const redrawAll = (type) => (options) => { fitties.forEach((f) => (f.dirty = type)); requestRedraw(options); }; // 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) => { if (f.vertical) { // get available width from parent node f.availableHeight = f.element.parentNode.clientHeight; // the space our target element uses f.currentHeight = f.element.scrollHeight; // remember current font size f.previousFontSize = f.currentFontSize; // let's calculate the new font size f.currentFontSize = Math.min( Math.max(f.minSize, (f.availableHeight / f.currentHeight) * f.previousFontSize), f.maxSize ); } else { // get available width from parent node f.availableWidth = f.element.parentNode.clientWidth; // the space our target element uses f.currentWidth = f.element.scrollWidth; // remember current font size f.previousFontSize = f.currentFontSize; // let's calculate the new font size f.currentFontSize = Math.min( Math.max(f.minSize, (f.availableWidth / f.currentWidth) * 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) => { if (f.vertical) { return f.dirty !== DrawState.DIRTY_LAYOUT || (f.dirty === DrawState.DIRTY_LAYOUT && f.element.parentNode.clientHeight !== f.availableHeight); } else { return f.dirty !== DrawState.DIRTY_LAYOUT || (f.dirty === DrawState.DIRTY_LAYOUT && f.element.parentNode.clientWidth !== f.availableWidth); } }; // 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'); // styles computed return true; }; // 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) => (options) => { f.dirty = type; if (!f.active) { return; } requestRedraw(options); }; 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, vertical: false, observeMutations: 'MutationObserver' in w ? mutationObserverDefaultSetting : false, }; /** * array of elements in, fitty instances out * @param {Array} elements * @param {object} options */ function fittyCreate(elements, options) { // set options object const fittyOptions = Object.assign( {}, // expand default options defaultOptions, // override with custom options options ); // create fitties const publicFitties = elements.map((element) => { // create fitty instance const f = Object.assign({}, 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 * @param {*} target * @param {*} options * @returns */ 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);