Add Token Select Modal
This commit is contained in:
parent
8847f4e4d2
commit
1d86c0d656
@ -7,6 +7,7 @@
|
|||||||
"axios": "^0.18.0",
|
"axios": "^0.18.0",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"d3": "^4.13.0",
|
"d3": "^4.13.0",
|
||||||
|
"fuse": "^0.4.0",
|
||||||
"jazzicon": "^1.5.0",
|
"jazzicon": "^1.5.0",
|
||||||
"node-sass": "^4.9.3",
|
"node-sass": "^4.9.3",
|
||||||
"npm": "^6.0.0",
|
"npm": "^6.0.0",
|
||||||
@ -20,6 +21,7 @@
|
|||||||
"react-scripts": "2.0.4",
|
"react-scripts": "2.0.4",
|
||||||
"react-scroll-to-component": "^1.0.2",
|
"react-scroll-to-component": "^1.0.2",
|
||||||
"react-select": "^1.2.1",
|
"react-select": "^1.2.1",
|
||||||
|
"react-transition-group": "1.x",
|
||||||
"redux": "^3.7.2",
|
"redux": "^3.7.2",
|
||||||
"redux-subscriber": "^1.1.0",
|
"redux-subscriber": "^1.1.0",
|
||||||
"redux-thunk": "^2.2.0",
|
"redux-thunk": "^2.2.0",
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
You need to enable JavaScript to run this app.
|
You need to enable JavaScript to run this app.
|
||||||
</noscript>
|
</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
<div id="modal-root"></div>
|
||||||
<!--
|
<!--
|
||||||
This HTML file is a template.
|
This HTML file is a template.
|
||||||
If you open it directly in the browser, you will see an empty page.
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
BIN
src/assets/images/ethereum-logo.png
Normal file
BIN
src/assets/images/ethereum-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
9
src/assets/images/magnifying-glass.svg
Normal file
9
src/assets/images/magnifying-glass.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 8.2 KiB |
@ -4,8 +4,7 @@
|
|||||||
@extend %col-nowrap;
|
@extend %col-nowrap;
|
||||||
|
|
||||||
&__input {
|
&__input {
|
||||||
color: $mine-shaft-gray;
|
font-size: .75rem;
|
||||||
font-size: .9rem;
|
|
||||||
outline: none;
|
outline: none;
|
||||||
border: none;
|
border: none;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
@ -41,16 +41,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__input {
|
&__input {
|
||||||
color: $mine-shaft-gray;
|
@extend %borderless-input;
|
||||||
font-size: 1.5rem;
|
|
||||||
outline: none;
|
|
||||||
border: none;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
width: 0;
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: $chalice-gray;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__currency-select {
|
&__currency-select {
|
||||||
@ -70,6 +61,17 @@
|
|||||||
&:active {
|
&:active {
|
||||||
background-color: rgba($zumthor-blue, .8);
|
background-color: rgba($zumthor-blue, .8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--selected {
|
||||||
|
background-color: $concrete-gray;
|
||||||
|
border-color: $mercury-gray;
|
||||||
|
color: $black;
|
||||||
|
padding: 0 .5rem;
|
||||||
|
|
||||||
|
.currency-input-panel__dropdown-icon {
|
||||||
|
background-image: url(../../assets/images/dropdown.svg);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__dropdown-icon {
|
&__dropdown-icon {
|
||||||
@ -81,5 +83,82 @@
|
|||||||
background-size: contain;
|
background-size: contain;
|
||||||
background-position: 50% 50%;
|
background-position: 50% 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__selected-token-logo {
|
||||||
|
margin-right: .4rem;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.token-modal {
|
||||||
|
background-color: $white;
|
||||||
|
position: relative;
|
||||||
|
bottom: 21rem;
|
||||||
|
width: 100%;
|
||||||
|
height: 21rem;
|
||||||
|
z-index: 2000;
|
||||||
|
border-top-left-radius: 1rem;
|
||||||
|
border-top-right-radius: 1rem;
|
||||||
|
transition: 250ms ease-in-out;
|
||||||
|
|
||||||
|
&__search-container {
|
||||||
|
@extend %row-nowrap;
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid $mercury-gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__search-input {
|
||||||
|
@extend %borderless-input;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__search-icon {
|
||||||
|
margin-right: .2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__token-list {
|
||||||
|
height: 17.5rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__token-row {
|
||||||
|
@extend %row-nowrap;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $concrete-gray;
|
||||||
|
|
||||||
|
.token-modal__token-label {
|
||||||
|
color: $black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: darken($concrete-gray, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__token-logo {
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__token-label {
|
||||||
|
color: $silver-gray;
|
||||||
|
font-weight: 200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.token-modal-appear {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-modal-appear.modal-container-appear-active {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,9 +1,32 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { CSSTransitionGroup } from "react-transition-group";
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import Fuse from '../../helpers/fuse';
|
||||||
|
import Modal from '../Modal';
|
||||||
|
import TokenLogo from '../TokenLogo';
|
||||||
|
import SearchIcon from '../../assets/images/magnifying-glass.svg';
|
||||||
|
|
||||||
import './currency-panel.scss';
|
import './currency-panel.scss';
|
||||||
|
|
||||||
|
const TOKEN_ICON_API = 'https://raw.githubusercontent.com/TrustWallet/tokens/master/images';
|
||||||
|
const FUSE_OPTIONS = {
|
||||||
|
includeMatches: false,
|
||||||
|
threshold: 0.0,
|
||||||
|
tokenize:true,
|
||||||
|
location: 0,
|
||||||
|
distance: 100,
|
||||||
|
maxPatternLength: 45,
|
||||||
|
minMatchCharLength: 1,
|
||||||
|
keys: [
|
||||||
|
{name:"address",weight:0.8},
|
||||||
|
{name:"label",weight:0.5},
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const TOKEN_ADDRESS_TO_LABEL = { ETH: 'ETH' };
|
||||||
|
|
||||||
class CurrencyInputPanel extends Component {
|
class CurrencyInputPanel extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
@ -11,6 +34,93 @@ class CurrencyInputPanel extends Component {
|
|||||||
extraText: PropTypes.string,
|
extraText: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
isShowingModal: false,
|
||||||
|
searchQuery: '',
|
||||||
|
selectedTokenAddress: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
createTokenList = () => {
|
||||||
|
let tokens = this.props.web3Store.tokenAddresses.addresses;
|
||||||
|
let tokenList = [ { value: 'ETH', label: 'ETH', address: 'ETH', clearableValue: false } ];
|
||||||
|
|
||||||
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
|
let entry = { value: '', label: '', clearableValue: false }
|
||||||
|
entry.value = tokens[i][0];
|
||||||
|
entry.label = tokens[i][0];
|
||||||
|
entry.address = tokens[i][1];
|
||||||
|
tokenList.push(entry);
|
||||||
|
TOKEN_ADDRESS_TO_LABEL[tokens[i][1]] = tokens[i][0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenList;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTokenList() {
|
||||||
|
const tokens = this.createTokenList();
|
||||||
|
const { searchQuery } = this.state;
|
||||||
|
let results;
|
||||||
|
|
||||||
|
if (!searchQuery) {
|
||||||
|
results = tokens;
|
||||||
|
} else {
|
||||||
|
const fuse = new Fuse(tokens, FUSE_OPTIONS);
|
||||||
|
results = fuse.search(this.state.searchQuery);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.map(({ label, address }) => (
|
||||||
|
<div
|
||||||
|
key={label}
|
||||||
|
className="token-modal__token-row"
|
||||||
|
onClick={() => this.setState({
|
||||||
|
selectedTokenAddress: address || 'ETH',
|
||||||
|
searchQuery: '',
|
||||||
|
isShowingModal: false,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<TokenLogo className="token-modal__token-logo" address={address} />
|
||||||
|
<div className="token-modal__token-label" >{label}</div>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
renderModal() {
|
||||||
|
if (!this.state.isShowingModal) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal onClose={() => this.setState({ isShowingModal: false })}>
|
||||||
|
<CSSTransitionGroup
|
||||||
|
transitionName="token-modal"
|
||||||
|
transitionAppear={true}
|
||||||
|
transitionLeave={true}
|
||||||
|
transitionAppearTimeout={200}
|
||||||
|
transitionLeaveTimeout={200}
|
||||||
|
transitionEnterTimeout={200}
|
||||||
|
>
|
||||||
|
<div className="token-modal">
|
||||||
|
<div className="token-modal__search-container">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search Token or Paste Address"
|
||||||
|
className="token-modal__search-input"
|
||||||
|
onChange={e => this.setState({
|
||||||
|
searchQuery: e.target.value,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<img src={SearchIcon} className="token-modal__search-icon" />
|
||||||
|
</div>
|
||||||
|
<div className="token-modal__token-list">
|
||||||
|
{this.renderTokenList()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CSSTransitionGroup>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
@ -18,6 +128,8 @@ class CurrencyInputPanel extends Component {
|
|||||||
extraText,
|
extraText,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const { selectedTokenAddress } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="currency-input-panel">
|
<div className="currency-input-panel">
|
||||||
<div className="currency-input-panel__container">
|
<div className="currency-input-panel__container">
|
||||||
@ -30,15 +142,33 @@ class CurrencyInputPanel extends Component {
|
|||||||
</div>
|
</div>
|
||||||
<div className="currency-input-panel__input-row">
|
<div className="currency-input-panel__input-row">
|
||||||
<input type="number" className="currency-input-panel__input" placeholder="0.0" />
|
<input type="number" className="currency-input-panel__input" placeholder="0.0" />
|
||||||
<button className="currency-input-panel__currency-select">
|
<button
|
||||||
Select a token
|
className={classnames("currency-input-panel__currency-select", {
|
||||||
|
'currency-input-panel__currency-select--selected': selectedTokenAddress,
|
||||||
|
})}
|
||||||
|
onClick={() => this.setState({ isShowingModal: true })}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
selectedTokenAddress
|
||||||
|
? (
|
||||||
|
<TokenLogo
|
||||||
|
className="currency-input-panel__selected-token-logo"
|
||||||
|
address={selectedTokenAddress}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
{ TOKEN_ADDRESS_TO_LABEL[selectedTokenAddress]|| 'Select a token' }
|
||||||
<span className="currency-input-panel__dropdown-icon" />
|
<span className="currency-input-panel__dropdown-icon" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{this.renderModal()}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect()(CurrencyInputPanel);
|
export default connect(
|
||||||
|
state => ({ web3Store: state.web3Store })
|
||||||
|
)(CurrencyInputPanel);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React, { Component }from 'react';
|
import React, { Component }from 'react';
|
||||||
|
import React, { Component }from 'react';
|
||||||
import SelectToken from './SelectToken';
|
import SelectToken from './SelectToken';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
57
src/components/Modal/index.js
Normal file
57
src/components/Modal/index.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { CSSTransitionGroup } from 'react-transition-group';
|
||||||
|
import './modal.scss';
|
||||||
|
|
||||||
|
const modalRoot = document.querySelector('#modal-root');
|
||||||
|
|
||||||
|
export default class Modal extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
// this.el = document.createElement('div');
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
// The portal element is inserted in the DOM tree after
|
||||||
|
// the Modal's children are mounted, meaning that children
|
||||||
|
// will be mounted on a detached DOM node. If a child
|
||||||
|
// component requires to be attached to the DOM tree
|
||||||
|
// immediately when mounted, for example to measure a
|
||||||
|
// DOM node, or uses 'autoFocus' in a descendant, add
|
||||||
|
// state to Modal and only render the children when Modal
|
||||||
|
// is inserted in the DOM tree.
|
||||||
|
// modalRoot.style.display = 'block';
|
||||||
|
// modalRoot.appendChild(this.el);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
setTimeout(() => {
|
||||||
|
// modalRoot.style.display = 'none';
|
||||||
|
// modalRoot.removeChild(this.el);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return ReactDOM.createPortal(
|
||||||
|
<div>
|
||||||
|
<CSSTransitionGroup
|
||||||
|
transitionName="modal-container"
|
||||||
|
transitionAppear={true}
|
||||||
|
transitionLeave={true}
|
||||||
|
transitionAppearTimeout={200}
|
||||||
|
transitionLeaveTimeout={200}
|
||||||
|
transitionEnterTimeout={200}
|
||||||
|
>
|
||||||
|
<div className="modal-container" onClick={this.props.onClose} key="modal" />
|
||||||
|
</CSSTransitionGroup>
|
||||||
|
{this.props.children}
|
||||||
|
</div>,
|
||||||
|
modalRoot,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
18
src/components/Modal/modal.scss
Normal file
18
src/components/Modal/modal.scss
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
@import '../../variables.scss';
|
||||||
|
|
||||||
|
.modal-container {
|
||||||
|
position: relative;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
background-color: rgba($black, .6);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-container-appear {
|
||||||
|
opacity: 0.01;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-container-appear.modal-container-appear-active {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 200ms ease-in-out;
|
||||||
|
}
|
49
src/components/TokenLogo/index.js
Normal file
49
src/components/TokenLogo/index.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import EthereumLogo from '../../assets/images/ethereum-logo.png';
|
||||||
|
|
||||||
|
const TOKEN_ICON_API = 'https://raw.githubusercontent.com/TrustWallet/tokens/master/images';
|
||||||
|
|
||||||
|
export default class TokenLogo extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
address: PropTypes.string,
|
||||||
|
size: PropTypes.string,
|
||||||
|
className: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
address: '',
|
||||||
|
size: '1.5rem',
|
||||||
|
className: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
error: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { address, size, className } = this.props;
|
||||||
|
let path = 'https://png2.kisspng.com/sh/59799e422ec61954a8e126f9203cd0b3/L0KzQYm3U8I6N5xniZH0aYP2gLBuTflxcJDzfZ9ubXBteX76gf10fZ9sRdlqbHH7iX7ulfV0e155gNc2cYXog8XwjB50NZR3kdt3Zz3ofbFxib02aZNoetUAYkC6QoXrVr4zP2Y1SKkBNkG4QoO6Ucg1Omg1Sqs8LoDxd1==/kisspng-iphone-emoji-samsung-galaxy-guess-the-questions-crying-emoji-5abcbc5b0724d6.2750076615223184270293.png';
|
||||||
|
|
||||||
|
if (address === 'ETH') {
|
||||||
|
path = EthereumLogo;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.state.error) {
|
||||||
|
path = `${TOKEN_ICON_API}/${address}.png`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={path}
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
}}
|
||||||
|
onError={() => this.setState({ error: true })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
26
src/helpers/fuse/bitap/bitap_matched_indices.js
Normal file
26
src/helpers/fuse/bitap/bitap_matched_indices.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
export default function (matchmask = [], minMatchCharLength = 1) {
|
||||||
|
let matchedIndices = []
|
||||||
|
let start = -1
|
||||||
|
let end = -1
|
||||||
|
let i = 0
|
||||||
|
|
||||||
|
for (let len = matchmask.length; i < len; i += 1) {
|
||||||
|
let match = matchmask[i]
|
||||||
|
if (match && start === -1) {
|
||||||
|
start = i
|
||||||
|
} else if (!match && start !== -1) {
|
||||||
|
end = i - 1
|
||||||
|
if ((end - start) + 1 >= minMatchCharLength) {
|
||||||
|
matchedIndices.push([start, end])
|
||||||
|
}
|
||||||
|
start = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// (i-1 - start) + 1 => i - start
|
||||||
|
if (matchmask[i - 1] && (i - start) >= minMatchCharLength) {
|
||||||
|
matchedIndices.push([start, i - 1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchedIndices
|
||||||
|
}
|
14
src/helpers/fuse/bitap/bitap_pattern_alphabet.js
Normal file
14
src/helpers/fuse/bitap/bitap_pattern_alphabet.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export default function (pattern) {
|
||||||
|
let mask = {}
|
||||||
|
let len = pattern.length
|
||||||
|
|
||||||
|
for (let i = 0; i < len; i += 1) {
|
||||||
|
mask[pattern.charAt(i)] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < len; i += 1) {
|
||||||
|
mask[pattern.charAt(i)] |= 1 << (len - i - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mask
|
||||||
|
}
|
22
src/helpers/fuse/bitap/bitap_regex_search.js
Normal file
22
src/helpers/fuse/bitap/bitap_regex_search.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
const SPECIAL_CHARS_REGEX = /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g
|
||||||
|
|
||||||
|
export default function (text, pattern, tokenSeparator = / +/g) {
|
||||||
|
let regex = new RegExp(pattern.replace(SPECIAL_CHARS_REGEX, '\\$&').replace(tokenSeparator, '|'))
|
||||||
|
let matches = text.match(regex)
|
||||||
|
let isMatch = !!matches
|
||||||
|
let matchedIndices = []
|
||||||
|
|
||||||
|
if (isMatch) {
|
||||||
|
for (let i = 0, matchesLen = matches.length; i < matchesLen; i += 1) {
|
||||||
|
let match = matches[i]
|
||||||
|
matchedIndices.push([text.indexOf(match), match.length - 1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// TODO: revisit this score
|
||||||
|
score: isMatch ? 0.5 : 1,
|
||||||
|
isMatch,
|
||||||
|
matchedIndices
|
||||||
|
}
|
||||||
|
}
|
11
src/helpers/fuse/bitap/bitap_score.js
Normal file
11
src/helpers/fuse/bitap/bitap_score.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export default function (pattern, { errors = 0, currentLocation = 0, expectedLocation = 0, distance = 100 }) {
|
||||||
|
const accuracy = errors / pattern.length
|
||||||
|
const proximity = Math.abs(expectedLocation - currentLocation)
|
||||||
|
|
||||||
|
if (!distance) {
|
||||||
|
// Dodge divide by zero error.
|
||||||
|
return proximity ? 1.0 : accuracy
|
||||||
|
}
|
||||||
|
|
||||||
|
return accuracy + (proximity / distance)
|
||||||
|
}
|
157
src/helpers/fuse/bitap/bitap_search.js
Normal file
157
src/helpers/fuse/bitap/bitap_search.js
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import Bitap from "./index";
|
||||||
|
|
||||||
|
import bitapScore from './bitap_score';
|
||||||
|
import matchedIndices from './bitap_matched_indices';
|
||||||
|
|
||||||
|
export default function (text, pattern, patternAlphabet, { location = 0, distance = 100, threshold = 0.6, findAllMatches = false, minMatchCharLength = 1 }) {
|
||||||
|
const expectedLocation = location
|
||||||
|
// Set starting location at beginning text and initialize the alphabet.
|
||||||
|
const textLen = text.length
|
||||||
|
// Highest score beyond which we give up.
|
||||||
|
let currentThreshold = threshold
|
||||||
|
// Is there a nearby exact match? (speedup)
|
||||||
|
let bestLocation = text.indexOf(pattern, expectedLocation)
|
||||||
|
|
||||||
|
const patternLen = pattern.length
|
||||||
|
|
||||||
|
// a mask of the matches
|
||||||
|
const matchMask = []
|
||||||
|
for (let i = 0; i < textLen; i += 1) {
|
||||||
|
matchMask[i] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestLocation !== -1) {
|
||||||
|
let score = bitapScore(pattern, {
|
||||||
|
errors: 0,
|
||||||
|
currentLocation: bestLocation,
|
||||||
|
expectedLocation,
|
||||||
|
distance
|
||||||
|
})
|
||||||
|
currentThreshold = Math.min(score, currentThreshold)
|
||||||
|
|
||||||
|
// What about in the other direction? (speed up)
|
||||||
|
bestLocation = text.lastIndexOf(pattern, expectedLocation + patternLen)
|
||||||
|
|
||||||
|
if (bestLocation !== -1) {
|
||||||
|
let score = bitapScore(pattern, {
|
||||||
|
errors: 0,
|
||||||
|
currentLocation: bestLocation,
|
||||||
|
expectedLocation,
|
||||||
|
distance
|
||||||
|
})
|
||||||
|
currentThreshold = Math.min(score, currentThreshold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the best location
|
||||||
|
bestLocation = -1
|
||||||
|
|
||||||
|
let lastBitArr = []
|
||||||
|
let finalScore = 1
|
||||||
|
let binMax = patternLen + textLen
|
||||||
|
|
||||||
|
const mask = 1 << (patternLen - 1)
|
||||||
|
|
||||||
|
for (let i = 0; i < patternLen; i += 1) {
|
||||||
|
// Scan for the best match; each iteration allows for one more error.
|
||||||
|
// Run a binary search to determine how far from the match location we can stray
|
||||||
|
// at this error level.
|
||||||
|
let binMin = 0
|
||||||
|
let binMid = binMax
|
||||||
|
|
||||||
|
while (binMin < binMid) {
|
||||||
|
const score = bitapScore(pattern, {
|
||||||
|
errors: i,
|
||||||
|
currentLocation: expectedLocation + binMid,
|
||||||
|
expectedLocation,
|
||||||
|
distance
|
||||||
|
});
|
||||||
|
|
||||||
|
if (score <= currentThreshold) {
|
||||||
|
binMin = binMid
|
||||||
|
} else {
|
||||||
|
binMax = binMid
|
||||||
|
}
|
||||||
|
|
||||||
|
binMid = Math.floor((binMax - binMin) / 2 + binMin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the result from this iteration as the maximum for the next.
|
||||||
|
binMax = binMid
|
||||||
|
|
||||||
|
let start = Math.max(1, expectedLocation - binMid + 1)
|
||||||
|
let finish = findAllMatches ? textLen : Math.min(expectedLocation + binMid, textLen) + patternLen
|
||||||
|
|
||||||
|
// Initialize the bit array
|
||||||
|
let bitArr = Array(finish + 2)
|
||||||
|
|
||||||
|
bitArr[finish + 1] = (1 << i) - 1
|
||||||
|
|
||||||
|
for (let j = finish; j >= start; j -= 1) {
|
||||||
|
let currentLocation = j - 1
|
||||||
|
let charMatch = patternAlphabet[text.charAt(currentLocation)]
|
||||||
|
|
||||||
|
if (charMatch) {
|
||||||
|
matchMask[currentLocation] = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// First pass: exact match
|
||||||
|
bitArr[j] = ((bitArr[j + 1] << 1) | 1) & charMatch
|
||||||
|
|
||||||
|
// Subsequent passes: fuzzy match
|
||||||
|
if (i !== 0) {
|
||||||
|
bitArr[j] |= (((lastBitArr[j + 1] | lastBitArr[j]) << 1) | 1) | lastBitArr[j + 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bitArr[j] & mask) {
|
||||||
|
finalScore = bitapScore(pattern, {
|
||||||
|
errors: i,
|
||||||
|
currentLocation,
|
||||||
|
expectedLocation,
|
||||||
|
distance
|
||||||
|
})
|
||||||
|
|
||||||
|
// This match will almost certainly be better than any existing match.
|
||||||
|
// But check anyway.
|
||||||
|
if (finalScore <= currentThreshold) {
|
||||||
|
// Indeed it is
|
||||||
|
currentThreshold = finalScore
|
||||||
|
bestLocation = currentLocation
|
||||||
|
|
||||||
|
// Already passed `loc`, downhill from here on in.
|
||||||
|
if (bestLocation <= expectedLocation) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// When passing `bestLocation`, don't exceed our current distance from `expectedLocation`.
|
||||||
|
start = Math.max(1, 2 * expectedLocation - bestLocation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No hope for a (better) match at greater error levels.
|
||||||
|
const score = bitapScore(pattern, {
|
||||||
|
errors: i + 1,
|
||||||
|
currentLocation: expectedLocation,
|
||||||
|
expectedLocation,
|
||||||
|
distance
|
||||||
|
})
|
||||||
|
|
||||||
|
// console.log('score', score, finalScore)
|
||||||
|
|
||||||
|
if (score > currentThreshold) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
lastBitArr = bitArr
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('FINAL SCORE', finalScore)
|
||||||
|
|
||||||
|
// Count exact matches (those with a score of 0) to be "almost" exact
|
||||||
|
return {
|
||||||
|
isMatch: bestLocation >= 0,
|
||||||
|
score: finalScore === 0 ? 0.001 : finalScore,
|
||||||
|
matchedIndices: matchedIndices(matchMask, minMatchCharLength)
|
||||||
|
}
|
||||||
|
}
|
84
src/helpers/fuse/bitap/index.js
Normal file
84
src/helpers/fuse/bitap/index.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import bitapRegexSearch from './bitap_regex_search';
|
||||||
|
import bitapSearch from './bitap_search';
|
||||||
|
import patternAlphabet from './bitap_pattern_alphabet';
|
||||||
|
|
||||||
|
class Bitap {
|
||||||
|
constructor (pattern, {
|
||||||
|
// Approximately where in the text is the pattern expected to be found?
|
||||||
|
location = 0,
|
||||||
|
// Determines how close the match must be to the fuzzy location (specified above).
|
||||||
|
// An exact letter match which is 'distance' characters away from the fuzzy location
|
||||||
|
// would score as a complete mismatch. A distance of '0' requires the match be at
|
||||||
|
// the exact location specified, a threshold of '1000' would require a perfect match
|
||||||
|
// to be within 800 characters of the fuzzy location to be found using a 0.8 threshold.
|
||||||
|
distance = 100,
|
||||||
|
// At what point does the match algorithm give up. A threshold of '0.0' requires a perfect match
|
||||||
|
// (of both letters and location), a threshold of '1.0' would match anything.
|
||||||
|
threshold = 0.6,
|
||||||
|
// Machine word size
|
||||||
|
maxPatternLength = 32,
|
||||||
|
// Indicates whether comparisons should be case sensitive.
|
||||||
|
isCaseSensitive = false,
|
||||||
|
// Regex used to separate words when searching. Only applicable when `tokenize` is `true`.
|
||||||
|
tokenSeparator = / +/g,
|
||||||
|
// When true, the algorithm continues searching to the end of the input even if a perfect
|
||||||
|
// match is found before the end of the same input.
|
||||||
|
findAllMatches = false,
|
||||||
|
// Minimum number of characters that must be matched before a result is considered a match
|
||||||
|
minMatchCharLength = 1
|
||||||
|
}) {
|
||||||
|
this.options = {
|
||||||
|
location,
|
||||||
|
distance,
|
||||||
|
threshold,
|
||||||
|
maxPatternLength,
|
||||||
|
isCaseSensitive,
|
||||||
|
tokenSeparator,
|
||||||
|
findAllMatches,
|
||||||
|
minMatchCharLength
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pattern = this.options.isCaseSensitive ? pattern : pattern.toLowerCase()
|
||||||
|
|
||||||
|
if (this.pattern.length <= maxPatternLength) {
|
||||||
|
this.patternAlphabet = patternAlphabet(this.pattern)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
search (text) {
|
||||||
|
if (!this.options.isCaseSensitive) {
|
||||||
|
text = text.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exact match
|
||||||
|
if (this.pattern === text) {
|
||||||
|
return {
|
||||||
|
isMatch: true,
|
||||||
|
score: 0,
|
||||||
|
matchedIndices: [[0, text.length - 1]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When pattern length is greater than the machine word length, just do a a regex comparison
|
||||||
|
const { maxPatternLength, tokenSeparator } = this.options
|
||||||
|
if (this.pattern.length > maxPatternLength) {
|
||||||
|
return bitapRegexSearch(text, this.pattern, tokenSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, use Bitap algorithm
|
||||||
|
const { location, distance, threshold, findAllMatches, minMatchCharLength } = this.options
|
||||||
|
return bitapSearch(text, this.pattern, this.patternAlphabet, {
|
||||||
|
location,
|
||||||
|
distance,
|
||||||
|
threshold,
|
||||||
|
findAllMatches,
|
||||||
|
minMatchCharLength
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// let x = new Bitap("od mn war", {})
|
||||||
|
// let result = x.search("Old Man's War")
|
||||||
|
// console.log(result)
|
||||||
|
|
||||||
|
export default Bitap
|
39
src/helpers/fuse/helpers/deep_value.js
Normal file
39
src/helpers/fuse/helpers/deep_value.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
const isArray = require('./is_array')
|
||||||
|
|
||||||
|
const deepValue = (obj, path, list) => {
|
||||||
|
if (!path) {
|
||||||
|
// If there's no path left, we've gotten to the object we care about.
|
||||||
|
list.push(obj)
|
||||||
|
} else {
|
||||||
|
const dotIndex = path.indexOf('.')
|
||||||
|
let firstSegment = path
|
||||||
|
let remaining = null
|
||||||
|
|
||||||
|
if (dotIndex !== -1) {
|
||||||
|
firstSegment = path.slice(0, dotIndex)
|
||||||
|
remaining = path.slice(dotIndex + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = obj[firstSegment]
|
||||||
|
|
||||||
|
if (value !== null && value !== undefined) {
|
||||||
|
if (!remaining && (typeof value === 'string' || typeof value === 'number')) {
|
||||||
|
list.push(value.toString())
|
||||||
|
} else if (isArray(value)) {
|
||||||
|
// Search each item in the array.
|
||||||
|
for (let i = 0, len = value.length; i < len; i += 1) {
|
||||||
|
deepValue(value[i], remaining, list)
|
||||||
|
}
|
||||||
|
} else if (remaining) {
|
||||||
|
// An object. Recurse further.
|
||||||
|
deepValue(value, remaining, list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = (obj, path) => {
|
||||||
|
return deepValue(obj, path, [])
|
||||||
|
}
|
1
src/helpers/fuse/helpers/is_array.js
Normal file
1
src/helpers/fuse/helpers/is_array.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = obj => !Array.isArray ? Object.prototype.toString.call(obj) === '[object Array]' : Array.isArray(obj)
|
414
src/helpers/fuse/index.js
Normal file
414
src/helpers/fuse/index.js
Normal file
@ -0,0 +1,414 @@
|
|||||||
|
import Bitap from'./bitap';
|
||||||
|
const deepValue = require('./helpers/deep_value')
|
||||||
|
const isArray = require('./helpers/is_array')
|
||||||
|
|
||||||
|
class Fuse {
|
||||||
|
constructor (list, {
|
||||||
|
// Approximately where in the text is the pattern expected to be found?
|
||||||
|
location = 0,
|
||||||
|
// Determines how close the match must be to the fuzzy location (specified above).
|
||||||
|
// An exact letter match which is 'distance' characters away from the fuzzy location
|
||||||
|
// would score as a complete mismatch. A distance of '0' requires the match be at
|
||||||
|
// the exact location specified, a threshold of '1000' would require a perfect match
|
||||||
|
// to be within 800 characters of the fuzzy location to be found using a 0.8 threshold.
|
||||||
|
distance = 100,
|
||||||
|
// At what point does the match algorithm give up. A threshold of '0.0' requires a perfect match
|
||||||
|
// (of both letters and location), a threshold of '1.0' would match anything.
|
||||||
|
threshold = 0.6,
|
||||||
|
// Machine word size
|
||||||
|
maxPatternLength = 32,
|
||||||
|
// Indicates whether comparisons should be case sensitive.
|
||||||
|
caseSensitive = false,
|
||||||
|
// Regex used to separate words when searching. Only applicable when `tokenize` is `true`.
|
||||||
|
tokenSeparator = / +/g,
|
||||||
|
// When true, the algorithm continues searching to the end of the input even if a perfect
|
||||||
|
// match is found before the end of the same input.
|
||||||
|
findAllMatches = false,
|
||||||
|
// Minimum number of characters that must be matched before a result is considered a match
|
||||||
|
minMatchCharLength = 1,
|
||||||
|
// The name of the identifier property. If specified, the returned result will be a list
|
||||||
|
// of the items' dentifiers, otherwise it will be a list of the items.
|
||||||
|
id = null,
|
||||||
|
// List of properties that will be searched. This also supports nested properties.
|
||||||
|
keys = [],
|
||||||
|
// Whether to sort the result list, by score
|
||||||
|
shouldSort = true,
|
||||||
|
// The get function to use when fetching an object's properties.
|
||||||
|
// The default will search nested paths *ie foo.bar.baz*
|
||||||
|
getFn = deepValue,
|
||||||
|
// Default sort function
|
||||||
|
sortFn = (a, b) => (a.score - b.score),
|
||||||
|
// When true, the search algorithm will search individual words **and** the full string,
|
||||||
|
// computing the final score as a function of both. Note that when `tokenize` is `true`,
|
||||||
|
// the `threshold`, `distance`, and `location` are inconsequential for individual tokens.
|
||||||
|
tokenize = false,
|
||||||
|
// When true, the result set will only include records that match all tokens. Will only work
|
||||||
|
// if `tokenize` is also true.
|
||||||
|
matchAllTokens = false,
|
||||||
|
|
||||||
|
includeMatches = false,
|
||||||
|
includeScore = false,
|
||||||
|
|
||||||
|
// Will print to the console. Useful for debugging.
|
||||||
|
verbose = false
|
||||||
|
}) {
|
||||||
|
this.options = {
|
||||||
|
location,
|
||||||
|
distance,
|
||||||
|
threshold,
|
||||||
|
maxPatternLength,
|
||||||
|
isCaseSensitive: caseSensitive,
|
||||||
|
tokenSeparator,
|
||||||
|
findAllMatches,
|
||||||
|
minMatchCharLength,
|
||||||
|
id,
|
||||||
|
keys,
|
||||||
|
includeMatches,
|
||||||
|
includeScore,
|
||||||
|
shouldSort,
|
||||||
|
getFn,
|
||||||
|
sortFn,
|
||||||
|
verbose,
|
||||||
|
tokenize,
|
||||||
|
matchAllTokens
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setCollection(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
setCollection (list) {
|
||||||
|
this.list = list
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
search (pattern) {
|
||||||
|
this._log(`---------\nSearch pattern: "${pattern}"`)
|
||||||
|
|
||||||
|
const {
|
||||||
|
tokenSearchers,
|
||||||
|
fullSearcher
|
||||||
|
} = this._prepareSearchers(pattern)
|
||||||
|
|
||||||
|
let { weights, results } = this._search(tokenSearchers, fullSearcher)
|
||||||
|
|
||||||
|
this._computeScore(weights, results)
|
||||||
|
|
||||||
|
if (this.options.shouldSort) {
|
||||||
|
this._sort(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._format(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
_prepareSearchers (pattern = '') {
|
||||||
|
const tokenSearchers = []
|
||||||
|
|
||||||
|
if (this.options.tokenize) {
|
||||||
|
// Tokenize on the separator
|
||||||
|
const tokens = pattern.split(this.options.tokenSeparator)
|
||||||
|
for (let i = 0, len = tokens.length; i < len; i += 1) {
|
||||||
|
tokenSearchers.push(new Bitap(tokens[i], this.options))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let fullSearcher = new Bitap(pattern, this.options)
|
||||||
|
|
||||||
|
return { tokenSearchers, fullSearcher }
|
||||||
|
}
|
||||||
|
|
||||||
|
_search (tokenSearchers = [], fullSearcher) {
|
||||||
|
const list = this.list
|
||||||
|
const resultMap = {}
|
||||||
|
const results = []
|
||||||
|
|
||||||
|
// Check the first item in the list, if it's a string, then we assume
|
||||||
|
// that every item in the list is also a string, and thus it's a flattened array.
|
||||||
|
if (typeof list[0] === 'string') {
|
||||||
|
// Iterate over every item
|
||||||
|
for (let i = 0, len = list.length; i < len; i += 1) {
|
||||||
|
this._analyze({
|
||||||
|
key: '',
|
||||||
|
value: list[i],
|
||||||
|
record: i,
|
||||||
|
index: i
|
||||||
|
}, {
|
||||||
|
resultMap,
|
||||||
|
results,
|
||||||
|
tokenSearchers,
|
||||||
|
fullSearcher
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return { weights: null, results }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, the first item is an Object (hopefully), and thus the searching
|
||||||
|
// is done on the values of the keys of each item.
|
||||||
|
const weights = {}
|
||||||
|
for (let i = 0, len = list.length; i < len; i += 1) {
|
||||||
|
let item = list[i]
|
||||||
|
// Iterate over every key
|
||||||
|
for (let j = 0, keysLen = this.options.keys.length; j < keysLen; j += 1) {
|
||||||
|
let key = this.options.keys[j]
|
||||||
|
if (typeof key !== 'string') {
|
||||||
|
weights[key.name] = {
|
||||||
|
weight: (1 - key.weight) || 1
|
||||||
|
}
|
||||||
|
if (key.weight <= 0 || key.weight > 1) {
|
||||||
|
throw new Error('Key weight has to be > 0 and <= 1')
|
||||||
|
}
|
||||||
|
key = key.name
|
||||||
|
} else {
|
||||||
|
weights[key] = {
|
||||||
|
weight: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._analyze({
|
||||||
|
key,
|
||||||
|
value: this.options.getFn(item, key),
|
||||||
|
record: item,
|
||||||
|
index: i
|
||||||
|
}, {
|
||||||
|
resultMap,
|
||||||
|
results,
|
||||||
|
tokenSearchers,
|
||||||
|
fullSearcher
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { weights, results }
|
||||||
|
}
|
||||||
|
|
||||||
|
_analyze ({ key, arrayIndex = -1, value, record, index }, { tokenSearchers = [], fullSearcher = [], resultMap = {}, results = [] }) {
|
||||||
|
// Check if the texvaluet can be searched
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let exists = false
|
||||||
|
let averageScore = -1
|
||||||
|
let numTextMatches = 0
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
this._log(`\nKey: ${key === '' ? '-' : key}`)
|
||||||
|
|
||||||
|
let mainSearchResult = fullSearcher.search(value)
|
||||||
|
this._log(`Full text: "${value}", score: ${mainSearchResult.score}`)
|
||||||
|
|
||||||
|
if (this.options.tokenize) {
|
||||||
|
let words = value.split(this.options.tokenSeparator)
|
||||||
|
let scores = []
|
||||||
|
|
||||||
|
for (let i = 0; i < tokenSearchers.length; i += 1) {
|
||||||
|
let tokenSearcher = tokenSearchers[i]
|
||||||
|
|
||||||
|
this._log(`\nPattern: "${tokenSearcher.pattern}"`)
|
||||||
|
|
||||||
|
// let tokenScores = []
|
||||||
|
let hasMatchInText = false
|
||||||
|
|
||||||
|
for (let j = 0; j < words.length; j += 1) {
|
||||||
|
let word = words[j]
|
||||||
|
let tokenSearchResult = tokenSearcher.search(word)
|
||||||
|
let obj = {}
|
||||||
|
if (tokenSearchResult.isMatch) {
|
||||||
|
obj[word] = tokenSearchResult.score
|
||||||
|
exists = true
|
||||||
|
hasMatchInText = true
|
||||||
|
scores.push(tokenSearchResult.score)
|
||||||
|
} else {
|
||||||
|
obj[word] = 1
|
||||||
|
if (!this.options.matchAllTokens) {
|
||||||
|
scores.push(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._log(`Token: "${word}", score: ${obj[word]}`)
|
||||||
|
// tokenScores.push(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasMatchInText) {
|
||||||
|
numTextMatches += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
averageScore = scores[0]
|
||||||
|
let scoresLen = scores.length
|
||||||
|
for (let i = 1; i < scoresLen; i += 1) {
|
||||||
|
averageScore += scores[i]
|
||||||
|
}
|
||||||
|
averageScore = averageScore / scoresLen
|
||||||
|
|
||||||
|
this._log('Token score average:', averageScore)
|
||||||
|
}
|
||||||
|
|
||||||
|
let finalScore = mainSearchResult.score
|
||||||
|
if (averageScore > -1) {
|
||||||
|
finalScore = (finalScore + averageScore) / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
this._log('Score average:', finalScore)
|
||||||
|
|
||||||
|
let checkTextMatches = (this.options.tokenize && this.options.matchAllTokens) ? numTextMatches >= tokenSearchers.length : true
|
||||||
|
|
||||||
|
this._log(`\nCheck Matches: ${checkTextMatches}`)
|
||||||
|
|
||||||
|
// If a match is found, add the item to <rawResults>, including its score
|
||||||
|
if ((exists || mainSearchResult.isMatch) && checkTextMatches) {
|
||||||
|
// Check if the item already exists in our results
|
||||||
|
let existingResult = resultMap[index]
|
||||||
|
if (existingResult) {
|
||||||
|
// Use the lowest score
|
||||||
|
// existingResult.score, bitapResult.score
|
||||||
|
existingResult.output.push({
|
||||||
|
key,
|
||||||
|
arrayIndex,
|
||||||
|
value,
|
||||||
|
score: finalScore,
|
||||||
|
matchedIndices: mainSearchResult.matchedIndices
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Add it to the raw result list
|
||||||
|
resultMap[index] = {
|
||||||
|
item: record,
|
||||||
|
output: [{
|
||||||
|
key,
|
||||||
|
arrayIndex,
|
||||||
|
value,
|
||||||
|
score: finalScore,
|
||||||
|
matchedIndices: mainSearchResult.matchedIndices
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push(resultMap[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (isArray(value)) {
|
||||||
|
for (let i = 0, len = value.length; i < len; i += 1) {
|
||||||
|
this._analyze({
|
||||||
|
key,
|
||||||
|
arrayIndex: i,
|
||||||
|
value: value[i],
|
||||||
|
record,
|
||||||
|
index
|
||||||
|
}, {
|
||||||
|
resultMap,
|
||||||
|
results,
|
||||||
|
tokenSearchers,
|
||||||
|
fullSearcher
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_computeScore (weights, results) {
|
||||||
|
this._log('\n\nComputing score:\n')
|
||||||
|
|
||||||
|
for (let i = 0, len = results.length; i < len; i += 1) {
|
||||||
|
const output = results[i].output
|
||||||
|
const scoreLen = output.length
|
||||||
|
|
||||||
|
let currScore = 1
|
||||||
|
let bestScore = 1
|
||||||
|
|
||||||
|
for (let j = 0; j < scoreLen; j += 1) {
|
||||||
|
let weight = weights ? weights[output[j].key].weight : 1
|
||||||
|
let score = weight === 1 ? output[j].score : (output[j].score || 0.001)
|
||||||
|
let nScore = score * weight
|
||||||
|
|
||||||
|
if (weight !== 1) {
|
||||||
|
bestScore = Math.min(bestScore, nScore)
|
||||||
|
} else {
|
||||||
|
output[j].nScore = nScore
|
||||||
|
currScore *= nScore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results[i].score = bestScore === 1 ? currScore : bestScore
|
||||||
|
|
||||||
|
this._log(results[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_sort (results) {
|
||||||
|
this._log('\n\nSorting....')
|
||||||
|
results.sort(this.options.sortFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
_format (results) {
|
||||||
|
const finalOutput = []
|
||||||
|
|
||||||
|
if (this.options.verbose) {
|
||||||
|
this._log('\n\nOutput:\n\n', JSON.stringify(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
let transformers = []
|
||||||
|
|
||||||
|
if (this.options.includeMatches) {
|
||||||
|
transformers.push((result, data) => {
|
||||||
|
const output = result.output
|
||||||
|
data.matches = []
|
||||||
|
|
||||||
|
for (let i = 0, len = output.length; i < len; i += 1) {
|
||||||
|
let item = output[i]
|
||||||
|
|
||||||
|
if (item.matchedIndices.length === 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let obj = {
|
||||||
|
indices: item.matchedIndices,
|
||||||
|
value: item.value
|
||||||
|
}
|
||||||
|
if (item.key) {
|
||||||
|
obj.key = item.key
|
||||||
|
}
|
||||||
|
if (item.hasOwnProperty('arrayIndex') && item.arrayIndex > -1) {
|
||||||
|
obj.arrayIndex = item.arrayIndex
|
||||||
|
}
|
||||||
|
data.matches.push(obj)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.includeScore) {
|
||||||
|
transformers.push((result, data) => {
|
||||||
|
data.score = result.score
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0, len = results.length; i < len; i += 1) {
|
||||||
|
const result = results[i]
|
||||||
|
|
||||||
|
if (this.options.id) {
|
||||||
|
result.item = this.options.getFn(result.item, this.options.id)[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!transformers.length) {
|
||||||
|
finalOutput.push(result.item)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
item: result.item
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let j = 0, len = transformers.length; j < len; j += 1) {
|
||||||
|
transformers[j](result, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
finalOutput.push(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
_log () {
|
||||||
|
if (this.options.verbose) {
|
||||||
|
console.log(...arguments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Fuse
|
@ -12,6 +12,7 @@ html, body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column nowrap;
|
flex-flow: column nowrap;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
@ -19,4 +20,13 @@ html, body {
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background-color: $white;
|
background-color: $white;
|
||||||
|
z-index: 100;
|
||||||
|
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#modal-root {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 200;
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,7 @@ $wisteria-purple: #AE60B9;
|
|||||||
color: $white;
|
color: $white;
|
||||||
outline: none;
|
outline: none;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: lighten($royal-blue, 5);
|
background-color: lighten($royal-blue, 5);
|
||||||
@ -46,3 +47,17 @@ $wisteria-purple: #AE60B9;
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
%borderless-input {
|
||||||
|
color: $mine-shaft-gray;
|
||||||
|
font-size: 1rem;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 0;
|
||||||
|
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: $chalice-gray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
70
yarn.lock
70
yarn.lock
@ -2021,6 +2021,10 @@ center-align@^0.1.1:
|
|||||||
align-text "^0.1.3"
|
align-text "^0.1.3"
|
||||||
lazy-cache "^1.0.3"
|
lazy-cache "^1.0.3"
|
||||||
|
|
||||||
|
chain-function@^1.0.0:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/chain-function/-/chain-function-1.0.1.tgz#c63045e5b4b663fb86f1c6e186adaf1de402a1cc"
|
||||||
|
|
||||||
chalk@2.4.1, chalk@^2.0.1, chalk@^2.4.1:
|
chalk@2.4.1, chalk@^2.0.1, chalk@^2.4.1:
|
||||||
version "2.4.1"
|
version "2.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
|
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
|
||||||
@ -2166,6 +2170,12 @@ cli-width@^2.0.0:
|
|||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
|
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
|
||||||
|
|
||||||
|
cli@0.4.3:
|
||||||
|
version "0.4.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/cli/-/cli-0.4.3.tgz#e6819c8d5faa957f64f98f66a8506268c1d1f17d"
|
||||||
|
dependencies:
|
||||||
|
glob ">= 3.1.4"
|
||||||
|
|
||||||
cliui@^2.1.0:
|
cliui@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1"
|
resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1"
|
||||||
@ -2285,7 +2295,7 @@ color@^3.0.0:
|
|||||||
color-convert "^1.9.1"
|
color-convert "^1.9.1"
|
||||||
color-string "^1.5.2"
|
color-string "^1.5.2"
|
||||||
|
|
||||||
colors@^1.1.2:
|
colors@>=0.6.x, colors@^1.1.2:
|
||||||
version "1.3.2"
|
version "1.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.2.tgz#2df8ff573dfbf255af562f8ce7181d6b971a359b"
|
resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.2.tgz#2df8ff573dfbf255af562f8ce7181d6b971a359b"
|
||||||
|
|
||||||
@ -3412,6 +3422,10 @@ dom-converter@~0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
utila "~0.3"
|
utila "~0.3"
|
||||||
|
|
||||||
|
dom-helpers@^3.2.0:
|
||||||
|
version "3.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6"
|
||||||
|
|
||||||
dom-serializer@0:
|
dom-serializer@0:
|
||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
|
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
|
||||||
@ -4485,6 +4499,16 @@ functional-red-black-tree@^1.0.1:
|
|||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
|
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
|
||||||
|
|
||||||
|
fuse@^0.4.0:
|
||||||
|
version "0.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/fuse/-/fuse-0.4.0.tgz#2c38eaf888abb0a9ba7960cfe3339d1f3f53f6e6"
|
||||||
|
dependencies:
|
||||||
|
colors ">=0.6.x"
|
||||||
|
jshint "0.9.x"
|
||||||
|
optimist ">=0.3.5"
|
||||||
|
uglify-js ">=2.2.x"
|
||||||
|
underscore ">=1.4.x"
|
||||||
|
|
||||||
gauge@~2.7.3:
|
gauge@~2.7.3:
|
||||||
version "2.7.4"
|
version "2.7.4"
|
||||||
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
|
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
|
||||||
@ -4586,7 +4610,7 @@ glob-parent@^3.1.0:
|
|||||||
is-glob "^3.1.0"
|
is-glob "^3.1.0"
|
||||||
path-dirname "^1.0.0"
|
path-dirname "^1.0.0"
|
||||||
|
|
||||||
glob@^7.0.0, glob@~7.1.1, glob@~7.1.2:
|
"glob@>= 3.1.4", glob@^7.0.0, glob@~7.1.1, glob@~7.1.2:
|
||||||
version "7.1.3"
|
version "7.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
|
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -6148,6 +6172,13 @@ jsesc@~0.5.0:
|
|||||||
version "0.5.0"
|
version "0.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
|
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
|
||||||
|
|
||||||
|
jshint@0.9.x:
|
||||||
|
version "0.9.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/jshint/-/jshint-0.9.1.tgz#ff32ec7f09f84001f7498eeafd63c9e4fbb2dc0e"
|
||||||
|
dependencies:
|
||||||
|
cli "0.4.3"
|
||||||
|
minimatch "0.0.x"
|
||||||
|
|
||||||
json-parse-better-errors@^1.0.0, json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2:
|
json-parse-better-errors@^1.0.0, json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
|
resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
|
||||||
@ -6559,6 +6590,10 @@ lru-cache@^4.1.1, lru-cache@^4.1.2, lru-cache@^4.1.3:
|
|||||||
pseudomap "^1.0.2"
|
pseudomap "^1.0.2"
|
||||||
yallist "^2.1.2"
|
yallist "^2.1.2"
|
||||||
|
|
||||||
|
lru-cache@~1.0.2:
|
||||||
|
version "1.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-1.0.6.tgz#aa50f97047422ac72543bda177a9c9d018d98452"
|
||||||
|
|
||||||
make-dir@^1.0.0:
|
make-dir@^1.0.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.1.0.tgz#19b4369fe48c116f53c2af95ad102c0e39e85d51"
|
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.1.0.tgz#19b4369fe48c116f53c2af95ad102c0e39e85d51"
|
||||||
@ -6817,6 +6852,12 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
|
|||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
|
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
|
||||||
|
|
||||||
|
minimatch@0.0.x:
|
||||||
|
version "0.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.0.5.tgz#96bb490bbd3ba6836bbfac111adf75301b1584de"
|
||||||
|
dependencies:
|
||||||
|
lru-cache "~1.0.2"
|
||||||
|
|
||||||
minimatch@0.3:
|
minimatch@0.3:
|
||||||
version "0.3.0"
|
version "0.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.3.0.tgz#275d8edaac4f1bb3326472089e7949c8394699dd"
|
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.3.0.tgz#275d8edaac4f1bb3326472089e7949c8394699dd"
|
||||||
@ -7572,7 +7613,7 @@ opn@^5.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-wsl "^1.1.0"
|
is-wsl "^1.1.0"
|
||||||
|
|
||||||
optimist@^0.6.1:
|
optimist@>=0.3.5, optimist@^0.6.1:
|
||||||
version "0.6.1"
|
version "0.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
|
resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -8623,7 +8664,7 @@ prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.0:
|
|||||||
loose-envify "^1.3.1"
|
loose-envify "^1.3.1"
|
||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
|
|
||||||
prop-types@^15.6.1, prop-types@^15.6.2:
|
prop-types@^15.5.6, prop-types@^15.6.1, prop-types@^15.6.2:
|
||||||
version "15.6.2"
|
version "15.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
|
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -9006,6 +9047,16 @@ react-side-effect@^1.1.0:
|
|||||||
exenv "^1.2.1"
|
exenv "^1.2.1"
|
||||||
shallowequal "^1.0.1"
|
shallowequal "^1.0.1"
|
||||||
|
|
||||||
|
react-transition-group@1.x:
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-1.2.1.tgz#e11f72b257f921b213229a774df46612346c7ca6"
|
||||||
|
dependencies:
|
||||||
|
chain-function "^1.0.0"
|
||||||
|
dom-helpers "^3.2.0"
|
||||||
|
loose-envify "^1.3.1"
|
||||||
|
prop-types "^15.5.6"
|
||||||
|
warning "^3.0.0"
|
||||||
|
|
||||||
react@^16.2.0:
|
react@^16.2.0:
|
||||||
version "16.2.0"
|
version "16.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba"
|
resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba"
|
||||||
@ -10761,6 +10812,13 @@ uglify-js@3.3.x:
|
|||||||
commander "~2.13.0"
|
commander "~2.13.0"
|
||||||
source-map "~0.6.1"
|
source-map "~0.6.1"
|
||||||
|
|
||||||
|
uglify-js@>=2.2.x:
|
||||||
|
version "3.4.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3"
|
||||||
|
dependencies:
|
||||||
|
commander "~2.17.1"
|
||||||
|
source-map "~0.6.1"
|
||||||
|
|
||||||
uglify-js@^2.6:
|
uglify-js@^2.6:
|
||||||
version "2.8.29"
|
version "2.8.29"
|
||||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"
|
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"
|
||||||
@ -10810,6 +10868,10 @@ underscore@1.8.3:
|
|||||||
version "1.8.3"
|
version "1.8.3"
|
||||||
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022"
|
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022"
|
||||||
|
|
||||||
|
underscore@>=1.4.x:
|
||||||
|
version "1.9.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961"
|
||||||
|
|
||||||
unicode-canonical-property-names-ecmascript@^1.0.4:
|
unicode-canonical-property-names-ecmascript@^1.0.4:
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
|
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
|
||||||
|
Loading…
Reference in New Issue
Block a user