aboutsummaryrefslogtreecommitdiffstats
path: root/web/src
diff options
context:
space:
mode:
authorMaximilian Hils <git@maximilianhils.com>2015-05-01 17:24:44 +0200
committerMaximilian Hils <git@maximilianhils.com>2015-05-01 17:24:44 +0200
commit3f5ca10c39a9f7d55e0f6943caf8f6ff762a0222 (patch)
tree03f697b75b1fc787573feed3df26bde446ff2319 /web/src
parent90dff4a8a15580cf3e86d29c6aba1f97410a0b89 (diff)
downloadmitmproxy-3f5ca10c39a9f7d55e0f6943caf8f6ff762a0222.tar.gz
mitmproxy-3f5ca10c39a9f7d55e0f6943caf8f6ff762a0222.tar.bz2
mitmproxy-3f5ca10c39a9f7d55e0f6943caf8f6ff762a0222.zip
mitmweb: add editor
Diffstat (limited to 'web/src')
-rw-r--r--web/src/js/components/common.js5
-rw-r--r--web/src/js/components/editor.js189
-rw-r--r--web/src/js/components/flowview/messages.js196
-rw-r--r--web/src/js/components/header.js2
-rw-r--r--web/src/js/components/prompt.js2
5 files changed, 239 insertions, 155 deletions
diff --git a/web/src/js/components/common.js b/web/src/js/components/common.js
index b0aa0977..965ae9a7 100644
--- a/web/src/js/components/common.js
+++ b/web/src/js/components/common.js
@@ -55,6 +55,11 @@ var SettingsState = {
var ChildFocus = {
contextTypes: {
returnFocus: React.PropTypes.func
+ },
+ returnFocus: function(){
+ React.findDOMNode(this).blur();
+ window.getSelection().removeAllRanges();
+ this.context.returnFocus();
}
};
diff --git a/web/src/js/components/editor.js b/web/src/js/components/editor.js
new file mode 100644
index 00000000..714a8e2a
--- /dev/null
+++ b/web/src/js/components/editor.js
@@ -0,0 +1,189 @@
+var React = require("react");
+var common = require("./common.js");
+var utils = require("../utils.js");
+
+var contentToHtml = function (content) {
+ return _.escape(content);
+};
+var nodeToContent = function (node) {
+ return node.textContent;
+};
+
+/*
+Basic Editor Functionality
+ */
+var EditorBase = React.createClass({
+ propTypes: {
+ content: React.PropTypes.string.isRequired,
+ onDone: React.PropTypes.func.isRequired,
+ contentToHtml: React.PropTypes.func,
+ nodeToContent: React.PropTypes.func, // content === nodeToContent( Node<innerHTML=contentToHtml(content)> )
+ submitOnEnter: React.PropTypes.bool,
+ className: React.PropTypes.string,
+ tag: React.PropTypes.string
+ },
+ getDefaultProps: function () {
+ return {
+ contentToHtml: contentToHtml,
+ nodeToContent: nodeToContent,
+ submitOnEnter: true,
+ className: "",
+ tag: "div"
+ };
+ },
+ getInitialState: function () {
+ return {
+ editable: false
+ };
+ },
+ render: function () {
+ var className = "inline-input " + this.props.className;
+ var html = {__html: this.props.contentToHtml(this.props.content)};
+ var Tag = this.props.tag;
+ return <Tag
+ {...this.props}
+ tabIndex="0"
+ className={className}
+ contentEditable={this.state.editable || undefined } // workaround: use undef instead of false to remove attr
+ onFocus={this.onFocus}
+ onBlur={this._stop}
+ onKeyDown={this.onKeyDown}
+ onInput={this.onInput}
+ dangerouslySetInnerHTML={html}
+ />;
+ },
+ onFocus: function (e) {
+ this.setState({editable: true}, function () {
+ React.findDOMNode(this).focus();
+ var range = document.createRange();
+ range.selectNodeContents(this.getDOMNode());
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+ sel.addRange(range);
+ });
+ this.props.onFocus && this.props.onFocus(e);
+ },
+ stop: function () {
+ // a stop would cause a blur as a side-effect.
+ // but a blur event must trigger a stop as well.
+ // to fix this, make stop = blur and do the actual stop in the onBlur handler.
+ React.findDOMNode(this).blur();
+ },
+ _stop: function (e) {
+ window.getSelection().removeAllRanges(); //make sure that selection is cleared on blur
+ var node = React.findDOMNode(this);
+ var content = this.props.nodeToContent(node);
+ this.setState({editable: false});
+ this.props.onDone(content);
+ this.props.onBlur && this.props.onBlur(e);
+ },
+ cancel: function () {
+ React.findDOMNode(this).innerHTML = this.props.contentToHtml(this.props.content);
+ this.stop();
+ },
+ onKeyDown: function (e) {
+ e.stopPropagation();
+ switch (e.keyCode) {
+ case utils.Key.ESC:
+ e.preventDefault();
+ this.cancel();
+ break;
+ case utils.Key.ENTER:
+ if (this.props.submitOnEnter) {
+ e.preventDefault();
+ this.stop();
+ }
+ break;
+ default:
+ break;
+ }
+ },
+ onInput: function () {
+ var node = React.findDOMNode(this);
+ var content = this.props.nodeToContent(node);
+ node.innerHTML = this.props.contentToHtml(content);
+ this.props.onInput && this.props.onInput(content);
+ }
+});
+
+/*
+Add Validation to EditorBase
+ */
+var ValidateEditor = React.createClass({
+ propTypes: {
+ content: React.PropTypes.string.isRequired,
+ onDone: React.PropTypes.func.isRequired,
+ onInput: React.PropTypes.func,
+ isValid: React.PropTypes.func,
+ className: React.PropTypes.string,
+ },
+ getInitialState: function(){
+ return {
+ currentContent: this.props.content
+ };
+ },
+ componentWillReceiveProps: function(){
+ this.setState({currentContent: this.props.content});
+ },
+ onInput: function(content){
+ this.setState({currentContent: content});
+ this.props.onInput && this.props.onInput(content);
+ },
+ render: function () {
+ var className = this.props.className || "";
+ if (this.props.isValid) {
+ if (this.props.isValid(this.state.currentContent)) {
+ className += " has-success";
+ } else {
+ className += " has-warning"
+ }
+ }
+ return <EditorBase
+ {...this.props}
+ ref="editor"
+ className={className}
+ onDone={this.onDone}
+ onInput={this.onInput}
+ />;
+ },
+ onDone: function (content) {
+ if(this.props.isValid && !this.props.isValid(content)){
+ this.refs.editor.cancel();
+ content = this.props.content;
+ }
+ this.props.onDone(content);
+ }
+});
+
+/*
+Text Editor with mitmweb-specific convenience features
+ */
+var ValueEditor = React.createClass({
+ mixins: [common.ChildFocus],
+ propTypes: {
+ content: React.PropTypes.string.isRequired,
+ onDone: React.PropTypes.func.isRequired,
+ inline: React.PropTypes.bool,
+ },
+ render: function () {
+ var tag = this.props.inline ? "span" : "div";
+ return <ValidateEditor
+ {...this.props}
+ onBlur={this.onBlur}
+ tag={tag}
+ />;
+ },
+ focus: function () {
+ React.findDOMNode(this).focus();
+ },
+ onBlur: function(e){
+ if(!e.relatedTarget){
+ this.returnFocus();
+ }
+ this.props.onBlur && this.props.onBlur(e);
+ }
+});
+
+module.exports = {
+ ValueEditor: ValueEditor
+}; \ No newline at end of file
diff --git a/web/src/js/components/flowview/messages.js b/web/src/js/components/flowview/messages.js
index cb166026..fa75efbe 100644
--- a/web/src/js/components/flowview/messages.js
+++ b/web/src/js/components/flowview/messages.js
@@ -6,6 +6,7 @@ var actions = require("../../actions.js");
var flowutils = require("../../flow/utils.js");
var utils = require("../../utils.js");
var ContentView = require("./contentview.js");
+var ValueEditor = require("../editor.js").ValueEditor;
var Headers = React.createClass({
propTypes: {
@@ -63,16 +64,16 @@ var Headers = React.createClass({
var rows = this.props.message.headers.map(function (header, i) {
- var kEdit = <HeaderInlineInput
+ var kEdit = <HeaderEditor
ref={i + "-key"}
content={header[0]}
- onChange={this.onChange.bind(null, i, 0)}
+ onDone={this.onChange.bind(null, i, 0)}
onRemove={this.onRemove.bind(null, i, 0)}
onTab={this.onTab.bind(null, i, 0)}/>;
- var vEdit = <HeaderInlineInput
+ var vEdit = <HeaderEditor
ref={i + "-value"}
content={header[1]}
- onChange={this.onChange.bind(null, i, 1)}
+ onDone={this.onChange.bind(null, i, 1)}
onRemove={this.onRemove.bind(null, i, 1)}
onTab={this.onTab.bind(null, i, 1)}/>;
return (
@@ -92,88 +93,9 @@ var Headers = React.createClass({
}
});
-
-var InlineInput = React.createClass({
- mixins: [common.ChildFocus],
- propTypes: {
- content: React.PropTypes.string.isRequired, //must be string to match strict equality.
- onChange: React.PropTypes.func.isRequired,
- },
- getInitialState: function () {
- return {
- editable: false
- };
- },
+var HeaderEditor = React.createClass({
render: function () {
- var Tag = this.props.tag || "span";
- var className = "inline-input " + (this.props.className || "");
- var html = {__html: _.escape(this.props.content)};
- return <Tag
- {...this.props}
- tabIndex="0"
- className={className}
- contentEditable={this.state.editable || undefined}
- onInput={this.onInput}
- onFocus={this.onFocus}
- onBlur={this.onBlur}
- onKeyDown={this.onKeyDown}
- dangerouslySetInnerHTML={html}
- />;
- },
- onKeyDown: function (e) {
- e.stopPropagation();
- switch (e.keyCode) {
- case utils.Key.ESC:
- this.blur();
- break;
- case utils.Key.ENTER:
- e.preventDefault();
- if (!e.ctrlKey) {
- this.blur();
- } else {
- this.props.onDone && this.props.onDone();
- }
- break;
- default:
- this.props.onKeyDown && this.props.onKeyDown(e);
- break;
- }
- },
- blur: function () {
- this.getDOMNode().blur();
- window.getSelection().removeAllRanges();
- this.context.returnFocus && this.context.returnFocus();
- },
- focus: function () {
- React.findDOMNode(this).focus();
- var range = document.createRange();
- range.selectNodeContents(this.getDOMNode());
- var sel = window.getSelection();
- sel.removeAllRanges();
- sel.addRange(range);
- },
- onFocus: function () {
- this.setState({editable: true}, this.focus);
- },
- onBlur: function (e) {
- this.setState({editable: false});
- this.handleChange();
- this.props.onDone && this.props.onDone();
- },
- onInput: function () {
- this.handleChange();
- },
- handleChange: function () {
- var content = this.getDOMNode().textContent;
- if (content !== this.props.content) {
- this.props.onChange(content);
- }
- }
-});
-
-var HeaderInlineInput = React.createClass({
- render: function () {
- return <InlineInput ref="input" {...this.props} onKeyDown={this.onKeyDown}/>;
+ return <ValueEditor ref="input" {...this.props} onKeyDown={this.onKeyDown} inline/>;
},
focus: function () {
this.getDOMNode().focus();
@@ -195,65 +117,6 @@ var HeaderInlineInput = React.createClass({
}
});
-var ValidateInlineInput = React.createClass({
- propTypes: {
- onChange: React.PropTypes.func.isRequired,
- isValid: React.PropTypes.func.isRequired,
- immediate: React.PropTypes.bool
- },
- getInitialState: function () {
- return {
- content: this.props.content,
- originalContent: this.props.content
- };
- },
- focus: function () {
- this.getDOMNode().focus();
- },
- onChange: function (val) {
- this.setState({
- content: val
- });
- if (this.props.immediate && val !== this.state.originalContent && this.props.isValid(val)) {
- this.props.onChange(val);
- }
- },
- onDone: function () {
- if (this.state.content === this.state.originalContent) {
- return true;
- }
- if (this.props.isValid(this.state.content)) {
- this.props.onChange(this.state.content);
- } else {
- this.setState({
- content: this.state.originalContent
- });
- }
- },
- componentWillReceiveProps: function (nextProps) {
- if (nextProps.content !== this.state.content) {
- this.setState({
- content: nextProps.content,
- originalContent: nextProps.content
- })
- }
- },
- render: function () {
- var className = this.props.className || "";
- if (this.props.isValid(this.state.content)) {
- className += " has-success";
- } else {
- className += " has-warning"
- }
- return <InlineInput {...this.props}
- className={className}
- content={this.state.content}
- onChange={this.onChange}
- onDone={this.onDone}
- />;
- }
-});
-
var RequestLine = React.createClass({
render: function () {
var flow = this.props.flow;
@@ -261,11 +124,25 @@ var RequestLine = React.createClass({
var httpver = "HTTP/" + flow.request.httpversion.join(".");
return <div className="first-line request-line">
- <InlineInput ref="method" content={flow.request.method} onChange={this.onMethodChange}/>
+ <ValueEditor
+ ref="method"
+ content={flow.request.method}
+ onDone={this.onMethodChange}
+ inline/>
&nbsp;
- <ValidateInlineInput ref="url" content={url} onChange={this.onUrlChange} isValid={this.isValidUrl} />
+ <ValueEditor
+ ref="url"
+ content={url}
+ onDone={this.onUrlChange}
+ isValid={this.isValidUrl}
+ inline/>
&nbsp;
- <ValidateInlineInput ref="httpVersion" immediate content={httpver} onChange={this.onHttpVersionChange} isValid={flowutils.isValidHttpVersion} />
+ <ValueEditor
+ ref="httpVersion"
+ content={httpver}
+ onDone={this.onHttpVersionChange}
+ isValid={flowutils.isValidHttpVersion}
+ inline/>
</div>
},
isValidUrl: function (url) {
@@ -300,12 +177,25 @@ var ResponseLine = React.createClass({
var flow = this.props.flow;
var httpver = "HTTP/" + flow.response.httpversion.join(".");
return <div className="first-line response-line">
- <ValidateInlineInput ref="httpVersion" immediate content={httpver} onChange={this.onHttpVersionChange} isValid={flowutils.isValidHttpVersion} />
+ <ValueEditor
+ ref="httpVersion"
+ content={httpver}
+ onDone={this.onHttpVersionChange}
+ isValid={flowutils.isValidHttpVersion}
+ inline/>
&nbsp;
- <ValidateInlineInput ref="code" immediate content={flow.response.code + ""} onChange={this.onCodeChange} isValid={this.isValidCode} />
+ <ValueEditor
+ ref="code"
+ content={flow.response.code + ""}
+ onDone={this.onCodeChange}
+ isValid={this.isValidCode}
+ inline/>
&nbsp;
- <InlineInput ref="msg" content={flow.response.msg} onChange={this.onMsgChange}/>
-
+ <ValueEditor
+ ref="msg"
+ content={flow.response.msg}
+ onDone={this.onMsgChange}
+ inline/>
</div>;
},
isValidCode: function (code) {
@@ -361,7 +251,7 @@ var Request = React.createClass({
this.refs.headers.edit();
break;
default:
- throw "Unimplemented: "+ k;
+ throw "Unimplemented: " + k;
}
},
onHeaderChange: function (nextHeaders) {
@@ -401,7 +291,7 @@ var Response = React.createClass({
this.refs.headers.edit();
break;
default:
- throw "Unimplemented: "+ k;
+ throw "Unimplemented: " + k;
}
},
onHeaderChange: function (nextHeaders) {
diff --git a/web/src/js/components/header.js b/web/src/js/components/header.js
index 225f5b9f..998a41df 100644
--- a/web/src/js/components/header.js
+++ b/web/src/js/components/header.js
@@ -119,7 +119,7 @@ var FilterInput = React.createClass({
},
blur: function () {
this.refs.input.getDOMNode().blur();
- this.context.returnFocus && this.context.returnFocus();
+ this.returnFocus();
},
select: function () {
this.refs.input.getDOMNode().select();
diff --git a/web/src/js/components/prompt.js b/web/src/js/components/prompt.js
index 229e82c8..121a1170 100644
--- a/web/src/js/components/prompt.js
+++ b/web/src/js/components/prompt.js
@@ -34,7 +34,7 @@ var Prompt = React.createClass({
},
done: function (ret) {
this.props.done(ret);
- this.context.returnFocus && this.context.returnFocus();
+ this.returnFocus();
},
getOptions: function () {
var opts = [];