[web] Editor & Prompt

This commit is contained in:
Jason 2016-06-15 00:46:59 +08:00
parent e5bf1e930a
commit f5c597a9e3
10 changed files with 392 additions and 391 deletions

File diff suppressed because one or more lines are too long

View File

@ -4,7 +4,7 @@ import _ from 'lodash'
import Nav from './FlowView/Nav' import Nav from './FlowView/Nav'
import { Request, Response, Error } from './FlowView/Messages' import { Request, Response, Error } from './FlowView/Messages'
import Details from './FlowView/Details' import Details from './FlowView/Details'
import Prompt from './prompt' import Prompt from './Prompt'
export default class FlowView extends Component { export default class FlowView extends Component {
@ -97,7 +97,7 @@ export default class FlowView extends Component {
active={active} active={active}
onSelectTab={this.selectTab} onSelectTab={this.selectTab}
/> />
<Tab flow={flow}/> <Tab ref="tab" flow={flow}/>
{this.state.prompt && ( {this.state.prompt && (
<Prompt {...this.state.prompt}/> <Prompt {...this.state.prompt}/>
)} )}

View File

@ -1,6 +1,6 @@
import React, { Component, PropTypes } from 'react' import React, { Component, PropTypes } from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import { ValueEditor } from '../editor' import ValueEditor from '../ValueEditor'
import { Key } from '../../utils.js' import { Key } from '../../utils.js'
class HeaderEditor extends Component { class HeaderEditor extends Component {

View File

@ -5,7 +5,7 @@ import { FlowActions } from '../../actions.js'
import { RequestUtils, isValidHttpVersion, parseUrl, parseHttpVersion } from '../../flow/utils.js' import { RequestUtils, isValidHttpVersion, parseUrl, parseHttpVersion } from '../../flow/utils.js'
import { Key, formatTimeStamp } from '../../utils.js' import { Key, formatTimeStamp } from '../../utils.js'
import ContentView from '../ContentView' import ContentView from '../ContentView'
import { ValueEditor } from '../editor' import ValueEditor from '../ValueEditor'
import Headers from './Headers' import Headers from './Headers'
class RequestLine extends Component { class RequestLine extends Component {

View File

@ -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()
}
}

View File

@ -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();
}
}

View File

@ -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)))
}
}

View File

@ -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}
/>
)
}
}

View File

@ -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();
}
});

View File

@ -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;