Browse Source

wysiwyg somewhat

master
Julian Steinwachs 9 years ago
parent
commit
e26d5fbc0b
  1. 1688
      build/app-bundle.js
  2. 345
      jsx/common/ContentEditable.js
  3. 3
      jsx/common/FollowButton.js
  4. 147
      jsx/common/PostContent.js
  5. 45
      jsx/common/TwistComposer.js
  6. 32
      jsx/home/NewPostModalButton.js
  7. 3
      package.json
  8. 5
      tests/PostContentTest.js

1688
build/app-bundle.js

File diff suppressed because it is too large Load Diff

345
jsx/common/ContentEditable.js

@ -1,345 +0,0 @@
var React = require('react')
var ReactDOM = require('react-dom')
var classNames = require('classnames')
var escapeHTML = require('escape-html')
var isServer = typeof window === 'undefined'
if (!isServer) {
var selectionRange = require('selection-range')
}
var noop = function(){}
/**
* Make a contenteditable element
*/
var ContentEditable = React.createClass({
propTypes: {
editing: React.PropTypes.bool,
html: React.PropTypes.string,
onChange: React.PropTypes.func.isRequired,
placeholder: React.PropTypes.bool,
placeholderText: React.PropTypes.string,
tagName: React.PropTypes.string,
onEnterKey: React.PropTypes.func,
onEscapeKey: React.PropTypes.func,
preventStyling: React.PropTypes.bool,
noLinebreaks: React.PropTypes.bool,
onBlur: React.PropTypes.func,
onFocus: React.PropTypes.func,
onBold: React.PropTypes.func,
onItalic: React.PropTypes.func,
onKeyDown: React.PropTypes.func,
onKeyPress: React.PropTypes.func,
placeholderStyle: React.PropTypes.object
},
getDefaultProps: function() {
return {
html: '',
placeholder: false,
placeholderText: '',
onBold: noop,
onItalic: noop,
onKeyDown: noop,
onKeyPress: noop
};
},
getInitialState: function(){
return {};
},
shouldComponentUpdate: function(nextProps) {
var el = ReactDOM.findDOMNode(this)
if (nextProps.html !== el.innerHTML) {
if (nextProps.html && document.activeElement === el) {
this._range = selectionRange(el)
}
return true
}
if (nextProps.placeholder !== this.props.placeholder) {
return true
}
if (nextProps.editing !== this.props.editing) {
return true
}
return false
},
componentWillReceiveProps: function (nextProps) {
if (!this.props.editing && nextProps.editing) {
if (this.contentIsEmpty(nextProps.html)) {
this.props.onChange('', true)
}
}
},
componentDidUpdate: function() {
if (!this.props.editing && !this.props.html) {
this.props.onChange('')
}
if (this._range) {
selectionRange(ReactDOM.findDOMNode(this), this._range)
delete this._range
}
},
autofocus: function(){
ReactDOM.findDOMNode(this).focus();
},
render: function() {
// todo: use destructuring
var editing = this.props.editing;
var className = this.props.className;
var tagName = this.props.tagName;
// setup our classes
var classes = {
ContentEditable: true
};
var placeholderStyle = this.props.placeholderStyle || {
color: '#bbbbbb'
}
if (className) {
classes[className] = true;
}
// set 'div' as our default tagname
tagName = tagName || 'div';
var content = this.props.html;
// return our newly created element
return React.createElement(tagName, {
tabIndex: 0,
key: '0',
className: classNames(classes),
contentEditable: editing,
onBlur: this.onBlur,
onFocus: this.onFocus,
onKeyDown: this.onKeyDown,
onPaste: this.onPaste,
onMouseDown: this.onMouseDown,
'aria-label': this.props.placeholderText,
onTouchStart: this.onMouseDown,
style: this.props.placeholder ? placeholderStyle : this.props.style || {},
onKeyPress: this.onKeyPress,
onInput: this.onInput,
onKeyUp: this.onKeyUp,
dangerouslySetInnerHTML: {
__html : this.props.placeholder ? this.props.placeholderText : content
}
});
},
unsetPlaceholder: function(){
this.props.onChange('', false, '')
},
setCursorToStart: function(){
ReactDOM.findDOMNode(this).focus();
var sel = window.getSelection();
var range = document.createRange();
range.setStart(ReactDOM.findDOMNode(this), 0);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
},
contentIsEmpty: function (content) {
if (this.state.placeholder) {
return true
}
if (!content) {
return true
}
if (content === '<br />') {
return true
}
if (!content.trim().length) {
return true
}
return false
},
onMouseDown: function(e) {
// prevent cursor placement if placeholder is set
if (this.contentIsEmpty(this.props.html)) {
this.setCursorToStart()
e.preventDefault()
}
},
onKeyDown: function(e) {
var self = this
this.props.onKeyDown(e)
function prev () {
e.preventDefault();
e.stopPropagation();
self.stop = true
}
var key = e.keyCode;
// bold & italic styling
if (e.metaKey) {
// bold
if (key === 66) {
this.props.onBold(e)
if (this.props.preventStyling) {
return prev()
}
// italic
} else if (key === 73) {
this.props.onItalic(e)
if (this.props.preventStyling) {
return prev()
}
//paste
} else if (key === 86) {
return;
}
}
// prevent linebreaks
if (this.props.noLinebreaks && (key === 13)) {
return prev()
}
// placeholder behaviour
if (this.contentIsEmpty(this.props.html)) { // If no text
if (e.metaKey || (e.shiftKey && (key === 16))) {
return prev()
}
switch (key) {
case 46: // 'Delete' key
case 8: // 'Backspace' key
case 9: // 'Tab' key
case 39: // 'Arrow right' key
case 37: // 'Arrow left' key
case 40: // 'Arrow left' key
case 38: // 'Arrow left' key
prev();
break;
case 13:
// 'Enter' key
prev();
if (this.props.onEnterKey) {
this.props.onEnterKey();
}
break;
case 27:
// 'Escape' key
prev();
if (this.props.onEscapeKey) {
this.props.onEscapeKey();
}
break;
default:
this.unsetPlaceholder();
break;
}
}
},
_replaceCurrentSelection: function(data) {
var selection = window.getSelection();
var range = selection.getRangeAt(0);
range.deleteContents();
var fragment = range.createContextualFragment('');
fragment.textContent = data;
var replacementEnd = fragment.lastChild;
range.insertNode(fragment);
// Set cursor at the end of the replaced content, just like browsers do.
range.setStartAfter(replacementEnd);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
},
onPaste: function(e){
// handle paste manually to ensure we unset our placeholder
e.preventDefault();
var data = e.clipboardData.getData('text/plain')
this._replaceCurrentSelection(data);
var target = ReactDOM.findDOMNode(this)
this.props.onChange(target.textContent, false, target.innerHTML)
},
onKeyPress: function(e){
this.props.onKeyPress(e)
},
onKeyUp: function(e) {
if (this._supportsInput) return
if (this.stop) {
this.stop = false
return
}
var target = ReactDOM.findDOMNode(this)
var self = this
if (!target.textContent.trim().length) {
this.props.onChange('', true, '')
setTimeout(function(){
self.setCursorToStart()
}, 1)
} else {
this.props.onChange(target.textContent, false, target.innerHTML)
}
},
onInput: function(e) {
this._supportsInput = true
var val = e.target.innerHTML
var text = e.target.textContent.trim()
if (!text) {
this.props.onChange('', true, '')
return
}
this.props.onChange(escapeHTML(e.target.textContent), false, e.target.innerHTML)
},
onBlur: function(e) {
if (this.props.onBlur) {
this.props.onBlur(e);
}
},
onFocus: function(e) {
if (this.props.onFocus) {
this.props.onFocus(e);
}
}
});
module.exports = ContentEditable;

