Merge pull request #4382 from blockscout/vb-autocomplete

Replace awesomplete with autocomplete.js
pull/4388/head
Victor Baranov 3 years ago committed by GitHub
commit 230ea9f5bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 24
      .github/workflows/config.yml
  2. 1
      CHANGELOG.md
  3. 2
      apps/block_scout_web/assets/css/_layout.scss
  4. 26
      apps/block_scout_web/assets/css/components/_navbar.scss
  5. 39
      apps/block_scout_web/assets/css/components/_search.scss
  6. 50
      apps/block_scout_web/assets/css/theme/_dark-theme.scss
  7. 23
      apps/block_scout_web/assets/css/theme/custom_contracts/_circles-theme.scss
  8. 23
      apps/block_scout_web/assets/css/theme/custom_contracts/_dark-forest-theme.scss
  9. 129
      apps/block_scout_web/assets/js/lib/autocomplete.js
  10. 640
      apps/block_scout_web/assets/js/lib/awesomplete-util.js
  11. 2
      apps/block_scout_web/assets/js/lib/awesomplete.js
  12. 36
      apps/block_scout_web/assets/js/pages/layout.js
  13. 5825
      apps/block_scout_web/assets/package-lock.json
  14. 26
      apps/block_scout_web/assets/package.json
  15. 9
      apps/block_scout_web/assets/webpack.config.js
  16. 2
      apps/block_scout_web/lib/block_scout_web.ex
  17. 2
      apps/block_scout_web/lib/block_scout_web/csp_header.ex
  18. 89
      apps/block_scout_web/lib/block_scout_web/templates/layout/_search.html.eex
  19. 9
      apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex
  20. 1
      apps/block_scout_web/mix.exs
  21. 52
      apps/block_scout_web/priv/gettext/default.pot
  22. 52
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  23. 9
      apps/explorer/lib/explorer/chain.ex
  24. 1
      mix.lock

@ -20,7 +20,7 @@ jobs:
- uses: actions/setup-elixir@v1
with:
otp-version: '23.3.4.1'
elixir-version: '1.11.3'
elixir-version: '1.11.4'
- run: mix local.hex --force
- run: mix local.rebar --force
- name: "ELIXIR_VERSION.lock"
@ -47,7 +47,7 @@ jobs:
- uses: actions/setup-elixir@v1
with:
otp-version: '23.3.4.1'
elixir-version: '1.11.3'
elixir-version: '1.11.4'
- run: mix local.hex --force
- run: mix local.rebar --force
- run: mix deps.get
@ -62,7 +62,7 @@ jobs:
- uses: actions/setup-elixir@v1
with:
otp-version: '23.3.4.1'
elixir-version: '1.11.3'
elixir-version: '1.11.4'
- run: mix local.hex --force
- run: mix local.rebar --force
- run: mix deps.get
@ -77,7 +77,7 @@ jobs:
- uses: actions/setup-elixir@v1
with:
otp-version: '23.3.4.1'
elixir-version: '1.11.3'
elixir-version: '1.11.4'
- run: mix local.hex --force
- run: mix local.rebar --force
- run: mix deps.get
@ -104,7 +104,7 @@ jobs:
- uses: actions/setup-elixir@v1
with:
otp-version: '23.3.4.1'
elixir-version: '1.11.3'
elixir-version: '1.11.4'
- run: mix local.hex --force
- run: mix local.rebar --force
- run: mix deps.get
@ -122,7 +122,7 @@ jobs:
- uses: actions/setup-elixir@v1
with:
otp-version: '23.3.4.1'
elixir-version: '1.11.3'
elixir-version: '1.11.4'
- run: mix local.hex --force
- run: mix local.rebar --force
- run: mix deps.get
@ -143,7 +143,7 @@ jobs:
- uses: actions/setup-elixir@v1
with:
otp-version: '23.3.4.1'
elixir-version: '1.11.3'
elixir-version: '1.11.4'
- run: mix local.hex --force
- run: mix local.rebar --force
- run: mix deps.get
@ -168,7 +168,7 @@ jobs:
- uses: actions/setup-elixir@v1
with:
otp-version: '23.3.4.1'
elixir-version: '1.11.3'
elixir-version: '1.11.4'
- run: mix local.hex --force
- run: mix local.rebar --force
- run: mix deps.get
@ -212,7 +212,7 @@ jobs:
- uses: actions/setup-elixir@v1
with:
otp-version: '23.3.4.1'
elixir-version: '1.11.3'
elixir-version: '1.11.4'
- run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
- run: echo 'export PATH=~/.cargo/bin/:$PATH' >> $GITHUB_ENV
- run: mix local.hex --force
@ -268,7 +268,7 @@ jobs:
- uses: actions/setup-elixir@v1
with:
otp-version: '23.3.4.1'
elixir-version: '1.11.3'
elixir-version: '1.11.4'
- run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
- run: echo 'export PATH=~/.cargo/bin/:$PATH' >> $GITHUB_ENV
- run: mix local.hex --force
@ -326,7 +326,7 @@ jobs:
- uses: actions/setup-elixir@v1
with:
otp-version: '23.3.4.1'
elixir-version: '1.11.3'
elixir-version: '1.11.4'
- run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
- run: echo 'export PATH=~/.cargo/bin/:$PATH' >> $GITHUB_ENV
- run: mix local.hex --force
@ -384,7 +384,7 @@ jobs:
- uses: actions/setup-elixir@v1
with:
otp-version: '23.3.4.1'
elixir-version: '1.11.3'
elixir-version: '1.11.4'
- run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
- run: echo 'export PATH=~/.cargo/bin/:$PATH' >> $GITHUB_ENV
- run: mix local.hex --force

