Add Token Select Modal
This commit is contained in:
parent
8847f4e4d2
commit
1d86c0d656
@ -7,6 +7,7 @@
|
||||
"axios": "^0.18.0",
|
||||
"classnames": "^2.2.6",
|
||||
"d3": "^4.13.0",
|
||||
"fuse": "^0.4.0",
|
||||
"jazzicon": "^1.5.0",
|
||||
"node-sass": "^4.9.3",
|
||||
"npm": "^6.0.0",
|
||||
@ -20,6 +21,7 @@
|
||||
"react-scripts": "2.0.4",
|
||||
"react-scroll-to-component": "^1.0.2",
|
||||
"react-select": "^1.2.1",
|
||||
"react-transition-group": "1.x",
|
||||
"redux": "^3.7.2",
|
||||
"redux-subscriber": "^1.1.0",
|
||||
"redux-thunk": "^2.2.0",
|
||||
|
@ -26,6 +26,7 @@
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<div id="root"></div>
|
||||
<div id="modal-root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
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;
|
||||
|
||||
&__input {
|
||||
color: $mine-shaft-gray;
|
||||
font-size: .9rem;
|
||||
font-size: .75rem;
|
||||
outline: none;
|
||||
border: none;
|
||||
flex: 1 1 auto;
|
||||
|
@ -41,16 +41,7 @@
|
||||
}
|
||||
|
||||
&__input {
|
||||
color: $mine-shaft-gray;
|
||||
font-size: 1.5rem;
|
||||
outline: none;
|
||||
border: none;
|
||||
flex: 1 1 auto;
|
||||
width: 0;
|
||||
|
||||
&::placeholder {
|
||||
color: $chalice-gray;
|
||||
}
|
||||
@extend %borderless-input;
|
||||
}
|
||||
|
||||
&__currency-select {
|
||||
@ -70,6 +61,17 @@
|
||||
&:active {
|
||||
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 {
|
||||
@ -81,5 +83,82 @@
|
||||
background-size: contain;
|
||||
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 { connect } from 'react-redux';
|
||||
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';
|
||||
|
||||
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 {
|
||||
static propTypes = {
|
||||
title: PropTypes.string,
|
||||
@ -11,6 +34,93 @@ class CurrencyInputPanel extends Component {
|
||||
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() {
|
||||
const {
|
||||
title,
|
||||
@ -18,6 +128,8 @@ class CurrencyInputPanel extends Component {
|
||||
extraText,
|
||||
} = this.props;
|
||||
|
||||
const { selectedTokenAddress } = this.state;
|
||||
|
||||
return (
|
||||
<div className="currency-input-panel">
|
||||
<div className="currency-input-panel__container">
|
||||
@ -30,15 +142,33 @@ class CurrencyInputPanel extends Component {
|
||||
</div>
|
||||
<div className="currency-input-panel__input-row">
|
||||
<input type="number" className="currency-input-panel__input" placeholder="0.0" />
|
||||
<button className="currency-input-panel__currency-select">
|
||||
Select a token
|
||||
<button
|
||||
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" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{this.renderModal()}
|
||||
</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 SelectToken from './SelectToken';
|
||||
import { bindActionCreators } from '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 {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
height: 100vh;
|
||||
@ -19,4 +20,13 @@ html, body {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
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;
|
||||
outline: none;
|
||||
border: 1px solid transparent;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
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"
|
||||
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:
|
||||
version "2.4.1"
|
||||
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"
|
||||
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:
|
||||
version "2.1.0"
|
||||
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-string "^1.5.2"
|
||||
|
||||
colors@^1.1.2:
|
||||
colors@>=0.6.x, colors@^1.1.2:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.2.tgz#2df8ff573dfbf255af562f8ce7181d6b971a359b"
|
||||
|
||||
@ -3412,6 +3422,10 @@ dom-converter@~0.1:
|
||||
dependencies:
|
||||
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:
|
||||
version "0.1.0"
|
||||
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"
|
||||
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:
|
||||
version "2.7.4"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
|
||||
dependencies:
|
||||
@ -6148,6 +6172,13 @@ jsesc@~0.5.0:
|
||||
version "0.5.0"
|
||||
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:
|
||||
version "1.0.2"
|
||||
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"
|
||||
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:
|
||||
version "1.1.0"
|
||||
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"
|
||||
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:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.3.0.tgz#275d8edaac4f1bb3326472089e7949c8394699dd"
|
||||
@ -7572,7 +7613,7 @@ opn@^5.1.0:
|
||||
dependencies:
|
||||
is-wsl "^1.1.0"
|
||||
|
||||
optimist@^0.6.1:
|
||||
optimist@>=0.3.5, optimist@^0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
|
||||
dependencies:
|
||||
@ -9006,6 +9047,16 @@ react-side-effect@^1.1.0:
|
||||
exenv "^1.2.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:
|
||||
version "16.2.0"
|
||||
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"
|
||||
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:
|
||||
version "2.8.29"
|
||||
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"
|
||||
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:
|
||||
version "1.0.4"
|
||||
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