mirror of
https://github.com/zhigang1992/mitmproxy.git
synced 2026-04-24 04:14:57 +08:00
web: add prompt for keyboard navigation
This commit is contained in:
@@ -14,5 +14,6 @@ html {
|
||||
@import (less) "flowtable.less";
|
||||
@import (less) "flowdetail.less";
|
||||
@import (less) "flowview.less";
|
||||
@import (less) "prompt.less";
|
||||
@import (less) "eventlog.less";
|
||||
@import (less) "footer.less";
|
||||
27
web/src/css/prompt.less
Normal file
27
web/src/css/prompt.less
Normal file
@@ -0,0 +1,27 @@
|
||||
.prompt-dialog {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.prompt-content {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 25px;
|
||||
padding: 2px 5px;
|
||||
background-color: white;
|
||||
box-shadow: 0 -1px 3px lightgray;
|
||||
|
||||
.option {
|
||||
cursor: pointer;
|
||||
&:not(:last-child)::after {
|
||||
content: ", ";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
|
||||
var React = require("react");
|
||||
var ReactRouter = require("react-router");
|
||||
var $ = require("jquery");
|
||||
var Connection = require("./connection");
|
||||
var proxyapp = require("./components/proxyapp.js");
|
||||
var EventLogActions = require("./actions.js").EventLogActions;
|
||||
|
||||
$(function () {
|
||||
window.ws = new Connection("/updates");
|
||||
|
||||
window.onerror = function (msg) {
|
||||
EventLogActions.add_event(msg);
|
||||
};
|
||||
|
||||
ReactRouter.run(proxyapp.routes, function (Handler, state) {
|
||||
React.render(<Handler/>, document.body);
|
||||
});
|
||||
|
||||
@@ -44,7 +44,6 @@ var EventLogContents = React.createClass({
|
||||
view.addListener("recalculate", this.onEventLogChange);
|
||||
|
||||
return {
|
||||
log: view.list,
|
||||
view: view
|
||||
};
|
||||
},
|
||||
@@ -74,12 +73,13 @@ var EventLogContents = React.createClass({
|
||||
return <LogMessage key={elem.id} entry={elem}/>;
|
||||
},
|
||||
render: function () {
|
||||
var rows = this.renderRows(this.state.log);
|
||||
var entries = this.state.view.list;
|
||||
var rows = this.renderRows(entries);
|
||||
|
||||
return <pre onScroll={this.onScroll}>
|
||||
{ this.getPlaceholderTop(this.state.log.length) }
|
||||
{ this.getPlaceholderTop(entries.length) }
|
||||
{rows}
|
||||
{ this.getPlaceholderBottom(this.state.log.length) }
|
||||
{ this.getPlaceholderBottom(entries.length) }
|
||||
</pre>;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,6 +5,8 @@ var common = require("../common.js");
|
||||
var Nav = require("./nav.js");
|
||||
var Messages = require("./messages.js");
|
||||
var Details = require("./details.js");
|
||||
var Prompt = require("../prompt.js");
|
||||
|
||||
|
||||
var allTabs = {
|
||||
request: Messages.Request,
|
||||
@@ -15,6 +17,11 @@ var allTabs = {
|
||||
|
||||
var FlowView = React.createClass({
|
||||
mixins: [common.StickyHeadMixin, common.Navigation, common.RouterState],
|
||||
getInitialState: function () {
|
||||
return {
|
||||
prompt: false
|
||||
};
|
||||
},
|
||||
getTabs: function (flow) {
|
||||
var tabs = [];
|
||||
["request", "response", "error"].forEach(function (e) {
|
||||
@@ -27,7 +34,7 @@ var FlowView = React.createClass({
|
||||
},
|
||||
nextTab: function (i) {
|
||||
var tabs = this.getTabs(this.props.flow);
|
||||
var currentIndex = tabs.indexOf(this.getParams().detailTab);
|
||||
var currentIndex = tabs.indexOf(this.getActive());
|
||||
// JS modulo operator doesn't correct negative numbers, make sure that we are positive.
|
||||
var nextIndex = (currentIndex + i + tabs.length) % tabs.length;
|
||||
this.selectTab(tabs[nextIndex]);
|
||||
@@ -41,10 +48,50 @@ var FlowView = React.createClass({
|
||||
}
|
||||
);
|
||||
},
|
||||
getActive: function(){
|
||||
return this.getParams().detailTab;
|
||||
},
|
||||
promptEdit: function () {
|
||||
var options;
|
||||
switch(this.getActive()){
|
||||
case "request":
|
||||
options = [
|
||||
"method",
|
||||
"url",
|
||||
{text:"http version", key:"v"},
|
||||
"header"
|
||||
/*, "content"*/];
|
||||
break;
|
||||
case "response":
|
||||
options = [
|
||||
{text:"http version", key:"v"},
|
||||
"code",
|
||||
"message",
|
||||
"header"
|
||||
/*, "content"*/];
|
||||
break;
|
||||
case "details":
|
||||
return;
|
||||
default:
|
||||
throw "Unknown tab for edit: " + this.getActive();
|
||||
}
|
||||
|
||||
this.setState({
|
||||
prompt: {
|
||||
done: function (k) {
|
||||
this.setState({prompt: false});
|
||||
if(k){
|
||||
this.refs.tab.edit(k);
|
||||
}
|
||||
}.bind(this),
|
||||
options: options
|
||||
}
|
||||
});
|
||||
},
|
||||
render: function () {
|
||||
var flow = this.props.flow;
|
||||
var tabs = this.getTabs(flow);
|
||||
var active = this.getParams().detailTab;
|
||||
var active = this.getActive();
|
||||
|
||||
if (!_.contains(tabs, active)) {
|
||||
if (active === "response" && flow.error) {
|
||||
@@ -57,6 +104,11 @@ var FlowView = React.createClass({
|
||||
this.selectTab(active);
|
||||
}
|
||||
|
||||
var prompt = null;
|
||||
if (this.state.prompt) {
|
||||
prompt = <Prompt {...this.state.prompt}/>;
|
||||
}
|
||||
|
||||
var Tab = allTabs[active];
|
||||
return (
|
||||
<div className="flow-detail" onScroll={this.adjustHead}>
|
||||
@@ -65,7 +117,8 @@ var FlowView = React.createClass({
|
||||
tabs={tabs}
|
||||
active={active}
|
||||
selectTab={this.selectTab}/>
|
||||
<Tab flow={flow}/>
|
||||
<Tab ref="tab" flow={flow}/>
|
||||
{prompt}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,13 +23,16 @@ var Headers = React.createClass({
|
||||
} else {
|
||||
nextHeaders.splice(row, 1);
|
||||
// manually move selection target if this has been the last row.
|
||||
if(row === nextHeaders.length){
|
||||
this._nextSel = (row-1)+"-value";
|
||||
if (row === nextHeaders.length) {
|
||||
this._nextSel = (row - 1) + "-value";
|
||||
}
|
||||
}
|
||||
}
|
||||
this.props.onChange(nextHeaders);
|
||||
},
|
||||
edit: function () {
|
||||
this.refs["0-key"].focus();
|
||||
},
|
||||
onTab: function (row, col, e) {
|
||||
var headers = this.props.message.headers;
|
||||
if (row === headers.length - 1 && col === 1) {
|
||||
@@ -138,9 +141,11 @@ var InlineInput = React.createClass({
|
||||
},
|
||||
blur: function () {
|
||||
this.getDOMNode().blur();
|
||||
window.getSelection().removeAllRanges();
|
||||
this.context.returnFocus && this.context.returnFocus();
|
||||
},
|
||||
selectContents: function () {
|
||||
focus: function () {
|
||||
React.findDOMNode(this).focus();
|
||||
var range = document.createRange();
|
||||
range.selectNodeContents(this.getDOMNode());
|
||||
var sel = window.getSelection();
|
||||
@@ -148,7 +153,7 @@ var InlineInput = React.createClass({
|
||||
sel.addRange(range);
|
||||
},
|
||||
onFocus: function () {
|
||||
this.setState({editable: true}, this.selectContents);
|
||||
this.setState({editable: true}, this.focus);
|
||||
},
|
||||
onBlur: function (e) {
|
||||
this.setState({editable: false});
|
||||
@@ -182,7 +187,7 @@ var HeaderInlineInput = React.createClass({
|
||||
}
|
||||
break;
|
||||
case utils.Key.TAB:
|
||||
if(!e.shiftKey){
|
||||
if (!e.shiftKey) {
|
||||
this.props.onTab(e);
|
||||
}
|
||||
break;
|
||||
@@ -202,6 +207,9 @@ var ValidateInlineInput = React.createClass({
|
||||
originalContent: this.props.content
|
||||
};
|
||||
},
|
||||
focus: function () {
|
||||
this.getDOMNode().focus();
|
||||
},
|
||||
onChange: function (val) {
|
||||
this.setState({
|
||||
content: val
|
||||
@@ -253,11 +261,11 @@ var RequestLine = React.createClass({
|
||||
var httpver = "HTTP/" + flow.request.httpversion.join(".");
|
||||
|
||||
return <div className="first-line request-line">
|
||||
<InlineInput content={flow.request.method} onChange={this.onMethodChange}/>
|
||||
<InlineInput ref="method" content={flow.request.method} onChange={this.onMethodChange}/>
|
||||
|
||||
<ValidateInlineInput content={url} onChange={this.onUrlChange} isValid={this.isValidUrl} />
|
||||
<ValidateInlineInput ref="url" content={url} onChange={this.onUrlChange} isValid={this.isValidUrl} />
|
||||
|
||||
<ValidateInlineInput immediate content={httpver} onChange={this.onHttpVersionChange} isValid={flowutils.isValidHttpVersion} />
|
||||
<ValidateInlineInput ref="httpVersion" immediate content={httpver} onChange={this.onHttpVersionChange} isValid={flowutils.isValidHttpVersion} />
|
||||
</div>
|
||||
},
|
||||
isValidUrl: function (url) {
|
||||
@@ -292,11 +300,11 @@ var ResponseLine = React.createClass({
|
||||
var flow = this.props.flow;
|
||||
var httpver = "HTTP/" + flow.response.httpversion.join(".");
|
||||
return <div className="first-line response-line">
|
||||
<ValidateInlineInput immediate content={httpver} onChange={this.onHttpVersionChange} isValid={flowutils.isValidHttpVersion} />
|
||||
<ValidateInlineInput ref="httpVersion" immediate content={httpver} onChange={this.onHttpVersionChange} isValid={flowutils.isValidHttpVersion} />
|
||||
|
||||
<ValidateInlineInput immediate content={flow.response.code + ""} onChange={this.onCodeChange} isValid={this.isValidCode} />
|
||||
<ValidateInlineInput ref="code" immediate content={flow.response.code + ""} onChange={this.onCodeChange} isValid={this.isValidCode} />
|
||||
|
||||
<InlineInput content={flow.response.msg} onChange={this.onMsgChange}/>
|
||||
<InlineInput ref="msg" content={flow.response.msg} onChange={this.onMsgChange}/>
|
||||
|
||||
</div>;
|
||||
},
|
||||
@@ -330,14 +338,32 @@ var Request = React.createClass({
|
||||
var flow = this.props.flow;
|
||||
return (
|
||||
<section className="request">
|
||||
<RequestLine flow={flow}/>
|
||||
<RequestLine ref="requestLine" flow={flow}/>
|
||||
{/*<ResponseLine flow={flow}/>*/}
|
||||
<Headers message={flow.request} onChange={this.onHeaderChange}/>
|
||||
<Headers ref="headers" message={flow.request} onChange={this.onHeaderChange}/>
|
||||
<hr/>
|
||||
<ContentView flow={flow} message={flow.request}/>
|
||||
</section>
|
||||
);
|
||||
},
|
||||
edit: function (k) {
|
||||
switch (k) {
|
||||
case "m":
|
||||
this.refs.requestLine.refs.method.focus();
|
||||
break;
|
||||
case "u":
|
||||
this.refs.requestLine.refs.url.focus();
|
||||
break;
|
||||
case "v":
|
||||
this.refs.requestLine.refs.httpVersion.focus();
|
||||
break;
|
||||
case "h":
|
||||
this.refs.headers.edit();
|
||||
break;
|
||||
default:
|
||||
throw "Unimplemented: "+ k;
|
||||
}
|
||||
},
|
||||
onHeaderChange: function (nextHeaders) {
|
||||
actions.FlowActions.update(this.props.flow, {
|
||||
request: {
|
||||
@@ -353,13 +379,31 @@ var Response = React.createClass({
|
||||
return (
|
||||
<section className="response">
|
||||
{/*<RequestLine flow={flow}/>*/}
|
||||
<ResponseLine flow={flow}/>
|
||||
<Headers message={flow.response} onChange={this.onHeaderChange}/>
|
||||
<ResponseLine ref="responseLine" flow={flow}/>
|
||||
<Headers ref="headers" message={flow.response} onChange={this.onHeaderChange}/>
|
||||
<hr/>
|
||||
<ContentView flow={flow} message={flow.response}/>
|
||||
</section>
|
||||
);
|
||||
},
|
||||
edit: function (k) {
|
||||
switch (k) {
|
||||
case "c":
|
||||
this.refs.responseLine.refs.code.focus();
|
||||
break;
|
||||
case "m":
|
||||
this.refs.responseLine.refs.msg.focus();
|
||||
break;
|
||||
case "v":
|
||||
this.refs.responseLine.refs.httpVersion.focus();
|
||||
break;
|
||||
case "h":
|
||||
this.refs.headers.edit();
|
||||
break;
|
||||
default:
|
||||
throw "Unimplemented: "+ k;
|
||||
}
|
||||
},
|
||||
onHeaderChange: function (nextHeaders) {
|
||||
actions.FlowActions.update(this.props.flow, {
|
||||
response: {
|
||||
|
||||
@@ -8,7 +8,7 @@ var Footer = React.createClass({
|
||||
var intercept = this.state.settings.intercept;
|
||||
return (
|
||||
<footer>
|
||||
{mode != "regular" ? <span className="label label-success">{mode} mode</span> : null}
|
||||
{mode && mode != "regular" ? <span className="label label-success">{mode} mode</span> : null}
|
||||
|
||||
{intercept ? <span className="label label-success">Intercept: {intercept}</span> : null}
|
||||
</footer>
|
||||
|
||||
@@ -165,7 +165,7 @@ var MainMenu = React.createClass({
|
||||
title: "Start",
|
||||
route: "flows"
|
||||
},
|
||||
onFilterChange: function (val) {
|
||||
onSearchChange: function (val) {
|
||||
var d = {};
|
||||
d[Query.SEARCH] = val;
|
||||
this.setQuery(d);
|
||||
@@ -192,7 +192,7 @@ var MainMenu = React.createClass({
|
||||
type="search"
|
||||
color="black"
|
||||
value={search}
|
||||
onChange={this.onFilterChange} />
|
||||
onChange={this.onSearchChange} />
|
||||
<FilterInput
|
||||
ref="highlight"
|
||||
placeholder="Highlight"
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
var React = require("react");
|
||||
|
||||
var common = require("./common.js");
|
||||
var actions = require("../actions.js");
|
||||
var Query = require("../actions.js").Query;
|
||||
var toputils = require("../utils.js");
|
||||
var utils = require("../utils.js");
|
||||
var views = require("../store/view.js");
|
||||
var Filt = require("../filt/filt.js");
|
||||
FlowTable = require("./flowtable.js");
|
||||
|
||||
var common = require("./common.js");
|
||||
var FlowTable = require("./flowtable.js");
|
||||
var FlowView = require("./flowview/index.js");
|
||||
|
||||
var MainView = React.createClass({
|
||||
@@ -105,7 +106,7 @@ var MainView = React.createClass({
|
||||
var flows = this.state.view.list;
|
||||
var index;
|
||||
if (!this.getParams().flowId) {
|
||||
if (shift > 0) {
|
||||
if (shift < 0) {
|
||||
index = flows.length - 1;
|
||||
} else {
|
||||
index = 0;
|
||||
@@ -131,49 +132,49 @@ var MainView = React.createClass({
|
||||
return;
|
||||
}
|
||||
switch (e.keyCode) {
|
||||
case toputils.Key.K:
|
||||
case toputils.Key.UP:
|
||||
case utils.Key.K:
|
||||
case utils.Key.UP:
|
||||
this.selectFlowRelative(-1);
|
||||
break;
|
||||
case toputils.Key.J:
|
||||
case toputils.Key.DOWN:
|
||||
case utils.Key.J:
|
||||
case utils.Key.DOWN:
|
||||
this.selectFlowRelative(+1);
|
||||
break;
|
||||
case toputils.Key.SPACE:
|
||||
case toputils.Key.PAGE_DOWN:
|
||||
case utils.Key.SPACE:
|
||||
case utils.Key.PAGE_DOWN:
|
||||
this.selectFlowRelative(+10);
|
||||
break;
|
||||
case toputils.Key.PAGE_UP:
|
||||
case utils.Key.PAGE_UP:
|
||||
this.selectFlowRelative(-10);
|
||||
break;
|
||||
case toputils.Key.END:
|
||||
case utils.Key.END:
|
||||
this.selectFlowRelative(+1e10);
|
||||
break;
|
||||
case toputils.Key.HOME:
|
||||
case utils.Key.HOME:
|
||||
this.selectFlowRelative(-1e10);
|
||||
break;
|
||||
case toputils.Key.ESC:
|
||||
case utils.Key.ESC:
|
||||
this.selectFlow(null);
|
||||
break;
|
||||
case toputils.Key.H:
|
||||
case toputils.Key.LEFT:
|
||||
case utils.Key.H:
|
||||
case utils.Key.LEFT:
|
||||
if (this.refs.flowDetails) {
|
||||
this.refs.flowDetails.nextTab(-1);
|
||||
}
|
||||
break;
|
||||
case toputils.Key.L:
|
||||
case toputils.Key.TAB:
|
||||
case toputils.Key.RIGHT:
|
||||
case utils.Key.L:
|
||||
case utils.Key.TAB:
|
||||
case utils.Key.RIGHT:
|
||||
if (this.refs.flowDetails) {
|
||||
this.refs.flowDetails.nextTab(+1);
|
||||
}
|
||||
break;
|
||||
case toputils.Key.C:
|
||||
case utils.Key.C:
|
||||
if (e.shiftKey) {
|
||||
actions.FlowActions.clear();
|
||||
}
|
||||
break;
|
||||
case toputils.Key.D:
|
||||
case utils.Key.D:
|
||||
if (flow) {
|
||||
if (e.shiftKey) {
|
||||
actions.FlowActions.duplicate(flow);
|
||||
@@ -182,24 +183,29 @@ var MainView = React.createClass({
|
||||
}
|
||||
}
|
||||
break;
|
||||
case toputils.Key.A:
|
||||
case utils.Key.A:
|
||||
if (e.shiftKey) {
|
||||
actions.FlowActions.accept_all();
|
||||
} else if (flow && flow.intercepted) {
|
||||
actions.FlowActions.accept(flow);
|
||||
}
|
||||
break;
|
||||
case toputils.Key.R:
|
||||
case utils.Key.R:
|
||||
if (!e.shiftKey && flow) {
|
||||
actions.FlowActions.replay(flow);
|
||||
}
|
||||
break;
|
||||
case toputils.Key.V:
|
||||
case utils.Key.V:
|
||||
if (e.shiftKey && flow && flow.modified) {
|
||||
actions.FlowActions.revert(flow);
|
||||
}
|
||||
break;
|
||||
case toputils.Key.SHIFT:
|
||||
case utils.Key.E:
|
||||
if (this.refs.flowDetails) {
|
||||
this.refs.flowDetails.promptEdit();
|
||||
}
|
||||
break;
|
||||
case utils.Key.SHIFT:
|
||||
break;
|
||||
default:
|
||||
console.debug("keydown", e.keyCode);
|
||||
|
||||
100
web/src/js/components/prompt.js
Normal file
100
web/src/js/components/prompt.js
Normal file
@@ -0,0 +1,100 @@
|
||||
var React = require("react");
|
||||
var _ = require("lodash");
|
||||
|
||||
var utils = require("../utils.js");
|
||||
var common = require("./common.js");
|
||||
|
||||
var Prompt = React.createClass({
|
||||
mixins: [common.ChildFocus],
|
||||
propTypes: {
|
||||
options: React.PropTypes.array.isRequired,
|
||||
done: React.PropTypes.func.isRequired,
|
||||
prompt: React.PropTypes.string
|
||||
},
|
||||
componentDidMount: function () {
|
||||
React.findDOMNode(this).focus();
|
||||
},
|
||||
onKeyDown: function (e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
var opts = this.getOptions();
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
var k = opts[i].key;
|
||||
if (utils.Key[k.toUpperCase()] === e.keyCode) {
|
||||
this.done(k);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (e.keyCode === utils.Key.ESC || e.keyCode === utils.Key.ENTER) {
|
||||
this.done(false);
|
||||
}
|
||||
},
|
||||
onClick: function (e) {
|
||||
this.done(false);
|
||||
},
|
||||
done: function (ret) {
|
||||
this.props.done(ret);
|
||||
this.context.returnFocus && this.context.returnFocus();
|
||||
},
|
||||
getOptions: function () {
|
||||
var opts = [];
|
||||
|
||||
var keyTaken = function (k) {
|
||||
return _.includes(_.pluck(opts, "key"), k);
|
||||
};
|
||||
|
||||
for (var i = 0; i < this.props.options.length; i++) {
|
||||
var opt = this.props.options[i];
|
||||
if (_.isString(opt)) {
|
||||
var str = opt;
|
||||
while (str.length > 0 && keyTaken(str[0])) {
|
||||
str = str.substr(1);
|
||||
}
|
||||
opt = {
|
||||
text: opt,
|
||||
key: str[0]
|
||||
};
|
||||
}
|
||||
if (!opt.text || !opt.key || keyTaken(opt.key)) {
|
||||
throw "invalid options";
|
||||
} else {
|
||||
opts.push(opt);
|
||||
}
|
||||
}
|
||||
return opts;
|
||||
},
|
||||
render: function () {
|
||||
var opts = this.getOptions();
|
||||
opts = _.map(opts, function (o) {
|
||||
var prefix, suffix;
|
||||
var idx = o.text.indexOf(o.key);
|
||||
if (idx !== -1) {
|
||||
prefix = o.text.substring(0, idx);
|
||||
suffix = o.text.substring(idx + 1);
|
||||
|
||||
} else {
|
||||
prefix = o.text + " (";
|
||||
suffix = ")";
|
||||
}
|
||||
var onClick = function (e) {
|
||||
this.done(o.key);
|
||||
e.stopPropagation();
|
||||
}.bind(this);
|
||||
return <span
|
||||
key={o.key}
|
||||
className="option"
|
||||
onClick={onClick}>
|
||||
{prefix}
|
||||
<strong className="text-primary">{o.key}</strong>{suffix}
|
||||
</span>;
|
||||
}.bind(this));
|
||||
return <div tabIndex="0" onKeyDown={this.onKeyDown} onClick={this.onClick} className="prompt-dialog">
|
||||
<div className="prompt-content">
|
||||
{this.props.prompt || <strong>Select: </strong> }
|
||||
{opts}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Prompt;
|
||||
@@ -72,7 +72,7 @@ var ProxyAppMain = React.createClass({
|
||||
selectFilterInput("intercept");
|
||||
break;
|
||||
case Key.L:
|
||||
selectFilterInput("filter");
|
||||
selectFilterInput("search");
|
||||
break;
|
||||
case Key.H:
|
||||
selectFilterInput("highlight");
|
||||
|
||||
@@ -2,10 +2,9 @@ var $ = require("jquery");
|
||||
var _ = require("lodash");
|
||||
var actions = require("./actions.js");
|
||||
|
||||
//Debug (don't expose by default, this increases compile time drastically)
|
||||
//window.$ = $;
|
||||
//window._ = _;
|
||||
//window.React = require("React");
|
||||
window.$ = $;
|
||||
window._ = _;
|
||||
window.React = require("react");
|
||||
|
||||
var Key = {
|
||||
UP: 38,
|
||||
|
||||
Reference in New Issue
Block a user