application/components/common/rich-editor2/plugins/suggestion.js
import React from 'react';
import ToolbarButton from '../common/toolbar-button';
import CurrentUser from '../../../../current-user';
import Immutable from 'immutable';
import Dialog from 'material-ui/Dialog';
import TextField from 'material-ui/TextField';
import CheckBox from 'material-ui/Checkbox';
import FlatButton from 'material-ui/FlatButton';
import RaisedButton from 'material-ui/RaisedButton';
export const SERIALIZER_RULES = [
];
let keywords;
let related;
let isActive;
class SuggestionButton extends ToolbarButton
{
handleOnClick(e) {
return suggestionClickHandler.call(this.props.editor, e);
}
getLabel() {
return <i class="fa fa-retweet"></i>;
}
}
class SuggestionNode extends React.Component
{
constructor(props) {
super(props);
this.parentEditor = this.props.editor;
}
render() {
const handleClick = e => {
e.preventDefault();
// open the modal and store the anchor data
this.props.editor.setState({
'mySuggestionModalOpen': true,
'mySuggestionKeyword': this.props.children
});
}
return (
<span {...this.props.attributes} style={{color: 'green', textDecoration: 'underline', cursor: 'pointer'}} onClick={handleClick}>
{this.props.children}
</span>
);
}
}
function suggestionClickHandler(e) {
const {editor} = this;
const {value} = editor;
const texts = value.document.getTexts();
const decorations = [];
if (isActive) {
isActive = false;
editor.withoutSaving(() => {
editor.setDecorations(decorations);
});
return;
}
isActive = true;
texts.forEach(node => {
const {key, text} = node;
keywords.forEach(keyword => {
let hasRelated = false;
related.forEach(item => {
if (item.get('keywords').contains(keyword.toLowerCase())) {
hasRelated = true;
}
});
if (!hasRelated) {
return;
}
const parts = text.toLowerCase().split(keyword);
let offset = 0;
parts.forEach((part, i) => {
if (i !== 0) {
decorations.push({
anchor: {key, offset: offset - keyword.length},
focus: {key, offset},
mark: {type: 'suggestion'},
});
}
offset = offset + part.length + keyword.length;
});
});
});
// Make the change to decorations without saving it into the undo history,
// so that there isn't a confusing behavior when undoing.
editor.withoutSaving(() => {
editor.setDecorations(decorations);
});
}
class SuggestionModal extends React.Component {
constructor(props) {
super(props);
this.state = {
url: ''
};
}
closeModal() {
this.props.editor.setState({
'mySuggestionModalOpen': false,
'mySuggestionKeyword': null,
});
this.setState({
'keyword': null
});
}
handleCancel(e) {
this.closeModal.call(this);
}
handlePreview(item, e) {
const url = '/content/preview/' + item.get('uuid');
window.open(url, 'preview');
}
componentWillReceiveProps(newProps) {
if (newProps.editor && newProps.editor.state.mySuggestionKeyword) {
this.setState({
'keyword': newProps.editor.state.mySuggestionKeyword
});
}
}
handleSave(item, e) {
const keyword = this.state.keyword.props.children;
const firstThree = keyword.substr(0, 3);
const lastThree = keyword.substr(-3);
let iterations = 0;
// insert a character, then remove it to unset the decoration
// because they don't really exist as selectable marks in the editor
// model
this.props.editor
.focus()
.insertText(' ')
.deleteBackward(1);
// start expanding the selection backward
let go = true;
while (go) {
iterations++;
if (iterations == 100) {
return;
}
let text = this.props.editor.value.fragment.text;
this.props.editor
.focus()
.moveAnchorBackward();
if (text == keyword) {
break;
}
if (text.indexOf(firstThree) === 0) {
go = false;
}
}
// start expanding it forward
iterations = 0;
go = true;
while (go) {
iterations++;
if (iterations == 100) {
return;
}
let text = this.props.editor.value.fragment.text;
this.props.editor
.focus()
.moveFocusForward();
if (text == keyword) {
break;
}
if (text.substr(-3).indexOf(lastThree) === 0) {
go = false;
}
}
// once the selection is full, it has the one too many characters selected
// so we contract it back down and insert the anchor.
if (this.props.editor.value.fragment.text.indexOf(keyword) !== 0) {
this.props.editor
.focus()
.moveAnchorForward();
}
if (this.props.editor.value.fragment.text.length !== keyword.length) {
this.props.editor
.focus()
.moveFocusBackward();
}
this.props.editor
.focus()
.wrapInline({
type: 'anchor',
data: {
'href': '/' + item.get('uuid'),
'target': ''
}
});
setTimeout(() => {
this.closeModal.call(this);
}, 250);
return;
}
render() {
const buttons = [
<FlatButton
label='Cancel'
onClick={this.handleCancel.bind(this)}
/>,
];
let suggestions = Immutable.fromJS([]);
if (this.state.keyword) {
const keyword = this.state.keyword.props.children.toLowerCase();
suggestions = related.filter(item => {
return item.get('keywords').indexOf(keyword) !== -1;
});
}
return (
<Dialog
modal={true}
actions={buttons}
title='Insert Suggested Link'
open={this.props.editor.state.mySuggestionModalOpen ? true : false}
>
{suggestions.map((suggestion, i) => (
<div key={i} className='clear-bottom'>
<span onClick={this.handleSave.bind(this, suggestion)} className='faux-link'>{suggestion.get('title')}</span><br />
{suggestion.get('published_at')} | <FlatButton primary={true} onClick={this.handlePreview.bind(this, suggestion)} label="Preview Article" />
</div>
))}
</Dialog>
);
}
};
class SuggestionNote extends React.Component {
render() {
if (this.props.open) {
return (
<div style={{backgroundColor:'#ffc', margin: '5px 0', padding: '2px 5px', fontSize: '0.8em'}}>Select a keyword to insert a link. Click the "Suggest Links" button again to hide suggested links.</div>
);
} else {
return '';
}
}
};
function SuggestionPlugin(options) {
keywords = options.keywords || Immutable.fromJS([]);
related = options.related || Immutable.fromJS([]);
return {
renderEditor(props, editor, next) {
const children = next();
return (
<React.Fragment>
<SuggestionNote open={isActive} />
{children}
<SuggestionModal editor={editor} />
</React.Fragment>
);
},
renderMark(props, editor, next) {
const {children, mark, attributes} = props;
switch (mark.type) {
case 'suggestion':
return <SuggestionNode {...props} editor={editor}/>;
default:
return next();
}
}
};
};
export {SuggestionPlugin, suggestionClickHandler, SuggestionButton};