lisn.js
Version:
Simply handle user gestures and actions. Includes widgets.
469 lines (444 loc) • 16.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.SmoothScroll = void 0;
var MC = _interopRequireWildcard(require("../globals/minification-constants.cjs"));
var MH = _interopRequireWildcard(require("../globals/minification-helpers.cjs"));
var _animations = require("../utils/animations.cjs");
var _browser = require("../utils/browser.cjs");
var _cssAlter = require("../utils/css-alter.cjs");
var _domAlter = require("../utils/dom-alter.cjs");
var _domOptimize = require("../utils/dom-optimize.cjs");
var _log = require("../utils/log.cjs");
var _misc = require("../utils/misc.cjs");
var _scroll = require("../utils/scroll.cjs");
var _text = require("../utils/text.cjs");
var _validation = require("../utils/validation.cjs");
var _scrollWatcher = require("../watchers/scroll-watcher.cjs");
var _sizeWatcher = require("../watchers/size-watcher.cjs");
var _widget = require("./widget.cjs");
var _debug = _interopRequireDefault(require("../debug/debug.cjs"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
/**
* @module Widgets
*/
/**
* Configures the given element as a {@link SmoothScroll} widget.
*
* The SmoothScroll widget creates a configurable smooth scrolling
* experience, including support for lag and parallax depth, and using a custom
* element that only takes up part of the page, all while preserving native
* scrolling behaviour (i.e. it does not disable native scroll and does not use
* fake scrollbars).
*
* **IMPORTANT:** The scrollable element you pass must have its children
* wrapped. This will be done automatically unless you create these wrappers
* yourself by ensuring your structure is as follows:
*
* ```html
* <!-- If using the document as the scrollable -->
* <body><!-- Element you instantiate as SmoothScroll, or you can pass documentElement -->
* <div class="lisn-smooth-scroll__content"><!-- Required wrapper; will be created if missing -->
* <div class="lisn-smooth-scroll__inner"><!-- Required inner wrapper; will be created if missing -->
* <!-- YOUR CONTENT -->
* </div>
* </div>
* </body>
* ```
*
* ```html
* <!-- If using a custom scrollable -->
* <div class="scrollable"><!-- Element you instantiate as SmoothScroll -->
* <div class="lisn-smooth-scroll__content"><!-- Required outer wrapper; will be created if missing -->
* <div class="lisn-smooth-scroll__inner"><!-- Required inner wrapper; will be created if missing -->
* <!-- YOUR CONTENT -->
* </div>
* </div>
* </div>
* ```
*
* **IMPORTANT:** If the scrollable element you pass is other than
* `document.documentElement` or `document.body`, SmoothScroll will then rely on
* position: sticky. XXX TODO
*
* **IMPORTANT:** You should not instantiate more than one
* {@link SmoothScroll} widget on a given element. Use
* {@link SmoothScroll.get} to get an existing instance if any. If there is
* already a widget instance, it will be destroyed!
*
* -----
*
* To use with auto-widgets (HTML API) (see
* {@link Settings.settings.autoWidgets | settings.autoWidgets}), the following
* CSS classes or data attributes are recognized:
* - `lisn-smooth-scroll` class or `data-lisn-smooth-scroll` attribute set
* on the container element that constitutes the scrollable container
*
* See below examples for what values you can use set for the data attribute
* in order to modify the configuration of the automatically created widget.
*
* @example
* This will create a smooth scroller for
* {@link settings.mainScrollableElementSelector | the main scrolling element}.
*
* This will work even if {@link settings.autoWidgets}) is false
*
* ```html
* <!-- LISN should be loaded beforehand -->
* <script>
* // You can also just customise global default settings:
* // LISN.settings.smoothScroll = "TODO";
*
* LISN.widgets.SmoothScroll.enableMain({
* XXX: "TODO",
* });
* </script>
* ```
*
* @example
* This will create a smooth scroller for a custom scrolling element (i.e. one
* with overflow "auto" or "scroll").
*
* ```html
* <div class="scrolling lisn-smooth-scroll">
* <!-- content here... -->
* </div>
* ```
*
* @example
* As above but with custom settings.
*
* ```html
* <div
* class="scrolling"
* data-lisn-smooth-scroll="XXX=TODO
* | XXX=TODO
* ">
* <!-- content here... -->
* </div>
* ```
*/
class SmoothScroll extends _widget.Widget {
// XXX TODO getScrollable ?
/**
* If element is omitted, returns the instance created by {@link enableMain}
* if any.
*/
static get(scrollable) {
if (!scrollable) {
return mainWidget;
}
if (scrollable === MH.getDocElement()) {
scrollable = MH.getBody();
}
const instance = super.get(scrollable, DUMMY_ID);
if (MH.isInstanceOf(instance, SmoothScroll)) {
return instance;
}
return null;
}
/**
* Creates a smooth scroller for the
* {@link settings.mainScrollableElementSelector | the main scrolling element}.
*
* **NOTE:** It returns a Promise to a widget because it will wait for the
* main scrollable element to be present in the DOM if not already.
*/
static async enableMain(config) {
const scrollable = await _scrollWatcher.ScrollWatcher.fetchMainScrollableElement();
const widget = new SmoothScroll(scrollable, config);
widget.onDestroy(() => {
if (mainWidget === widget) {
mainWidget = null;
}
});
mainWidget = widget;
return widget;
}
static register() {
(0, _widget.registerWidget)(WIDGET_NAME, (element, config) => {
if (MH.isHTMLElement(element)) {
if (!SmoothScroll.get(element)) {
return new SmoothScroll(element, config);
}
} else {
(0, _log.logError)(MH.usageError("Only HTMLElement is supported for SmoothScroll widget"));
}
return null;
}, configValidator);
}
/**
* Note that passing `document.body` is considered equivalent to
* `document.documentElement`.
*/
constructor(scrollable, config) {
var _SmoothScroll$get;
if (scrollable === MH.getDocElement()) {
scrollable = MH.getBody();
}
const destroyPromise = (_SmoothScroll$get = SmoothScroll.get(scrollable)) === null || _SmoothScroll$get === void 0 ? void 0 : _SmoothScroll$get.destroy();
super(scrollable, {
id: DUMMY_ID
});
// const props = getScrollableProps(scrollable); // XXX
// const ourScrollable = props.scrollable; // XXX
(destroyPromise || MH.promiseResolve()).then(async () => {
if (this.isDestroyed()) {
return;
}
init(this, scrollable, config);
// XXX init(this, scrollable, props, config);
});
}
}
/**
* @interface
*/
exports.SmoothScroll = SmoothScroll;
// --------------------
const WIDGET_NAME = "smooth-scroll";
const PREFIXED_NAME = MH.prefixName(WIDGET_NAME);
// Only one SmoothScroll widget per element is allowed, but Widget requires a
// non-blank ID.
const DUMMY_ID = PREFIXED_NAME;
const PREFIX_ROOT = `${PREFIXED_NAME}__root`;
const PREFIX_DUMMY = `${PREFIXED_NAME}__dummy`;
const PREFIX_OUTER_WRAPPER = `${PREFIXED_NAME}__content`;
const PREFIX_INNER_WRAPPER = `${PREFIXED_NAME}__inner`;
const PREFIX_HAS_H_SCROLL = MH.prefixName("has-h-scroll");
const PREFIX_HAS_V_SCROLL = MH.prefixName("has-v-scroll");
const PREFIX_USES_STICKY = MH.prefixName("uses-sticky");
let mainWidget = null;
const configValidator = {
id: _validation.validateString,
className: _validation.validateStrList,
lag: _validation.validateNumber
};
const createWrappers = (element, classNamesEntries) => {
const wrapContentNow = (element, classNames) => (0, _domAlter.tryWrapContentNow)(element, {
_classNames: classNames,
_required: true,
_requiredBy: "SmoothScroll"
});
let lastWrapper = element;
const result = {};
let createdByUs = [];
const unwrapFn = () => {
for (const [wrapper, classNames] of createdByUs) {
(0, _domAlter.unwrapContentNow)(wrapper, classNames);
}
createdByUs = [];
};
for (const [key, classNames] of classNamesEntries) {
// Add generic lisn-wrapper class to allow ScrollWatcher to reuse it
const allClassNames = [...classNames, MC.PREFIX_WRAPPER];
let wrapper = (0, _domAlter.getContentWrapper)(lastWrapper, {
_classNames: allClassNames
});
if (!wrapper) {
wrapper = wrapContentNow(lastWrapper, allClassNames);
createdByUs.push([wrapper, classNames]); // only remove the specific classes
}
lastWrapper = wrapper;
result[key] = wrapper;
}
return {
wrappers: result,
unwrapFn
};
};
// XXX TODO children can use unique lag factor
const init = async (widget, scrollable, config) => {
const docEl = MH.getDocElement();
const body = MH.getBody();
const defaultScrollable = (0, _scroll.getDefaultScrollingElement)();
let needsSticky = true;
let root = scrollable;
if (scrollable === docEl || scrollable === body) {
scrollable = defaultScrollable;
root = body;
needsSticky = false;
}
const logger = _debug.default ? new _debug.default.Logger({
name: `SmoothScroll-${(0, _text.formatAsString)(scrollable)}`,
logAtCreation: {
config,
needsSticky
}
}) : null;
if (needsSticky && !(0, _browser.supportsSticky)()) {
(0, _log.logError)("SmoothScroll on elements other than the document relies on " + "position: sticky, but this browser does not support sticky.");
return;
}
const scrollWatcher = _scrollWatcher.ScrollWatcher.reuse({
[MC.S_DEBOUNCE_WINDOW]: 0
});
const sizeWatcher = _sizeWatcher.SizeWatcher.reuse({
[MC.S_DEBOUNCE_WINDOW]: 0
});
await (0, _domOptimize.waitForMeasureTime)();
const initialContentWidth = scrollable[MC.S_SCROLL_WIDTH];
const initialContentHeight = scrollable[MC.S_SCROLL_HEIGHT];
debug: logger === null || logger === void 0 || logger.debug5({
clientWidth: scrollable.clientWidth,
clientHeight: scrollable.clientHeight,
scrollWidth: initialContentWidth,
scrollHeight: initialContentHeight
});
// We only care if it has horizontal/vertical scroll if we're using a custom
// scrollable, so no need to check otherwise.
const hasHScroll = needsSticky ? (0, _scroll.isScrollable)(scrollable, {
axis: "x"
}) : false;
const hasVScroll = needsSticky ? (0, _scroll.isScrollable)(scrollable, {
axis: "y"
}) : false;
// ----------
const setSizeVars = (element, width, height, now = false) => {
(now ? _cssAlter.setNumericStyleJsVarsNow : _cssAlter.setNumericStyleJsVars)(element, {
width,
height
}, {
_units: "px",
_numDecimal: 2
});
};
// If there's a scroll or size change for the scrollable container, update the
// transforms and possibly the width/height of the content (if it uses sticky)
// .
const updatePropsOnScroll = (target, scrollData) => {
updateTargetPosition(scrollData);
// If the scrollable scrolls horizontally we need to set a fixed width on
// the inner wrapper, and if it scrolls vertically we need to set a fixed
// height.
if (needsSticky) {
setSizeVars(innerWrapper, hasHScroll ? scrollData[MC.S_CLIENT_WIDTH] : NaN, hasVScroll ? scrollData[MC.S_CLIENT_HEIGHT] : NaN);
}
};
// If content is resized, update the dummy overflow to match its size
const updatePropsOnResize = (target, sizeData) => {
setSizeVars(dummy, sizeData.border[MC.S_WIDTH], sizeData.border[MC.S_HEIGHT]);
};
// ----------
const currentPositions = {
x: 0,
y: 0
};
const targetPositions = MH.copyObject(currentPositions);
const updateTargetPosition = scrollData => {
for (const d of ["x", "y"]) {
const current = currentPositions[d];
const target = targetPositions[d];
const newTarget = scrollData[d === "x" ? MC.S_SCROLL_LEFT : MC.S_SCROLL_TOP];
const isOngoing = current !== target;
targetPositions[d] = newTarget;
if (!isOngoing) {
animateTransforms(d);
}
}
};
const animateTransforms = async d => {
var _config$lag;
const lag = (_config$lag = config === null || config === void 0 ? void 0 : config.lag) !== null && _config$lag !== void 0 ? _config$lag : 1000; // XXX
debug: logger === null || logger === void 0 || logger.debug10(`Starting animating ${d} transforms with lag ${lag}`);
let target = targetPositions[d];
let current = currentPositions[d];
const iterator = (0, _animations.newCriticallyDampedAnimationIterator)({
l: current,
lTarget: target,
lag
});
while ({
l: current
} = (await iterator.next(target)).value) {
currentPositions[d] = current;
target = targetPositions[d];
(0, _cssAlter.setNumericStyleJsVars)(innerWrapper, {
[d]: -current
}, {
_prefix: "offset-",
_units: "px",
_numDecimal: 2
});
console.log("XXX", JSON.stringify({
d,
current,
target
}));
if (current === target) {
debug: logger === null || logger === void 0 || logger.debug10(`Done animating ${d} transforms`, target);
return;
}
}
};
// ----------
const addWatchers = () => {
// Track scroll in any direction as well as changes in border or content size
// of the element and its contents.
scrollWatcher.trackScroll(updatePropsOnScroll, {
threshold: 0,
scrollable
});
// Track changes in content or border size of the inner content wrapper.
sizeWatcher.onResize(updatePropsOnResize, {
target: innerWrapper,
threshold: 0
});
};
const removeWatchers = () => {
scrollWatcher.noTrackScroll(updatePropsOnScroll, scrollable);
sizeWatcher.offResize(updatePropsOnResize, innerWrapper);
};
// SETUP ------------------------------
await (0, _domOptimize.waitForMutateTime)();
(0, _cssAlter.addClassesNow)(root, PREFIX_ROOT);
// Wrap the contents in a fixed/sticky positioned wrapper and insert a dummy
// overflow element of the same size.
// [TODO v2]: Better way to centrally manage wrapping and wrapping of elements
const {
wrappers,
unwrapFn
} = createWrappers(root, [["o", [PREFIX_OUTER_WRAPPER]], ["i", [PREFIX_INNER_WRAPPER]]]);
const outerWrapper = wrappers.o;
const innerWrapper = wrappers.i;
if (needsSticky) {
(0, _cssAlter.setBooleanDataNow)(root, PREFIX_HAS_H_SCROLL, hasHScroll);
(0, _cssAlter.setBooleanDataNow)(root, PREFIX_HAS_V_SCROLL, hasVScroll);
(0, _cssAlter.setBooleanDataNow)(root, PREFIX_USES_STICKY);
}
if (config !== null && config !== void 0 && config.id) {
outerWrapper.id = config.id;
}
if (config !== null && config !== void 0 && config.className) {
(0, _cssAlter.addClassesNow)(outerWrapper, ...(0, _misc.toArrayIfSingle)(config.className));
}
const dummy = MH.createElement("div");
(0, _cssAlter.addClassesNow)(dummy, PREFIX_DUMMY);
// set its size now to prevent initial layout shifts
setSizeVars(dummy, initialContentWidth, initialContentHeight, true);
(0, _domAlter.moveElementNow)(dummy, {
to: root,
ignoreMove: true
});
addWatchers();
widget.onDisable(() => {
removeWatchers();
// XXX TODO re-enable regular scrolling
});
widget.onEnable(() => {
addWatchers();
// XXX TODO re-enable smooth scrolling
});
widget.onDestroy(async () => {
await (0, _domOptimize.waitForMutateTime)();
unwrapFn();
(0, _domAlter.moveElementNow)(dummy); // remove
(0, _cssAlter.removeClassesNow)(root, PREFIX_ROOT);
(0, _cssAlter.delDataNow)(root, PREFIX_HAS_H_SCROLL);
(0, _cssAlter.delDataNow)(root, PREFIX_HAS_V_SCROLL);
(0, _cssAlter.delDataNow)(root, PREFIX_USES_STICKY);
});
};
//# sourceMappingURL=smooth-scroll.cjs.map