lisn.js
Version:
Simply handle user gestures and actions. Includes widgets.
334 lines (323 loc) • 12.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.ScrollToTop = void 0;
var MC = _interopRequireWildcard(require("../globals/minification-constants.cjs"));
var MH = _interopRequireWildcard(require("../globals/minification-helpers.cjs"));
var _cssAlter = require("../utils/css-alter.cjs");
var _domAlter = require("../utils/dom-alter.cjs");
var _domEvents = require("../utils/dom-events.cjs");
var _domOptimize = require("../utils/dom-optimize.cjs");
var _domSearch = require("../utils/dom-search.cjs");
var _event = require("../utils/event.cjs");
var _validation = require("../utils/validation.cjs");
var _views = require("../utils/views.cjs");
var _scrollWatcher = require("../watchers/scroll-watcher.cjs");
var _viewWatcher = require("../watchers/view-watcher.cjs");
var _widget = require("./widget.cjs");
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 ScrollToTop} widget.
*
* The ScrollToTop widget adds a scroll-to-top button in the lower right corder
* of the screen (can be changed to bottom left) which scrolls smoothly (and
* more slowly than the native scroll) back to the top.
*
* The button is only shown when the scroll offset from the top is more than a
* given configurable amount.
*
* **IMPORTANT:** When configuring an existing element as the button (i.e. using
* `new ScrollToTop` or auto-widgets, rather than {@link ScrollToTop.enableMain}):
* - if using
* {@link Settings.settings.mainScrollableElementSelector | the main scrolling element}
* as the scrollable, the button element will have it's CSS position set to `fixed`;
* - otherwise, if using a custom scrollable element, the button element may be
* moved in the DOM tree in order to position it on top of the scrollable
* If you don't want the button element changed in any way, then consider using
* the {@link Triggers.ClickTrigger | ClickTrigger} with a
* {@link Actions.ScrollTo | ScrollTo} action.
*
* **IMPORTANT:** You should not instantiate more than one {@link ScrollToTop}
* widget on a given element. Use {@link ScrollToTop.get} to get an existing
* instance if any. If there is already a widget instance, it will be destroyed!
*
* -----
*
* You can use the following dynamic attributes or CSS properties in your
* stylesheet:
*
* The following dynamic attributes are set on the element:
* - `data-lisn-place`: `"left"` or `"right"`
*
* -----
*
* To use with auto-widgets (HTML API) (see
* {@link Settings.settings.autoWidgets | settings.autoWidgets}), the following
* CSS classes or data attributes are recognized:
* - `lisn-scroll-to-top` class or `data-lisn-scroll-to-top` attribute set on
* the element that constitutes the widget. The element should be empty.
*
* See below examples for what values you can use set for the data attributes
* in order to modify the configuration of the automatically created widget.
*
* @example
* This will create a scroll-to-top button using the JavaScript API.
*
* This will work even if
* {@link Settings.settings.autoWidgets | settings.autoWidgets}) is false.
*
* ```html
* <!-- LISN should be loaded beforehand -->
* <script>
* LISN.widgets.ScrollToTop.enableMain({
* position: "left",
* offset: "top: 300vh"
* });
* </script>
* ```
*
* You can customise the offset show/hide trigger via CSS as well as long as
* {@link ScrollToTopConfig.offset} is left at its default value which uses
* this CSS property:
*
* ```html
* <style type="text/css" media="screen">
* :root {
* --lisn-scroll-to-top--offset: 300vh;
* }
* </style>
* ```
*
* @example
* This will configure the given element as a scroll-to-top button for the main
* scrolling element using an existing element for the button with default
* {@link ScrollToTopConfig}.
*
* ```html
* <button class="lisn-scroll-to-top"></button>
* ```
* @example
* As above but with custom settings.
*
* ```html
* <button data-lisn-scroll-to-top="position=left | offset=top:300vh"></button>
* ```
*
* @example
* This will configure the given element as a scroll-to-top button for a custom
* scrolling element (i.e. one with overflow "auto" or "scroll").
*
* ```html
* <div id="scrollable">
* <!-- content here... -->
* </div>
* <button data-lisn-scroll-to-top="scrollable=#scrollable"></button>
* ```
*
* @example
* As above, but using a reference specification with a class name to find the
* scrollable.
*
* ```html
* <div class="scrollable">
* <!-- content here... -->
* </div>
* <button data-lisn-scroll-to-top="scrollable=prev.scrollable"></button>
* ```
*
* @example
* As above but with all custom settings.
*
* ```html
* <div class="scrollable">
* <!-- content here... -->
* </div>
* <button data-lisn-scroll-to-top="scrollable=prev.scrollable
* | position=left
* | offset=top:300vh
* "></button>
* ```
*/
class ScrollToTop extends _widget.Widget {
/**
* If element is omitted, returns the instance created by {@link enableMain}
* if any.
*/
static get(element) {
if (!element) {
return mainWidget;
}
const instance = super.get(element, DUMMY_ID);
if (MH.isInstanceOf(instance, ScrollToTop)) {
return instance;
}
return null;
}
static register() {
(0, _widget.registerWidget)(WIDGET_NAME, (element, config) => {
if (!ScrollToTop.get(element)) {
return new ScrollToTop(element, config);
}
return null;
}, newConfigValidator);
}
/**
* Creates a new button element, inserts it into the document body and
* configures it as a {@link ScrollToTop}.
*/
static enableMain(config) {
const button = MH.createButton("Back to top");
const widget = new ScrollToTop(button, config);
widget.onDestroy(() => {
if (mainWidget === widget) {
mainWidget = null;
}
return (0, _domAlter.moveElement)(button);
});
(0, _domEvents.waitForElement)(MH.getBody).then(body => {
if (!widget.isDestroyed()) {
(0, _domAlter.moveElement)(button, {
to: body
});
}
});
mainWidget = widget;
return widget;
}
constructor(element, config) {
var _ScrollToTop$get;
const destroyPromise = (_ScrollToTop$get = ScrollToTop.get(element)) === null || _ScrollToTop$get === void 0 ? void 0 : _ScrollToTop$get.destroy();
super(element, {
id: DUMMY_ID
});
const offset = (config === null || config === void 0 ? void 0 : config.offset) || `${MC.S_TOP}: var(${MH.prefixCssVar("scroll-to-top--offset")}, 200vh)`;
const position = (config === null || config === void 0 ? void 0 : config.position) || MC.S_RIGHT;
const scrollable = config === null || config === void 0 ? void 0 : config.scrollable;
const hasCustomScrollable = scrollable && scrollable !== MH.getDocElement() && scrollable !== MH.getBody();
const scrollWatcher = _scrollWatcher.ScrollWatcher.reuse();
const viewWatcher = _viewWatcher.ViewWatcher.reuse(hasCustomScrollable ? {
root: scrollable
} : {});
const clickListener = () => scrollWatcher.scrollTo({
top: 0,
left: 0
}, {
scrollable
});
let arrow;
let placeholder;
let root = element;
const showIt = () => {
(0, _cssAlter.showElement)(root);
};
const hideIt = () => {
(0, _cssAlter.hideElement)(root);
};
// SETUP ------------------------------
(destroyPromise || MH.promiseResolve()).then(async () => {
const flexDirection = scrollable ? await (0, _cssAlter.getParentFlexDirection)(scrollable) : null;
await (0, _domOptimize.waitForMutateTime)();
if (this.isDestroyed()) {
return;
}
if (hasCustomScrollable) {
// Add a placeholder to restore its position on destroy.
placeholder = MH.createElement("div");
(0, _domAlter.moveElementNow)(placeholder, {
to: element,
position: "before",
ignoreMove: true
});
// Then move it to immediately after the scrollable.
// If the parent is a horizontal flexbox and position is left, then
// we need to insert it before the scrollable.
const shouldInsertBefore = flexDirection === "column-reverse" || position === MC.S_LEFT && flexDirection === "row" || position === MC.S_RIGHT && flexDirection === "row-reverse";
(0, _domAlter.moveElementNow)(element, {
to: scrollable,
position: shouldInsertBefore ? "before" : "after",
ignoreMove: true
});
// Wrap the button.
root = (0, _domAlter.wrapElementNow)(element, {
wrapper: "div",
ignoreMove: true
});
}
(0, _cssAlter.disableInitialTransition)(root);
(0, _cssAlter.addClassesNow)(root, PREFIX_ROOT);
(0, _cssAlter.addClassesNow)(element, PREFIX_BTN);
(0, _cssAlter.setBooleanDataNow)(root, PREFIX_FIXED, !hasCustomScrollable);
(0, _cssAlter.setDataNow)(root, MC.PREFIX_PLACE, position);
arrow = (0, _domAlter.insertArrow)(element, MC.S_UP);
hideIt(); // initial
(0, _event.addEventListenerTo)(element, MC.S_CLICK, clickListener);
viewWatcher.onView(offset, showIt, {
views: [MC.S_AT, MC.S_BELOW]
});
viewWatcher.onView(offset, hideIt, {
views: [MC.S_ABOVE]
});
this.onDisable(() => {
(0, _cssAlter.undisplayElement)(root);
});
this.onEnable(() => {
(0, _cssAlter.displayElement)(root);
});
this.onDestroy(async () => {
await (0, _domOptimize.waitForMutateTime)();
(0, _event.removeEventListenerFrom)(element, MC.S_CLICK, clickListener);
(0, _cssAlter.removeClassesNow)(root, PREFIX_ROOT);
(0, _cssAlter.removeClassesNow)(element, PREFIX_BTN);
(0, _cssAlter.delDataNow)(root, PREFIX_FIXED);
(0, _cssAlter.delDataNow)(root, MC.PREFIX_PLACE);
(0, _cssAlter.displayElementNow)(root); // revert undisplay by onDisable
if (arrow) {
(0, _domAlter.moveElementNow)(arrow); // remove
}
if (root !== element) {
// Unwrap the button.
(0, _domAlter.replaceElementNow)(root, element, {
ignoreMove: true
});
}
if (placeholder) {
// Move it back into its original position.
(0, _domAlter.replaceElementNow)(placeholder, element, {
ignoreMove: true
});
}
viewWatcher.offView(offset, showIt);
viewWatcher.offView(offset, hideIt);
});
});
}
}
/**
* @interface
*/
exports.ScrollToTop = ScrollToTop;
// --------------------
const WIDGET_NAME = "scroll-to-top";
const PREFIXED_NAME = MH.prefixName(WIDGET_NAME);
// Only one ScrollToTop 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_BTN = `${PREFIXED_NAME}__btn`;
const PREFIX_FIXED = MH.prefixName("fixed");
let mainWidget = null;
const newConfigValidator = element => {
return {
offset: (key, value) => (0, _validation.validateString)(key, value, _views.isValidScrollOffset),
position: (key, value) => (0, _validation.validateString)(key, value, v => v === MC.S_LEFT || v === MC.S_RIGHT),
scrollable: (key, value) => {
var _ref;
return (_ref = MH.isLiteralString(value) ? (0, _domSearch.waitForReferenceElement)(value, element) : null) !== null && _ref !== void 0 ? _ref : undefined;
}
};
};
//# sourceMappingURL=scroll-to-top.cjs.map