From f5c597a9e351b8dfb84f0fe3f09046e772482fc6 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 15 Jun 2016 00:46:59 +0800 Subject: [web] Editor & Prompt --- web/src/js/components/FlowView.jsx | 4 +- web/src/js/components/FlowView/Headers.jsx | 2 +- web/src/js/components/FlowView/Messages.jsx | 2 +- web/src/js/components/Prompt.jsx | 75 +++++++ web/src/js/components/ValueEditor.jsx | 36 ++++ web/src/js/components/ValueEditor/EditorBase.jsx | 166 ++++++++++++++ .../js/components/ValueEditor/ValidateEditor.jsx | 58 +++++ web/src/js/components/editor.js | 238 --------------------- web/src/js/components/prompt.js | 102 --------- 9 files changed, 339 insertions(+), 344 deletions(-) create mode 100755 web/src/js/components/Prompt.jsx create mode 100755 web/src/js/components/ValueEditor.jsx create mode 100755 web/src/js/components/ValueEditor/EditorBase.jsx create mode 100755 web/src/js/components/ValueEditor/ValidateEditor.jsx delete mode 100644 web/src/js/components/editor.js delete mode 100644 web/src/js/components/prompt.js (limited to 'web/src') 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} /> - + {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 ( +
+
+ {prompt || Select: } + {opts.map(opt => { + const idx = opt.text.indexOf(opt.key) + function onClick(event) { + done(opt.key) + event.stopPropagation() + } + return ( + + {idx !== -1 ? opt.text.substring(0, idx) : opt.text + '('} + {prefix}{opt.key} + {idx !== -1 ? opt.text.substring(idx + 1) : ')'} + + ) + })} +
+
+ ) + + 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 ( + 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 ( + + ) + } + + 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 ( + + ) + } +} 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 ) - 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 ; - }, - 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 ; - }, - 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 ; - }, - 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 - {prefix} - {o.key}{suffix} - ; - }.bind(this)); - return
-
- {this.props.prompt || Select: } - {opts} -
-
; - } -}); - -export default Prompt; -- cgit v1.2.3