Merge pull request #4382 from blockscout/vb-autocomplete
Replace awesomplete with autocomplete.jspull/4388/head
commit
230ea9f5bb
@ -0,0 +1,129 @@ |
||||
import AutoComplete from '@tarekraafat/autocomplete.js/dist/autoComplete.js' |
||||
|
||||
const placeHolder = 'Search by address, token symbol, name, transaction hash, or block number' |
||||
const dataSrc = async (query, id) => { |
||||
try { |
||||
// Loading placeholder text
|
||||
const searchInput = document |
||||
.getElementById(id) |
||||
|
||||
searchInput.setAttribute('placeholder', 'Loading...') |
||||
|
||||
// Fetch External Data Source
|
||||
const source = await fetch( |
||||
`/token-autocomplete?q=${query}` |
||||
) |
||||
const data = await source.json() |
||||
// Post Loading placeholder text
|
||||
|
||||
searchInput.setAttribute('placeholder', placeHolder) |
||||
// Returns Fetched data
|
||||
return data |
||||
} catch (error) { |
||||
return error |
||||
} |
||||
} |
||||
const resultsListElement = (list, data) => { |
||||
const info = document.createElement('p') |
||||
const adv = ` |
||||
<div class="ad mb-3 d-none"> |
||||
Sponsored: <img class="ad-img-url" width=20 height=20 /> <b><span class="ad-name"></span></b> - <span class="ad-short-description"></span> <a class="ad-url"><b><span class="ad-cta-button"></span></a></b> |
||||
</div>` |
||||
info.innerHTML = adv |
||||
if (data.results.length > 0) { |
||||
info.innerHTML += `Displaying <strong>${data.results.length}</strong> results` |
||||
} else if (data.query !== '###') { |
||||
info.innerHTML += `Found <strong>${data.matches.length}</strong> matching results for <strong>"${data.query}"</strong>` |
||||
} |
||||
|
||||
list.prepend(info) |
||||
} |
||||
const searchEngine = (query, record) => { |
||||
if (record.name.toLowerCase().includes(query.toLowerCase()) || |
||||
record.symbol.toLowerCase().includes(query.toLowerCase()) || |
||||
record.contract_address_hash.toLowerCase().includes(query.toLowerCase())) { |
||||
var searchResult = `${record.contract_address_hash}<br/><b>${record.name}</b>` |
||||
if (record.symbol) { |
||||
searchResult = searchResult + ` (${record.symbol})` |
||||
} |
||||
if (record.holder_count) { |
||||
searchResult = searchResult + ` <i>${record.holder_count} holder(s)</i>` |
||||
} |
||||
var re = new RegExp(query, 'ig') |
||||
searchResult = searchResult.replace(re, '<mark class=\'autoComplete_highlight\'>$&</mark>') |
||||
return searchResult |
||||
} |
||||
} |
||||
const resultItemElement = (item, data) => { |
||||
// Modify Results Item Style
|
||||
item.style = 'display: flex; justify-content: space-between;' |
||||
// Modify Results Item Content
|
||||
item.innerHTML = ` |
||||
<span style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden;"> |
||||
${data.match} |
||||
</span>` |
||||
} |
||||
const config = (id) => { |
||||
return { |
||||
selector: `#${id}`, |
||||
data: { |
||||
src: (query) => dataSrc(query, id), |
||||
cache: false |
||||
}, |
||||
placeHolder: placeHolder, |
||||
searchEngine: (query, record) => searchEngine(query, record), |
||||
threshold: 2, |
||||
resultsList: { |
||||
element: (list, data) => resultsListElement(list, data), |
||||
noResults: true, |
||||
maxResults: 100, |
||||
tabSelect: true |
||||
}, |
||||
resultItem: { |
||||
element: (item, data) => resultItemElement(item, data), |
||||
highlight: 'autoComplete_highlight' |
||||
}, |
||||
events: { |
||||
input: { |
||||
focus: () => { |
||||
if (autoCompleteJS.input.value.length) autoCompleteJS.start() |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
const autoCompleteJS = new AutoComplete(config('main-search-autocomplete')) |
||||
// eslint-disable-next-line
|
||||
const _autoCompleteJSMobile = new AutoComplete(config('main-search-autocomplete-mobile')) |
||||
|
||||
const selection = (event) => { |
||||
const selectionValue = event.detail.selection.value |
||||
|
||||
if (selectionValue.symbol) { |
||||
window.location = `/tokens/${selectionValue.contract_address_hash}` |
||||
} else { |
||||
window.location = `/address/${selectionValue.contract_address_hash}` |
||||
} |
||||
} |
||||
|
||||
document.querySelector('#main-search-autocomplete').addEventListener('selection', function (event) { |
||||
selection(event) |
||||
}) |
||||
document.querySelector('#main-search-autocomplete-mobile').addEventListener('selection', function (event) { |
||||
selection(event) |
||||
}) |
||||
|
||||
const openOnFocus = (event) => { |
||||
const query = event.target.value |
||||
if (query) { |
||||
autoCompleteJS.start(query) |
||||
} |
||||
} |
||||
|
||||
document.querySelector('#main-search-autocomplete').addEventListener('focus', function (event) { |
||||
openOnFocus(event) |
||||
}) |
||||
|
||||
document.querySelector('#main-search-autocomplete-mobile').addEventListener('focus', function (event) { |
||||
openOnFocus(event) |
||||
}) |
@ -1,640 +0,0 @@ |
||||
/* eslint-env browser */ |
||||
/* global Awesomplete */ |
||||
/* exported AwesompleteUtil */ |
||||
|
||||
/* |
||||
* Library endorsing Lea Verou's Awesomplete widget, providing: |
||||
* - dynamic remote data loading |
||||
* - labels with HTML markup |
||||
* - events and styling for exact matches |
||||
* - events and styling for mismatches |
||||
* - select item when TAB key is used |
||||
* |
||||
* (c) Nico Hoogervorst |
||||
* License: MIT |
||||
* |
||||
*/ |
||||
window.AwesompleteUtil = (function () { |
||||
//
|
||||
// event names and css classes
|
||||
//
|
||||
var _AWE = 'awesomplete-' |
||||
var _AWE_LOAD = _AWE + 'loadcomplete' |
||||
var _AWE_CLOSE = _AWE + 'close' |
||||
var _AWE_MATCH = _AWE + 'match' |
||||
var _AWE_PREPOP = _AWE + 'prepop' |
||||
var _AWE_SELECT = _AWE + 'select' |
||||
var _CLS_FOUND = 'awe-found' |
||||
var _CLS_NOT_FOUND = 'awe-not-found' |
||||
var $ = Awesomplete.$ /* shortcut for document.querySelector */ |
||||
|
||||
//
|
||||
// private functions
|
||||
//
|
||||
|
||||
// Some parts are shamelessly copied from Awesomplete.js like the logic inside this _suggestion function.
|
||||
// Returns an object with label and value properties. Data parameter is plain text or Object/Array with label and value.
|
||||
function _suggestion (data) { |
||||
var lv = Array.isArray(data) |
||||
? { label: data[0], value: data[1] } |
||||
: typeof data === 'object' && 'label' in data && 'value' in data ? data : { label: data, value: data } |
||||
return { label: lv.label || lv.value, value: lv.value } |
||||
} |
||||
|
||||
// Helper to send events with detail property.
|
||||
function _fire (target, name, detail) { |
||||
// $.fire uses deprecated methods but other methods don't work in IE11.
|
||||
return $.fire(target, name, { detail: detail }) |
||||
} |
||||
|
||||
// Look if there is an exact match or a mismatch, set awe-found, awe-not-found css class and send match events.
|
||||
function _matchValue (awe, prepop) { |
||||
var input = awe.input /* the input field */ |
||||
var classList = input.classList |
||||
var utilprops = awe.utilprops /* extra properties piggybacked on Awesomplete object */ |
||||
var selected = utilprops.selected /* the exact selected Suggestion with label and value */ |
||||
var val = utilprops.convertInput.call(awe, input.value) /* trimmed lowercased value */ |
||||
var opened = awe.opened /* is the suggestion list opened? */ |
||||
var result = [] /* matches with value */ |
||||
var list = awe._list /* current list of suggestions */ |
||||
var suggestion, fake, rec, j /* function scoped variables */ |
||||
utilprops.prepop = false /* after the first call it's not a prepopulation phase anymore */ |
||||
if (list) { /* if there is a suggestion list */ |
||||
for (j = 0; j < list.length; j++) { /* loop all suggestions */ |
||||
rec = list[j] |
||||
suggestion = _suggestion(awe.data(rec, val)) /* call data convert function */ |
||||
// with maxItems = 0 cannot look if suggestion list is opened to determine if there are still matches,
|
||||
// instead call the filter method to see if there are still some options.
|
||||
if (awe.maxItems === 0) { |
||||
// Awesomplete.FILTER_CONTAINS and Awesomplete.FILTER_STARTSWITH use the toString method.
|
||||
suggestion.toString = function () { return '' + this.label } |
||||
if (awe.filter(suggestion, val)) { |
||||
// filter returns true, so there is at least one partial match.
|
||||
opened = true |
||||
} |
||||
} |
||||
// Don't want to change the real input field, emulate a fake one.
|
||||
fake = { input: { value: '' } } |
||||
// Determine how this suggestion would look like if it is replaced in the input field,
|
||||
// it is an exact match if somebody types exactly that.
|
||||
// Use the fake input here. fake.input.value will contain the result of the replace function.
|
||||
awe.replace.call(fake, suggestion) |
||||
// Trim and lowercase also the fake input and compare that with the currently typed-in value.
|
||||
if (utilprops.convertInput.call(awe, fake.input.value) === val) { |
||||
// This is an exact match. However there might more suggestions with the same value.
|
||||
// If the user selected a suggestion from the list, check if this one matches, assuming that
|
||||
// value + label is unique (if not it will be difficult for the user to make an informed decision).
|
||||
if (selected && selected.value === suggestion.value && selected.label === suggestion.label) { |
||||
// this surely is the selected one
|
||||
result = [rec] |
||||
break |
||||
} |
||||
// add the matching record to the result set.
|
||||
result.push(rec) |
||||
} // end if
|
||||
} // end loop
|
||||
|
||||
// if the result differs from the previous result
|
||||
if (utilprops.prevSelected !== result) { |
||||
// if there is an exact match
|
||||
if (result.length > 0) { |
||||
// if prepopulation phase (initial/autofill value); not triggered by user input
|
||||
if (prepop) { |
||||
_fire(input, _AWE_PREPOP, result) |
||||
} else if (utilprops.changed) { /* if input is changed */ |
||||
utilprops.prevSelected = result /* new result */ |
||||
classList.remove(_CLS_NOT_FOUND) /* remove class */ |
||||
classList.add(_CLS_FOUND) /* add css class */ |
||||
_fire(input, _AWE_MATCH, result) /* fire event */ |
||||
} |
||||
} else if (prepop) { /* no exact match, if in prepopulation phase */ |
||||
_fire(input, _AWE_PREPOP, []) |
||||
} else if (utilprops.changed) { /* no exact match, if input is changed */ |
||||
utilprops.prevSelected = [] |
||||
classList.remove(_CLS_FOUND) |
||||
// Mark as not-found if there are no suggestions anymore or if another field is now active
|
||||
if (!opened || (input !== document.activeElement)) { |
||||
if (val.length > 0) { |
||||
classList.add(_CLS_NOT_FOUND) |
||||
_fire(input, _AWE_MATCH, []) |
||||
} |
||||
} else { |
||||
classList.remove(_CLS_NOT_FOUND) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Listen to certain events of THIS awesomplete object to trigger input validation.
|
||||
function _match (ev) { |
||||
var awe = this |
||||
if ((ev.type === _AWE_CLOSE || ev.type === _AWE_LOAD || ev.type === 'blur') && ev.target === awe.input) { |
||||
_matchValue(awe, awe.utilprops.prepop && ev.type === _AWE_LOAD) |
||||
} |
||||
} |
||||
|
||||
// Select currently selected item if tab or shift-tab key is used.
|
||||
function _onKeydown (ev) { |
||||
var awe = this |
||||
if (ev.target === awe.input && ev.keyCode === 9) { // TAB key
|
||||
awe.select() // take current selected item
|
||||
} |
||||
} |
||||
|
||||
// Handle selection event. State changes when an item is selected.
|
||||
function _select (ev) { |
||||
var awe = this |
||||
awe.utilprops.changed = true // yes, user made a change
|
||||
awe.utilprops.selected = ev.text // Suggestion object
|
||||
const address = ev.text.split(/<p>/)[0] |
||||
window.open(`/search?q=${address}`, '_self') |
||||
} |
||||
|
||||
// check if the object is empty {} object
|
||||
function _isEmpty (val) { |
||||
return Object.keys(val).length === 0 && val.constructor === Object |
||||
} |
||||
|
||||
// Need an updated suggestion list if:
|
||||
// - There is no result yet, or there is a result but not for the characters we entered
|
||||
// - or there might be more specific results because the limit was reached.
|
||||
function _ifNeedListUpdate (awe, val, queryVal) { |
||||
var utilprops = awe.utilprops |
||||
return (!utilprops.listQuery || |
||||
(!utilprops.loadall && /* with loadall, if there is a result, there is no need for new lists */ |
||||
val.lastIndexOf(queryVal, 0) === 0 && |
||||
(val.lastIndexOf(utilprops.listQuery, 0) !== 0 || |
||||
(typeof utilprops.limit === 'number' && awe._list.length >= utilprops.limit)))) |
||||
} |
||||
|
||||
// Set a new suggestion list. Trigger loadcomplete event.
|
||||
function _loadComplete (awe, list, queryVal) { |
||||
awe.list = list |
||||
awe.utilprops.listQuery = queryVal |
||||
_fire(awe.input, _AWE_LOAD, queryVal) |
||||
} |
||||
|
||||
// Handle ajax response. Expects HTTP OK (200) response with JSON object with suggestion(s) (array).
|
||||
function _onLoad () { |
||||
var t = this |
||||
var awe = t.awe |
||||
var xhr = t.xhr |
||||
var queryVal = t.queryVal |
||||
var val = awe.utilprops.val |
||||
var data |
||||
var prop |
||||
if (xhr.status === 200) { |
||||
data = JSON.parse(xhr.responseText) |
||||
if (awe.utilprops.convertResponse) data = awe.utilprops.convertResponse(data) |
||||
if (!Array.isArray(data)) { |
||||
if (awe.utilprops.limit === 0 || awe.utilprops.limit === 1) { |
||||
// if there is max 1 result expected, the array is not needed.
|
||||
// Fur further processing, take the whole result and put it as one element in an array.
|
||||
data = _isEmpty(data) ? [] : [data] |
||||
} else { |
||||
// search for the first property that contains an array
|
||||
for (prop in data) { |
||||
if (Array.isArray(data[prop])) { |
||||
data = data[prop] |
||||
break |
||||
} |
||||
} |
||||
} |
||||
} |
||||
// can only handle arrays
|
||||
if (Array.isArray(data)) { |
||||
// are we still interested in this response?
|
||||
if (_ifNeedListUpdate(awe, val, queryVal)) { |
||||
// accept the new suggestion list
|
||||
_loadComplete(awe, data, queryVal || awe.utilprops.loadall) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Perform suggestion list lookup for the current value and validate. Use ajax when there is an url specified.
|
||||
function _lookup (awe, val) { |
||||
var xhr |
||||
if (awe.utilprops.url) { |
||||
// are we still interested in this response?
|
||||
if (_ifNeedListUpdate(awe, val, val)) { |
||||
xhr = new XMLHttpRequest() |
||||
awe.utilprops.ajax.call(awe, |
||||
awe.utilprops.url, |
||||
awe.utilprops.urlEnd, |
||||
awe.utilprops.loadall ? '' : val, |
||||
_onLoad.bind({ awe: awe, xhr: xhr, queryVal: val }), |
||||
xhr |
||||
) |
||||
} else { |
||||
_matchValue(awe, awe.utilprops.prepop) |
||||
} |
||||
} else { |
||||
_matchValue(awe, awe.utilprops.prepop) |
||||
} |
||||
} |
||||
|
||||
// Restart autocomplete search: clear css classes and send match-event with empty list.
|
||||
function _restart (awe) { |
||||
var elem = awe.input |
||||
var classList = elem.classList |
||||
// IE11 only handles the first parameter of the remove method.
|
||||
classList.remove(_CLS_NOT_FOUND) |
||||
classList.remove(_CLS_FOUND) |
||||
_fire(elem, _AWE_MATCH, []) |
||||
} |
||||
|
||||
// handle new input value
|
||||
function _update (awe, val, prepop) { |
||||
// prepop parameter is optional. Default value is false.
|
||||
awe.utilprops.prepop = prepop || false |
||||
// if value changed
|
||||
if (awe.utilprops.val !== val) { |
||||
// new value, clear previous selection
|
||||
awe.utilprops.selected = null |
||||
// yes, user made a change
|
||||
awe.utilprops.changed = true |
||||
awe.utilprops.val = val |
||||
// value is empty or smaller than minChars
|
||||
if (val.length < awe.minChars || val.length === 0) { |
||||
// restart autocomplete search
|
||||
_restart(awe) |
||||
} |
||||
if (val.length >= awe.minChars) { |
||||
// lookup suggestions and validate input
|
||||
_lookup(awe, val) |
||||
} |
||||
} |
||||
return awe |
||||
} |
||||
|
||||
// handle input changed event for THIS awesomplete object
|
||||
function _onInput (e) { |
||||
var awe = this |
||||
var val |
||||
if (e.target === awe.input) { |
||||
// lowercase and trim input value
|
||||
val = awe.utilprops.convertInput.call(awe, awe.input.value) |
||||
_update(awe, val) |
||||
} |
||||
} |
||||
|
||||
// item function (as specified in Awesomplete) which just creates the 'li' HTML tag.
|
||||
function _item (html /* , input */) { |
||||
return $.create('li', { |
||||
innerHTML: html, |
||||
'aria-selected': 'false' |
||||
}) |
||||
} |
||||
|
||||
// Escape HTML characters in text.
|
||||
function _htmlEscape (text) { |
||||
return text.replace('&', '&').replace('<', '<').replace('>', '>') |
||||
} |
||||
|
||||
// Function to copy a field from the selected autocomplete item to another DOM element.
|
||||
function _copyFun (e) { |
||||
var t = this |
||||
var sourceId = t.sourceId |
||||
var dataField = t.dataField |
||||
var targetId = t.targetId |
||||
var elem |
||||
var val |
||||
if (e.target === $(sourceId)) { |
||||
if (typeof targetId === 'function') { |
||||
targetId(e, dataField) |
||||
} else { |
||||
// lookup target element if it isn't resolved yet
|
||||
elem = $(targetId) |
||||
// don't override target inputs if user is currently editing it.
|
||||
if (elem && elem !== document.activeElement) { |
||||
// event must contain 1 item from suggestion list
|
||||
val = Array.isArray(e.detail) && e.detail.length === 1 ? e.detail[0] : null |
||||
// if a datafield is specified, take that value
|
||||
val = (dataField && val ? val[dataField] : val) || '' |
||||
// if it is an input control
|
||||
if (typeof elem.value !== 'undefined') { |
||||
// set new value
|
||||
elem.value = val |
||||
// not really sure if it is an input control, check if it has a classList
|
||||
if (elem.classList && elem.classList.remove) { |
||||
// it might be another awesomplete control, if so the input is not wrong anymore because it's changed now
|
||||
elem.classList.remove(_CLS_NOT_FOUND) |
||||
} |
||||
} else if (typeof elem.src !== 'undefined') { /* is it an image tag? */ |
||||
elem.src = val |
||||
} else { |
||||
// use innerHTML to set the new value, because value might intentionally contain HTML markup
|
||||
elem.innerHTML = val |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// click function for the combobox button
|
||||
function _clickFun (e) { |
||||
var t = this |
||||
var awe |
||||
var minChars |
||||
if (e.target === $(t.btnId)) { |
||||
e.preventDefault() |
||||
awe = t.awe |
||||
// toggle open/close
|
||||
if (awe.ul.childNodes.length === 0 || awe.ul.hasAttribute('hidden')) { |
||||
minChars = awe.minChars |
||||
// ignore that the input value is empty
|
||||
awe.minChars = 0 |
||||
// show the suggestion list
|
||||
awe.evaluate() |
||||
awe.minChars = minChars |
||||
} else { |
||||
awe.close() |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Return text with mark tags arround matching input. Don't replace inside <HTML> tags.
|
||||
// When startsWith is true, mark only the matching begin text.
|
||||
function _mark (text, input, startsWith) { |
||||
var searchText = $.regExpEscape(_htmlEscape(input).trim()) |
||||
var regExp = searchText.length <= 0 ? null : startsWith ? RegExp('^' + searchText, 'i') : RegExp('(?!<[^>]+?>)' + searchText + '(?![^<]*?>)', 'gi') |
||||
return text.replace(regExp, '<mark>$&</mark>') |
||||
} |
||||
|
||||
// Recursive jsonFlatten function
|
||||
function _jsonFlatten (result, cur, prop, level, opts) { |
||||
var root = opts.root /* filter resulting json tree on root property (optional) */ |
||||
var value = opts.value /* search for this property and copy it's value to a new 'value' property |
||||
(optional, do not specify it if the json array contains plain strings) */ |
||||
var label = opts.label || opts.value /* search this property and copy it's value to a new 'label' property. |
||||
If there is a 'opts.value' field but no 'opts.label', assume label is the same. */ |
||||
var isEmpty = true |
||||
var arrayResult = [] |
||||
var j |
||||
// at top level, look if there is a property which starts with root (if specified)
|
||||
if (level === 0 && root && prop && (prop + '.').lastIndexOf(root + '.', 0) !== 0 && (root + '.').lastIndexOf(prop + '.', 0) !== 0) { |
||||
return result |
||||
} |
||||
// handle current part of the json tree
|
||||
if (Object(cur) !== cur) { |
||||
if (prop) { |
||||
result[prop] = cur |
||||
} else { |
||||
result = cur |
||||
} |
||||
} else if (Array.isArray(cur)) { |
||||
for (j = 0; j < cur.length; j++) { |
||||
arrayResult.push(_jsonFlatten({}, cur[j], '', level + 1, opts)) |
||||
} |
||||
if (prop) { |
||||
result[prop] = arrayResult |
||||
} else { |
||||
result = arrayResult |
||||
} |
||||
} else { |
||||
for (j in cur) { |
||||
isEmpty = false |
||||
_jsonFlatten(result, cur[j], prop ? prop + '.' + j : j, level, opts) |
||||
} |
||||
if (isEmpty && prop) result[prop] = {} |
||||
} |
||||
// for arrays at top and subtop level
|
||||
if (level < 2 && prop) { |
||||
// if a 'value' is specified and found a mathing property, create extra 'value' property.
|
||||
if (value && (prop + '.').lastIndexOf(value + '.', 0) === 0) { result.value = result[prop] } |
||||
// if a 'label' is specified and found a mathing property, create extra 'label' property.
|
||||
if (label && (prop + '.').lastIndexOf(label + '.', 0) === 0) { result.label = result[prop] } |
||||
} |
||||
if (level === 0) { |
||||
// Make sure that both value and label properties exist, even if they are nil.
|
||||
// This is handy with limit 0 or 1 when the result doesn't have to contain an array.
|
||||
if (value && !('value' in result)) { result.value = null } |
||||
if (label && !('label' in result)) { result.label = null } |
||||
} |
||||
return result |
||||
} |
||||
|
||||
// Stop AwesompleteUtil; detach event handlers from the Awesomplete object.
|
||||
function _detach () { |
||||
var t = this |
||||
var elem = t.awe.input |
||||
var boundMatch = t.boundMatch |
||||
var boundOnInput = t.boundOnInput |
||||
var boundOnKeydown = t.boundOnKeydown |
||||
var boundSelect = t.boundSelect |
||||
|
||||
elem.removeEventListener(_AWE_SELECT, boundSelect) |
||||
elem.removeEventListener(_AWE_LOAD, boundMatch) |
||||
elem.removeEventListener(_AWE_CLOSE, boundMatch) |
||||
elem.removeEventListener('blur', boundMatch) |
||||
elem.removeEventListener('input', boundOnInput) |
||||
elem.removeEventListener('keydown', boundOnKeydown) |
||||
} |
||||
|
||||
//
|
||||
// public methods
|
||||
//
|
||||
|
||||
return { |
||||
|
||||
// ajax call for url + val + urlEnd. fn is the callback function. xhr parameter is optional.
|
||||
ajax: function (url, urlEnd, val, fn, xhr) { |
||||
xhr = xhr || new XMLHttpRequest() |
||||
xhr.open('GET', url + encodeURIComponent(val) + (urlEnd || '')) |
||||
xhr.onload = fn |
||||
xhr.send() |
||||
return xhr |
||||
}, |
||||
|
||||
// Convert input before comparing it with suggestion. lowercase and trim the text
|
||||
convertInput: function (text) { |
||||
return typeof text === 'string' ? text.trim().toLowerCase() : '' |
||||
}, |
||||
|
||||
// item function as defined in Awesomplete.
|
||||
// item(html, input). input is optional and ignored in this implementation
|
||||
item: _item, |
||||
|
||||
// Set a new suggestion list. Trigger loadcomplete event.
|
||||
// load(awesomplete, list, queryVal)
|
||||
load: _loadComplete, |
||||
|
||||
// Return text with mark tags arround matching input. Don't replace inside <HTML> tags.
|
||||
// When startsWith is true, mark only the matching begin text.
|
||||
// mark(text, input, startsWith)
|
||||
mark: _mark, |
||||
|
||||
// highlight items: Marks input in the first line, not in the optional description
|
||||
itemContains: function (text, input) { |
||||
var arr |
||||
if (input.trim().length > 0) { |
||||
arr = ('' + text).split(/<p>/) |
||||
arr[0] = _mark(arr[0], input) |
||||
text = arr.join('<p>') |
||||
} |
||||
return _item(text, input) |
||||
}, |
||||
|
||||
// highlight items: mark all occurrences of the input text
|
||||
itemMarkAll: function (text, input) { |
||||
return _item(input.trim() === '' ? '' + text : _mark('' + text, input), input) |
||||
}, |
||||
|
||||
// highlight items: mark input in the begin text
|
||||
itemStartsWith: function (text, input) { |
||||
return _item(input.trim() === '' ? '' + text : _mark('' + text, input, true), input) |
||||
}, |
||||
|
||||
// create Awesomplete object for input control elemId. opts are passed unchanged to Awesomplete.
|
||||
create: function (elemId, utilOpts, opts) { |
||||
opts.item = opts.item || this.itemContains /* by default uses itemContains, can be overriden */ |
||||
var awe = new Awesomplete(elemId, opts) |
||||
awe.utilprops = utilOpts || {} |
||||
// loadall is true if there is no url (there is a static data-list)
|
||||
if (!awe.utilprops.url && typeof awe.utilprops.loadall === 'undefined') { |
||||
awe.utilprops.loadall = true |
||||
} |
||||
awe.utilprops.ajax = awe.utilprops.ajax || this.ajax /* default ajax function can be overriden */ |
||||
awe.utilprops.convertInput = awe.utilprops.convertInput || this.convertInput /* the same applies for convertInput */ |
||||
return awe |
||||
}, |
||||
|
||||
// attach Awesomplete object to event listeners
|
||||
attach: function (awe) { |
||||
var elem = awe.input |
||||
var boundMatch = _match.bind(awe) |
||||
var boundOnKeydown = _onKeydown.bind(awe) |
||||
var boundOnInput = _onInput.bind(awe) |
||||
var boundSelect = _select.bind(awe) |
||||
var boundDetach = _detach.bind({ |
||||
awe: awe, |
||||
boundMatch: boundMatch, |
||||
boundOnInput: boundOnInput, |
||||
boundOnKeydown: boundOnKeydown, |
||||
boundSelect: boundSelect |
||||
}) |
||||
var events = { |
||||
keydown: boundOnKeydown, |
||||
input: boundOnInput |
||||
} |
||||
events.blur = events[_AWE_CLOSE] = events[_AWE_LOAD] = boundMatch |
||||
events[_AWE_SELECT] = boundSelect |
||||
$.bind(elem, events) |
||||
|
||||
awe.utilprops.detach = boundDetach |
||||
// Perform ajax call if prepop is true and there is an initial input value, or when all values must be loaded (loadall)
|
||||
if (awe.utilprops.prepop && (awe.utilprops.loadall || elem.value.length > 0)) { |
||||
awe.utilprops.val = awe.utilprops.convertInput.call(awe, elem.value) |
||||
_lookup(awe, awe.utilprops.val) |
||||
} |
||||
return awe |
||||
}, |
||||
|
||||
// update input value via javascript. Use prepop=true when this is an initial/prepopulation value.
|
||||
update: function (awe, value, prepop) { |
||||
awe.input.value = value |
||||
return _update(awe, value, prepop) |
||||
}, |
||||
|
||||
// create and attach Awesomplete object for input control elemId. opts are passed unchanged to Awesomplete.
|
||||
start: function (elemId, utilOpts, opts) { |
||||
return this.attach(this.create(elemId, utilOpts, opts)) |
||||
}, |
||||
|
||||
// Stop AwesompleteUtil; detach event handlers from the Awesomplete object.
|
||||
detach: function (awe) { |
||||
if (awe.utilprops.detach) { |
||||
awe.utilprops.detach() |
||||
delete awe.utilprops.detach |
||||
} |
||||
return awe |
||||
}, |
||||
|
||||
// Create function to copy a field from the selected autocomplete item to another DOM element.
|
||||
// dataField can be null.
|
||||
createCopyFun: function (sourceId, dataField, targetId) { |
||||
return _copyFun.bind({ sourceId: sourceId, dataField: dataField, targetId: $(targetId) || targetId }) |
||||
}, |
||||
|
||||
// attach copy function to event listeners. prepop is optional and by default true.
|
||||
// if true the copy function will also listen to awesomplete-prepop events.
|
||||
// The optional listenEl is the element that listens, defaults to document.body.
|
||||
attachCopyFun: function (fun, prepop, listenEl) { |
||||
// prepop parameter defaults to true
|
||||
prepop = typeof prepop === 'boolean' ? prepop : true |
||||
listenEl = listenEl || document.body |
||||
listenEl.addEventListener(_AWE_MATCH, fun) |
||||
if (prepop) listenEl.addEventListener(_AWE_PREPOP, fun) |
||||
return fun |
||||
}, |
||||
|
||||
// Create and attach copy function.
|
||||
startCopy: function (sourceId, dataField, targetId, prepop) { |
||||
var sourceEl = $(sourceId) |
||||
return this.attachCopyFun(this.createCopyFun(sourceEl || sourceId, dataField, targetId), prepop, sourceEl) |
||||
}, |
||||
|
||||
// Stop copy function. Detach it from event listeners.
|
||||
// The optional listenEl must be the same element that was used during startCopy/attachCopyFun;
|
||||
// in general: Awesomplete.$(sourceId). listenEl defaults to document.body.
|
||||
detachCopyFun: function (fun, listenEl) { |
||||
listenEl = listenEl || document.body |
||||
listenEl.removeEventListener(_AWE_PREPOP, fun) |
||||
listenEl.removeEventListener(_AWE_MATCH, fun) |
||||
return fun |
||||
}, |
||||
|
||||
// Create function for combobox button (btnId) to toggle dropdown list.
|
||||
createClickFun: function (btnId, awe) { |
||||
return _clickFun.bind({ btnId: btnId, awe: awe }) |
||||
}, |
||||
|
||||
// Attach click function for combobox to click event.
|
||||
// The optional listenEl is the element that listens, defaults to document.body.
|
||||
attachClickFun: function (fun, listenEl) { |
||||
listenEl = listenEl || document.body |
||||
listenEl.addEventListener('click', fun) |
||||
return fun |
||||
}, |
||||
|
||||
// Create and attach click function for combobox button. Toggles open/close of suggestion list.
|
||||
startClick: function (btnId, awe) { |
||||
var btnEl = $(btnId) |
||||
return this.attachClickFun(this.createClickFun(btnEl || btnId, awe), btnEl) |
||||
}, |
||||
|
||||
// Stop click function. Detach it from event listeners.
|
||||
// The optional listenEl must be the same element that was used during startClick/attachClickFun;
|
||||
// in general: Awesomplete.$(btnId). listenEl defaults to document.body.
|
||||
detachClickFun: function (fun, listenEl) { |
||||
listenEl = listenEl || document.body |
||||
listenEl.removeEventListener('click', fun) |
||||
return fun |
||||
}, |
||||
|
||||
// filter function as specified in Awesomplete. Filters suggestion list on items containing input value.
|
||||
// Awesomplete.FILTER_CONTAINS filters on data.label, however
|
||||
// this function filters on value and not on the shown label which may contain markup.
|
||||
filterContains: function (data, input) { |
||||
return Awesomplete.FILTER_CONTAINS(data.value, input) |
||||
}, |
||||
|
||||
// filter function as specified in Awesomplete. Filters suggestion list on matching begin text.
|
||||
// Awesomplete.FILTER_STARTSWITH filters on data.label, however
|
||||
// this function filters on value and not on the shown label which may contain markup.
|
||||
filterStartsWith: function (data, input) { |
||||
return Awesomplete.FILTER_STARTSWITH(data.value, input) |
||||
}, |
||||
|
||||
// Flatten JSON.
|
||||
// { "a":{"b":{"c":[{"d":{"e":1}}]}}} becomes {"a.b.c":[{"d.e":1}]}.
|
||||
// This function can be bind to configure it with extra options;
|
||||
// bind({root: '<root path>', value: '<value property>', label: '<label property>'})
|
||||
jsonFlatten: function (data) { |
||||
// start json tree recursion
|
||||
return _jsonFlatten({}, data, '', 0, this) |
||||
} |
||||
} |
||||
}()) |
@ -1,2 +0,0 @@ |
||||
import 'awesomplete/awesomplete.css' |
||||
import 'awesomplete' |
File diff suppressed because it is too large
Load Diff
@ -1,64 +1,43 @@ |
||||
<!-- Search navbar --> |
||||
<%= if Application.get_env(:block_scout_web, BlockScoutWeb.WebRouter)[:enabled] do %> |
||||
<div class="search-form d-lg-flex d-inline-block <%= if assigns[:additional_classes] do @additional_classes |> Enum.join(" ") end %>"> |
||||
<%= form_for @conn, chain_path(@conn, :search), [class: "form-inline my-2 my-lg-0", method: :get, enforce_utf8: false], fn f -> %> |
||||
<div class="input-group" style="width: 100%;" title='<%= gettext("Search by address, token symbol name, transaction hash, or block number") %>'> |
||||
<%= awesomplete(f, :q, |
||||
[ |
||||
class: "form-control search-control me auto", |
||||
placeholder: gettext("Search by address, token symbol, name, transaction hash, or block number"), |
||||
"aria-describedby": "search-icon", |
||||
"aria-label": gettext("Search"), |
||||
"data-test": "search_input" |
||||
], |
||||
[ url: "#{chain_path(@conn, :token_autocomplete)}?q=", |
||||
limit: 0, |
||||
maxItems: 1000, |
||||
minChars: 2, |
||||
value: "contract_address_hash", |
||||
label: "contract_address_hash", |
||||
descrSearch: true, |
||||
descr: "name", |
||||
sort: "function(x1, x2){ |
||||
const tokenName1 = x1.split('<b>').length > 1 ? x1.split('<b>')[1].split('</b>')[0].toLowerCase() : '' |
||||
const tokenName2 = x2.split('<b>').length > 1 ? x2.split('<b>')[1].split('</b>')[0].toLowerCase() : '' |
||||
const holdersCount1 = x1.split('<i>').length > 1 ? parseInt(x1.split('<i>')[1].split('</i>')[0].split('holder')[0], 10) : null |
||||
const holdersCount2 = x2.split('<i>').length > 1 ? parseInt(x2.split('<i>')[1].split('</i>')[0].split('holder')[0], 10) : null |
||||
if (holdersCount1 && holdersCount2 && holdersCount1 !== holdersCount2 || (holdersCount1 && !holdersCount2) || (!holdersCount1 && holdersCount2)) { |
||||
holdersCount1 > holdersCount2 |
||||
} else { |
||||
if (tokenName1 < tokenName2) { return -1 } |
||||
if (tokenName1 > tokenName2) { return 1 } |
||||
return 0 |
||||
} |
||||
}" |
||||
]) %> |
||||
<div class="input-group-append left"> |
||||
<button class="input-group-text" id="search-icon"> |
||||
<%= render BlockScoutWeb.IconsView, "_search_icon.html" %> |
||||
</button> |
||||
</div> |
||||
<div class="input-group-append right desktop-only"> |
||||
<div |
||||
id="slash-icon" |
||||
class="input-group-text border" |
||||
data-placement="bottom" |
||||
data-toggle="tooltip" |
||||
title="" |
||||
data-original-title='<%= gettext("Press / and focus will be moved to the search field") %>' |
||||
data-template="<div class='tooltip tooltip-pale-color' role='tooltip'><div class='arrow'></div><div class='tooltip-inner'></div></div>" |
||||
> |
||||
/ |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<button class="btn btn-outline-success my-2 my-sm-0 sr-only hidden" type="submit"><%= gettext "Search" %></button> |
||||
<div class="input-group" style="width: 100%;" title='<%= gettext("Search by address, token symbol name, transaction hash, or block number") %>'> |
||||
<div class="form-control search-control me auto <%= if assigns[:additional_classes] do @additional_classes |> Enum.join(" ") end %>"> |
||||
<input id="<%= @id %>" class="main-search-autocomplete" data-test="search_input" type="text" tabindex="1"> |
||||
</div> |
||||
<div class="input-group-append left"> |
||||
<button class="input-group-text" id="search-icon"> |
||||
<%= render BlockScoutWeb.IconsView, "_search_icon.html" %> |
||||
</button> |
||||
</div> |
||||
<div class="input-group-append right desktop-only"> |
||||
<div |
||||
id="slash-icon" |
||||
class="input-group-text border" |
||||
data-placement="bottom" |
||||
data-toggle="tooltip" |
||||
title="" |
||||
data-original-title='<%= gettext("Press / and focus will be moved to the search field") %>' |
||||
data-template="<div class='tooltip tooltip-pale-color' role='tooltip'><div class='arrow'></div><div class='tooltip-inner'></div></div>" |
||||
> |
||||
/ |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<button class="btn btn-outline-success my-2 my-sm-0 sr-only hidden" type="submit"><%= gettext "Search" %></button> |
||||
<script> |
||||
if (localStorage.getItem("current-color-mode") === "dark") { |
||||
document.getElementById("q").style.backgroundColor = "#22223a"; |
||||
document.getElementById("q").style.borderColor = "#22223a"; |
||||
const search = document.getElementById("main-search-autocomplete") |
||||
const searchMobile = document.getElementById("main-search-autocomplete-mobile") |
||||
if (search && search.style) { |
||||
search.style.backgroundColor = "#22223a"; |
||||
search.style.borderColor = "#22223a"; |
||||
} |
||||
if (searchMobile && searchMobile.style) { |
||||
searchMobile.style.backgroundColor = "#22223a"; |
||||
searchMobile.style.borderColor = "#22223a"; |
||||
} |
||||
} |
||||
</script> |
||||
<% end %> |
||||
</div> |
||||
<% end %> |
Loading…
Reference in new issue