@ -5,6 +5,7 @@
### Fixes
### Chore
- [#4382](https://github.com/blockscout/blockscout/pull/4382) - Replace awesomplete with autocomplete.js
- [#4371] - (https://github.com/blockscout/blockscout/pull/4371) - Place search outside of burger in mobile view
- [#4355](https://github.com/blockscout/blockscout/pull/4355) - Do not redirect to 404 page with empty string in the search field

@ -1,3 +1,5 @@
@import '~@tarekraafat/autocomplete.js/dist/css/autoComplete.01.css';
.layout-container {
display: flex;
flex-direction: column;

@ -209,22 +209,6 @@ $navbar-logo-width: auto !default;
}
}
.input-group {
.awesomplete > ul {
overflow-y: auto !important;
max-height: 300px !important;
}
.awesomplete mark {
background: #feff54 !important;
}
.awesomplete li:hover mark {
background: #feff54 !important;
}
}
.navbar-collapse.collapsing,
.navbar-collapse.collapse.show {
display: flex;
@ -242,12 +226,6 @@ $navbar-logo-width: auto !default;
}
.input-group {
width: 100%;
.awesomplete {
@include media-breakpoint-down(lg) {
width: 100%;
}
}
}
.navbar-nav {
white-space: nowrap;
@ -313,8 +291,4 @@ $navbar-logo-width: auto !default;
.visually-hidden {
display: block;
}
.focused-field {
border: 1px solid $secondary !important;
}

@ -18,4 +18,43 @@
@include media-breakpoint-down(lg) {
height: 33px !important;
}
&.focused-field {
border: 1px solid $secondary !important;
}
}
.autoComplete_wrapper {
width: 100% !important;
}
.main-search-autocomplete {
width: 100% !important;
padding: 0 !important;
border: none !important;
border-radius: 0 !important;
background-color: #f5f6fa !important;
@include media-breakpoint-down(lg) {
height: auto !important;
}
}
.main-search-autocomplete, .main-search-autocomplete-mobile {
font-size: 14px !important;
color: #828ba0 !important;
}
.autoComplete_highlight {
background-color: #feff54 !important;
color: #212121 !important;
}
ul[id^='autoComplete_list_'] {
margin-left: -39px !important;
border-radius: 0px !important;
padding: 5px !important;
}
li[id^='autoComplete_result_'] {
font-size: 12px !important;
border-radius: 0 !important;
}

@ -766,29 +766,6 @@ $dark-stakes-banned-background: #3e314c;
}
}
.awesomplete {
& > ul {
background: $dark-light-bg;
&:before {
background: $dark-light-bg;
}
li {
&:hover {
background-color: $dark-primary;
color: #fff;
mark {
background: darken($dark-primary, 10);
color: #fff;
}
}
}
}
mark {
background: $dark-primary;
color: #fff;
}
}
// Decoded data
.table.thead-light.table-bordered {
color: #fff !important;
@ -1091,15 +1068,6 @@ $dark-stakes-banned-background: #3e314c;
background-image: none;
}
.input-group {
.awesomplete mark {
background: $yellow !important;
}
.awesomplete li:hover mark {
background: $yellow !important;
}
}
.contract-plus-btn {
color: $dark-primary;
}
@ -1111,6 +1079,24 @@ $dark-stakes-banned-background: #3e314c;
fill: #fff;
}
}
.main-search-autocomplete {
background-color: $dark-bg !important;
color: #fff !important;
}
ul[id^='autoComplete_list_'] {
background-color: $dark-light-bg !important;
}
li[id^='autoComplete_result_'] {
background-color: $dark-light-bg !important;
color: #fff !important;
&:hover {
background-color: $dark-primary !important;
}
&[aria-selected="true"] {
background-color: $dark-primary !important;
}
}
}
.navbar-dark .navbar-toggler {

@ -376,29 +376,6 @@ $c-dark-text-color: #8a8dba;
}
}
.awesomplete {
& > ul {
background: $c-primary;
&:before {
background: $c-primary;
}
li {
&:hover {
background-color: $c-primary;
color: #fff;
mark {
background: darken($c-primary, 10);
color: #fff;
}
}
}
}
mark {
background: $c-primary;
color: #fff;
}
}
#qrModal {
.modal-content {

@ -714,29 +714,6 @@ $dark-primary-alternate: $dark-primary;
}
}
.awesomplete {
& > ul {
background: $dark-light-bg;
&:before {
background: $dark-light-bg;
}
li {
&:hover {
background-color: $dark-primary;
color: #fff;
mark {
background: darken($dark-primary, 10);
color: #fff;
}
}
}
}
mark {
background: $dark-primary;
color: #fff;
}
}
// Decoded data
.table.thead-light.table-bordered {
color: #fff !important;

@ -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('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
}
// 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'

@ -8,18 +8,44 @@ $(document).click(function (event) {
}
})
const search = (value) => {
if (value) {
window.location.href = `/search?q=${value}`
}
}
$(document).on('keyup', function (event) {
if (event.key === '/') {
$('#q').trigger('focus')
$('.main-search-autocomplete').trigger('focus')
}
})
$('#q').on('focus', function (_event) {
$('.main-search-autocomplete').on('keyup', function (event) {
if (event.key === 'Enter') {
console.log($('li[id^="autoComplete_result_"]'))
var selected = false
$('li[id^="autoComplete_result_"]').each(function () {
if ($(this).attr('aria-selected')) {
selected = true
}
})
if (!selected) {
search(event.target.value)
}
}
})
$('#search-icon').on('click', function (event) {
const value = $('.main-search-autocomplete').val() || $('.main-search-autocomplete-mobile').val()
search(value)
})
$('.main-search-autocomplete').on('focus', function (_event) {
$('#slash-icon').hide()
$(this).addClass('focused-field')
$('.search-control').addClass('focused-field')
})
$('#q').on('focusout', function (_event) {
$('.main-search-autocomplete').on('focusout', function (_event) {
$('#slash-icon').show()
$(this).removeClass('focused-field')
$('.search-control').removeClass('focused-field')
})

File diff suppressed because it is too large Load Diff

@ -20,8 +20,8 @@
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.3",
"@tarekraafat/autocomplete.js": "^10.2.5",
"assert": "^2.0.0",
"awesomplete": "^1.1.5",
"bignumber.js": "^9.0.0",
"bootstrap": "^4.3.1",
"chart.js": "^3.3.2",
@ -55,14 +55,14 @@
"web3": "^1.3.5"
},
"devDependencies": {
"@babel/core": "^7.7.2",
"@babel/polyfill": "^7.7.0",
"@babel/preset-env": "^7.7.1",
"@babel/core": "^7.14.6",
"@babel/polyfill": "^7.12.1",
"@babel/preset-env": "^7.14.7",
"autoprefixer": "^8.4.1",
"babel-loader": "^8.0.6",
"babel-loader": "^8.2.2",
"copy-webpack-plugin": "^6.0.3",
"css-loader": "^3.1.0",
"css-minimizer-webpack-plugin": "^3.0.1",
"css-loader": "^3.6.0",
"css-minimizer-webpack-plugin": "^3.0.2",
"eslint": "^6.6.0",
"eslint-config-standard": "^14.1.0",
"eslint-plugin-import": "^2.18.2",
@ -71,14 +71,14 @@
"eslint-plugin-standard": "^4.0.1",
"file-loader": "^6.2.0",
"jest": "^25.1.0",
"mini-css-extract-plugin": "^1.3.9",
"mini-css-extract-plugin": "^1.6.2",
"node-sass": "^5.0.0",
"postcss": "^8.1.10",
"postcss-loader": "^4.1.0",
"sass-loader": "^11.0.1",
"postcss": "^8.3.5",
"postcss-loader": "^4.3.0",
"sass-loader": "^11.1.1",
"style-loader": "^1.3.0",
"webpack": "^5.8.0",
"webpack-cli": "^4.2.0"
"webpack": "^5.44.0",
"webpack-cli": "^4.7.2"
},
"jest": {
"moduleNameMapper": {

@ -32,10 +32,9 @@ const jsOptimizationParams = {
parallel: true
}
const awesompleteJs = {
const autocompleteJs = {
entry: {
awesomplete: './js/lib/awesomplete.js',
'awesomplete-util': './js/lib/awesomplete-util.js',
autocomplete: './js/lib/autocomplete.js',
},
output: {
filename: '[name].min.js',
@ -61,7 +60,7 @@ const awesompleteJs = {
},
plugins: [
new MiniCssExtractPlugin({
filename: '../css/awesomplete.css'
filename: '../css/autocomplete.css'
})
]
}
@ -211,4 +210,4 @@ const appJs =
const viewScripts = glob.sync('./js/view_specific/**/*.js').map(transpileViewScript)
module.exports = viewScripts.concat(appJs, awesompleteJs, dropzoneJs)
module.exports = viewScripts.concat(appJs, autocompleteJs, dropzoneJs)

@ -57,8 +57,6 @@ defmodule BlockScoutWeb do
}
import BlockScoutWeb.WebRouter.Helpers, except: [static_path: 2]
import PhoenixFormAwesomplete
end
end

@ -11,7 +11,7 @@ defmodule BlockScoutWeb.CSPHeader do
def call(conn, _opts) do
Controller.put_secure_browser_headers(conn, %{
"content-security-policy" => "\
connect-src 'self' #{websocket_endpoints(conn)}; \
connect-src 'self' #{websocket_endpoints(conn)};\
default-src 'self';\
script-src 'self' 'unsafe-inline' 'unsafe-eval';\
style-src 'self' 'unsafe-inline' 'unsafe-eval' https://fonts.googleapis.com;\

@ -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 %>

@ -1,7 +1,3 @@
<link rel="preload" href="<%= static_path(@conn, "/css/awesomplete.css") %>" as="style" onload="this.rel='stylesheet'">
<link rel="stylesheet" href="<%= static_path(@conn, "/css/awesomplete.css") %>">
<script src="<%= static_path(@conn, "/js/awesomplete.min.js") %>"></script>
<script src="<%= static_path(@conn, "/js/awesomplete-util.min.js") %>"></script>
<% staking_enabled_in_menu = Application.get_env(:block_scout_web, BlockScoutWeb.Chain)[:staking_enabled_in_menu] %>
<% apps_menu = Application.get_env(:block_scout_web, :apps_menu) %>
<nav class="navbar navbar-dark navbar-expand-lg navbar-primary" data-selector="navbar" id="top-navbar">
@ -204,10 +200,10 @@
<path fill="#9B62FF" fill-rule="evenodd" d="M14.88 11.578a.544.544 0 0 0-.599-.166 5.7 5.7 0 0 1-1.924.321c-3.259 0-5.91-2.632-5.91-5.866 0-1.947.968-3.759 2.59-4.849a.534.534 0 0 0-.225-.97A5.289 5.289 0 0 0 8.059 0C3.615 0 0 3.588 0 8s3.615 8 8.059 8c2.82 0 5.386-1.423 6.862-3.806a.533.533 0 0 0-.041-.616z"/>
</svg>
</button>
<%= render BlockScoutWeb.LayoutView, "_search.html", conn: @conn, additional_classes: ["mobile-search-hide"] %>
<%= render BlockScoutWeb.LayoutView, "_search.html", conn: @conn, id: "main-search-autocomplete", additional_classes: ["mobile-search-hide"] %>
</div>
</div>
<%= render BlockScoutWeb.LayoutView, "_search.html", conn: @conn, additional_classes: ["mobile-search-show"] %>
<%= render BlockScoutWeb.LayoutView, "_search.html", conn: @conn, id: "main-search-autocomplete-mobile", additional_classes: ["mobile-search-show"] %>
</nav>
<script>
if (localStorage.getItem("current-color-mode") === "dark") {
@ -215,3 +211,4 @@
modeChanger.className += " " + "dark-mode-changer--dark";
}
</script>
<script defer src="<%= static_path(@conn, "/js/autocomplete.min.js") %>"></script>

@ -125,7 +125,6 @@ defmodule BlockScoutWeb.Mixfile do
{:wallaby, "~> 0.28", only: :test, runtime: false},
# `:cowboy` `~> 2.0` and Phoenix 1.4 compatibility
{:wobserver, "~> 0.2.0", github: "poanetwork/wobserver", branch: "support-https"},
{:phoenix_form_awesomplete, "~> 0.1.4"},
{:ex_json_schema, "~> 0.6.2"}
]
end

@ -148,7 +148,7 @@ msgid "API for the %{subnetwork} - BlockScout"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:126
#: lib/block_scout_web/templates/layout/_topnav.html.eex:122
msgid "APIs"
msgstr ""
@ -164,7 +164,7 @@ msgid "APY & Predicted Reward"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:80
#: lib/block_scout_web/templates/layout/_topnav.html.eex:76
msgid "Accounts"
msgstr ""
@ -190,7 +190,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:28
#: lib/block_scout_web/templates/address_transaction/index.html.eex:26
#: lib/block_scout_web/templates/layout/_network_selector.html.eex:21
#: lib/block_scout_web/templates/layout/_topnav.html.eex:93
#: lib/block_scout_web/templates/layout/_topnav.html.eex:89
#: lib/block_scout_web/views/address_internal_transaction_view.ex:10
#: lib/block_scout_web/views/address_token_transfer_view.ex:10
#: lib/block_scout_web/views/address_transaction_view.ex:10
@ -250,7 +250,7 @@ msgid "Approximate Current Annual Percentage Yield. If you see N/A, please wait
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:154
#: lib/block_scout_web/templates/layout/_topnav.html.eex:150
msgid "Apps"
msgstr ""
@ -374,8 +374,8 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/chain/show.html.eex:178
#: lib/block_scout_web/templates/layout/_topnav.html.eex:32
#: lib/block_scout_web/templates/layout/_topnav.html.eex:36
#: lib/block_scout_web/templates/layout/_topnav.html.eex:40
msgid "Blocks"
msgstr ""
@ -407,12 +407,12 @@ msgid "Bridged Tokens from "
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:103
#: lib/block_scout_web/templates/layout/_topnav.html.eex:99
msgid "Bridged from BSC"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:98
#: lib/block_scout_web/templates/layout/_topnav.html.eex:94
msgid "Bridged from Ethereum"
msgstr ""
@ -969,7 +969,7 @@ msgid "Error: Could not determine contract creator."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:140
#: lib/block_scout_web/templates/layout/_topnav.html.eex:136
msgid "Eth RPC"
msgstr ""
@ -1034,7 +1034,7 @@ msgid "For any existing contracts in the database, insert all ABI entries into t
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:46
#: lib/block_scout_web/templates/layout/_topnav.html.eex:42
msgid "Forked Blocks (Reorgs)"
msgstr ""
@ -1083,7 +1083,7 @@ msgid "Github"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:130
#: lib/block_scout_web/templates/layout/_topnav.html.eex:126
msgid "GraphQL"
msgstr ""
@ -1523,7 +1523,7 @@ msgid "Parent Hash"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:67
#: lib/block_scout_web/templates/layout/_topnav.html.eex:63
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:184
#: lib/block_scout_web/views/transaction_view.ex:261
#: lib/block_scout_web/views/transaction_view.ex:295
@ -1590,7 +1590,7 @@ msgid "Potential matches from our contract method database:"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_search.html.eex:48
#: lib/block_scout_web/templates/layout/_search.html.eex:20
msgid "Press / and focus will be moved to the search field"
msgstr ""
@ -1613,7 +1613,7 @@ msgid "Query"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:135
#: lib/block_scout_web/templates/layout/_topnav.html.eex:131
msgid "RPC"
msgstr ""
@ -1729,21 +1729,15 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_logs/index.html.eex:16
#: lib/block_scout_web/templates/layout/_search.html.eex:11
#: lib/block_scout_web/templates/layout/_search.html.eex:55
#: lib/block_scout_web/templates/layout/_search.html.eex:27
msgid "Search"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_search.html.eex:5
#: lib/block_scout_web/templates/layout/_search.html.eex:4
msgid "Search by address, token symbol name, transaction hash, or block number"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_search.html.eex:9
msgid "Search by address, token symbol, name, transaction hash, or block number"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_network_selector.html.eex:18
msgid "Search network"
@ -1889,7 +1883,7 @@ msgid "Staker's Address"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:156
#: lib/block_scout_web/templates/layout/_topnav.html.eex:152
msgid "Stakes"
msgstr ""
@ -1901,7 +1895,7 @@ msgid "Stakes Ratio"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:162
#: lib/block_scout_web/templates/layout/_topnav.html.eex:158
msgid "Staking"
msgstr ""
@ -2197,7 +2191,7 @@ msgid "To see accurate decoded input data, the contract must be verified."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:25
#: lib/block_scout_web/templates/layout/_topnav.html.eex:21
msgid "Toggle navigation"
msgstr ""
@ -2260,8 +2254,8 @@ msgstr ""
#: lib/block_scout_web/templates/address/_tabs.html.eex:21
#: lib/block_scout_web/templates/address_token/index.html.eex:10
#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:13
#: lib/block_scout_web/templates/layout/_topnav.html.eex:89
#: lib/block_scout_web/templates/layout/_topnav.html.eex:115
#: lib/block_scout_web/templates/layout/_topnav.html.eex:85
#: lib/block_scout_web/templates/layout/_topnav.html.eex:111
#: lib/block_scout_web/templates/tokens/index.html.eex:4
#: lib/block_scout_web/views/address_view.ex:343
msgid "Tokens"
@ -2346,7 +2340,7 @@ msgstr ""
#: lib/block_scout_web/templates/block_transaction/index.html.eex:10
#: lib/block_scout_web/templates/block_transaction/index.html.eex:18
#: lib/block_scout_web/templates/chain/show.html.eex:236
#: lib/block_scout_web/templates/layout/_topnav.html.eex:55
#: lib/block_scout_web/templates/layout/_topnav.html.eex:51
#: lib/block_scout_web/views/address_view.ex:345
msgid "Transactions"
msgstr ""
@ -2408,7 +2402,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/overview.html.eex:80
#: lib/block_scout_web/templates/layout/_topnav.html.eex:43
#: lib/block_scout_web/templates/layout/_topnav.html.eex:39
msgid "Uncles"
msgstr ""
@ -2445,7 +2439,7 @@ msgid "Used"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:59
#: lib/block_scout_web/templates/layout/_topnav.html.eex:55
msgid "Validated"
msgstr ""

@ -148,7 +148,7 @@ msgid "API for the %{subnetwork} - BlockScout"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:126
#: lib/block_scout_web/templates/layout/_topnav.html.eex:122
msgid "APIs"
msgstr ""
@ -164,7 +164,7 @@ msgid "APY & Predicted Reward"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:80
#: lib/block_scout_web/templates/layout/_topnav.html.eex:76
msgid "Accounts"
msgstr ""
@ -190,7 +190,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:28
#: lib/block_scout_web/templates/address_transaction/index.html.eex:26
#: lib/block_scout_web/templates/layout/_network_selector.html.eex:21
#: lib/block_scout_web/templates/layout/_topnav.html.eex:93
#: lib/block_scout_web/templates/layout/_topnav.html.eex:89
#: lib/block_scout_web/views/address_internal_transaction_view.ex:10
#: lib/block_scout_web/views/address_token_transfer_view.ex:10
#: lib/block_scout_web/views/address_transaction_view.ex:10
@ -250,7 +250,7 @@ msgid "Approximate Current Annual Percentage Yield. If you see N/A, please wait
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:154
#: lib/block_scout_web/templates/layout/_topnav.html.eex:150
msgid "Apps"
msgstr ""
@ -374,8 +374,8 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/chain/show.html.eex:178
#: lib/block_scout_web/templates/layout/_topnav.html.eex:32
#: lib/block_scout_web/templates/layout/_topnav.html.eex:36
#: lib/block_scout_web/templates/layout/_topnav.html.eex:40
msgid "Blocks"
msgstr ""
@ -407,12 +407,12 @@ msgid "Bridged Tokens from "
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:103
#: lib/block_scout_web/templates/layout/_topnav.html.eex:99
msgid "Bridged from BSC"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:98
#: lib/block_scout_web/templates/layout/_topnav.html.eex:94
msgid "Bridged from Ethereum"
msgstr ""
@ -969,7 +969,7 @@ msgid "Error: Could not determine contract creator."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:140
#: lib/block_scout_web/templates/layout/_topnav.html.eex:136
msgid "Eth RPC"
msgstr ""
@ -1034,7 +1034,7 @@ msgid "For any existing contracts in the database, insert all ABI entries into t
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:46
#: lib/block_scout_web/templates/layout/_topnav.html.eex:42
msgid "Forked Blocks (Reorgs)"
msgstr ""
@ -1083,7 +1083,7 @@ msgid "Github"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:130
#: lib/block_scout_web/templates/layout/_topnav.html.eex:126
msgid "GraphQL"
msgstr ""
@ -1523,7 +1523,7 @@ msgid "Parent Hash"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:67
#: lib/block_scout_web/templates/layout/_topnav.html.eex:63
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:184
#: lib/block_scout_web/views/transaction_view.ex:261
#: lib/block_scout_web/views/transaction_view.ex:295
@ -1590,7 +1590,7 @@ msgid "Potential matches from our contract method database:"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_search.html.eex:48
#: lib/block_scout_web/templates/layout/_search.html.eex:20
msgid "Press / and focus will be moved to the search field"
msgstr ""
@ -1613,7 +1613,7 @@ msgid "Query"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:135
#: lib/block_scout_web/templates/layout/_topnav.html.eex:131
msgid "RPC"
msgstr ""
@ -1729,21 +1729,15 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_logs/index.html.eex:16
#: lib/block_scout_web/templates/layout/_search.html.eex:11
#: lib/block_scout_web/templates/layout/_search.html.eex:55
#: lib/block_scout_web/templates/layout/_search.html.eex:27
msgid "Search"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_search.html.eex:5
#: lib/block_scout_web/templates/layout/_search.html.eex:4
msgid "Search by address, token symbol name, transaction hash, or block number"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_search.html.eex:9
msgid "Search by address, token symbol, name, transaction hash, or block number"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_network_selector.html.eex:18
msgid "Search network"
@ -1889,7 +1883,7 @@ msgid "Staker's Address"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:156
#: lib/block_scout_web/templates/layout/_topnav.html.eex:152
msgid "Stakes"
msgstr ""
@ -1901,7 +1895,7 @@ msgid "Stakes Ratio"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:162
#: lib/block_scout_web/templates/layout/_topnav.html.eex:158
msgid "Staking"
msgstr ""
@ -2197,7 +2191,7 @@ msgid "To see accurate decoded input data, the contract must be verified."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:25
#: lib/block_scout_web/templates/layout/_topnav.html.eex:21
msgid "Toggle navigation"
msgstr ""
@ -2260,8 +2254,8 @@ msgstr ""
#: lib/block_scout_web/templates/address/_tabs.html.eex:21
#: lib/block_scout_web/templates/address_token/index.html.eex:10
#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:13
#: lib/block_scout_web/templates/layout/_topnav.html.eex:89
#: lib/block_scout_web/templates/layout/_topnav.html.eex:115
#: lib/block_scout_web/templates/layout/_topnav.html.eex:85
#: lib/block_scout_web/templates/layout/_topnav.html.eex:111
#: lib/block_scout_web/templates/tokens/index.html.eex:4
#: lib/block_scout_web/views/address_view.ex:343
msgid "Tokens"
@ -2346,7 +2340,7 @@ msgstr ""
#: lib/block_scout_web/templates/block_transaction/index.html.eex:10
#: lib/block_scout_web/templates/block_transaction/index.html.eex:18
#: lib/block_scout_web/templates/chain/show.html.eex:236
#: lib/block_scout_web/templates/layout/_topnav.html.eex:55
#: lib/block_scout_web/templates/layout/_topnav.html.eex:51
#: lib/block_scout_web/views/address_view.ex:345
msgid "Transactions"
msgstr ""
@ -2408,7 +2402,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/overview.html.eex:80
#: lib/block_scout_web/templates/layout/_topnav.html.eex:43
#: lib/block_scout_web/templates/layout/_topnav.html.eex:39
msgid "Uncles"
msgstr ""
@ -2445,7 +2439,7 @@ msgid "Used"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:59
#: lib/block_scout_web/templates/layout/_topnav.html.eex:55
msgid "Validated"
msgstr ""

@ -1113,13 +1113,8 @@ defmodule Explorer.Chain do
select: %{
contract_address_hash: token.contract_address_hash,
symbol: token.symbol,
name:
fragment(
"'<b>' || coalesce(?, '') || '</b>' || ' (' || coalesce(?, '') || ') ' || '<i>' || coalesce(?::varchar(255), '') || ' holder(s)' || '</i>'",
token.name,
token.symbol,
token.holder_count
)
name: token.name,
holder_count: token.holder_count
},
order_by: [desc: token.holder_count]
)

@ -88,7 +88,6 @@
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"phoenix": {:hex, :phoenix, "1.5.6", "8298cdb4e0f943242ba8410780a6a69cbbe972fef199b341a36898dd751bdd66", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0dc4d39af1306b6aa5122729b0a95ca779e42c708c6fe7abbb3d336d5379e956"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.2.1", "13f124cf0a3ce0f1948cf24654c7b9f2347169ff75c1123f44674afee6af3b03", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "478a1bae899cac0a6e02be1deec7e2944b7754c04e7d4107fc5a517f877743c0"},
"phoenix_form_awesomplete": {:hex, :phoenix_form_awesomplete, "0.1.6", "c7195aeed29eb0e18ead82cb7d81a1fd43cfc5beb8789f50c37ffe5eeff31d82", [:mix], [{:phoenix_html, "~> 2.10", [hex: :phoenix_html, repo: "hexpm", optional: false]}], "hexpm", "4066a8222d31efec70c2e5e927406314923544b9c9d3611c456fd2fc096d1b18"},
"phoenix_html": {:hex, :phoenix_html, "2.14.3", "51f720d0d543e4e157ff06b65de38e13303d5778a7919bcc696599e5934271b8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "efd697a7fff35a13eeeb6b43db884705cba353a1a41d127d118fda5f90c8e80f"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.0", "f35f61c3f959c9a01b36defaa1f0624edd55b87e236b606664a556d6f72fd2e7", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "02c1007ae393f2b76ec61c1a869b1e617179877984678babde131d716f95b582"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},

Loading…
Cancel
Save