@algolia/autocomplete-js
Version:
Fast and fully-featured autocomplete JavaScript library.
302 lines (268 loc) • 12.5 kB
JavaScript
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
import { createAutocomplete } from '@algolia/autocomplete-core';
import { createRef, debounce, getItemsCount } from '@algolia/autocomplete-shared';
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 { mergeDeep, setProperties } from './utils';
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;
}
}));
});
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
};
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,
createElement: props.value.renderer.renderer.createElement,
dom: dom.value,
Fragment: props.value.renderer.renderer.Fragment,
panelContainer: isDetached.value ? dom.value.detachedContainer : props.value.renderer.panelContainer,
propGetters: propGetters,
state: lastStateRef.current
};
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);
}
};
});
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() {
cleanupEffects();
}
function update() {
var updatedOptions = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
cleanupEffects();
optionsRef.current = mergeDeep(props.value.renderer, props.value.core, {
initialState: lastStateRef.current
}, updatedOptions);
runReactives();
runEffects();
autocomplete.value.refresh().then(function () {
scheduleRender(lastStateRef.current);
});
}
function setIsModalOpen(value) {
requestAnimationFrame(function () {
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');
autocomplete.value.setQuery('');
autocomplete.value.refresh();
}
});
}
return _objectSpread(_objectSpread({}, autocompleteScopeApi), {}, {
update: update,
destroy: destroy
});
}