3
jsx/common/FollowButton.js

@ -6,6 +6,7 @@ var ReactBootstrap = require('react-bootstrap')
, Glyphicon = ReactBootstrap.Glyphicon , Glyphicon = ReactBootstrap.Glyphicon
, Modal = ReactBootstrap.Modal , Modal = ReactBootstrap.Modal
, Input = ReactBootstrap.Input , Input = ReactBootstrap.Input
, Button = ReactBootstrap.Button
var React = require('react'); var React = require('react');
@ -74,7 +75,7 @@ module.exports = FollowButton = React.createClass({
var methodName = this.state.isCurrentlyFollowing ? "Unfollow" : "Follow"; var methodName = this.state.isCurrentlyFollowing ? "Unfollow" : "Follow";
return ( return (
<button onClick={this.handleClick}>{methodName}</button> <Button onClick={this.handleClick}>{methodName}</Button>
); );
} }
}); });

147
jsx/common/PostContent.js

@ -1,155 +1,12 @@
var React = require('react'); var React = require('react');
var PostContentHelper = require('../common/PostContentHelper.js');
module.exports = Post = React.createClass({ module.exports = Post = React.createClass({
extractUsername: function(s) {
var username = "";
for( var i = 0; i < s.length; i++ ) {
var c = s.charCodeAt(i);
if( (c >= 'a'.charCodeAt(0) && c <= 'z'.charCodeAt(0)) ||
(c >= 'A'.charCodeAt(0) && c <= 'Z'.charCodeAt(0)) ||
(c >= '0'.charCodeAt(0) && c <= '9'.charCodeAt(0)) ||
c == '_'.charCodeAt(0) ) {
username += s[i];
} else {
break;
}
}
return username;
},
extractHashtag: function(s) {
var hashtag = "";
s = this.reverseHtmlEntities(s);
for( var i = 0; i < s.length; i++ ) {
if( " \n\t.,:/?!;'\"()[]{}*#".indexOf(s[i]) < 0 ) {
hashtag += s[i];
} else {
break;
}
}
return hashtag;
},
escapeHtmlEntities: function(str) {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
},
reverseHtmlEntities: function(str) {
return str
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&apos;/g, "'")
.replace(/&amp;/g, '&');
},
parseContent: function( msg ) {
//return [{type:"text",raw:msg}];
var output = [];
var tmp;
var match = null;
var index;
var strUrlRegexp = "http[s]?://";
var strEmailRegexp = "\\S+@\\S+\\.\\S+";
var strSplitCounterR = "\\(\\d{1,2}\\/\\d{1,2}\\)$";
var reAll = new RegExp("(?:^|[ \\n\\t.,:\\/?!])(#|@|" + strUrlRegexp + "|" + strEmailRegexp + "|" + strSplitCounterR + ")");
var reHttp = new RegExp(strUrlRegexp);
var reEmail = new RegExp(strEmailRegexp);
var reSplitCounter = new RegExp(strSplitCounterR);
//msg = this.escapeHtmlEntities(msg);
while( msg != undefined && msg.length ) {
match = reAll.exec(msg);
if( match ) {
index = (match[0] === match[1]) ? match.index : match.index + 1;
if( match[1] == "@" ) {
output.push({type:"text",raw:(msg.substr(0, index))});
tmp = msg.substr(index+1);
var username = this.extractUsername(tmp);
if( username.length ) {
output.push({type:"mention",raw:"@"+username});
}
msg = tmp.substr(String(username).length);
continue;
}
if( reHttp.exec(match[1]) ) {
output.push({type:"text",raw:(msg.substr(0, index))});
tmp = msg.substring(index);
var space = tmp.search(/[ \n\t]/);
var url;
if( space != -1 ) url = tmp.substring(0,space); else url = tmp;
if( url.length ) {
output.push({type:"url",raw:url});
}
msg = tmp.substr(String(url).length);
continue;
}
if( reEmail.exec(match[1]) ) {
output.push({type:"text",raw:(msg.substr(0, index))});
tmp = msg.substring(index);
var space = tmp.search(/[ \n\t]/);
var email;
if( space != -1 ) email = tmp.substring(0,space); else email = tmp;
if( email.length ) {
output.push({type:"email",raw:email});
}
msg = tmp.substr(String(email).length);
continue;
}
if( match[1] == "#" ) {
output.push({type:"text",raw:(msg.substr(0, index))});
tmp = msg.substr(index+1);
var hashtag = this.extractHashtag(tmp);
if( hashtag.length ) {
// var hashtag_lc='';
// for( var i = 0; i < hashtag.length; i++ ) {
// var c = hashtag[i];
// hashtag_lc += (c >= 'A' && c <= 'Z') ? c.toLowerCase() : c;
// }
output.push({type:"hashtag",raw:"#"+hashtag});
}
msg = tmp.substr(String(hashtag).length);
continue;
}
/*if (reSplitCounter.exec(match[1])) {
output.append({type:"text",raw:(msg.substr(0, index))});
tmp = msg.substring(index);
if( tmp.length ) {
var splitCounter = $('<span class="splited-post-counter"></span>');
splitCounter.text(tmp);
output.append(splitCounter);
msg = "";
continue;
}
msg = tmp.substr(String(hashtag).length);
continue;
}*/
}
output.push({type:"text",raw:this.reverseHtmlEntities(msg)});
msg = "";
}
return output;
},
render: function() { render: function() {
var parsedContent = this.parseContent(this.props.content); var parsedContent = PostContentHelper.parseContent(this.props.content);
//console.log(parsedContent) //console.log(parsedContent)

45
jsx/common/TwistComposer.js

@ -1,45 +0,0 @@
var React = require('react');
var ContentEditable = require('../common/ContentEditable.js');
module.exports = TwistComposer = React.createClass({ displayName: "TwistComposer",
getInitialState: function () {
return {
html: 'default text',
placeholder: true,
editing: true
};
},
render: function () {
return React.createElement("div", null, React.createElement(ContentEditable, {
tagName: "textarea",
onChange: this.onChange,
html: this.state.html,
preventStyling: true,
noLinebreaks: true,
placeholder: this.state.placeholder,
placeholderText: "Your Name",
editing: this.state.editing }), React.createElement("button", { onClick: this.enableEditing }, "Enable Editing"));
},
onChange: function (textContent, setPlaceholder) {
if (setPlaceholder) {
this.setState({
placeholder: true,
html: ''
});
} else {
this.setState({
placeholder: false,
html: textContent
});
}
},
enableEditing: function () {
// set your contenteditable field into editing mode.
this.setState({ editing: true });
}
});

32
jsx/home/NewPostModalButton.js

@ -10,7 +10,7 @@ var React = require('react');
var SafeStateChangeMixin = require('../common/SafeStateChangeMixin.js'); var SafeStateChangeMixin = require('../common/SafeStateChangeMixin.js');
var SetIntervalMixin = require("../common/SetIntervalMixin.js"); var SetIntervalMixin = require("../common/SetIntervalMixin.js");
var TwistComposer = require("../common/TwistComposer.js"); var PostComposer = require("../common/PostComposer.js");
module.exports = NewPostModalButton = React.createClass({ module.exports = NewPostModalButton = React.createClass({
getInitialState: function () { getInitialState: function () {
@ -23,10 +23,9 @@ module.exports = NewPostModalButton = React.createClass({
isModalOpen: !this.state.isModalOpen isModalOpen: !this.state.isModalOpen
}); });
}, },
handleNewPost: function (e) { handleNewPost: function (text) {
e.preventDefault(); console.log(text)
//console.log(e) var msg = text;
var msg = JSON.parse(JSON.stringify(e.target[0].value));
if (!msg) { if (!msg) {
console.log("empty post was passed as new post") console.log("empty post was passed as new post")
return; return;
@ -40,37 +39,22 @@ module.exports = NewPostModalButton = React.createClass({
}); });
e.target[0].value = "";
this.handleToggle(); this.handleToggle();
//React.findDOMNode(this.refs.msg).value = ''; //React.findDOMNode(this.refs.msg).value = '';
return; return;
}, },
render: function() { render: function() {
return ( return (
<div>
<Button onClick={this.handleToggle} className="link-button-gray pull-right fullytight_all" bsStyle="link"> <Button onClick={this.handleToggle} className="link-button-gray pull-right fullytight_all" bsStyle="link">
<Glyphicon glyph='pencil' /> <Glyphicon glyph='pencil' />
</Button> <Modal title={<Glyphicon glyph='pencil'/>} show={this.state.isModalOpen} bsStyle='primary' onHide={this.handleToggle}>
<Modal show={this.state.isModalOpen} bsStyle='primary' title={<Glyphicon glyph='pencil'/>} onRequestHide={this.handleToggle}>
<div className='modal-body'> <div className='modal-body'>
<form onSubmit={this.handleNewPost}> <PostComposer onSubmit={this.handleNewPost} />
<Input type='textarea' label='Write Something' placeholder='textarea'/>
<Input type='submit' value='Submit Post' data-dismiss="modal" />
</form>
</div> </div>
</Modal> </Modal>
</div> </Button>
); );
},
renderOverlay: function() {
if (!this.state.isModalOpen) {
return <span/>;
}
} }
}); });

3
package.json

@ -10,8 +10,7 @@
"react-router": "*", "react-router": "*",
"react-router-bootstrap": "*", "react-router-bootstrap": "*",
"react-dom": "*", "react-dom": "*",
"selection-range": "*", "react-wysiwyg": "*",
"escape-html": "*",
"react-addons-css-transition-group": "*" "react-addons-css-transition-group": "*"
}, },
"devDependencies": { "devDependencies": {

5
tests/PostContentTest.js

@ -1,5 +1,4 @@
var PostContent = require("../build-buffer/common/PostContent.js"); var PostContentHelper = require("../build-buffer/common/PostContentHelper.js");
var sut = new PostContent();
console.log(sut.parseContent(".@tschaul so cool, great work!")); console.log(PostContentHelper.parseContent("asd http://"));
Loading…
Cancel
Save