aboutsummaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
authorJason <jason.daurus@gmail.com>2016-06-15 00:46:59 +0800
committerJason <jason.daurus@gmail.com>2016-06-17 04:38:35 +0800
commitf5c597a9e351b8dfb84f0fe3f09046e772482fc6 (patch)
tree8f44cc55513a04b64b4e79d4d9c18f59f100cfaf /web
parente5bf1e930a5b6ba0b3300b02daf792d65d795202 (diff)
downloadmitmproxy-f5c597a9e351b8dfb84f0fe3f09046e772482fc6.tar.gz
mitmproxy-f5c597a9e351b8dfb84f0fe3f09046e772482fc6.tar.bz2
mitmproxy-f5c597a9e351b8dfb84f0fe3f09046e772482fc6.zip
[web] Editor & Prompt
Diffstat (limited to 'web')
-rw-r--r--web/src/js/components/FlowView.jsx4
-rw-r--r--web/src/js/components/FlowView/Headers.jsx2
-rw-r--r--web/src/js/components/FlowView/Messages.jsx2
-rwxr-xr-xweb/src/js/components/Prompt.jsx75
-rwxr-xr-xweb/src/js/components/ValueEditor.jsx36
-rwxr-xr-xweb/src/js/components/ValueEditor/EditorBase.jsx166
-rwxr-xr-xweb/src/js/components/ValueEditor/ValidateEditor.jsx58
-rw-r--r--web/src/js/components/editor.js238
-rw-r--r--web/src/js/components/prompt.js102
9 files changed, 339 insertions, 344 deletions
diff --git a/web/src/js/components/FlowView.jsx b/web/src/js/components/FlowView.jsx
index be2cb460..23f8b3ea 100644
--- a/web/src/js/components/FlowView.jsx
+++ b/web/src/js/components/FlowView.jsx
@@ -4,7 +4,7 @@ import _ from 'lodash'
import Nav from './FlowView/Nav'
import { Request, Response, Error } from './FlowView/Messages'
import Details from './FlowView/Details'
-import Prompt from './prompt'
+import Prompt from './Prompt'
export default class FlowView extends Component {
@@ -97,7 +97,7 @@ export default class FlowView extends Component {
active={active}
onSelectTab={this.selectTab}
/>
- <Tab flow={flow}/>
+ <Tab ref="tab" flow={flow}/>
{this.state.prompt && (
<Prompt {...this.state.prompt}/>
)}
diff --git a/web/src/js/components/FlowView/Headers.jsx b/web/src/js/components/FlowView/Headers.jsx
index b8f9b50f..880eeda1 100644
--- a/web/src/js/components/FlowView/Headers.jsx
+++ b/web/src/js/components/FlowView/Headers.jsx
@@ -1,6 +1,6 @@
import React, { Component, PropTypes } from 'react'
import ReactDOM from 'react-dom'
-import { ValueEditor } from '../editor'
+import ValueEditor from '../ValueEditor'
import { Key } from '../../utils.js'
class HeaderEditor extends Component {
diff --git a/web/src/js/components/FlowView/Messages.jsx b/web/src/js/components/FlowView/Messages.jsx
index ce17c294..ba6a5f2b 100644
--- a/web/src/js/components/FlowView/Messages.jsx
+++ b/web/src/js/components/FlowView/Messages.jsx
@@ -5,7 +5,7 @@ import { FlowActions } from '../../actions.js'
import { RequestUtils, isValidHttpVersion, parseUrl, parseHttpVersion } from '../../flow/utils.js'
import { Key, formatTimeStamp } from '../../utils.js'
import ContentView from '../ContentView'
-import { ValueEditor } from '../editor'
+import ValueEditor from '../ValueEditor'
import Headers from './Headers'
class RequestLine extends Component {
diff --git a/web/src/js/components/Prompt.jsx b/web/src/js/components/Prompt.jsx
new file mode 100755
index 00000000..878aae29
--- /dev/null
+++ b/web/src/js/components/Prompt.jsx
@@ -0,0 +1,75 @@
+import React, { PropTypes } from 'react'
+import ReactDOM from 'react-dom'
+import _ from 'lodash'
+
+import {Key} from '../utils.js'
+
+Prompt.contextTypes = {
+ returnFocus: PropTypes.func
+}
+
+Prompt.propTypes = {
+ options: PropTypes.array.isRequired,
+ done: PropTypes.func.isRequired,
+ prompt: PropTypes.string,
+}
+
+Prompt.componentDidMount = function() {
+ ReactDOM.findDOMNode(this).focus()
+}
+
+export default function Prompt({ prompt, done, options }, context) {
+ const opts = []
+
+ function keyTaken(k) {
+ return _.map(opts, 'key').includes(k)
+ }
+
+ for (let i = 0; i < options.length; i++) {
+ let opt = options[i]
+ if (_.isString(opt)) {
+ let 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'
+ }
+ opts.push(opt)
+ }
+
+ return (
+ <div tabIndex="0" onKeyDown={onKeyDown} onClick={onClick} className="prompt-dialog">
+ <div className="prompt-content">
+ {prompt || <strong>Select: </strong> }
+ {opts.map(opt => {
+ const idx = opt.text.indexOf(opt.key)
+ function onClick(event) {
+ done(opt.key)
+ event.stopPropagation()
+ }
+ return (
+ <span key={opt.key} className="option" onClick={onClick}>
+ {idx !== -1 ? opt.text.substring(0, idx) : opt.text + '('}
+ {prefix}<strong className="text-primary">{opt.key}</strong>
+ {idx !== -1 ? opt.text.substring(idx + 1) : ')'}
+ </span>
+ )
+ })}
+ </div>
+ </div>
+ )
+
+ function onKeyDown(event) {
+ event.stopPropagation()
+ event.preventDefault()
+ const key = opts.find(opt => Key[opt.key.toUpperCase()] === event.keyCode)
+ if (!key && event.keyCode !== Key.ESC && event.keyCode !== Key.ENTER) {
+ return
+ }
+ done(k || false)
+ context.returnFocus()
+ }
+}
diff --git a/web/src/js/components/ValueEditor.jsx b/web/src/js/components/ValueEditor.jsx
new file mode 100755
index 00000000..0316924f
--- /dev/null
+++ b/web/src/js/components/ValueEditor.jsx
@@ -0,0 +1,36 @@
+import React, { Component, PropTypes } from 'react'
+import ReactDOM from 'react-dom'
+import ValidateEditor from './ValueEditor/ValidateEditor'
+
+export default class ValueEditor extends Component {
+
+ static contextTypes = {
+ returnFocus: PropTypes.func,
+ }
+
+ static propTypes = {
+ content: PropTypes.string.isRequired,
+ onDone: PropTypes.func.isRequired,
+ inline: PropTypes.bool,
+ }
+
+ constructor(props) {
+ super(props)
+ this.focus = this.focus.bind(this)
+ }
+
+ render() {
+ var tag = this.props.inline ? "span" : 'div'
+ return (
+ <ValidateEditor
+ {...this.props}
+ onStop={() => this.context.returnFocus()}
+ tag={tag}
+ />
+ )
+ }
+
+ focus() {
+ ReactDOM.findDOMNode(this).focus();
+ }
+}
diff --git a/web/src/js/components/ValueEditor/EditorBase.jsx b/web/src/js/components/ValueEditor/EditorBase.jsx
new file mode 100755
index 00000000..b659cf6d
--- /dev/null
+++ b/web/src/js/components/ValueEditor/EditorBase.jsx
@@ -0,0 +1,166 @@
+import React, { Component, PropTypes } from 'react'
+import ReactDOM from 'react-dom'
+import {Key} from '../../utils.js'
+
+export default class EditorBase extends Component {
+
+ static propTypes = {
+ content: PropTypes.string.isRequired,
+ onDone: PropTypes.func.isRequired,
+ contentToHtml: PropTypes.func,
+ nodeToContent: PropTypes.func,
+ onStop: PropTypes.func,
+ submitOnEnter: PropTypes.bool,
+ className: PropTypes.string,
+ tag: PropTypes.string,
+ }
+
+ static defaultProps = {
+ contentToHtml: content => _.escape(content),
+ nodeToContent: node => node.textContent,
+ submitOnEnter: true,
+ className: '',
+ tag: 'div',
+ onStop: _.noop,
+ onMouseDown: _.noop,
+ onBlur: _.noop,
+ onInput: _.noop,
+ }
+
+ constructor(props) {
+ super(props)
+ this.state = {editable: false}
+
+ this.onPaste = this.onPaste.bind(this)
+ this.onMouseDown = this.onMouseDown.bind(this)
+ this.onMouseUp = this.onMouseUp.bind(this)
+ this.onFocus = this.onFocus.bind(this)
+ this.onClick = this.onClick.bind(this)
+ this.stop = this.stop.bind(this)
+ this.onBlur = this.onBlur.bind(this)
+ this.reset = this.reset.bind(this)
+ this.onKeyDown = this.onKeyDown.bind(this)
+ this.onInput = this.onInput.bind(this)
+ }
+
+ stop() {
+ // 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.
+ ReactDOM.findDOMNode(this).blur()
+ this.props.onStop()
+ }
+
+ render() {
+ return (
+ <this.props.tag
+ {...this.props}
+ tabIndex="0"
+ className={`inline-input ${this.props.className}`}
+ contentEditable={this.state.editable || undefined}
+ onFocus={this.onFocus}
+ onMouseDown={this.onMouseDown}
+ onClick={this.onClick}
+ onBlur={this.onBlur}
+ onKeyDown={this.onKeyDown}
+ onInput={this.onInput}
+ onPaste={this.onPaste}
+ dangerouslySetInnerHTML={{ __html: this.props.contentToHtml(this.props.content) }}
+ />
+ )
+ }
+
+ onPaste(e) {
+ e.preventDefault()
+ var content = e.clipboardData.getData('text/plain')
+ document.execCommand('insertHTML', false, content)
+ }
+
+ onMouseDown(e) {
+ this._mouseDown = true
+ window.addEventListener('mouseup', this.onMouseUp)
+ this.props.onMouseDown(e)
+ }
+
+ onMouseUp() {
+ if (this._mouseDown) {
+ this._mouseDown = false
+ window.removeEventListener('mouseup', this.onMouseUp)
+ }
+ }
+
+ onClick(e) {
+ this.onMouseUp()
+ this.onFocus(e)
+ }
+
+ onFocus(e) {
+ if (this._mouseDown || this._ignore_events || this.state.editable) {
+ return
+ }
+
+ // contenteditable in FireFox is more or less broken.
+ // - we need to blur() and then focus(), otherwise the caret is not shown.
+ // - blur() + focus() == we need to save the caret position before
+ // Firefox sometimes just doesn't set a caret position => use caretPositionFromPoint
+ const sel = window.getSelection()
+ let range
+ if (sel.rangeCount > 0) {
+ range = sel.getRangeAt(0)
+ } else if (document.caretPositionFromPoint && e.clientX && e.clientY) {
+ const pos = document.caretPositionFromPoint(e.clientX, e.clientY)
+ range = document.createRange()
+ range.setStart(pos.offsetNode, pos.offset)
+ } else if (document.caretRangeFromPoint && e.clientX && e.clientY) {
+ range = document.caretRangeFromPoint(e.clientX, e.clientY)
+ } else {
+ range = document.createRange()
+ range.selectNodeContents(ReactDOM.findDOMNode(this))
+ }
+
+ this._ignore_events = true
+ this.setState({ editable: true }, () => {
+ const node = ReactDOM.findDOMNode(this)
+ node.blur()
+ node.focus()
+ this._ignore_events = false
+ })
+ }
+
+ onBlur(e) {
+ if (this._ignore_events) {
+ return
+ }
+ window.getSelection().removeAllRanges() //make sure that selection is cleared on blur
+ this.setState({ editable: false })
+ this.props.onDone(this.props.nodeToContent(ReactDOM.findDOMNode(this)))
+ this.props.onBlur(e)
+ }
+
+ reset() {
+ ReactDOM.findDOMNode(this).innerHTML = this.props.contentToHtml(this.props.content)
+ }
+
+ onKeyDown(e) {
+ e.stopPropagation()
+ switch (e.keyCode) {
+ case Key.ESC:
+ e.preventDefault()
+ this.reset()
+ this.stop()
+ break
+ case Key.ENTER:
+ if (this.props.submitOnEnter && !e.shiftKey) {
+ e.preventDefault()
+ this.stop()
+ }
+ break
+ default:
+ break
+ }
+ }
+
+ onInput() {
+ this.props.onInput(this.props.nodeToContent(ReactDOM.findDOMNode(this)))
+ }
+}
diff --git a/web/src/js/components/ValueEditor/ValidateEditor.jsx b/web/src/js/components/ValueEditor/ValidateEditor.jsx
new file mode 100755
index 00000000..ea4e8803
--- /dev/null
+++ b/web/src/js/components/ValueEditor/ValidateEditor.jsx
@@ -0,0 +1,58 @@
+import React, { Component, PropTypes } from 'react'
+import ReactDOM from 'react-dom'
+import EditorBase from './EditorBase'
+
+export default class ValidateEditor extends Component {
+
+ static propTypes = {
+ content: PropTypes.string.isRequired,
+ onDone: PropTypes.func.isRequired,
+ onInput: PropTypes.func,
+ isValid: PropTypes.func,
+ className: PropTypes.string,
+ }
+
+ constructor(props) {
+ super(props)
+ this.state = { currentContent: props.content }
+ this.onInput = this.onInput.bind(this)
+ this.onDone = this.onDone.bind(this)
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.setState({ currentContent: nextProps.content })
+ }
+
+ onInput(currentContent) {
+ this.setState({ currentContent })
+ this.props.onInput && this.props.onInput(currentContent)
+ }
+
+ onDone(content) {
+ if (this.props.isValid && !this.props.isValid(content)) {
+ this.refs.editor.reset()
+ content = this.props.content
+ }
+ this.props.onDone(content)
+ }
+
+ render() {
+ let 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}
+ />
+ )
+ }
+}
diff --git a/web/src/js/components/editor.js b/web/src/js/components/editor.js
deleted file mode 100644
index eed2f7c6..00000000
--- a/web/src/js/components/editor.js
+++ /dev/null
@@ -1,238 +0,0 @@
-import React from "react";
-import ReactDOM from 'react-dom';
-import {Key} from "../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)> )
- onStop: React.PropTypes.func,
- 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}
- onMouseDown={this.onMouseDown}
- onClick={this.onClick}
- onBlur={this._stop}
- onKeyDown={this.onKeyDown}
- onInput={this.onInput}
- onPaste={this.onPaste}
- dangerouslySetInnerHTML={html}
- />;
- },
- onPaste: function (e) {
- e.preventDefault();
- var content = e.clipboardData.getData("text/plain");
- document.execCommand("insertHTML", false, content);
- },
- onMouseDown: function (e) {
- this._mouseDown = true;
- window.addEventListener("mouseup", this.onMouseUp);
- this.props.onMouseDown && this.props.onMouseDown(e);
- },
- onMouseUp: function () {
- if (this._mouseDown) {
- this._mouseDown = false;
- window.removeEventListener("mouseup", this.onMouseUp)
- }
- },
- onClick: function (e) {
- this.onMouseUp();
- this.onFocus(e);
- },
- onFocus: function (e) {
- console.log("onFocus", this._mouseDown, this._ignore_events, this.state.editable);
- if (this._mouseDown || this._ignore_events || this.state.editable) {
- return;
- }
-
- //contenteditable in FireFox is more or less broken.
- // - we need to blur() and then focus(), otherwise the caret is not shown.
- // - blur() + focus() == we need to save the caret position before
- // Firefox sometimes just doesn't set a caret position => use caretPositionFromPoint
- var sel = window.getSelection();
- var range;
- if (sel.rangeCount > 0) {
- range = sel.getRangeAt(0);
- } else if (document.caretPositionFromPoint && e.clientX && e.clientY) {
- var pos = document.caretPositionFromPoint(e.clientX, e.clientY);
- range = document.createRange();
- range.setStart(pos.offsetNode, pos.offset);
- } else if (document.caretRangeFromPoint && e.clientX && e.clientY) {
- range = document.caretRangeFromPoint(e.clientX, e.clientY);
- } else {
- range = document.createRange();
- range.selectNodeContents(ReactDOM.findDOMNode(this));
- }
-
- this._ignore_events = true;
- this.setState({editable: true}, function () {
- var node = ReactDOM.findDOMNode(this);
- node.blur();
- node.focus();
- this._ignore_events = false;
- //sel.removeAllRanges();
- //sel.addRange(range);
-
-
- });
- },
- 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.
- ReactDOM.findDOMNode(this).blur();
- this.props.onStop && this.props.onStop();
- },
- _stop: function (e) {
- if (this._ignore_events) {
- return;
- }
- console.log("_stop", _.extend({}, e));
- window.getSelection().removeAllRanges(); //make sure that selection is cleared on blur
- var node = ReactDOM.findDOMNode(this);
- var content = this.props.nodeToContent(node);
- this.setState({editable: false});
- this.props.onDone(content);
- this.props.onBlur && this.props.onBlur(e);
- },
- reset: function () {
- ReactDOM.findDOMNode(this).innerHTML = this.props.contentToHtml(this.props.content);
- },
- onKeyDown: function (e) {
- e.stopPropagation();
- switch (e.keyCode) {
- case Key.ESC:
- e.preventDefault();
- this.reset();
- this.stop();
- break;
- case Key.ENTER:
- if (this.props.submitOnEnter && !e.shiftKey) {
- e.preventDefault();
- this.stop();
- }
- break;
- default:
- break;
- }
- },
- onInput: function () {
- var node = ReactDOM.findDOMNode(this);
- var content = this.props.nodeToContent(node);
- 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.reset();
- content = this.props.content;
- }
- this.props.onDone(content);
- }
-});
-
-/*
- Text Editor with mitmweb-specific convenience features
- */
-export var ValueEditor = React.createClass({
- contextTypes: {
- returnFocus: React.PropTypes.func
- },
- 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}
- onStop={this.onStop}
- tag={tag}
- />;
- },
- focus: function () {
- ReactDOM.findDOMNode(this).focus();
- },
- onStop: function () {
- this.context.returnFocus();
- }
-}); \ No newline at end of file
diff --git a/web/src/js/components/prompt.js b/web/src/js/components/prompt.js
deleted file mode 100644
index 5ab26b82..00000000
--- a/web/src/js/components/prompt.js
+++ /dev/null
@@ -1,102 +0,0 @@
-import React from "react";
-import ReactDOM from 'react-dom';
-import _ from "lodash";
-
-import {Key} from "../utils.js";
-
-var Prompt = React.createClass({
- contextTypes: {
- returnFocus: React.PropTypes.func
- },
- propTypes: {
- options: React.PropTypes.array.isRequired,
- done: React.PropTypes.func.isRequired,
- prompt: React.PropTypes.string
- },
- componentDidMount: function () {
- ReactDOM.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 (Key[k.toUpperCase()] === e.keyCode) {
- this.done(k);
- return;
- }
- }
- if (e.keyCode === Key.ESC || e.keyCode === Key.ENTER) {
- this.done(false);
- }
- },
- onClick: function (e) {
- this.done(false);
- },
- done: function (ret) {
- this.props.done(ret);
- this.context.returnFocus();
- },
- getOptions: function () {
- var opts = [];
-
- var keyTaken = function (k) {
- return _.includes(_.map(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>;
- }
-});
-
-export default Prompt;