diff options
author | Jason <jason.daurus@gmail.com> | 2016-06-15 00:46:59 +0800 |
---|---|---|
committer | Jason <jason.daurus@gmail.com> | 2016-06-17 04:38:35 +0800 |
commit | f5c597a9e351b8dfb84f0fe3f09046e772482fc6 (patch) | |
tree | 8f44cc55513a04b64b4e79d4d9c18f59f100cfaf /web | |
parent | e5bf1e930a5b6ba0b3300b02daf792d65d795202 (diff) | |
download | mitmproxy-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.jsx | 4 | ||||
-rw-r--r-- | web/src/js/components/FlowView/Headers.jsx | 2 | ||||
-rw-r--r-- | web/src/js/components/FlowView/Messages.jsx | 2 | ||||
-rwxr-xr-x | web/src/js/components/Prompt.jsx | 75 | ||||
-rwxr-xr-x | web/src/js/components/ValueEditor.jsx | 36 | ||||
-rwxr-xr-x | web/src/js/components/ValueEditor/EditorBase.jsx | 166 | ||||
-rwxr-xr-x | web/src/js/components/ValueEditor/ValidateEditor.jsx | 58 | ||||
-rw-r--r-- | web/src/js/components/editor.js | 238 | ||||
-rw-r--r-- | web/src/js/components/prompt.js | 102 |
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; |