UNPKG

@algolia/autocomplete-js

Version:

Fast and fully-featured autocomplete JavaScript library.

303 lines (300 loc) 14.9 kB
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 }); }