diff --git a/.circleci/config.yml b/.circleci/config.yml index f410162b48..b3abd48786 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ jobs: build: docker: # Ensure .tool-versions matches - - image: circleci/elixir:1.9.0-node-browsers + - image: circleci/elixir:1.9.1-node-browsers environment: MIX_ENV: test # match POSTGRES_PASSWORD for postgres image below @@ -129,7 +129,7 @@ jobs: check_formatted: docker: # Ensure .tool-versions matches - - image: circleci/elixir:1.9.0 + - image: circleci/elixir:1.9.1 environment: MIX_ENV: test @@ -143,7 +143,7 @@ jobs: credo: docker: # Ensure .tool-versions matches - - image: circleci/elixir:1.9.0 + - image: circleci/elixir:1.9.1 environment: MIX_ENV: test @@ -177,7 +177,7 @@ jobs: dialyzer: docker: # Ensure .tool-versions matches - - image: circleci/elixir:1.9.0 + - image: circleci/elixir:1.9.1 environment: MIX_ENV: test @@ -247,7 +247,7 @@ jobs: gettext: docker: # Ensure .tool-versions matches - - image: circleci/elixir:1.9.0 + - image: circleci/elixir:1.9.1 environment: MIX_ENV: test @@ -286,7 +286,7 @@ jobs: release: docker: # Ensure .tool-versions matches - - image: circleci/elixir:1.9.0 + - image: circleci/elixir:1.9.1 environment: MIX_ENV: prod @@ -312,7 +312,7 @@ jobs: sobelow: docker: # Ensure .tool-versions matches - - image: circleci/elixir:1.9.0 + - image: circleci/elixir:1.9.1 environment: MIX_ENV: test @@ -336,7 +336,7 @@ jobs: test_geth_http_websocket: docker: # Ensure .tool-versions matches - - image: circleci/elixir:1.9.0-node-browsers + - image: circleci/elixir:1.9.1-node-browsers environment: MIX_ENV: test # match POSTGRES_PASSWORD for postgres image below @@ -390,7 +390,7 @@ jobs: test_geth_mox: docker: # Ensure .tool-versions matches - - image: circleci/elixir:1.9.0-node-browsers + - image: circleci/elixir:1.9.1-node-browsers environment: MIX_ENV: test # match POSTGRES_PASSWORD for postgres image below @@ -444,7 +444,7 @@ jobs: test_parity_http_websocket: docker: # Ensure .tool-versions matches - - image: circleci/elixir:1.9.0-node-browsers + - image: circleci/elixir:1.9.1-node-browsers environment: MIX_ENV: test # match POSTGRES_PASSWORD for postgres image below @@ -498,7 +498,7 @@ jobs: test_parity_mox: docker: # Ensure .tool-versions matches - - image: circleci/elixir:1.9.0-node-browsers + - image: circleci/elixir:1.9.1-node-browsers environment: MIX_ENV: test # match POSTGRES_PASSWORD for postgres image below @@ -552,7 +552,7 @@ jobs: coveralls_merge: docker: # Ensure .tool-versions matches - - image: circleci/elixir:1.9.0 + - image: circleci/elixir:1.9.1 environment: MIX_ENV: test diff --git a/.tool-versions b/.tool-versions index 71379b2fa7..785ba751f8 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ -elixir 1.9 -erlang 21.0.4 +elixir 1.9.1-otp-22 +erlang 22.0 nodejs 10.11.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 9667ccf378..62d1cc05b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,28 @@ ## Current ### Features +- [#2477](https://github.com/poanetwork/blockscout/pull/2477) - aggregate token transfers on transaction page +- [#2458](https://github.com/poanetwork/blockscout/pull/2458) - Add LAST_BLOCK var to add ability indexing in the range of blocks - [#2456](https://github.com/poanetwork/blockscout/pull/2456) - fetch pending transactions for geth ### Fixes +- [#2503](https://github.com/poanetwork/blockscout/pull/2503) - Mitigate autocompletion library influence to page loading performance +- [#2502](https://github.com/poanetwork/blockscout/pull/2502) - increase reward task timeout +- [#2463](https://github.com/poanetwork/blockscout/pull/2463) - dark theme fixes +- [#2496](https://github.com/poanetwork/blockscout/pull/2496) - fix docker build +- [#2495](https://github.com/poanetwork/blockscout/pull/2495) - fix logs for indexed chain - [#2459](https://github.com/poanetwork/blockscout/pull/2459) - fix top addresses query - [#2425](https://github.com/poanetwork/blockscout/pull/2425) - Force to show address view for checksummed address even if it is not in DB ### Chore +- [#2492](https://github.com/poanetwork/blockscout/pull/2492) - hide decoded row if event is not decoded - [#2490](https://github.com/poanetwork/blockscout/pull/2490) - enable credo duplicated code check - [#2432](https://github.com/poanetwork/blockscout/pull/2432) - bump credo version - [#2457](https://github.com/poanetwork/blockscout/pull/2457) - update mix.lock - [#2435](https://github.com/poanetwork/blockscout/pull/2435) - Replace deprecated extract-text-webpack-plugin with mini-css-extract-plugin - [#2450](https://github.com/poanetwork/blockscout/pull/2450) - Fix clearance of logs and node_modules folders in clearing script - [#2434](https://github.com/poanetwork/blockscout/pull/2434) - get rid of timex warnings +- [#2402](https://github.com/poanetwork/blockscout/pull/2402) - bump otp version to 22.0 ## 2.0.2-beta diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index 3ac2a5a1d1..09dbdd6b2f 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -35,3 +35,4 @@ - [ ] If I added new functionality, I added tests covering it. - [ ] If I fixed a bug, I added a regression test to prevent the bug from silently reappearing again. - [ ] I checked whether I should update the docs and did so if necessary + - [ ] If I added/changed/removed ENV var, I should update the list of env vars in https://github.com/poanetwork/blockscout/blob/master/docs/env-variables.md to reflect changes in the table here https://poanetwork.github.io/blockscout/#/env-variables?id=blockscout-env-variables diff --git a/apps/block_scout_web/assets/css/theme/_dark-theme.scss b/apps/block_scout_web/assets/css/theme/_dark-theme.scss index e648e01982..c0adb0a84b 100644 --- a/apps/block_scout_web/assets/css/theme/_dark-theme.scss +++ b/apps/block_scout_web/assets/css/theme/_dark-theme.scss @@ -464,7 +464,220 @@ $labels-dark: #8a8dba; // header nav, labels // dark text .text-dark { + color: #fff !important; + } + + // Contract Verification + .new-smart-contract-container { + background-color: $dark-light-bg; + background-image: linear-gradient(to bottom right, $dark-light 100%, $dark-light 100%); + @media (max-width: 991.98px) { + background-image: none; + } + } + .smart-contract-form-group-inner-wrapper .smart-contract-form-group-tooltip { + color: $labels-dark; + } + .smart-contract-title { + color: #fff; + } + .smart-contract-form-group-inner-wrapper > label { + color: $labels-dark; + } + .smart-contract-form-buttons { + border-top-color: darken($labels-dark, 30); + .btn-no-border { + background-color: $dark-light; + border-color: $dark-light; + color: #fff; + &:hover { + background-color: $dark-primary; + color: #fff; + } + } + } + .add-contract-libraries-wrapper { + border-top-color: darken($labels-dark, 30); + } + + .token-tile-view-more:before, .token-tile-view-more:after { + border-top-color: darken($labels-dark, 30); + border-bottom-color: darken($labels-dark, 30); + } + + // Form Controlls + .form-control { + background-color: $dark-light; + border-color: $dark-light; + color: #fff; + &::placeholder { + color: $labels-dark; + } + &:focus { + background-color: $dark-light; + border-color: $dark-primary; + color: #fff; + } + } + .radio-big .radio-icon { + border-color: $labels-dark; + } + .radio-big input[type="radio"]:checked + .radio-icon { + border-color: $dark-primary; + } + .radio-big input[type="radio"]:checked + .radio-icon::before { + background: $dark-primary; + } + .radio-big .radio-text { + color: #fff; + } + + // Content loading placeholders + .tile-loader, .table-content-loader { + background-color: $dark-bg !important; + &:before { + background: linear-gradient(to right, $dark-bg 2%, lighten($dark-bg, 3) 18%, $dark-bg 33%); + } + } + + // Verify other explorers + .verify-other-explorers-elem { + border-color: darken($labels-dark, 30); + .exp-logo { + border-right-color: darken($labels-dark, 30); + } + .exp-content { + h3 { + color: #fff; + } + div { + color: $labels-dark; + } + } + } + .verify-other-explorers-more { + border-color: $dark-primary; + svg path { + fill: $dark-primary; + } + &:hover { + background-color: $dark-primary; + svg path { + fill: #fff; + } + } + } + .verify-other-explorers-elem { + &:hover { + text-decoration: none; + color: #fff; + .exp-content { + h3, div { + color: #fff; + } + } + } + } + .verify-other-explorers-cell { + .exp-logo { + color: #333 !important; + } + } + + // API docs dropdown content + .api-doc-parameters-list-item-description, + .api-doc-parameters-list-item-title, + .api-doc-parameters-list-title, + .api-doc-list-item-controls-view-more { + color: #fff; + } + + .api-doc-parameters-list { + border-bottom-color: darken($labels-dark, 30); + } + .api-doc-parameters-container { + border-top-color: darken($labels-dark, 30); + } + .api-doc-tab { + color: $dark-primary !important; + &.active { + border-bottom-color: $dark-primary; + } + } + + // Common Buttons + .btn-secondary, .button-secondary { + background-color: transparent; + border-color: $dark-primary; + color: $dark-primary; + &:hover { + background-color: $dark-primary; + border-color: $dark-primary; + color: #fff; + } + } + + .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; + } + .table-bordered, .table-bordered td, .table-bordered th { + border-color: darken($labels-dark, 30); + } + .dark-theme-applied .table td, .dark-theme-applied .table th, .dark-theme-applied hr { + border-top-color: darken($labels-dark, 30); + } + .btn-copy-ico svg path { + fill: #fff; + } + + // pre + .pre-scrollable.line-numbers, .hljs { + background: $dark-light; color: #fff; } + .hljs-comment { + color: $labels-dark; + } + + .hljs-title, .hljs-section { + color: #ff2294; + } + + .hljs-type, .hljs-string, .hljs-number, .hljs-selector-id, .hljs-selector-class, .hljs-quote, .hljs-template-tag, .hljs-deletion { + color: #ff2294; + } + + .hljs-literal, .hljs-built_in, .hljs-bullet, .hljs-code, .hljs-addition { + color: #20dd94; + } + + .line-numbers [data-line-number]:before { + color: #3f436b !important; + border-right-color: #3f436b !important; + } } \ No newline at end of file diff --git a/apps/block_scout_web/assets/js/lib/awesomplete-util.js b/apps/block_scout_web/assets/js/lib/awesomplete-util.js new file mode 100644 index 0000000000..5b37e1dd2f --- /dev/null +++ b/apps/block_scout_web/assets/js/lib/awesomplete-util.js @@ -0,0 +1,637 @@ +/* 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 + } + + // 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 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, '$&') + } + + // 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 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(/

/) + arr[0] = _mark(arr[0], input) + text = arr.join('

') + } + 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: '', value: '', label: '

<%= gettext "Decoded" %>
-
<%= case decode(@log, @log.transaction) do %> <% {:error, :contract_not_verified} -> %> +
<%= gettext "Decoded" %>
+
<%= gettext "To see decoded input data, the contract must be verified." %> <%= case @log.transaction do %> @@ -25,10 +25,14 @@ <% end %>
<% {:error, :could_not_decode} -> %> +
<%= gettext "Decoded" %>
+
<%= gettext "Failed to decode log data." %>
<% {:ok, method_id, text, mapping} -> %> +
<%= gettext "Decoded" %>
+
@@ -78,7 +82,7 @@ <% end %>
Method Id
- <% _ -> %> + <% _ -> %> <%= nil %> <% end %>
<%= gettext "Topics" %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex index 2c221e4471..fd37cbb057 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex @@ -1,3 +1,7 @@ + + + +