UNPKG

@pi0/framework7

Version:

Full featured mobile HTML framework for building iOS & Android apps

779 lines (727 loc) 25.5 kB
/* eslint "no-useless-escape": "off" */ import $ from 'dom7'; import Utils from '../../utils/utils'; import Framework7Class from '../../utils/class'; class Autocomplete extends Framework7Class { constructor(app, params = {}) { super(params, [app]); const ac = this; ac.app = app; const defaults = Utils.extend({ on: {}, }, app.modules.autocomplete.params.autocomplete); // Extend defaults with modules params ac.useModulesParams(defaults); ac.params = Utils.extend(defaults, params); let $openerEl; if (ac.params.openerEl) { $openerEl = $(ac.params.openerEl); if ($openerEl.length) $openerEl[0].f7Autocomplete = ac; } let $inputEl; if (ac.params.inputEl) { $inputEl = $(ac.params.inputEl); if ($inputEl.length) $inputEl[0].f7Autocomplete = ac; } let view; if (ac.params.view) { view = ac.params.view; } else if ($openerEl || $inputEl) { view = app.views.get($openerEl || $inputEl); } if (!view) view = app.views.main; const id = Utils.now(); let url = params.url; if (!url && $openerEl && $openerEl.length) { if ($openerEl.attr('href')) url = $openerEl.attr('href'); else if ($openerEl.find('a').length > 0) { url = $openerEl.find('a').attr('href'); } } if (!url || url === '#' || url === '') url = ac.params.url; const inputType = ac.params.multiple ? 'checkbox' : 'radio'; Utils.extend(ac, { $openerEl, openerEl: $openerEl && $openerEl[0], $inputEl, inputEl: $inputEl && $inputEl[0], id, view, url, value: ac.params.value || [], inputType, inputName: `${inputType}-${id}`, $modalEl: undefined, $dropdownEl: undefined, }); let previousQuery = ''; function onInputChange() { let query = ac.$inputEl.val().trim(); if (!ac.params.source) return; ac.params.source.call(ac, query, (items) => { let itemsHTML = ''; const limit = ac.params.limit ? Math.min(ac.params.limit, items.length) : items.length; ac.items = items; let regExp; if (ac.params.highlightMatches) { query = query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); regExp = new RegExp(`(${query})`, 'i'); } let firstValue; let firstItem; for (let i = 0; i < limit; i += 1) { const itemValue = typeof items[i] === 'object' ? items[i][ac.params.valueProperty] : items[i]; const itemText = typeof items[i] === 'object' ? items[i][ac.params.textProperty] : items[i]; if (i === 0) { firstValue = itemValue; firstItem = ac.items[i]; } itemsHTML += ac.renderItem({ value: itemValue, text: ac.params.highlightMatches ? itemText.replace(regExp, '<b>$1</b>') : itemText, }, i); } if (itemsHTML === '' && query === '' && ac.params.dropdownPlaceholderText) { itemsHTML += ac.renderItem({ placeholder: true, text: ac.params.dropdownPlaceholderText, }); } ac.$dropdownEl.find('ul').html(itemsHTML); if (ac.params.typeahead) { if (!firstValue || !firstItem) { return; } if (firstValue.toLowerCase().indexOf(query.toLowerCase()) !== 0) { return; } if (previousQuery.toLowerCase() === query.toLowerCase()) { ac.value = []; return; } if (previousQuery.toLowerCase().indexOf(query.toLowerCase()) === 0) { previousQuery = query; ac.value = []; return; } $inputEl.val(firstValue); $inputEl[0].setSelectionRange(query.length, firstValue.length); const previousValue = typeof ac.value[0] === 'object' ? ac.value[0][ac.params.valueProperty] : ac.value[0]; if (!previousValue || firstValue.toLowerCase() !== previousValue.toLowerCase()) { ac.value = [firstItem]; ac.emit('local::change autocompleteChange', [firstItem]); } } previousQuery = query; }); } function onPageInputChange() { const input = this; const value = input.value; const isValues = $(input).parents('.autocomplete-values').length > 0; let item; let itemValue; let aValue; if (isValues) { if (ac.inputType === 'checkbox' && !input.checked) { for (let i = 0; i < ac.value.length; i += 1) { aValue = typeof ac.value[i] === 'string' ? ac.value[i] : ac.value[i][ac.params.valueProperty]; if (aValue === value || aValue * 1 === value * 1) { ac.value.splice(i, 1); } } ac.updateValues(); ac.emit('local::change autocompleteChange', ac.value); } return; } // Find Related Item for (let i = 0; i < ac.items.length; i += 1) { itemValue = typeof ac.items[i] === 'object' ? ac.items[i][ac.params.valueProperty] : ac.items[i]; if (itemValue === value || itemValue * 1 === value * 1) item = ac.items[i]; } if (ac.inputType === 'radio') { ac.value = [item]; } else if (input.checked) { ac.value.push(item); } else { for (let i = 0; i < ac.value.length; i += 1) { aValue = typeof ac.value[i] === 'object' ? ac.value[i][ac.params.valueProperty] : ac.value[i]; if (aValue === value || aValue * 1 === value * 1) { ac.value.splice(i, 1); } } } // Update Values Block ac.updateValues(); // On Select Callback if (((ac.inputType === 'radio' && input.checked) || ac.inputType === 'checkbox')) { ac.emit('local::change autocompleteChange', ac.value); } } function onHtmlClick(e) { const $targetEl = $(e.target); if ($targetEl.is(ac.$inputEl[0]) || ($targetEl.closest(ac.$dropdownEl[0]).length)) return; ac.close(); } function onOpenerClick() { ac.open(); } function onInputFocus() { ac.open(); } function onInputBlur() { if (ac.$dropdownEl.find('label.active-state').length > 0) return; ac.close(); } function onResize() { ac.positionDropDown(); } function onKeyDown(e) { if (ac.opened && e.keyCode === 13) { e.preventDefault(); ac.$inputEl.blur(); } } function onDropdownclick() { const $clickedEl = $(this); let clickedItem; for (let i = 0; i < ac.items.length; i += 1) { const itemValue = typeof ac.items[i] === 'object' ? ac.items[i][ac.params.valueProperty] : ac.items[i]; const value = $clickedEl.attr('data-value'); if (itemValue === value || itemValue * 1 === value * 1) { clickedItem = ac.items[i]; } } if (ac.params.updateInputValueOnSelect) { ac.$inputEl.val(typeof clickedItem === 'object' ? clickedItem[ac.params.valueProperty] : clickedItem); ac.$inputEl.trigger('input change'); } ac.value = [clickedItem]; ac.emit('local::change autocompleteChange', [clickedItem]); ac.close(); } ac.attachEvents = function attachEvents() { if (ac.params.openIn !== 'dropdown' && ac.$openerEl) { ac.$openerEl.on('click', onOpenerClick); } if (ac.params.openIn === 'dropdown' && ac.$inputEl) { ac.$inputEl.on('focus', onInputFocus); ac.$inputEl.on('input', onInputChange); if (app.device.android) { $('html').on('click', onHtmlClick); } else { ac.$inputEl.on('blur', onInputBlur); } if (ac.params.typeahead) { ac.$inputEl.on('keydown', onKeyDown); } } }; ac.detachEvents = function attachEvents() { if (ac.params.openIn !== 'dropdown' && ac.$openerEl) { ac.$openerEl.off('click', onOpenerClick); } if (ac.params.openIn === 'dropdown' && ac.$inputEl) { ac.$inputEl.off('focus', onInputFocus); ac.$inputEl.off('input', onInputChange); if (app.device.android) { $('html').off('click', onHtmlClick); } else { ac.$inputEl.off('blur', onInputBlur); } if (ac.params.typeahead) { ac.$inputEl.off('keydown', onKeyDown); } } }; ac.attachDropdownEvents = function attachDropdownEvents() { ac.$dropdownEl.on('click', 'label', onDropdownclick); app.on('resize', onResize); }; ac.detachDropdownEvents = function detachDropdownEvents() { ac.$dropdownEl.off('click', 'label', onDropdownclick); app.off('resize', onResize); }; ac.attachPageEvents = function attachPageEvents() { ac.$containerEl.on('change', 'input[type="radio"], input[type="checkbox"]', onPageInputChange); if (ac.params.closeOnSelect && !ac.params.multiple) { ac.$containerEl.once('click', '.list label', () => { Utils.nextTick(() => { ac.close(); }); }); } }; ac.detachPageEvents = function detachPageEvents() { ac.$containerEl.off('change', 'input[type="radio"], input[type="checkbox"]', onPageInputChange); }; // Install Modules ac.useModules(); // Init ac.init(); return ac; } positionDropDown() { const ac = this; const { $inputEl, app, $dropdownEl } = ac; const $pageContentEl = $inputEl.parents('.page-content'); if ($pageContentEl.length === 0) return; const inputOffset = $inputEl.offset(); const inputOffsetWidth = $inputEl[0].offsetWidth; const inputOffsetHeight = $inputEl[0].offsetHeight; const $listEl = $inputEl.parents('.list'); const listOffset = $listEl.offset(); const paddingBottom = parseInt($pageContentEl.css('padding-top'), 10); const listOffsetLeft = $listEl.length > 0 ? listOffset.left - $listEl.parent().offset().left : 0; const inputOffsetLeft = inputOffset.left - ($listEl.length > 0 ? listOffset.left : 0) - (app.rtl ? 0 : 0); const inputOffsetTop = inputOffset.top - ($pageContentEl.offset().top - $pageContentEl[0].scrollTop); const maxHeight = $pageContentEl[0].scrollHeight - paddingBottom - (inputOffsetTop + $pageContentEl[0].scrollTop) - $inputEl[0].offsetHeight; const paddingProp = app.rtl ? 'padding-right' : 'padding-left'; let paddingValue; if ($listEl.length && !ac.params.expandInput) { paddingValue = (app.rtl ? $listEl[0].offsetWidth - inputOffsetLeft - inputOffsetWidth : inputOffsetLeft) - (app.theme === 'md' ? 16 : 15); } $dropdownEl.css({ left: `${$listEl.length > 0 ? listOffsetLeft : inputOffsetLeft}px`, top: `${inputOffsetTop + $pageContentEl[0].scrollTop + inputOffsetHeight}px`, width: `${$listEl.length > 0 ? $listEl[0].offsetWidth : inputOffsetWidth}px`, }); $dropdownEl.children('.autocomplete-dropdown-inner').css({ maxHeight: `${maxHeight}px`, [paddingProp]: $listEl.length > 0 && !ac.params.expandInput ? `${paddingValue}px` : '', }); } focus() { const ac = this; ac.$containerEl.find('input[type=search]').focus(); } source(query) { const ac = this; if (!ac.params.source) return; const { $containerEl } = ac; ac.params.source.call(ac, query, (items) => { let itemsHTML = ''; const limit = ac.params.limit ? Math.min(ac.params.limit, items.length) : items.length; ac.items = items; for (let i = 0; i < limit; i += 1) { let selected = false; const itemValue = typeof items[i] === 'object' ? items[i][ac.params.valueProperty] : items[i]; for (let j = 0; j < ac.value.length; j += 1) { const aValue = typeof ac.value[j] === 'object' ? ac.value[j][ac.params.valueProperty] : ac.value[j]; if (aValue === itemValue || aValue * 1 === itemValue * 1) selected = true; } itemsHTML += ac.renderItem({ value: itemValue, text: typeof items[i] === 'object' ? items[i][ac.params.textProperty] : items[i], inputType: ac.inputType, id: ac.id, inputName: ac.inputName, selected, }, i); } $containerEl.find('.autocomplete-found ul').html(itemsHTML); if (items.length === 0) { if (query.length !== 0) { $containerEl.find('.autocomplete-not-found').show(); $containerEl.find('.autocomplete-found, .autocomplete-values').hide(); } else { $containerEl.find('.autocomplete-values').show(); $containerEl.find('.autocomplete-found, .autocomplete-not-found').hide(); } } else { $containerEl.find('.autocomplete-found').show(); $containerEl.find('.autocomplete-not-found, .autocomplete-values').hide(); } }); } updateValues() { const ac = this; let valuesHTML = ''; for (let i = 0; i < ac.value.length; i += 1) { valuesHTML += ac.renderItem({ value: typeof ac.value[i] === 'object' ? ac.value[i][ac.params.valueProperty] : ac.value[i], text: typeof ac.value[i] === 'object' ? ac.value[i][ac.params.textProperty] : ac.value[i], inputType: ac.inputType, id: ac.id, inputName: `${ac.inputName}-checked}`, selected: true, }, i); } ac.$containerEl.find('.autocomplete-values ul').html(valuesHTML); } preloaderHide() { const ac = this; if (ac.params.openIn === 'dropdown' && ac.$dropdownEl) { ac.$dropdownEl.find('.autocomplete-preloader').removeClass('autocomplete-preloader-visible'); } else { $('.autocomplete-preloader').removeClass('autocomplete-preloader-visible'); } } preloaderShow() { const ac = this; if (ac.params.openIn === 'dropdown' && ac.$dropdownEl) { ac.$dropdownEl.find('.autocomplete-preloader').addClass('autocomplete-preloader-visible'); } else { $('.autocomplete-preloader').addClass('autocomplete-preloader-visible'); } } renderPreloader() { const ac = this; return ` <div class="autocomplete-preloader preloader ${ac.params.preloaderColor ? `color-${ac.params.preloaderColor}` : ''}">${ac.app.theme === 'md' ? ` <span class="preloader-inner"> <span class="preloader-inner-gap"></span> <span class="preloader-inner-left"> <span class="preloader-inner-half-circle"></span> </span> <span class="preloader-inner-right"> <span class="preloader-inner-half-circle"></span> </span> </span> `.trim() : ''}</div> `.trim(); } renderSearchbar() { const ac = this; if (ac.params.renderSearchbar) return ac.params.renderSearchbar.call(ac); const searchbarHTML = ` <form class="searchbar"> <div class="searchbar-inner"> <div class="searchbar-input-wrap"> <input type="search" placeholder="${ac.params.searchbarPlaceholder}"/> <i class="searchbar-icon"></i> <span class="input-clear-button"></span> </div> <span class="searchbar-disable-button">${ac.params.searchbarDisableText}</span> </div> </form> `.trim(); return searchbarHTML; } renderItem(item, index) { const ac = this; if (ac.params.renderItem) return ac.params.renderItem.call(ac, item, index); let itemHtml; if (ac.params.openIn !== 'dropdown') { itemHtml = ` <li> <label class="item-${item.inputType} item-content"> <input type="${item.inputType}" name="${item.inputName}" value="${item.value}" ${item.selected ? 'checked' : ''}> <i class="icon icon-${item.inputType}"></i> <div class="item-inner"> <div class="item-title">${item.text}</div> </div> </label> </li> `; } else if (!item.placeholder) { // Dropdown itemHtml = ` <li> <label class="item-radio item-content" data-value="${item.value}"> <div class="item-inner"> <div class="item-title">${item.text}</div> </div> </label> </li> `; } else { // Dropwdown placeholder itemHtml = ` <li class="autocomplete-dropdown-placeholder"> <div class="item-content"> <div class="item-inner"> <div class="item-title">${item.text}</div> </div> </label> </li> `; } return itemHtml.trim(); } renderNavbar() { const ac = this; if (ac.params.renderNavbar) return ac.params.renderNavbar.call(ac); let pageTitle = ac.params.pageTitle; if (typeof pageTitle === 'undefined' && ac.$openerEl && ac.$openerEl.length) { pageTitle = ac.$openerEl.find('.item-title').text().trim(); } const navbarHtml = ` <div class="navbar ${ac.params.navbarColorTheme ? `color-theme-${ac.params.navbarColorTheme}` : ''}"> <div class="navbar-inner ${ac.params.navbarColorTheme ? `color-theme-${ac.params.navbarColorTheme}` : ''}"> <div class="left sliding"> <a href="#" class="link ${ac.params.openIn === 'page' ? 'back' : 'popup-close'}"> <i class="icon icon-back"></i> <span class="ios-only">${ac.params.openIn === 'page' ? ac.params.pageBackLinkText : ac.params.popupCloseLinkText}</span> </a> </div> ${pageTitle ? `<div class="title sliding">${pageTitle}</div>` : ''} ${ac.params.preloader ? ` <div class="right"> ${ac.renderPreloader()} </div> ` : ''} <div class="subnavbar sliding">${ac.renderSearchbar()}</div> </div> </div> `.trim(); return navbarHtml; } renderDropdown() { const ac = this; if (ac.params.renderDropdown) return ac.params.renderDropdown.call(ac, ac.items); const dropdownHtml = ` <div class="autocomplete-dropdown"> <div class="autocomplete-dropdown-inner"> <div class="list"> <ul></ul> </div> </div> ${ac.params.preloader ? ac.renderPreloader() : ''} </div> `.trim(); return dropdownHtml; } renderPage() { const ac = this; if (ac.params.renderPage) return ac.params.renderPage.call(ac, ac.items); const pageHtml = ` <div class="page page-with-subnavbar autocomplete-page" data-name="autocomplete-page"> ${ac.renderNavbar()} <div class="searchbar-backdrop"></div> <div class="page-content"> <div class="list autocomplete-list autocomplete-found autocomplete-list-${ac.id} ${ac.params.formColorTheme ? `color-theme-${ac.params.formColorTheme}` : ''}"> <ul></ul> </div> <div class="list autocomplete-not-found"> <ul> <li class="item-content"><div class="item-inner"><div class="item-title">${ac.params.notFoundText}</div></div></li> </ul> </div> <div class="list autocomplete-values"> <ul></ul> </div> </div> </div> `.trim(); return pageHtml; } renderPopup() { const ac = this; if (ac.params.renderPopup) return ac.params.renderPopup.call(ac, ac.items); const popupHtml = ` <div class="popup autocomplete-popup"> <div class="view"> ${ac.renderPage()}; </div> </div> `.trim(); return popupHtml; } onOpen(type, containerEl) { const ac = this; const app = ac.app; const $containerEl = $(containerEl); ac.$containerEl = $containerEl; ac.openedIn = type; ac.opened = true; if (ac.params.openIn === 'dropdown') { ac.attachDropdownEvents(); ac.$dropdownEl.addClass('autocomplete-dropdown-in'); ac.$inputEl.trigger('input'); } else { // Init SB let $searchbarEl = $containerEl.find('.searchbar'); if (ac.params.openIn === 'page' && app.theme === 'ios' && $searchbarEl.length === 0) { $searchbarEl = $(app.navbar.getElByPage($containerEl)).find('.searchbar'); } ac.searchbar = app.searchbar.create({ el: $searchbarEl, backdropEl: $containerEl.find('.searchbar-backdrop'), customSearch: true, on: { searchbarSearch(query) { if (query.length === 0 && ac.searchbar.enabled) { ac.searchbar.backdropShow(); } else { ac.searchbar.backdropHide(); } ac.source(query); }, }, }); // Attach page events ac.attachPageEvents(); // Update Values On Page Init ac.updateValues(); // Source on load if (ac.params.requestSourceOnOpen) ac.source(''); } ac.emit('local::open autocompleteOpen', ac); } onOpened() { const ac = this; if (ac.params.openIn !== 'dropdown' && ac.params.autoFocus) { ac.autoFocus(); } ac.emit('local::opened autocompleteOpened', ac); } onClose() { const ac = this; if (ac.destroyed) return; // Destroy SB if (ac.searchbar && ac.searchbar.destroy) { ac.searchbar.destroy(); ac.searchbar = null; delete ac.searchbar; } if (ac.params.openIn === 'dropdown') { ac.detachDropdownEvents(); ac.$dropdownEl.removeClass('autocomplete-dropdown-in').remove(); ac.$inputEl.parents('.item-content-dropdown-expanded').removeClass('item-content-dropdown-expanded'); } else { ac.detachPageEvents(); } ac.emit('local::close autocompleteClose', ac); } onClosed() { const ac = this; if (ac.destroyed) return; ac.opened = false; ac.$containerEl = null; delete ac.$containerEl; ac.emit('local::closed autocompleteClosed', ac); } openPage() { const ac = this; if (ac.opened) return ac; const pageHtml = ac.renderPage(); ac.view.router.navigate(ac.url, { createRoute: { content: pageHtml, path: ac.url, options: { animate: ac.params.animate, pageEvents: { pageBeforeIn(e, page) { ac.onOpen('page', page.el); }, pageAfterIn(e, page) { ac.onOpened('page', page.el); }, pageBeforeOut(e, page) { ac.onClose('page', page.el); }, pageAfterOut(e, page) { ac.onClosed('page', page.el); }, }, }, }, }); return ac; } openPopup() { const ac = this; if (ac.opened) return ac; const popupHtml = ac.renderPopup(); const popupParams = { content: popupHtml, animate: ac.params.animate, on: { popupOpen(popup) { ac.onOpen('popup', popup.el); }, popupOpened(popup) { ac.onOpened('popup', popup.el); }, popupClose(popup) { ac.onClose('popup', popup.el); }, popupClosed(popup) { ac.onClosed('popup', popup.el); }, }, }; if (ac.params.routableModals) { ac.view.router.navigate(ac.url, { createRoute: { path: ac.url, popup: popupParams, }, }); } else { ac.modal = ac.app.popup.create(popupParams).open(ac.params.animate); } return ac; } openDropdown() { const ac = this; if (!ac.$dropdownEl) { ac.$dropdownEl = $(ac.renderDropdown()); } const $listEl = ac.$inputEl.parents('.list'); if ($listEl.length && ac.$inputEl.parents('.item-content').length > 0 && ac.params.expandInput) { ac.$inputEl.parents('.item-content').addClass('item-content-dropdown-expanded'); } ac.positionDropDown(); const $pageContentEl = ac.$inputEl.parents('.page-content'); if (ac.params.dropdownContainerEl) { $(ac.params.dropdownContainerEl).append(ac.$dropdownEl); } else if ($pageContentEl.length === 0) { ac.$dropdownEl.insertAfter(ac.$inputEl); } else { $pageContentEl.append(ac.$dropdownEl); } ac.onOpen('dropdown', ac.$dropdownEl); ac.onOpened('dropdown', ac.$dropdownEl); } open() { const ac = this; if (ac.opened) return ac; const openIn = ac.params.openIn; ac[`open${openIn.split('').map((el, index) => { if (index === 0) return el.toUpperCase(); return el; }).join('')}`](); return ac; } close() { const ac = this; if (!ac.opened) return ac; if (ac.params.openIn === 'dropdown') { ac.onClose(); ac.onClosed(); } else if (ac.params.routableModals || ac.openedIn === 'page') { ac.view.router.back({ animate: ac.params.animate }); } else { ac.modal.once('modalClosed', () => { Utils.nextTick(() => { ac.modal.destroy(); delete ac.modal; }); }); ac.modal.close(); } return ac; } init() { const ac = this; ac.attachEvents(); } destroy() { const ac = this; ac.emit('local::beforeDestroy autocompleteBeforeDestroy', ac); ac.detachEvents(); if (ac.$inputEl && ac.$inputEl[0]) { delete ac.$inputEl[0].f7Autocomplete; } if (ac.$openerEl && ac.$openerEl[0]) { delete ac.$openerEl[0].f7Autocomplete; } Utils.deleteProps(ac); ac.destroyed = true; } } export default Autocomplete;