@algolia/autocomplete-js
Version:
Fast and fully-featured autocomplete JavaScript library.
303 lines (300 loc) • 14.9 kB
JavaScript
var _excluded = ["components"];
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
function _objectWithoutProperties(e, t) { if (null == e) return {}; var o, r, i = _objectWithoutPropertiesLoose(e, t); if (Object.getOwnPropertySymbols) { var n = Object.getOwnPropertySymbols(e); for (r = 0; r < n.length; r++) o = n[r], -1 === t.indexOf(o) && {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]); } return i; }
function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
import { createAutocomplete } from '@algolia/autocomplete-core';
import { createRef, debounce, getItemsCount, warn } from '@algolia/autocomplete-shared';
import htm from 'htm';
import { createAutocompleteDom } from './createAutocompleteDom';
import { createEffectWrapper } from './createEffectWrapper';
import { createReactiveWrapper } from './createReactiveWrapper';
import { getDefaultOptions } from './getDefaultOptions';
import { getPanelPlacementStyle } from './getPanelPlacementStyle';
import { renderPanel, renderSearchBox } from './render';
import { userAgents } from './userAgents';
import { mergeDeep, pickBy, setProperties } from './utils';
var instancesCount = 0;
export function autocomplete(options) {
var _createEffectWrapper = createEffectWrapper(),
runEffect = _createEffectWrapper.runEffect,
cleanupEffects = _createEffectWrapper.cleanupEffects,
runEffects = _createEffectWrapper.runEffects;
var _createReactiveWrappe = createReactiveWrapper(),
reactive = _createReactiveWrappe.reactive,
runReactives = _createReactiveWrappe.runReactives;
var hasNoResultsSourceTemplateRef = createRef(false);
var optionsRef = createRef(options);
var onStateChangeRef = createRef(undefined);
var props = reactive(function () {
return getDefaultOptions(optionsRef.current);
});
var isDetached = reactive(function () {
return props.value.core.environment.matchMedia(props.value.renderer.detachedMediaQuery).matches;
});
var autocomplete = reactive(function () {
return createAutocomplete(_objectSpread(_objectSpread({}, props.value.core), {}, {
onStateChange: function onStateChange(params) {
var _onStateChangeRef$cur, _props$value$core$onS, _props$value$core;
hasNoResultsSourceTemplateRef.current = params.state.collections.some(function (collection) {
return collection.source.templates.noResults;
});
(_onStateChangeRef$cur = onStateChangeRef.current) === null || _onStateChangeRef$cur === void 0 ? void 0 : _onStateChangeRef$cur.call(onStateChangeRef, params);
(_props$value$core$onS = (_props$value$core = props.value.core).onStateChange) === null || _props$value$core$onS === void 0 ? void 0 : _props$value$core$onS.call(_props$value$core, params);
},
shouldPanelOpen: optionsRef.current.shouldPanelOpen || function (_ref) {
var state = _ref.state;
if (isDetached.value) {
return true;
}
var hasItems = getItemsCount(state) > 0;
if (!props.value.core.openOnFocus && !state.query) {
return hasItems;
}
var hasNoResultsTemplate = Boolean(hasNoResultsSourceTemplateRef.current || props.value.renderer.renderNoResults);
return !hasItems && hasNoResultsTemplate || hasItems;
},
__autocomplete_metadata: {
userAgents: userAgents,
options: options
}
}));
});
var lastStateRef = createRef(_objectSpread({
collections: [],
completion: null,
context: {},
isOpen: false,
query: '',
activeItemId: null,
status: 'idle'
}, props.value.core.initialState));
var propGetters = {
getEnvironmentProps: props.value.renderer.getEnvironmentProps,
getFormProps: props.value.renderer.getFormProps,
getInputProps: props.value.renderer.getInputProps,
getItemProps: props.value.renderer.getItemProps,
getLabelProps: props.value.renderer.getLabelProps,
getListProps: props.value.renderer.getListProps,
getPanelProps: props.value.renderer.getPanelProps,
getRootProps: props.value.renderer.getRootProps
};
var autocompleteScopeApi = {
setActiveItemId: autocomplete.value.setActiveItemId,
setQuery: autocomplete.value.setQuery,
setCollections: autocomplete.value.setCollections,
setIsOpen: autocomplete.value.setIsOpen,
setStatus: autocomplete.value.setStatus,
setContext: autocomplete.value.setContext,
refresh: autocomplete.value.refresh,
navigator: autocomplete.value.navigator
};
var html = reactive(function () {
return htm.bind(props.value.renderer.renderer.createElement);
});
var dom = reactive(function () {
return createAutocompleteDom({
autocomplete: autocomplete.value,
autocompleteScopeApi: autocompleteScopeApi,
classNames: props.value.renderer.classNames,
environment: props.value.core.environment,
isDetached: isDetached.value,
placeholder: props.value.core.placeholder,
propGetters: propGetters,
setIsModalOpen: setIsModalOpen,
state: lastStateRef.current,
translations: props.value.renderer.translations
});
});
function setPanelPosition() {
setProperties(dom.value.panel, {
style: isDetached.value ? {} : getPanelPlacementStyle({
panelPlacement: props.value.renderer.panelPlacement,
container: dom.value.root,
form: dom.value.form,
environment: props.value.core.environment
})
});
}
function scheduleRender(state) {
lastStateRef.current = state;
var renderProps = {
autocomplete: autocomplete.value,
autocompleteScopeApi: autocompleteScopeApi,
classNames: props.value.renderer.classNames,
components: props.value.renderer.components,
container: props.value.renderer.container,
html: html.value,
dom: dom.value,
panelContainer: isDetached.value ? dom.value.detachedContainer : props.value.renderer.panelContainer,
propGetters: propGetters,
state: lastStateRef.current,
renderer: props.value.renderer.renderer
};
var render = !getItemsCount(state) && !hasNoResultsSourceTemplateRef.current && props.value.renderer.renderNoResults || props.value.renderer.render;
renderSearchBox(renderProps);
renderPanel(render, renderProps);
}
runEffect(function () {
var environmentProps = autocomplete.value.getEnvironmentProps({
formElement: dom.value.form,
panelElement: dom.value.panel,
inputElement: dom.value.input
});
setProperties(props.value.core.environment, environmentProps);
return function () {
setProperties(props.value.core.environment, Object.keys(environmentProps).reduce(function (acc, key) {
return _objectSpread(_objectSpread({}, acc), {}, _defineProperty({}, key, undefined));
}, {}));
};
});
runEffect(function () {
var panelContainerElement = isDetached.value ? props.value.core.environment.document.body : props.value.renderer.panelContainer;
var panelElement = isDetached.value ? dom.value.detachedOverlay : dom.value.panel;
if (isDetached.value && lastStateRef.current.isOpen) {
setIsModalOpen(true);
}
scheduleRender(lastStateRef.current);
return function () {
if (panelContainerElement.contains(panelElement)) {
panelContainerElement.removeChild(panelElement);
panelContainerElement.classList.remove('aa-Detached');
}
};
});
runEffect(function () {
var containerElement = props.value.renderer.container;
containerElement.appendChild(dom.value.root);
return function () {
containerElement.removeChild(dom.value.root);
};
});
runEffect(function () {
var debouncedRender = debounce(function (_ref2) {
var state = _ref2.state;
scheduleRender(state);
}, 0);
onStateChangeRef.current = function (_ref3) {
var state = _ref3.state,
prevState = _ref3.prevState;
if (isDetached.value && prevState.isOpen !== state.isOpen) {
setIsModalOpen(state.isOpen);
}
// The outer DOM might have changed since the last time the panel was
// positioned. The layout might have shifted vertically for instance.
// It's therefore safer to re-calculate the panel position before opening
// it again.
if (!isDetached.value && state.isOpen && !prevState.isOpen) {
setPanelPosition();
}
// We scroll to the top of the panel whenever the query changes (i.e. new
// results come in) so that users don't have to.
if (state.query !== prevState.query) {
var scrollablePanels = props.value.core.environment.document.querySelectorAll('.aa-Panel--scrollable');
scrollablePanels.forEach(function (scrollablePanel) {
if (scrollablePanel.scrollTop !== 0) {
scrollablePanel.scrollTop = 0;
}
});
}
debouncedRender({
state: state
});
};
return function () {
onStateChangeRef.current = undefined;
};
});
runEffect(function () {
var onResize = debounce(function () {
var previousIsDetached = isDetached.value;
isDetached.value = props.value.core.environment.matchMedia(props.value.renderer.detachedMediaQuery).matches;
if (previousIsDetached !== isDetached.value) {
update({});
} else {
requestAnimationFrame(setPanelPosition);
}
}, 20);
props.value.core.environment.addEventListener('resize', onResize);
return function () {
props.value.core.environment.removeEventListener('resize', onResize);
};
});
runEffect(function () {
if (!isDetached.value) {
return function () {};
}
function toggleModalClassname(isActive) {
dom.value.detachedContainer.classList.toggle('aa-DetachedContainer--modal', isActive);
}
function onChange(event) {
toggleModalClassname(event.matches);
}
var isModalDetachedMql = props.value.core.environment.matchMedia(getComputedStyle(props.value.core.environment.document.documentElement).getPropertyValue('--aa-detached-modal-media-query'));
toggleModalClassname(isModalDetachedMql.matches);
// Prior to Safari 14, `MediaQueryList` isn't based on `EventTarget`,
// so we must use `addListener` and `removeListener` to observe media query lists.
// See https://developer.mozilla.org/en-US/docs/Web/API/MediaQueryList/addListener
var hasModernEventListener = Boolean(isModalDetachedMql.addEventListener);
hasModernEventListener ? isModalDetachedMql.addEventListener('change', onChange) : isModalDetachedMql.addListener(onChange);
return function () {
hasModernEventListener ? isModalDetachedMql.removeEventListener('change', onChange) : isModalDetachedMql.removeListener(onChange);
};
});
runEffect(function () {
requestAnimationFrame(setPanelPosition);
return function () {};
});
function destroy() {
instancesCount--;
cleanupEffects();
}
function update() {
var updatedOptions = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
cleanupEffects();
var _props$value$renderer = props.value.renderer,
components = _props$value$renderer.components,
rendererProps = _objectWithoutProperties(_props$value$renderer, _excluded);
optionsRef.current = mergeDeep(rendererProps, props.value.core, {
// We need to filter out default components so they can be replaced with
// a new `renderer`, without getting rid of user components.
// @MAJOR Deal with registering components with the same name as the
// default ones. If we disallow overriding default components, we'd just
// need to pass all `components` here.
components: pickBy(components, function (_ref4) {
var value = _ref4.value;
return !value.hasOwnProperty('__autocomplete_componentName');
}),
initialState: lastStateRef.current
}, updatedOptions);
runReactives();
runEffects();
autocomplete.value.refresh().then(function () {
scheduleRender(lastStateRef.current);
});
}
function setIsModalOpen(value) {
var prevValue = props.value.core.environment.document.body.contains(dom.value.detachedOverlay);
if (value === prevValue) {
return;
}
if (value) {
props.value.core.environment.document.body.appendChild(dom.value.detachedOverlay);
props.value.core.environment.document.body.classList.add('aa-Detached');
dom.value.input.focus();
} else {
props.value.core.environment.document.body.removeChild(dom.value.detachedOverlay);
props.value.core.environment.document.body.classList.remove('aa-Detached');
}
}
process.env.NODE_ENV !== 'production' ? warn(instancesCount === 0, "Autocomplete doesn't support multiple instances running at the same time. Make sure to destroy the previous instance before creating a new one.\n\nSee: https://www.algolia.com/doc/ui-libraries/autocomplete/api-reference/autocomplete-js/autocomplete/#param-destroy") : void 0;
instancesCount++;
return _objectSpread(_objectSpread({}, autocompleteScopeApi), {}, {
update: update,
destroy: destroy
});
}