Remove webpack warnings and react-intl errors

This commit is contained in:
cyril lopez 2018-06-28 10:01:59 +02:00
parent f09cace582
commit 799c6c3f0f
52 changed files with 3508 additions and 11 deletions

View File

@ -33,13 +33,15 @@ const tryRequireRoot = (source) => {
const bootstrap = tryRequireRoot('bootstrap');
const pluginRequirements = tryRequireRoot('requirements');
const layout = (() => {
try {
return require('../../../../config/layout.js'); // eslint-disable-line import/no-unresolved
} catch(err) {
return null;
}
})();
// NOTE: I'm commenting this line for the moment since we don't use the layout in the other plugins anymore
// Initially it was developed for the content-manager but we acces the layout via a request instead of building it
// const layout = (() => {
// try {
// return require('../../../../config/layout.js'); // eslint-disable-line import/no-unresolved
// } catch(err) {
// return null;
// }
// })();
const injectedComponents = (() => {
try {
@ -97,7 +99,7 @@ strapi.registerPlugin({
icon: pluginPkg.strapi.icon,
id: pluginId,
injectedComponents,
layout,
layout: null,
leftMenuLinks: [],
mainComponent: Comp,
name: pluginPkg.strapi.name,

View File

@ -115,4 +115,4 @@
"webpack-hot-middleware": "^2.18.2",
"whatwg-fetch": "^2.0.3"
}
}
}

View File

@ -0,0 +1 @@
<svg width="19" height="10" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><text font-family="Lato-Semibold, Lato" font-size="11" font-weight="500" fill="#41464E" transform="translate(0 -2)"><tspan x="1" y="11">abc</tspan></text><path d="M.5 6.5h18" stroke="#2C3039" stroke-linecap="square"/></g></svg>

After

Width:  |  Height:  |  Size: 325 B

View File

@ -0,0 +1 @@
<svg width="9" height="10" xmlns="http://www.w3.org/2000/svg"><text transform="translate(-12 -10)" fill="#333740" fill-rule="evenodd" font-size="13" font-family="Baskerville-SemiBold, Baskerville" font-weight="500"><tspan x="12" y="20">B</tspan></text></svg>

After

Width:  |  Height:  |  Size: 258 B

View File

@ -0,0 +1 @@
<svg width="13" height="7" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#333740" d="M5 0h8v1H5zM5 2h8v1H5zM5 4h8v1H5zM5 6h8v1H5z"/><rect stroke="#333740" x=".5" y=".5" width="2" height="2" rx="1"/><rect stroke="#333740" x=".5" y="4.5" width="2" height="2" rx="1"/></g></svg>

After

Width:  |  Height:  |  Size: 311 B

View File

@ -0,0 +1 @@
<svg width="12" height="8" xmlns="http://www.w3.org/2000/svg"><g fill="#333740" fill-rule="evenodd"><path d="M3.653 7.385a.632.632 0 0 1-.452-.191L.214 4.154a.66.66 0 0 1 0-.922L3.201.19a.632.632 0 0 1 .905 0 .66.66 0 0 1 0 .921l-2.534 2.58 2.534 2.58a.66.66 0 0 1 0 .922.632.632 0 0 1-.453.19zM8.347 7.385a.632.632 0 0 0 .452-.191l2.987-3.04a.66.66 0 0 0 0-.922L8.799.19a.632.632 0 0 0-.905 0 .66.66 0 0 0 0 .921l2.534 2.58-2.534 2.58a.66.66 0 0 0 0 .922c.125.127.289.19.453.19z"/></g></svg>

After

Width:  |  Height:  |  Size: 492 B

View File

@ -0,0 +1 @@
<svg width="6" height="9" xmlns="http://www.w3.org/2000/svg"><text transform="translate(-13 -11)" fill="#333740" fill-rule="evenodd" font-weight="500" font-size="13" font-family="Baskerville-SemiBoldItalic, Baskerville" font-style="italic"><tspan x="13" y="20">I</tspan></text></svg>

After

Width:  |  Height:  |  Size: 283 B

View File

@ -0,0 +1 @@
<svg width="12" height="6" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M6.063 1.5H6h.063z" fill="#000"/><path d="M9.516 0H8s.813.531.988 1.5h.528c.55 0 .984.434.984.984v1c0 .55-.434 1.016-.984 1.016h-3.5A1.03 1.03 0 0 1 5 3.484V2.5H3.5v.984A2.518 2.518 0 0 0 6.016 6h3.5C10.896 6 12 4.866 12 3.484v-1A2.473 2.473 0 0 0 9.516 0z" fill="#333740"/><path d="M8.3 1.5A2.473 2.473 0 0 0 6.016 0h-3.5C1.134 0 0 1.103 0 2.484v1A2.526 2.526 0 0 0 2.516 6H4s-.806-.531-1.003-1.5h-.481A1.03 1.03 0 0 1 1.5 3.484v-1c0-.55.466-.984 1.016-.984h3.5c.55 0 .984.434.984.984V3.5h1.5V2.484c0-.35-.072-.684-.2-.984z" fill="#333740"/></g></svg>

After

Width:  |  Height:  |  Size: 658 B

View File

@ -0,0 +1 @@
<svg width="12" height="11" xmlns="http://www.w3.org/2000/svg"><g fill="#333740" fill-rule="evenodd"><path d="M9 4.286a1.286 1.286 0 1 0 0-2.572 1.286 1.286 0 0 0 0 2.572z"/><path d="M11.25 0H.75C.332 0 0 .34 0 .758v8.77c0 .418.332.758.75.758h10.5c.418 0 .75-.34.75-.758V.758A.752.752 0 0 0 11.25 0zM8.488 5.296a.46.46 0 0 0-.342-.167c-.137 0-.234.065-.343.153l-.501.423c-.105.075-.188.126-.308.126a.443.443 0 0 1-.295-.11 3.5 3.5 0 0 1-.115-.11L5.143 4.054a.59.59 0 0 0-.897.008L.857 8.148V1.171a.353.353 0 0 1 .351-.314h9.581a.34.34 0 0 1 .346.322l.008 6.975-2.655-2.858z"/></g></svg>

After

Width:  |  Height:  |  Size: 586 B

View File

@ -0,0 +1 @@
<svg width="12" height="8" xmlns="http://www.w3.org/2000/svg"><g fill="#333740" fill-rule="evenodd"><path d="M2.4 3H.594v-.214h.137c.123 0 .212-.01.266-.032.053-.022.086-.052.1-.092a.67.67 0 0 0 .018-.188V.74a.46.46 0 0 0-.03-.194C1.064.504 1.021.476.955.46A1.437 1.437 0 0 0 .643.435H.539V.23c.332-.035.565-.067.7-.096.135-.03.258-.075.37-.134h.275v2.507c0 .104.023.177.07.218.047.04.14.061.278.061H2.4V3zM2.736 6.695l-.132.528h-.246a.261.261 0 0 0 .015-.074c0-.058-.049-.087-.146-.087H.293v-.198c.258-.173.511-.367.76-.581.25-.215.457-.437.623-.667.166-.23.249-.447.249-.653a.49.49 0 0 0-.321-.478.794.794 0 0 0-.582-.006.482.482 0 0 0-.196.138.284.284 0 0 0-.07.182c0 .074.04.17.12.289.006.008.009.015.009.02 0 .012-.041.03-.123.053l-.19.057a.693.693 0 0 1-.115.03c-.031 0-.067-.038-.108-.114a.516.516 0 0 1 .071-.586.899.899 0 0 1 .405-.238c.18-.058.4-.087.657-.087.317 0 .566.044.749.132.183.087.306.187.37.3a.64.64 0 0 1 .094.312c0 .197-.089.389-.266.575a5.296 5.296 0 0 1-.916.74 62.947 62.947 0 0 1-.62.413h1.843zM4 0h8v1H4zM4 2h8v1H4zM4 4h8v1H4zM4 6h8v1H4z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<svg width="9" height="9" xmlns="http://www.w3.org/2000/svg"><g fill="#333740" fill-rule="evenodd"><path d="M3 0C2.047 0 1.301.263.782.782.263 1.302 0 2.047 0 3v6h3.75V3H1.5c0-.54.115-.93.343-1.157C2.07 1.615 2.46 1.5 3 1.5M8.25 0c-.953 0-1.699.263-2.218.782-.519.52-.782 1.265-.782 2.218v6H9V3H6.75c0-.54.115-.93.343-1.157.227-.228.617-.343 1.157-.343"/></g></svg>

After

Width:  |  Height:  |  Size: 365 B

View File

@ -0,0 +1 @@
<svg width="10" height="10" xmlns="http://www.w3.org/2000/svg"><text transform="translate(-10 -11)" fill="#101622" fill-rule="evenodd" font-size="13" font-family="Baskerville-SemiBold, Baskerville" font-weight="500"><tspan x="10" y="20">U</tspan></text></svg>

After

Width:  |  Height:  |  Size: 259 B

View File

@ -0,0 +1,201 @@
/**
*
* InputJSON
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import cm from 'codemirror';
import 'codemirror/lib/codemirror.css';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/addon/lint/lint';
import 'codemirror/addon/lint/javascript-lint';
import 'codemirror/addon/edit/closebrackets';
import 'codemirror/addon/selection/mark-selection';
import 'codemirror/theme/liquibyte.css';
import 'codemirror/theme/xq-dark.css';
import 'codemirror/theme/3024-day.css';
import 'codemirror/theme/3024-night.css';
import 'codemirror/theme/blackboard.css';
import 'codemirror/theme/monokai.css';
import 'codemirror/theme/cobalt.css';
import { isEmpty, isObject, trimStart } from 'lodash';
import jsonlint from './jsonlint';
import styles from './styles.scss';
const WAIT = 600;
const stringify = JSON.stringify;
const parse = JSON.parse;
const DEFAULT_THEME = 'monokai';
const THEMES = ['blackboard', 'cobalt', 'monokai', '3024-day', '3024-night', 'liquibyte', 'xq-dark'];
class InputJSON extends React.Component {
constructor(props) {
super(props);
this.editor = React.createRef();
this.state = { error: false, markedText: null };
}
componentDidMount() {
// Init codemirror component
this.codeMirror = cm.fromTextArea(this.editor.current, {
autoCloseBrackets: true,
lineNumbers: true,
matchBrackets: true,
mode: 'application/json',
smartIndent: true,
styleSelectedText: true,
tabSize: 2,
theme: DEFAULT_THEME,
});
this.codeMirror.on('change', this.handleChange);
this.codeMirror.on('blur', this.handleBlur);
this.setSize();
this.setInitValue();
}
componentDidUpdate(prevProps) {
if (isEmpty(prevProps.value) && !isEmpty(this.props.value) && !this.state.hasInitValue) {
this.setInitValue();
}
}
setInitValue = () => {
const { value } = this.props;
if (isObject(value) && value !== null) {
try {
parse(stringify(value));
this.setState({ hasInitValue: true });
return this.codeMirror.setValue(stringify(value, null, 2));
} catch(err) {
return this.setState({ error: true });
}
}
}
setSize = () => this.codeMirror.setSize('100%', 'auto');
setTheme = (theme) => this.codeMirror.setOption('theme', theme);
getContentAtLine = (line) => this.codeMirror.getLine(line);
getEditorOption = (opt) => this.codeMirror.getOption(opt);
getValue = () => this.codeMirror.getValue();
markSelection = ({ message }) => {
let line = parseInt(
message
.split(':')[0]
.split('line ')[1],
10,
) - 1;
let content = this.getContentAtLine(line);
if (content === '{') {
line = line + 1;
content = this.getContentAtLine(line);
}
const chEnd = content.length;
const chStart = chEnd - trimStart(content, ' ').length;
const markedText = this.codeMirror.markText({ line, ch: chStart }, { line, ch: chEnd }, { className: styles.colored });
this.setState({ markedText });
}
timer = null;
handleBlur = ({ target }) => {
const { name, onBlur } = this.props;
if (target === undefined) { // codemirror catches multiple events
onBlur({
target: {
name,
type: 'json',
value: this.getValue(),
},
});
}
}
handleChange = () => {
const { hasInitValue } = this.state;
const { name, onChange } = this.props;
let value = this.codeMirror.getValue();
try {
value = parse(value);
} catch(err) {
// Silent
}
// Update the parent
onChange({
target: {
name,
value,
type: 'json',
},
});
if (!hasInitValue) {
this.setState({ hasInitValue: true });
}
// Remove higlight error
if (this.state.markedText) {
this.state.markedText.clear();
this.setState({ markedText: null, error: null });
}
clearTimeout(this.timer);
this.timer = setTimeout(() => this.testJSON(this.codeMirror.getValue()), WAIT);
}
testJSON = (value) => {
try {
jsonlint.parse(value);
} catch(err) {
this.markSelection(err);
}
}
render() {
if (this.state.error) {
return <div>error json</div>;
}
return (
<div className={styles.jsonWrapper}>
<textarea ref={this.editor} autoComplete='off' defaultValue="" />
<select className={styles.select} onChange={({ target }) => this.setTheme(target.value)} defaultValue={DEFAULT_THEME}>
{THEMES.sort().map(theme => <option key={theme} value={theme}>{theme}</option>)}
</select>
</div>
);
}
}
InputJSON.defaultProps = {
onBlur: () => {},
onChange: () => {},
value: null,
};
InputJSON.propTypes = {
name: PropTypes.string.isRequired,
onBlur: PropTypes.func,
onChange: PropTypes.func,
value: PropTypes.object,
};
export default InputJSON;

View File

@ -0,0 +1,424 @@
/* Jison generated parser */
/* eslint-disable */
var jsonlint = (function(){
var parser = {trace: function trace() { },
yy: {},
symbols_: {"error":2,"JSONString":3,"STRING":4,"JSONNumber":5,"NUMBER":6,"JSONNullLiteral":7,"NULL":8,"JSONBooleanLiteral":9,"TRUE":10,"FALSE":11,"JSONText":12,"JSONValue":13,"EOF":14,"JSONObject":15,"JSONArray":16,"{":17,"}":18,"JSONMemberList":19,"JSONMember":20,":":21,",":22,"[":23,"]":24,"JSONElementList":25,"$accept":0,"$end":1},
terminals_: {2:"error",4:"STRING",6:"NUMBER",8:"NULL",10:"TRUE",11:"FALSE",14:"EOF",17:"{",18:"}",21:":",22:",",23:"[",24:"]"},
productions_: [0,[3,1],[5,1],[7,1],[9,1],[9,1],[12,2],[13,1],[13,1],[13,1],[13,1],[13,1],[13,1],[15,2],[15,3],[20,3],[19,1],[19,3],[16,2],[16,3],[25,1],[25,3]],
performAction: function anonymous(yytext,yyleng,yylineno,yy,yystate,$$,_$) {
var $0 = $$.length - 1;
switch (yystate) {
case 1: // replace escaped characters with actual character
this.$ = yytext.replace(/\\(\\|")/g, "$"+"1")
.replace(/\\n/g,'\n')
.replace(/\\r/g,'\r')
.replace(/\\t/g,'\t')
.replace(/\\v/g,'\v')
.replace(/\\f/g,'\f')
.replace(/\\b/g,'\b');
break;
case 2:this.$ = Number(yytext);
break;
case 3:this.$ = null;
break;
case 4:this.$ = true;
break;
case 5:this.$ = false;
break;
case 6:return this.$ = $$[$0-1];
break;
case 13:this.$ = {};
break;
case 14:this.$ = $$[$0-1];
break;
case 15:this.$ = [$$[$0-2], $$[$0]];
break;
case 16:this.$ = {}; this.$[$$[$0][0]] = $$[$0][1];
break;
case 17:this.$ = $$[$0-2]; $$[$0-2][$$[$0][0]] = $$[$0][1];
break;
case 18:this.$ = [];
break;
case 19:this.$ = $$[$0-1];
break;
case 20:this.$ = [$$[$0]];
break;
case 21:this.$ = $$[$0-2]; $$[$0-2].push($$[$0]);
break;
}
},
table: [{3:5,4:[1,12],5:6,6:[1,13],7:3,8:[1,9],9:4,10:[1,10],11:[1,11],12:1,13:2,15:7,16:8,17:[1,14],23:[1,15]},{1:[3]},{14:[1,16]},{14:[2,7],18:[2,7],22:[2,7],24:[2,7]},{14:[2,8],18:[2,8],22:[2,8],24:[2,8]},{14:[2,9],18:[2,9],22:[2,9],24:[2,9]},{14:[2,10],18:[2,10],22:[2,10],24:[2,10]},{14:[2,11],18:[2,11],22:[2,11],24:[2,11]},{14:[2,12],18:[2,12],22:[2,12],24:[2,12]},{14:[2,3],18:[2,3],22:[2,3],24:[2,3]},{14:[2,4],18:[2,4],22:[2,4],24:[2,4]},{14:[2,5],18:[2,5],22:[2,5],24:[2,5]},{14:[2,1],18:[2,1],21:[2,1],22:[2,1],24:[2,1]},{14:[2,2],18:[2,2],22:[2,2],24:[2,2]},{3:20,4:[1,12],18:[1,17],19:18,20:19},{3:5,4:[1,12],5:6,6:[1,13],7:3,8:[1,9],9:4,10:[1,10],11:[1,11],13:23,15:7,16:8,17:[1,14],23:[1,15],24:[1,21],25:22},{1:[2,6]},{14:[2,13],18:[2,13],22:[2,13],24:[2,13]},{18:[1,24],22:[1,25]},{18:[2,16],22:[2,16]},{21:[1,26]},{14:[2,18],18:[2,18],22:[2,18],24:[2,18]},{22:[1,28],24:[1,27]},{22:[2,20],24:[2,20]},{14:[2,14],18:[2,14],22:[2,14],24:[2,14]},{3:20,4:[1,12],20:29},{3:5,4:[1,12],5:6,6:[1,13],7:3,8:[1,9],9:4,10:[1,10],11:[1,11],13:30,15:7,16:8,17:[1,14],23:[1,15]},{14:[2,19],18:[2,19],22:[2,19],24:[2,19]},{3:5,4:[1,12],5:6,6:[1,13],7:3,8:[1,9],9:4,10:[1,10],11:[1,11],13:31,15:7,16:8,17:[1,14],23:[1,15]},{18:[2,17],22:[2,17]},{18:[2,15],22:[2,15]},{22:[2,21],24:[2,21]}],
defaultActions: {16:[2,6]},
parseError: function parseError(str, hash) {
throw new Error(str);
},
parse: function parse(input) {
var self = this,
stack = [0],
vstack = [null], // semantic value stack
lstack = [], // location stack
table = this.table,
yytext = '',
yylineno = 0,
yyleng = 0,
recovering = 0,
TERROR = 2,
EOF = 1;
//this.reductionCount = this.shiftCount = 0;
this.lexer.setInput(input);
this.lexer.yy = this.yy;
this.yy.lexer = this.lexer;
if (typeof this.lexer.yylloc == 'undefined')
this.lexer.yylloc = {};
var yyloc = this.lexer.yylloc;
lstack.push(yyloc);
if (typeof this.yy.parseError === 'function')
this.parseError = this.yy.parseError;
function popStack (n) {
stack.length = stack.length - 2*n;
vstack.length = vstack.length - n;
lstack.length = lstack.length - n;
}
function lex() {
var token;
token = self.lexer.lex() || 1; // $end = 1
// if token isn't its numeric value, convert
if (typeof token !== 'number') {
token = self.symbols_[token] || token;
}
return token;
}
var symbol, preErrorSymbol, state, action, a, r, yyval={},p,len,newState, expected;
while (true) {
// retreive state number from top of stack
state = stack[stack.length-1];
// use default actions if available
if (this.defaultActions[state]) {
action = this.defaultActions[state];
} else {
if (symbol == null)
symbol = lex();
// read action for current state and first input
action = table[state] && table[state][symbol];
}
// handle parse error
_handle_error:
if (typeof action === 'undefined' || !action.length || !action[0]) {
if (!recovering) {
// Report error
expected = [];
for (p in table[state]) if (this.terminals_[p] && p > 2) {
expected.push("'"+this.terminals_[p]+"'");
}
var errStr = '';
if (this.lexer.showPosition) {
errStr = 'Parse error on line '+(yylineno+1)+":\n"+this.lexer.showPosition()+"\nExpecting "+expected.join(', ') + ", got '" + this.terminals_[symbol]+ "'";
} else {
errStr = 'Parse error on line '+(yylineno+1)+": Unexpected " +
(symbol == 1 /*EOF*/ ? "end of input" :
("'"+(this.terminals_[symbol] || symbol)+"'"));
}
this.parseError(errStr,
{text: this.lexer.match, token: this.terminals_[symbol] || symbol, line: this.lexer.yylineno, loc: yyloc, expected: expected});
}
// just recovered from another error
if (recovering == 3) {
if (symbol == EOF) {
throw new Error(errStr || 'Parsing halted.');
}
// discard current lookahead and grab another
yyleng = this.lexer.yyleng;
yytext = this.lexer.yytext;
yylineno = this.lexer.yylineno;
yyloc = this.lexer.yylloc;
symbol = lex();
}
// try to recover from error
while (1) {
// check for error recovery rule in this state
if ((TERROR.toString()) in table[state]) {
break;
}
if (state == 0) {
throw new Error(errStr || 'Parsing halted.');
}
popStack(1);
state = stack[stack.length-1];
}
preErrorSymbol = symbol; // save the lookahead token
symbol = TERROR; // insert generic error symbol as new lookahead
state = stack[stack.length-1];
action = table[state] && table[state][TERROR];
recovering = 3; // allow 3 real symbols to be shifted before reporting a new error
}
// this shouldn't happen, unless resolve defaults are off
if (action[0] instanceof Array && action.length > 1) {
throw new Error('Parse Error: multiple actions possible at state: '+state+', token: '+symbol);
}
switch (action[0]) {
case 1: // shift
//this.shiftCount++;
stack.push(symbol);
vstack.push(this.lexer.yytext);
lstack.push(this.lexer.yylloc);
stack.push(action[1]); // push state
symbol = null;
if (!preErrorSymbol) { // normal execution/no error
yyleng = this.lexer.yyleng;
yytext = this.lexer.yytext;
yylineno = this.lexer.yylineno;
yyloc = this.lexer.yylloc;
if (recovering > 0)
recovering--;
} else { // error just occurred, resume old lookahead f/ before error
symbol = preErrorSymbol;
preErrorSymbol = null;
}
break;
case 2: // reduce
//this.reductionCount++;
len = this.productions_[action[1]][1];
// perform semantic action
yyval.$ = vstack[vstack.length-len]; // default to $$ = $1
// default location, uses first token for firsts, last for lasts
yyval._$ = {
first_line: lstack[lstack.length-(len||1)].first_line,
last_line: lstack[lstack.length-1].last_line,
first_column: lstack[lstack.length-(len||1)].first_column,
last_column: lstack[lstack.length-1].last_column
};
r = this.performAction.call(yyval, yytext, yyleng, yylineno, this.yy, action[1], vstack, lstack);
if (typeof r !== 'undefined') {
return r;
}
// pop off stack
if (len) {
stack = stack.slice(0,-1*len*2);
vstack = vstack.slice(0, -1*len);
lstack = lstack.slice(0, -1*len);
}
stack.push(this.productions_[action[1]][0]); // push nonterminal (reduce)
vstack.push(yyval.$);
lstack.push(yyval._$);
// goto new state = table[STATE][NONTERMINAL]
newState = table[stack[stack.length-2]][stack[stack.length-1]];
stack.push(newState);
break;
case 3: // accept
return true;
}
}
return true;
}};
/* Jison generated lexer */
var lexer = (function(){
var lexer = ({EOF:1,
parseError:function parseError(str, hash) {
if (this.yy.parseError) {
this.yy.parseError(str, hash);
} else {
throw new Error(str);
}
},
setInput:function (input) {
this._input = input;
this._more = this._less = this.done = false;
this.yylineno = this.yyleng = 0;
this.yytext = this.matched = this.match = '';
this.conditionStack = ['INITIAL'];
this.yylloc = {first_line:1,first_column:0,last_line:1,last_column:0};
return this;
},
input:function () {
var ch = this._input[0];
this.yytext+=ch;
this.yyleng++;
this.match+=ch;
this.matched+=ch;
var lines = ch.match(/\n/);
if (lines) this.yylineno++;
this._input = this._input.slice(1);
return ch;
},
unput:function (ch) {
this._input = ch + this._input;
return this;
},
more:function () {
this._more = true;
return this;
},
less:function (n) {
this._input = this.match.slice(n) + this._input;
},
pastInput:function () {
var past = this.matched.substr(0, this.matched.length - this.match.length);
return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, "");
},
upcomingInput:function () {
var next = this.match;
if (next.length < 20) {
next += this._input.substr(0, 20-next.length);
}
return (next.substr(0,20)+(next.length > 20 ? '...':'')).replace(/\n/g, "");
},
showPosition:function () {
var pre = this.pastInput();
var c = new Array(pre.length + 1).join("-");
return pre + this.upcomingInput() + "\n" + c+"^";
},
next:function () {
if (this.done) {
return this.EOF;
}
if (!this._input) this.done = true;
var token,
match,
tempMatch,
index,
col,
lines;
if (!this._more) {
this.yytext = '';
this.match = '';
}
var rules = this._currentRules();
for (var i=0;i < rules.length; i++) {
tempMatch = this._input.match(this.rules[rules[i]]);
if (tempMatch && (!match || tempMatch[0].length > match[0].length)) {
match = tempMatch;
index = i;
if (!this.options.flex) break;
}
}
if (match) {
lines = match[0].match(/\n.*/g);
if (lines) this.yylineno += lines.length;
this.yylloc = {first_line: this.yylloc.last_line,
last_line: this.yylineno+1,
first_column: this.yylloc.last_column,
last_column: lines ? lines[lines.length-1].length-1 : this.yylloc.last_column + match[0].length}
this.yytext += match[0];
this.match += match[0];
this.yyleng = this.yytext.length;
this._more = false;
this._input = this._input.slice(match[0].length);
this.matched += match[0];
token = this.performAction.call(this, this.yy, this, rules[index],this.conditionStack[this.conditionStack.length-1]);
if (this.done && this._input) this.done = false;
if (token) return token;
else return;
}
if (this._input === "") {
return this.EOF;
} else {
this.parseError('Lexical error on line '+(this.yylineno+1)+'. Unrecognized text.\n'+this.showPosition(),
{text: "", token: null, line: this.yylineno});
}
},
lex:function lex() {
var r = this.next();
if (typeof r !== 'undefined') {
return r;
} else {
return this.lex();
}
},
begin:function begin(condition) {
this.conditionStack.push(condition);
},
popState:function popState() {
return this.conditionStack.pop();
},
_currentRules:function _currentRules() {
return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules;
},
topState:function () {
return this.conditionStack[this.conditionStack.length-2];
},
pushState:function begin(condition) {
this.begin(condition);
}});
lexer.options = {};
lexer.performAction = function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) {
var YYSTATE=YY_START
switch($avoiding_name_collisions) {
case 0:/* skip whitespace */
break;
case 1:return 6
break;
case 2:yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2); return 4
break;
case 3:return 17
break;
case 4:return 18
break;
case 5:return 23
break;
case 6:return 24
break;
case 7:return 22
break;
case 8:return 21
break;
case 9:return 10
break;
case 10:return 11
break;
case 11:return 8
break;
case 12:return 14
break;
case 13:return 'INVALID'
break;
}
};
lexer.rules = [/^(?:\s+)/,/^(?:(-?([0-9]|[1-9][0-9]+))(\.[0-9]+)?([eE][-+]?[0-9]+)?\b)/,/^(?:"(?:\\[\\"bfnrt/]|\\u[a-fA-F0-9]{4}|[^\\\0-\x09\x0a-\x1f"])*")/,/^(?:\{)/,/^(?:\})/,/^(?:\[)/,/^(?:\])/,/^(?:,)/,/^(?::)/,/^(?:true\b)/,/^(?:false\b)/,/^(?:null\b)/,/^(?:$)/,/^(?:.)/];
lexer.conditions = {"INITIAL":{"rules":[0,1,2,3,4,5,6,7,8,9,10,11,12,13],"inclusive":true}};
;
return lexer;})()
parser.lexer = lexer;
return parser;
})();
if (typeof require !== 'undefined' && typeof exports !== 'undefined') {
exports.parser = jsonlint;
exports.parse = function () { return jsonlint.parse.apply(jsonlint, arguments); }
exports.main = function commonjsMain(args) {
if (!args[1]) {
throw new Error('Usage: '+args[0]+' FILE');
}
}
}

View File

@ -0,0 +1,40 @@
.jsonWrapper {
position: relative;
margin-top: 14px;
margin-bottom: -14px;
line-height: 18px;
>div {
border-radius: 3px;
>div:last-of-type{
min-height: 320px;
max-height: 635px;
font-weight: 500;
font-size: 1.3rem !important;
}
}
}
.select {
z-index: 100;
position: absolute;
bottom: 0; right: 0;
height: 24px;
padding: 0 10px;
background: rgba(255, 255, 255, 0.04);
border-radius: 0 !important;
border-top-left-radius: 3px !important;
-webkit-appearance: none;
text-transform: capitalize;
color: #787E8F;
&:focus, &:active {
outline: 0;
}
}
.colored {
background-color: yellow;
color: black !important;
}

View File

@ -0,0 +1,227 @@
/**
*
* InputJSONWithErrors
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import { isEmpty, isFunction } from 'lodash';
import cn from 'classnames';
// Design
import Label from 'components/Label';
import InputDescription from 'components/InputDescription';
import InputErrors from 'components/InputErrors';
import InputSpacer from 'components/InputSpacer';
import InputJSON from 'components/InputJSON';
// Utils
import validateInput from 'utils/inputsValidations';
import styles from './styles.scss';
class InputJSONWithErrors extends React.Component { // eslint-disable-line react/prefer-stateless-function
state = { errors: [], hasInitialValue: false };
componentDidMount() {
const { value, errors } = this.props;
// Prevent the input from displaying an error when the user enters and leaves without filling it
if (!isEmpty(value)) {
this.setState({ hasInitialValue: true });
}
// Display input error if it already has some
if (!isEmpty(errors)) {
this.setState({ errors });
}
}
componentDidUpdate(prevProps) {
// Show required error if the input's value is received after the compo is mounted
if (!isEmpty(this.props.value) && !this.state.hasInitialValue) {
this.setInit();
}
// Check if errors have been updated during validations
if (prevProps.didCheckErrors !== this.props.didCheckErrors) {
// Remove from the state the errors that have already been set
const errors = isEmpty(this.props.errors) ? [] : this.props.errors;
this.setErrors(errors);
}
}
setErrors = errors => this.setState({ errors });
setInit = () => this.setState({ hasInitialValue: true });
/**
* Set the errors depending on the validations given to the input
* @param {Object} target
*/
handleBlur = ({ target }) => {
// Prevent from displaying error if the input is initially isEmpty
if (!isEmpty(target.value) || this.state.hasInitialValue) {
const errors = validateInput(target.value, this.props.validations);
this.setErrors(errors);
this.setInit();
}
}
handleChange = (e) => {
this.setState({ errors: [] });
this.props.onChange(e);
}
render() {
const {
autoFocus,
className,
customBootstrapClass,
deactivateErrorHighlight,
disabled,
errorsClassName,
errorsStyle,
inputClassName,
inputDescription,
inputDescriptionClassName,
inputStyle,
label,
labelClassName,
labelStyle,
name,
noErrorsDescription,
onBlur,
placeholder,
resetProps,
style,
tabIndex,
value,
} = this.props;
const handleBlur = isFunction(onBlur) ? onBlur : this.handleBlur;
let spacer = !isEmpty(inputDescription) ? <InputSpacer /> : <div />;
if (!noErrorsDescription && !isEmpty(this.state.errors)) {
spacer = <div />;
}
return (
<div
className={cn(
styles.containerJSON,
customBootstrapClass,
!isEmpty(className) && className,
)}
style={style}
>
<Label
className={labelClassName}
htmlFor={name}
message={label}
style={labelStyle}
/>
<InputJSON
autoFocus={autoFocus}
className={inputClassName}
disabled={disabled}
deactivateErrorHighlight={deactivateErrorHighlight}
name={name}
onBlur={handleBlur}
onChange={this.handleChange}
placeholder={placeholder}
resetProps={resetProps}
style={inputStyle}
tabIndex={tabIndex}
value={value}
/>
<InputDescription
className={inputDescriptionClassName}
message={inputDescription}
style={{ marginTop: '3.2rem'}}
/>
<InputErrors
className={errorsClassName}
errors={!noErrorsDescription && this.state.errors || []}
style={errorsStyle}
/>
{spacer}
</div>
);
}
}
InputJSONWithErrors.defaultProps = {
autoFocus: false,
className: '',
customBootstrapClass: 'col-md-12',
deactivateErrorHighlight: false,
didCheckErrors: false,
disabled: false,
errors: [],
errorsClassName: '',
errorsStyle: {},
inputClassName: '',
inputDescription: '',
inputDescriptionClassName: '',
inputStyle: {},
label: '',
labelClassName: '',
labelStyle: {},
noErrorsDescription: false,
onBlur: false,
placeholder: '',
resetProps: false,
style: {},
tabIndex: '0',
validations: {},
value: null,
};
InputJSONWithErrors.propTypes = {
autoFocus: PropTypes.bool,
className: PropTypes.string,
customBootstrapClass: PropTypes.string,
deactivateErrorHighlight: PropTypes.bool,
didCheckErrors: PropTypes.bool,
disabled: PropTypes.bool,
errors: PropTypes.array,
errorsClassName: PropTypes.string,
errorsStyle: PropTypes.object,
inputClassName: PropTypes.string,
inputDescription: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func,
PropTypes.shape({
id: PropTypes.string,
params: PropTypes.object,
}),
]),
inputDescriptionClassName: PropTypes.string,
inputStyle: PropTypes.object,
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func,
PropTypes.shape({
id: PropTypes.string,
params: PropTypes.object,
}),
]),
labelClassName: PropTypes.string,
labelStyle: PropTypes.object,
name: PropTypes.string.isRequired,
noErrorsDescription: PropTypes.bool,
onBlur: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.func,
]),
onChange: PropTypes.func.isRequired,
placeholder: PropTypes.string,
resetProps: PropTypes.bool,
style: PropTypes.object,
tabIndex: PropTypes.string,
validations: PropTypes.object,
value: PropTypes.object,
};
export default InputJSONWithErrors;

View File

@ -0,0 +1,5 @@
.containerJSON {
margin-bottom: 1.6rem;
font-size: 1.3rem;
font-family: 'Lato';
}

View File

@ -0,0 +1,209 @@
.editor {
max-height: 555px;
min-height: 303px;
overflow: auto;
padding: 20px 20px 0 20px;
font-size: 16px;
background-color: #fff;
line-height: 24px !important;
font-family: "OpenSans";
cursor: text;
// TODO define rules for header's margin
h1, h2, h3, h4, h5, h6 {
margin: 0;
line-height: 24px !important;
font-family: "OpenSans";
}
h1 {
margin-top: 11px !important;
font-size: 36px;
font-weight: 600;
}
h2 {
margin-top: 26px;
font-size: 30px;
font-weight: 500;
}
h3 {
margin-top: 26px;
font-size: 24px;
font-weight: 500;
}
h4 {
margin-top: 26px;
font-size: 20px;
font-weight: 500;
}
> div {
> div {
> div {
margin-bottom: 32px;
}
}
}
li {
margin-top: 0;
}
ul, ol {
padding: 0;
margin-top: 27px;
}
ol {
padding-left: 20px;
}
// NOTE: we might need this later
span {
white-space: pre-line;
}
}
h1+.editorParagraph{
margin-top: 31px;
}
.editorParagraph+* {
margin-bottom: -2px !important;
}
.editorParagraph+.editorBlockquote {
margin-bottom: 32px !important;
}
.editorBlockquote+ul {
margin-top: 38px !important;
}
.editorParagraph {
color: #333740;
margin-top: 27px;
margin-bottom: -3px;
font-size: 16px;
font-weight: 400;
}
.editorBlockquote {
margin-top: 41px;
margin-bottom: 34px;
font-size: 16px;
font-weight: 400;
border-left: 5px solid #eee;
font-style: italic;
padding: 10px 20px;
}
.unorderedList {
padding: 0;
margin-left: 18px;
}
.editorCodeBlock {
padding: 16px;
margin-top: 26px;
padding-bottom: 0;
background-color: rgba(0, 0, 0, 0.05);
border-radius: 3px;
span {
font-family: Consolas, monospace !important;
font-size: 12px;
line-height: 16px;
white-space: pre;
}
}
.editorFullScreen {
max-height: calc(100% - 70px) !important;
margin-bottom: 0;
overflow: auto;
}
.editorInput {
height: 0;
width: 0;
}
.editorSelect {
min-width: 161px;
margin-left: 15px;
margin-right: 5px;
> select {
box-shadow: 0 0 0 rgba(0,0,0,0)!important;
}
}
.fullscreenPreviewEditor {
margin-top: 9px;
padding: 10px 20px;
}
.previewControlsWrapper {
display: flex;
height: 49px;
width: 100%;
padding: 0 17px;
justify-content: space-between;
background-color: #FAFAFB;
line-height: 30px;
font-size: 12px;
font-family: Lato;
background-color: #fff;
border-bottom: 1px solid #F3F4F4;
line-height: 49px;
font-size: 13px;
> div:first-child {
> span:last-child {
font-size: 12px;
}
}
cursor: pointer;
}
.selectFullscreen {
min-width: 115px;
margin-left: 15px;
> select {
min-width: 110px !important;
box-shadow: 0 0 0 rgba(0,0,0,0)!important;
}
}
.toggleModeButton {
height: 32px;
min-width: 32px;
padding-left: 10px;
padding-right: 10px;
border: 1px solid rgba(16,22,34,0.10);
border-radius: 3px;
background: #F3F4F4;
font-size: 13px;
font-weight: 500;
text-align: center;
cursor: pointer;
&:focus, &:active {
outline: 0;
}
}
.toggleModeWrapper {
margin-left: auto;
margin-right: 15px;
padding-top: 8px;
}
.wysiwygCollapse {
&:after {
content: '\f066';
font-family: FontAwesome;
margin-left: 8px;
}
}

View File

@ -0,0 +1,98 @@
export const SELECT_OPTIONS = [
{ id: 'components.Wysiwyg.selectOptions.title', value: '' },
{ id: 'components.Wysiwyg.selectOptions.H1', value: '#' },
{ id: 'components.Wysiwyg.selectOptions.H2', value: '##' },
{ id: 'components.Wysiwyg.selectOptions.H3', value: '###' },
{ id: 'components.Wysiwyg.selectOptions.H4', value: '####' },
{ id: 'components.Wysiwyg.selectOptions.H5', value: '#####' },
{ id: 'components.Wysiwyg.selectOptions.H6', value: '######' },
];
export const CONTROLS = [
[
{
label: 'B',
style: 'BOLD',
className: 'styleButtonBold',
hideLabel: true,
handler: 'addContent',
text: '**textToReplace**',
},
{
label: 'I',
style: 'ITALIC',
className: 'styleButtonItalic',
hideLabel: true,
handler: 'addContent',
text: '*textToReplace*',
},
{
label: 'U',
style: 'UNDERLINE',
className: 'styleButtonUnderline',
hideLabel: true,
handler: 'addContent',
text: '__textToReplace__',
},
{
label: 'S',
style: 'STRIKED',
className: 'styleButtonStrikedOut',
hideLabel: true,
handler: 'addContent',
text: '~~textToReplace~~',
},
{
label: 'UL',
style: 'unordered-list-item',
className: 'styleButtonUL',
hideLabel: true,
handler: 'addUlBlock',
text: '- textToReplace',
},
{
label: 'OL',
style: 'ordered-list-item',
className: 'styleButtonOL',
hideLabel: true,
handler: 'addOlBlock',
text: '1. textToReplace',
},
],
[
{
label: '<>',
style: 'CODE',
className: 'styleButtonCodeBlock',
hideLabel: true,
handler: 'addSimpleBlockWithSelection',
text: '```textToReplace```',
},
{
label: 'img',
style: 'IMG',
className: 'styleButtonImg',
hideLabel: true,
handler: 'addSimpleBlockWithSelection',
text: '![text](textToReplace)',
},
{
label: 'Link',
style: 'LINK',
className: 'styleButtonLink',
hideLabel: true,
handler: 'addContent',
text: '[text](textToReplace)',
},
{
label: 'quotes',
style: 'BLOCKQUOTE',
className: 'styleButtonBlockQuote',
hideLabel: true,
handler: 'addSimpleBlockWithSelection',
text: '> textToReplace',
},
],
];
export const DEFAULT_INDENTATION = ' ';

View File

@ -0,0 +1,19 @@
import showdown from 'showdown';
const converterOptions = {
backslashEscapesHTMLTags: true,
emoji: true,
parseImgDimensions: true,
simpleLineBreaks: true,
simplifiedAutoLink: true,
smoothLivePreview: true,
splitAdjacentBlockquotes: false,
strikethrough: true,
tables: true,
tasklists: true,
underline: true,
};
const converter = new showdown.Converter(converterOptions);
export default converter;

View File

@ -0,0 +1,42 @@
/**
*
*
* CustomSelect
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import Select from 'components/InputSelect';
import { SELECT_OPTIONS } from './constants';
import styles from './componentsStyles.scss';
class CustomSelect extends React.Component {
render() {
const { isPreviewMode, headerValue, isFullscreen, handleChangeSelect } = this.context;
const selectClassName = isFullscreen ? styles.selectFullscreen : styles.editorSelect;
return (
<div className={selectClassName}>
<Select
disabled={isPreviewMode}
name="headerSelect"
onChange={handleChangeSelect}
value={headerValue}
selectOptions={SELECT_OPTIONS}
/>
</div>
);
}
}
CustomSelect.contextTypes = {
handleChangeSelect: PropTypes.func,
headerValue: PropTypes.string,
isPreviewMode: PropTypes.bool,
isFullscreen: PropTypes.bool,
};
export default CustomSelect;

View File

@ -0,0 +1,116 @@
import { trimEnd, trimStart } from 'lodash';
/**
* Override the editor css
* @param {[type]} block [description]
* @return {[type]} [description]
*/
export function getBlockStyle() {
return null;
}
export function getBlockContent(style) {
switch (style) {
case 'IMG':
return {
innerContent: 'link',
endReplacer: ')',
startReplacer: '![text](',
};
case 'CODE':
return {
innerContent: 'code block',
endReplacer: '`',
startReplacer: '`',
};
case 'BLOCKQUOTE':
return {
innerContent: 'quote',
endReplacer: '',
startReplacer: '> ',
};
case 'BOLD':
return {
innerContent: 'bold text',
endReplacer: '*',
startReplacer: '*',
};
case 'ITALIC':
return {
innerContent: 'italic text',
endReplacer: '*',
startReplacer: '*',
};
case 'STRIKED':
return {
innerContent: 'striked out',
endReplacer: '~',
startReplacer: '~',
};
case 'UNDERLINE':
return {
innerContent: 'underlined text',
endReplacer: '_',
startReplacer: '_',
};
case 'LINK':
return {
innerContent: 'link',
endReplacer: ')',
startReplacer: '[text](',
};
default:
return {
innerContent: '',
endReplacer: '',
startReplacer: '',
};
}
}
export const getDefaultSelectionOffsets = (
content,
startReplacer,
endReplacer,
initPosition = 0,
) => ({
anchorOffset: initPosition + content.length - trimStart(content, startReplacer).length,
focusOffset: initPosition + trimEnd(content, endReplacer).length,
});
/**
* Get the start and end offset
* @param {Object} selection
* @return {Object}
*/
export function getOffSets(selection) {
return {
end: selection.getEndOffset(),
start: selection.getStartOffset(),
};
}
export function getKeyCommandData(command) {
let content;
let style;
switch (command) {
case 'bold':
content = '**textToReplace**';
style = 'BOLD';
break;
case 'italic':
content = '*textToReplace*';
style = 'ITALIC';
break;
case 'underline':
content = '__textToReplace__';
style = 'UNDERLINE';
break;
default:
content = '';
style = '';
}
return { content, style };
}

View File

@ -0,0 +1,22 @@
/**
*
* Image
*
*/
import React from 'react';
import PropTypes from 'prop-types';
const Image = props => {
const { alt, height, src, width } = props.contentState.getEntity(props.entityKey).getData();
return <img alt={alt} src={src} height={height} width={width} style={{ maxWidth: '100%' }} />;
};
Image.propTypes = {
contentState: PropTypes.object.isRequired,
entityKey: PropTypes.string.isRequired,
};
export default Image;

View File

@ -0,0 +1,792 @@
/**
*
* Wysiwyg
*
*/
import React from 'react';
import {
ContentState,
EditorState,
getDefaultKeyBinding,
genKey,
Modifier,
RichUtils,
SelectionState,
} from 'draft-js';
import PropTypes from 'prop-types';
import { isEmpty, isNaN, replace, words } from 'lodash';
import cn from 'classnames';
import Controls from 'components/WysiwygInlineControls';
import Drop from 'components/WysiwygDropUpload';
import WysiwygBottomControls from 'components/WysiwygBottomControls';
import WysiwygEditor from 'components/WysiwygEditor';
import request from 'utils/request';
import CustomSelect from './customSelect';
import PreviewControl from './previewControl';
import PreviewWysiwyg from './previewWysiwyg';
import ToggleMode from './toggleMode';
import { CONTROLS } from './constants';
import {
getBlockContent,
getBlockStyle,
getDefaultSelectionOffsets,
getKeyCommandData,
getOffSets,
} from './helpers';
import {
createNewBlock,
getNextBlocksList,
getSelectedBlocksList,
onTab,
updateSelection,
} from './utils';
import styles from './styles.scss';
/* eslint-disable react/jsx-handler-names */
/* eslint-disable react/sort-comp */
class Wysiwyg extends React.Component {
constructor(props) {
super(props);
this.state = {
editorState: EditorState.createEmpty(),
isDraging: false,
isFocused: false,
isFullscreen: false,
isPreviewMode: false,
headerValue: '',
};
this.focus = () => {
this.setState({ isFocused: true });
return this.domEditor.focus();
};
this.blur = () => {
this.setState({ isFocused: false });
return this.domEditor.blur();
};
}
getChildContext = () => ({
handleChangeSelect: this.handleChangeSelect,
headerValue: this.state.headerValue,
html: this.props.value,
isPreviewMode: this.state.isPreviewMode,
isFullscreen: this.state.isFullscreen,
placeholder: this.props.placeholder,
});
componentDidMount() {
if (this.props.autoFocus) {
this.focus();
}
if (!isEmpty(this.props.value)) {
this.setInitialValue(this.props);
}
}
shouldComponentUpdate(nextProps, nextState) {
if (nextState.editorState !== this.state.editorState) {
return true;
}
if (nextProps.resetProps !== this.props.resetProps) {
return true;
}
if (nextState.isDraging !== this.state.isDraging) {
return true;
}
if (nextState.isFocused !== this.state.isFocused) {
return true;
}
if (nextState.isFullscreen !== this.state.isFullscreen) {
return true;
}
if (nextState.isPreviewMode !== this.state.isPreviewMode) {
return true;
}
if (nextState.headerValue !== this.state.headerValue) {
return true;
}
return false;
}
componentDidUpdate(prevProps) {
// Handle resetProps
if (prevProps.resetProps !== this.props.resetProps) {
this.setInitialValue(this.props);
}
}
/**
* Init the editor with data from
* @param {[type]} props [description]
*/
setInitialValue = props => {
const contentState = ContentState.createFromText(props.value);
const newEditorState = EditorState.createWithContent(contentState);
const editorState = this.state.isFocused
? EditorState.moveFocusToEnd(newEditorState)
: newEditorState;
return this.setState({ editorState });
};
/**
* Handler to add B, I, Strike, U, link
* @param {String} content usually something like **textToReplace**
* @param {String} style
*/
addContent = (content, style) => {
const selectedText = this.getSelectedText();
// Retrieve the associated data for the type to add
const { innerContent, endReplacer, startReplacer } = getBlockContent(style);
// Replace the selected text by the markdown command or insert default text
const defaultContent =
selectedText === ''
? replace(content, 'textToReplace', innerContent)
: replace(content, 'textToReplace', selectedText);
// Get the current cursor position
const cursorPosition = getOffSets(this.getSelection()).start;
const textWithEntity = this.modifyBlockContent(defaultContent);
// Highlight the text
const { anchorOffset, focusOffset } = getDefaultSelectionOffsets(
defaultContent,
startReplacer,
endReplacer,
cursorPosition,
);
// Merge the current selection with the new one
const updatedSelection = this.getSelection().merge({ anchorOffset, focusOffset });
const newEditorState = EditorState.push(
this.getEditorState(),
textWithEntity,
'insert-character',
);
// Update the parent reducer
this.sendData(newEditorState);
// Don't handle selection : the user has selected some text to be changed with the appropriate markdown
if (selectedText !== '') {
return this.setState(
{
// Move the cursor to the end (this line forces the cursor to be at the end of the content)
// It may go at the end of the last block
editorState: EditorState.moveFocusToEnd(newEditorState),
},
() => {
this.focus();
},
);
}
return this.setState({
// Highlight the text if the selection wad empty
editorState: EditorState.forceSelection(newEditorState, updatedSelection),
});
};
/**
* Create an ordered list block
* @return ContentBlock
*/
addOlBlock = () => {
// Get all the selected blocks
const selectedBlocksList = getSelectedBlocksList(this.getEditorState());
let newEditorState = this.getEditorState();
// Check if the cursor is NOT at the beginning of a new line
// So we need to move all the next blocks
if (getOffSets(this.getSelection()).start !== 0) {
// Retrieve all the blocks after the current position
const nextBlocks = getNextBlocksList(newEditorState, this.getSelection().getStartKey());
let liNumber = 1;
// Loop to update each block after the inserted li
nextBlocks.map((block, index) => {
const previousContent =
index === 0
? this.getEditorState()
.getCurrentContent()
.getBlockForKey(this.getCurrentAnchorKey())
: newEditorState.getCurrentContent().getBlockBefore(block.getKey());
// Check if there was an li before the position so we update the entire list bullets
const number = previousContent ? parseInt(previousContent.getText().split('.')[0], 10) : 0;
liNumber = isNaN(number) ? 1 : number + 1;
const nextBlockText = index === 0 ? `${liNumber}. ` : nextBlocks.get(index - 1).getText();
// Update the current block
const newBlock = createNewBlock(nextBlockText, 'block-list', block.getKey());
// Update the contentState
const newContentState = this.createNewContentStateFromBlock(
newBlock,
newEditorState.getCurrentContent(),
);
newEditorState = EditorState.push(newEditorState, newContentState);
});
// Move the cursor to the correct position and add a space after '.'
// 2 for the dot and the space after, we add the number length (10 = offset of 2)
const offset = 2 + liNumber.toString().length;
const updatedSelection = updateSelection(this.getSelection(), nextBlocks, offset);
return this.setState({
editorState: EditorState.acceptSelection(newEditorState, updatedSelection),
});
}
// If the cursor is at the beginning we need to move all the content after the cursor so we don't loose the data
selectedBlocksList.map((block, i) => {
const selectedText = block.getText();
const li = selectedText === '' ? `${i + 1}. ` : `${i + 1}. ${selectedText}`;
const newBlock = createNewBlock(li, 'block-list', block.getKey());
const newContentState = this.createNewContentStateFromBlock(
newBlock,
newEditorState.getCurrentContent(),
);
newEditorState = EditorState.push(newEditorState, newContentState);
});
// Update the parent reducer
this.sendData(newEditorState);
return this.setState({ editorState: EditorState.moveFocusToEnd(newEditorState) });
};
/**
* Create an unordered list
* @return ContentBlock
*/
// NOTE: it's pretty much the same dynamic as above
// We don't use the same handler because it needs less logic than a ordered list
// so it's easier to maintain the code
addUlBlock = () => {
const selectedBlocksList = getSelectedBlocksList(this.getEditorState());
let newEditorState = this.getEditorState();
if (getOffSets(this.getSelection()).start !== 0) {
const nextBlocks = getNextBlocksList(newEditorState, this.getSelection().getStartKey());
nextBlocks.map((block, index) => {
const nextBlockText = index === 0 ? '- ' : nextBlocks.get(index - 1).getText();
const newBlock = createNewBlock(nextBlockText, 'block-list', block.getKey());
const newContentState = this.createNewContentStateFromBlock(
newBlock,
newEditorState.getCurrentContent(),
);
newEditorState = EditorState.push(newEditorState, newContentState);
});
const updatedSelection = updateSelection(this.getSelection(), nextBlocks, 2);
return this.setState({
editorState: EditorState.acceptSelection(newEditorState, updatedSelection),
});
}
selectedBlocksList.map(block => {
const selectedText = block.getText();
const li = selectedText === '' ? '- ' : `- ${selectedText}`;
const newBlock = createNewBlock(li, 'block-list', block.getKey());
const newContentState = this.createNewContentStateFromBlock(
newBlock,
newEditorState.getCurrentContent(),
);
newEditorState = EditorState.push(newEditorState, newContentState);
});
this.sendData(newEditorState);
return this.setState({ editorState: EditorState.moveFocusToEnd(newEditorState) });
};
/**
* Handler to create header
* @param {String} text header content
*/
addBlock = text => {
const nextBlockKey = this.getNextBlockKey(this.getCurrentAnchorKey()) || genKey();
const newBlock = createNewBlock(text, 'header', nextBlockKey);
const newContentState = this.createNewContentStateFromBlock(newBlock);
const newEditorState = this.createNewEditorState(newContentState, text);
return this.setState({ editorState: EditorState.moveFocusToEnd(newEditorState) });
};
/**
* Handler used for code block and Img controls
* @param {String} content the text that will be added
* @param {String} style the type
*/
addSimpleBlockWithSelection = (content, style) => {
const selectedText = this.getSelectedText();
const { innerContent, endReplacer, startReplacer } = getBlockContent(style);
const defaultContent =
selectedText === ''
? replace(content, 'textToReplace', innerContent)
: replace(content, 'textToReplace', selectedText);
const newBlock = createNewBlock(defaultContent);
const newContentState = this.createNewContentStateFromBlock(newBlock);
const { anchorOffset, focusOffset } = getDefaultSelectionOffsets(
defaultContent,
startReplacer,
endReplacer,
);
let newEditorState = this.createNewEditorState(newContentState, defaultContent);
const updatedSelection =
getOffSets(this.getSelection()).start === 0
? this.getSelection().merge({ anchorOffset, focusOffset })
: new SelectionState({
anchorKey: newBlock.getKey(),
anchorOffset,
focusOffset,
focusKey: newBlock.getKey(),
isBackward: false,
});
newEditorState = EditorState.acceptSelection(newEditorState, updatedSelection);
return this.setState({
editorState: EditorState.forceSelection(newEditorState, newEditorState.getSelection()),
});
};
/**
* Update the current editorState
* @param {Map} newContentState
* @param {String} text The text to add
* @return {Map} EditorState
*/
createNewEditorState = (newContentState, text) => {
let newEditorState;
if (getOffSets(this.getSelection()).start !== 0) {
newEditorState = EditorState.push(this.getEditorState(), newContentState);
} else {
const textWithEntity = this.modifyBlockContent(text);
newEditorState = EditorState.push(this.getEditorState(), textWithEntity, 'insert-characters');
}
return newEditorState;
};
/**
* Update the content of a block
* @param {Map} newBlock The new block
* @param {Map} contentState The ContentState
* @return {Map} The updated block
*/
createNewBlockMap = (newBlock, contentState) =>
contentState.getBlockMap().set(newBlock.key, newBlock);
createNewContentStateFromBlock = (
newBlock,
contentState = this.getEditorState().getCurrentContent(),
) =>
ContentState.createFromBlockArray(this.createNewBlockMap(newBlock, contentState).toArray())
.set('selectionBefore', contentState.getSelectionBefore())
.set('selectionAfter', contentState.getSelectionAfter());
getCharactersNumber = (editorState = this.getEditorState()) => {
const plainText = editorState.getCurrentContent().getPlainText();
const spacesNumber = plainText.split(' ').length;
return words(plainText).join('').length + spacesNumber - 1;
};
getEditorState = () => this.state.editorState;
/**
* Retrieve the selected text
* @return {Map}
*/
getSelection = () => this.getEditorState().getSelection();
/**
* Retrieve the cursor anchor key
* @return {String}
*/
getCurrentAnchorKey = () => this.getSelection().getAnchorKey();
/**
* Retrieve the current content block
* @return {Map} ContentBlock
*/
getCurrentContentBlock = () =>
this.getEditorState()
.getCurrentContent()
.getBlockForKey(this.getSelection().getAnchorKey());
/**
* Retrieve the block key after a specific one
* @param {String} currentBlockKey
* @param {Map} [editorState=this.getEditorState()] The current EditorState or the updated one
* @return {String} The next block key
*/
getNextBlockKey = (currentBlockKey, editorState = this.getEditorState()) =>
editorState.getCurrentContent().getKeyAfter(currentBlockKey);
getSelectedText = ({ start, end } = getOffSets(this.getSelection())) =>
this.getCurrentContentBlock()
.getText()
.slice(start, end);
handleBlur = () => {
const target = {
name: this.props.name,
type: 'textarea',
value: this.getEditorState()
.getCurrentContent()
.getPlainText(),
};
this.props.onBlur({ target });
this.blur();
};
handleChangeSelect = ({ target }) => {
this.setState({ headerValue: target.value });
const selectedText = this.getSelectedText();
const title = selectedText === '' ? `${target.value} ` : `${target.value} ${selectedText}`;
this.addBlock(title);
return this.setState({ headerValue: '' });
};
handleClickPreview = () => this.setState({ isPreviewMode: !this.state.isPreviewMode });
handleDragEnter = e => {
e.preventDefault();
e.stopPropagation();
if (!this.state.isDraging) {
this.setState({ isDraging: true });
}
};
handleDragLeave = () => this.setState({ isDraging: false });
handleDragOver = e => {
e.preventDefault();
e.stopPropagation();
};
handleDrop = e => {
e.preventDefault();
if (this.state.isPreviewMode) {
return this.setState({ isDraging: false });
}
const files = e.dataTransfer ? e.dataTransfer.files : e.target.files;
return this.uploadFile(files);
};
/**
* Handler that listens for specific key commands
* @param {String} command
* @param {Map} editorState
* @return {Bool}
*/
handleKeyCommand = (command, editorState) => {
const newState = RichUtils.handleKeyCommand(editorState, command);
if (command === 'bold' || command === 'italic' || command === 'underline') {
const { content, style } = getKeyCommandData(command);
this.addContent(content, style);
return false;
}
if (newState && command !== 'backspace') {
this.onChange(newState);
return true;
}
return false;
};
/**
* Handler to upload files on paste
* @param {Array<Blob>} files [description]
* @return {} DraftHandleValue
*/
handlePastedFiles = files => this.uploadFile(files);
handleReturn = (e, editorState) => {
const selection = editorState.getSelection();
const currentBlock = editorState.getCurrentContent().getBlockForKey(selection.getStartKey());
if (currentBlock.getText().split('')[0] === '-') {
this.addUlBlock();
return true;
}
if (
currentBlock.getText().split('.').length > 1 &&
!isNaN(parseInt(currentBlock.getText().split('.')[0], 10))
) {
this.addOlBlock();
return true;
}
return false;
};
mapKeyToEditorCommand = e => {
if (e.keyCode === 9 /* TAB */) {
const newEditorState = RichUtils.onTab(e, this.state.editorState, 4 /* maxDepth */);
if (newEditorState !== this.state.editorState) {
this.onChange(newEditorState);
}
return;
}
return getDefaultKeyBinding(e);
};
/**
* Change the content of a block
* @param {String]} text
* @param {Map} [contentState=this.getEditorState().getCurrentContent()]
* @return {Map}
*/
modifyBlockContent = (text, contentState = this.getEditorState().getCurrentContent()) =>
Modifier.replaceText(contentState, this.getSelection(), text);
onChange = editorState => {
this.setState({ editorState });
this.sendData(editorState);
};
handleTab = e => {
e.preventDefault();
const newEditorState = onTab(this.getEditorState());
return this.onChange(newEditorState);
};
/**
* Update the parent reducer
* @param {Map} editorState [description]
*/
sendData = editorState => {
if (this.getEditorState().getCurrentContent() === editorState.getCurrentContent())
return;
this.props.onChange({
target: {
value: editorState.getCurrentContent().getPlainText(),
name: this.props.name,
type: 'textarea',
},
});
}
toggleFullScreen = e => {
e.preventDefault();
this.setState({
isFullscreen: !this.state.isFullscreen,
isPreviewMode: false,
});
};
uploadFile = files => {
const formData = new FormData();
formData.append('files', files[0]);
const headers = {
'X-Forwarded-Host': 'strapi',
};
let newEditorState = this.getEditorState();
const nextBlocks = getNextBlocksList(newEditorState, this.getSelection().getStartKey());
// Loop to update each block after the inserted li
nextBlocks.map((block, index) => {
// Update the current block
const nextBlockText = index === 0 ? `![Uploading ${files[0].name}]()` : nextBlocks.get(index - 1).getText();
const newBlock = createNewBlock(nextBlockText, 'unstyled', block.getKey());
// Update the contentState
const newContentState = this.createNewContentStateFromBlock(
newBlock,
newEditorState.getCurrentContent(),
);
newEditorState = EditorState.push(newEditorState, newContentState);
});
const offset = `![Uploading ${files[0].name}]()`.length;
const updatedSelection = updateSelection(this.getSelection(), nextBlocks, offset);
this.setState({ editorState: EditorState.acceptSelection(newEditorState, updatedSelection) });
return request('/upload', { method: 'POST', headers, body: formData }, false, false)
.then(response => {
const nextBlockKey = newEditorState
.getCurrentContent()
.getKeyAfter(newEditorState.getSelection().getStartKey());
const content = `![text](${response[0].url})`;
const newContentState = this.createNewContentStateFromBlock(
createNewBlock(content, 'unstyled', nextBlockKey),
);
newEditorState = EditorState.push(newEditorState, newContentState);
const updatedSelection = updateSelection(this.getSelection(), nextBlocks, 2);
this.setState({ editorState: EditorState.acceptSelection(newEditorState, updatedSelection) });
this.sendData(newEditorState);
})
.catch(() => {
this.setState({ editorState: EditorState.undo(this.getEditorState()) });
})
.finally(() => {
this.setState({ isDraging: false });
});
};
renderDrop = () => (
<Drop
onDrop={this.handleDrop}
onDragOver={this.handleDragOver}
onDragLeave={this.handleDragLeave}
/>
);
render() {
const { editorState, isPreviewMode, isFullscreen } = this.state;
const editorStyle = isFullscreen ? { marginTop: '0' } : this.props.style;
return (
<div className={cn(isFullscreen && styles.fullscreenOverlay)}>
{/* FIRST EDITOR WITH CONTROLS} */}
<div
className={cn(
styles.editorWrapper,
!this.props.deactivateErrorHighlight && this.props.error && styles.editorError,
!isEmpty(this.props.className) && this.props.className,
)}
onClick={e => {
if (isFullscreen) {
e.preventDefault();
e.stopPropagation();
}
}}
onDragEnter={this.handleDragEnter}
onDragOver={this.handleDragOver}
style={editorStyle}
>
{this.state.isDraging && this.renderDrop()}
<div className={styles.controlsContainer}>
<CustomSelect />
{CONTROLS.map((value, key) => (
<Controls
key={key}
buttons={value}
disabled={isPreviewMode}
editorState={editorState}
handlers={{
addContent: this.addContent,
addOlBlock: this.addOlBlock,
addSimpleBlockWithSelection: this.addSimpleBlockWithSelection,
addUlBlock: this.addUlBlock,
}}
onToggle={this.toggleInlineStyle}
onToggleBlock={this.toggleBlockType}
/>
))}
{!isFullscreen ? (
<ToggleMode isPreviewMode={isPreviewMode} onClick={this.handleClickPreview} />
) : (
<div style={{ marginRight: '10px' }} />
)}
</div>
{/* WYSIWYG PREVIEW NOT FULLSCREEN */}
{isPreviewMode ? (
<PreviewWysiwyg data={this.props.value} />
) : (
<div
className={cn(styles.editor, isFullscreen && styles.editorFullScreen)}
onClick={this.focus}
>
<WysiwygEditor
blockStyleFn={getBlockStyle}
editorState={editorState}
handleKeyCommand={this.handleKeyCommand}
handlePastedFiles={this.handlePastedFiles}
handleReturn={this.handleReturn}
keyBindingFn={this.mapKeyToEditorCommand}
onBlur={this.handleBlur}
onChange={this.onChange}
onTab={this.handleTab}
placeholder={this.props.placeholder}
setRef={editor => (this.domEditor = editor)}
stripPastedStyles
tabIndex={this.props.tabIndex}
/>
<input className={styles.editorInput} value="" tabIndex="-1" />
</div>
)}
{!isFullscreen && (
<WysiwygBottomControls
isPreviewMode={isPreviewMode}
onClick={this.toggleFullScreen}
onChange={this.handleDrop}
/>
)}
</div>
{/* PREVIEW WYSIWYG FULLSCREEN */}
{isFullscreen && (
<div
className={cn(styles.editorWrapper)}
onClick={e => {
e.preventDefault();
e.stopPropagation();
}}
style={{ marginTop: '0' }}
>
<PreviewControl
onClick={this.toggleFullScreen}
characters={this.getCharactersNumber()}
/>
<PreviewWysiwyg data={this.props.value} />
</div>
)}
</div>
);
}
}
Wysiwyg.childContextTypes = {
handleChangeSelect: PropTypes.func,
headerValue: PropTypes.string,
html: PropTypes.string,
isFullscreen: PropTypes.bool,
isPreviewMode: PropTypes.bool,
placeholder: PropTypes.string,
previewHTML: PropTypes.func,
};
Wysiwyg.defaultProps = {
autoFocus: false,
className: '',
deactivateErrorHighlight: false,
error: false,
onBlur: () => {},
onChange: () => {},
placeholder: '',
resetProps: false,
style: {},
tabIndex: '0',
value: '',
};
Wysiwyg.propTypes = {
autoFocus: PropTypes.bool,
className: PropTypes.string,
deactivateErrorHighlight: PropTypes.bool,
error: PropTypes.bool,
name: PropTypes.string.isRequired,
onBlur: PropTypes.func,
onChange: PropTypes.func,
placeholder: PropTypes.string,
resetProps: PropTypes.bool,
style: PropTypes.object,
tabIndex: PropTypes.string,
value: PropTypes.string,
};
export default Wysiwyg;

View File

@ -0,0 +1,45 @@
/**
*
* Link
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import { includes } from 'lodash';
const Link = props => {
const { url, aHref, aInnerHTML } = props.contentState.getEntity(props.entityKey).getData();
let content = aInnerHTML;
if (includes(aInnerHTML, '<img', 'src=')) {
const src = aInnerHTML.split('src="')[1].split('" ')[0];
const width = includes(aInnerHTML, 'width=') ? aInnerHTML.split('width="')[1].split('" ')[0] : '';
const height = includes(aInnerHTML, 'height=') ? aInnerHTML.split('height="')[1].split('" ')[0] : '';
content = <img src={src} alt="img" width={width} height={height} style={{ marginTop: '27px', maxWidth: '100%' }} />;
}
return (
<a
href={url || aHref}
onClick={() => {
window.open(url || aHref, '_blank');
}}
style={{ cursor: 'pointer' }}
>
{content || props.children}
</a>
);
};
Link.defaultProps = {
children: '',
};
Link.propTypes = {
children: PropTypes.node,
contentState: PropTypes.object.isRequired,
entityKey: PropTypes.string.isRequired,
};
export default Link;

View File

@ -0,0 +1,29 @@
/**
*
*
* PreviewControl
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import styles from './componentsStyles.scss';
const PreviewControl = ({ onClick }) => (
<div className={styles.previewControlsWrapper} onClick={onClick}>
<div />
<div className={styles.wysiwygCollapse}>
<FormattedMessage id="components.Wysiwyg.collapse" />
</div>
</div>
);
PreviewControl.defaultProps = {
onClick: () => {},
};
PreviewControl.propTypes = {
onClick: PropTypes.func,
};
export default PreviewControl;

View File

@ -0,0 +1,261 @@
/**
*
* PreviewWysiwyg
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import {
CompositeDecorator,
ContentState,
convertFromHTML,
EditorState,
ContentBlock,
genKey,
Entity,
CharacterMetadata,
} from 'draft-js';
import { List, OrderedSet, Repeat, fromJS } from 'immutable';
import cn from 'classnames';
import { isEmpty, toArray } from 'lodash';
import WysiwygEditor from 'components/WysiwygEditor';
import converter from './converter';
import {
findAtomicEntities,
findLinkEntities,
findImageEntities,
findVideoEntities,
} from './strategies';
import Image from './image';
import Link from './link';
import Video from './video';
import styles from './componentsStyles.scss';
/* eslint-disable react/no-unused-state */
function getBlockStyle(block) {
switch (block.getType()) {
case 'blockquote':
return styles.editorBlockquote;
case 'code-block':
return styles.editorCodeBlock;
case 'unstyled':
return styles.editorParagraph;
case 'unordered-list-item':
return styles.unorderedList;
case 'ordered-list-item':
case 'header-one':
case 'header-two':
case 'header-three':
case 'header-four':
case 'header-five':
case 'header-six':
default:
return null;
}
}
const decorator = new CompositeDecorator([
{
strategy: findLinkEntities,
component: Link,
},
{
strategy: findImageEntities,
component: Image,
},
{
strategy: findVideoEntities,
component: Video,
},
{
strategy: findAtomicEntities,
component: Link,
},
]);
const getBlockSpecForElement = aElement => ({
contentType: 'link',
aHref: aElement.href,
aInnerHTML: aElement.innerHTML,
});
const elementToBlockSpecElement = element => wrapBlockSpec(getBlockSpecForElement(element));
const wrapBlockSpec = blockSpec => {
if (blockSpec == null) {
return null;
}
const tempEl = document.createElement('blockquote');
// stringify meta data and insert it as text content of temp HTML element. We will later extract
// and parse it.
tempEl.innerText = JSON.stringify(blockSpec);
return tempEl;
};
const replaceElement = (oldEl, newEl) => {
if (!(newEl instanceof HTMLElement)) {
return;
}
const parentNode = oldEl.parentNode;
return parentNode.replaceChild(newEl, oldEl);
};
const aReplacer = aElement => replaceElement(aElement, elementToBlockSpecElement(aElement));
const createContentBlock = (blockData = {}) => {
const { key, type, text, data, inlineStyles, entityData } = blockData;
let blockSpec = {
type: type !== null && type !== undefined ? type : 'unstyled',
text: text !== null && text !== undefined ? text : '',
key: key !== null && key !== undefined ? key : genKey(),
};
if (data) {
blockSpec.data = fromJS(data);
}
if (inlineStyles || entityData) {
let entityKey;
if (entityData) {
const { type, mutability, data } = entityData;
entityKey = Entity.create(type, mutability, data);
} else {
entityKey = null;
}
const style = OrderedSet(inlineStyles || []);
const charData = CharacterMetadata.applyEntity(
CharacterMetadata.create({ style, entityKey }),
entityKey,
);
blockSpec.characterList = List(Repeat(charData, text.length));
}
return new ContentBlock(blockSpec);
};
class PreviewWysiwyg extends React.PureComponent {
state = { editorState: EditorState.createEmpty(), isMounted: false };
componentDidMount() {
const { data } = this.props;
this.setState({ isMounted: true });
if (!isEmpty(data)) {
this.previewHTML(data);
}
}
// NOTE: This is not optimal and this lifecycle should be removed
// I couldn't find a better way to decrease the fullscreen preview's data conversion time
// Trying with componentDidUpdate didn't work
UNSAFE_componentWillUpdate(nextProps, nextState) {
if (nextProps.data !== this.props.data) {
new Promise(resolve => {
setTimeout(() => {
if (nextProps.data === this.props.data && nextState.isMounted) {
// I use an handler here to update the state wich is fine since the condition above prevent
// from entering into an infinite loop
this.previewHTML(nextProps.data);
}
resolve();
}, 300);
});
}
}
componentWillUnmount() {
this.setState({ isMounted: false });
}
getClassName = () => {
if (this.context.isFullscreen) {
return cn(styles.editor, styles.editorFullScreen, styles.fullscreenPreviewEditor);
}
return styles.editor;
};
previewHTML = rawContent => {
const initHtml = isEmpty(rawContent) ? '<p></p>' : rawContent;
const html = new DOMParser().parseFromString(converter.makeHtml(initHtml), 'text/html');
toArray(html.getElementsByTagName('a')) // Retrieve all the links <a> tags
.filter((value) => value.getElementsByTagName('img').length > 0) // Filter by checking if they have any <img> children
.forEach(aReplacer); // Change those links into <blockquote> elements so we can set some metacharacters with the img content
// TODO:
// in the same way, retrieve all <pre> tags
// create custom atomic block
// create custom code block
let blocksFromHTML = convertFromHTML(html.body.innerHTML);
if (blocksFromHTML.contentBlocks) {
blocksFromHTML = blocksFromHTML.contentBlocks.reduce((acc, block) => {
if (block.getType() === 'blockquote') {
try {
const { aHref, aInnerHTML } = JSON.parse(block.getText());
const entityData = {
type: 'LINK',
mutability: 'IMMUTABLE',
data: {
aHref,
aInnerHTML,
},
};
const blockSpec = Object.assign(
{ type: 'atomic', text: ' ', key: block.getKey() },
{ entityData },
);
const atomicBlock = createContentBlock(blockSpec); // Create an atomic block so we can identify it easily
return acc.concat([atomicBlock]);
} catch (err) {
return acc.concat(block);
}
}
return acc.concat(block);
}, []);
const contentState = ContentState.createFromBlockArray(blocksFromHTML);
return this.setState({ editorState: EditorState.createWithContent(contentState, decorator) });
}
return this.setState({ editorState: EditorState.createEmpty() });
};
render() {
const { placeholder } = this.context;
// this.previewHTML2(this.props.data);
return (
<div className={this.getClassName()}>
<WysiwygEditor
blockStyleFn={getBlockStyle}
editorState={this.state.editorState}
onChange={() => {}}
placeholder={placeholder}
/>
<input className={styles.editorInput} value="" tabIndex="-1" />
</div>
);
}
}
PreviewWysiwyg.contextTypes = {
isFullscreen: PropTypes.bool,
placeholder: PropTypes.string,
};
PreviewWysiwyg.defaultProps = {
data: '',
};
PreviewWysiwyg.propTypes = {
data: PropTypes.string,
};
export default PreviewWysiwyg;

View File

@ -0,0 +1,35 @@
/* eslint-disable no-unused-vars */
function findLinkEntities(contentBlock, callback, contentState) {
contentBlock.findEntityRanges(character => {
const entityKey = character.getEntity();
return entityKey !== null && contentState.getEntity(entityKey).getType() === 'LINK';
}, callback);
}
function findAtomicEntities(contentBlock, callback, contentState) {
contentBlock.findEntityRanges(character => {
const entityKey = character.getEntity();
return entityKey !== null && contentBlock.getType() === 'atomic';
}, callback);
}
function findImageEntities(contentBlock, callback, contentState) {
contentBlock.findEntityRanges(character => {
const entityKey = character.getEntity();
return entityKey !== null && contentState.getEntity(entityKey).getType() === 'IMAGE' && !isVideoType(contentState.getEntity(entityKey).getData().src);
}, callback);
}
function findVideoEntities(contentBlock, cb, contentState) {
contentBlock.findEntityRanges(character => {
const entityKey = character.getEntity();
return entityKey !== null && contentState.getEntity(entityKey).getType() === 'IMAGE' && isVideoType(contentState.getEntity(entityKey).getData().src);
}, cb);
}
const isVideoType = (fileName) => /\.(mp4|mpg|mpeg|mov|avi)$/i.test(fileName);
export { findAtomicEntities, findLinkEntities, findImageEntities, findVideoEntities };

View File

@ -0,0 +1,115 @@
.controlsContainer {
display: flex;
background-color: #F3F4F4;
select {
min-height: 31px !important;
min-width: 161px !important;
font-weight: 600;
outline: none;
&:focus, &:active{
border: 1px solid #E3E9F3;
}
}
}
.editorWrapper {
min-height: 320px;
margin-top: .9rem;
border: 1px solid #F3F4F4;
border-radius: 0.25rem;
line-height: 18px!important;
font-size: 13px;
box-shadow: 0px 1px 1px rgba(104, 118, 142, 0.05);
background-color: #fff;
position: relative;
}
.editor {
min-height: 303px;
max-height: 555px;
padding: 20px 20px 0 20px;
font-size: 16px;
background-color: #fff;
line-height: 18px !important;
cursor: text;
overflow: auto;
h1, h2, h3, h4, h5, h6 {
margin: 0;
line-height: 18px !important;
}
h1 {
margin-top: 13px !important;
margin-bottom: 22px;
}
> div {
> div {
> div {
margin-bottom: 32px;
}
}
}
li {
margin-top: 0;
}
ul, ol {
margin-bottom: 18px;
}
}
.editorError {
border-color: #ff203c !important;
}
.editorInput {
height: 0;
width: 0;
}
.editorBlockquote {
border-left: 5px solid #eee;
font-family: Lato;
font-style: italic;
margin: 16px 0;
padding: 10px 20px;
}
.editorCodeBlock {
margin-bottom: 0!important;
padding: 5px;
background-color: rgba(0, 0, 0, 0.05);
font-family: Lato;
}
.fullscreenOverlay {
position: fixed;
z-index: 1040;
top: calc(6rem + 90px);
left: calc(24rem + 28px);
right: 28px;
bottom: 32px;
display: flex;
background-color: transparent;
z-index: 99999;
> div {
min-width: 50%;
}
> div:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
> div:last-child {
border-left: 0;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
.editorFullScreen {
max-height: calc(100% - 70px) !important;
margin-bottom: 0;
overflow: auto;
}

View File

@ -0,0 +1,36 @@
/**
*
*
* ToggleMode
*/
import React from 'react';
import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
import styles from './componentsStyles.scss';
const ToggleMode = props => {
const label = props.isPreviewMode
? 'components.Wysiwyg.ToggleMode.markdown'
: 'components.Wysiwyg.ToggleMode.preview';
return (
<div className={styles.toggleModeWrapper}>
<button type="button" className={styles.toggleModeButton} onClick={props.onClick}>
<FormattedMessage id={label} />
</button>
</div>
);
};
ToggleMode.defaultProps = {
isPreviewMode: false,
onClick: () => {},
};
ToggleMode.propTypes = {
isPreviewMode: PropTypes.bool,
onClick: PropTypes.func,
};
export default ToggleMode;

View File

@ -0,0 +1,74 @@
/**
*
* Utils
*
*/
import { ContentBlock, EditorState, genKey, Modifier } from 'draft-js';
import { List } from 'immutable';
import { DEFAULT_INDENTATION } from './constants';
export function createNewBlock(text = '', type = 'unstyled', key = genKey()) {
return new ContentBlock({ key, type, text, charaterList: List([]) });
}
export function getNextBlocksList(editorState, startKey) {
return editorState
.getCurrentContent()
.getBlockMap()
.toSeq()
.skipUntil((_, k) => k === startKey)
.toList()
.shift()
.concat([createNewBlock()]);
}
export function updateSelection(selection, blocks, offset) {
return selection.merge({
anchorKey: blocks.get(0).getKey(),
focusKey: blocks.get(0).getKey(),
anchorOffset: offset,
focusOffset: offset,
});
}
export function getSelectedBlocksList(editorState) {
const selectionState = editorState.getSelection();
const contentState = editorState.getCurrentContent();
const startKey = selectionState.getStartKey();
const endKey = selectionState.getEndKey();
const blockMap = contentState.getBlockMap();
return blockMap
.toSeq()
.skipUntil((_, k) => k === startKey)
.takeUntil((_, k) => k === endKey)
.concat([[endKey, blockMap.get(endKey)]])
.toList();
}
export function onTab(editorState) {
const contentState = editorState.getCurrentContent();
const selection = editorState.getSelection();
let newContentState;
if (selection.isCollapsed()) {
newContentState = Modifier.insertText(
contentState,
selection,
DEFAULT_INDENTATION,
);
} else {
newContentState = Modifier.replaceText(
contentState,
selection,
DEFAULT_INDENTATION,
);
}
return EditorState.push(
editorState,
newContentState,
'insert-characters'
);
}

View File

@ -0,0 +1,26 @@
/**
*
* Video
*
*/
import React from 'react';
import PropTypes from 'prop-types';
/* eslint-disable jsx-a11y/media-has-caption */
const Video = props => {
const { height, src, width } = props.contentState.getEntity(props.entityKey).getData();
return (
<video height={height} width={width} style={{ maxWidth: '100%' }} controls>
<source src={src} />
</video>
);
};
Video.propTypes = {
contentState: PropTypes.object.isRequired,
entityKey: PropTypes.string.isRequired,
};
export default Video;

View File

@ -0,0 +1,57 @@
/**
*
* WysiwygBottomControls
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import styles from './styles.scss';
/* eslint-disable jsx-a11y/label-has-for */
const WysiwygBottomControls = ({ isPreviewMode, onChange, onClick }) => {
const browse = (
<FormattedMessage id="components.WysiwygBottomControls.uploadFiles.browse">
{(message) => <span className={styles.underline}>{message}</span>}
</FormattedMessage>
);
return (
<div className={styles.wysiwygBottomControlsWrapper}>
<div>
<label
className={styles.dropLabel}
onClick={(e) => {
if (isPreviewMode) {
e.preventDefault();
}
}}
>
<FormattedMessage
id="components.WysiwygBottomControls.uploadFiles"
values={{ browse }}
/>
<input type="file" onChange={onChange} />
</label>
</div>
<div className={styles.fullScreenWrapper} onClick={onClick}>
<FormattedMessage id="components.WysiwygBottomControls.fullscreen" />
</div>
</div>
);
};
WysiwygBottomControls.defaultProps = {
isPreviewMode: false,
onChange: () => {},
onClick: () => {},
};
WysiwygBottomControls.propTypes = {
isPreviewMode: PropTypes.bool,
onChange: PropTypes.func,
onClick: PropTypes.func,
};
export default WysiwygBottomControls;

View File

@ -0,0 +1,41 @@
.wysiwygBottomControlsWrapper {
display: flex;
height: 30px;
width: 100%;
padding: 0 15px;
justify-content: space-between;
background-color: #FAFAFB;
line-height: 30px;
font-size: 13px;
font-family: Lato;
border-top: 1px dashed #e3e4e4;
> div:first-child {
> span:last-child {
font-size: 12px;
}
}
}
.fullScreenWrapper {
cursor: pointer;
&:after {
content: '\f065';
margin-left: 8px;
font-family: FontAwesome;
font-size: 12px;
}
}
.underline {
color: #1C5DE7;
text-decoration: underline;
cursor: pointer;
}
.dropLabel {
> input {
display: none;
}
}

View File

@ -0,0 +1,26 @@
/**
*
* WysiwygDropUpload
*
*/
import React from 'react';
import styles from './styles.scss';
/* eslint-disable jsx-a11y/label-has-for */
const WysiwygDropUpload = (props) => {
return (
<label
{...props}
className={styles.wysiwygDropUpload}
>
<input
onChange={() => {}}
type="file"
tabIndex="-1"
/>
</label>
);
};
export default WysiwygDropUpload;

View File

@ -0,0 +1,12 @@
.wysiwygDropUpload {
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
background-color: rgba(28,93,231,0.01);
> input {
display: none;
}
}

View File

@ -0,0 +1,27 @@
/**
*
* WysiwygEditor
*
*/
import React from 'react';
import { Editor } from 'draft-js';
import PropTypes from 'prop-types';
class WysiwygEditor extends React.Component {
render() {
return (
<Editor {...this.props} ref={this.props.setRef} />
);
}
}
WysiwygEditor.defaultProps = {
setRef: () => {},
};
WysiwygEditor.propTypes = {
setRef: PropTypes.func,
};
export default WysiwygEditor;

View File

@ -0,0 +1,111 @@
/**
*
* WysiwygInlineControls
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './styles.scss';
class StyleButton extends React.Component {
handleClick = (e) => {
e.preventDefault();
if (!this.props.disabled) {
this.props.handlers[this.props.handler](this.props.text, this.props.style);
}
}
render() {
return (
<div
className={cn(
this.props.active && styles.styleButtonActive,
styles.styleButton,
this.props.className && styles[this.props.className],
this.props.disabled && styles.styleButtonDisabled,
)}
onMouseDown={this.handleClick}
>
{!this.props.hideLabel && this.props.label}
</div>
);
}
}
const WysiwygInlineControls = ({ buttons, disabled, editorState, handlers, onToggle, onToggleBlock }) => {
const selection = editorState.getSelection();
const blockType = editorState
.getCurrentContent()
.getBlockForKey(selection.getStartKey())
.getType();
const currentStyle = editorState.getCurrentInlineStyle();
return (
<div className={cn(styles.wysiwygInlineControls)}>
{buttons.map(type => (
<StyleButton
key={type.label}
active={type.style === blockType || currentStyle.has(type.style)}
className={type.className}
disabled={disabled}
handler={type.handler}
handlers={handlers}
hideLabel={type.hideLabel || false}
label={type.label}
onToggle={onToggle}
onToggleBlock={onToggleBlock}
style={type.style}
text={type.text}
/>
))}
</div>
);
};
/* eslint-disable react/default-props-match-prop-types */
StyleButton.defaultProps = {
active: false,
className: '',
disabled: false,
hideLabel: false,
label: '',
onToggle: () => {},
onToggleBlock: () => {},
style: '',
text: '',
};
StyleButton.propTypes = {
active: PropTypes.bool,
className: PropTypes.string,
disabled: PropTypes.bool,
handler: PropTypes.string.isRequired,
handlers: PropTypes.object.isRequired,
hideLabel: PropTypes.bool,
label: PropTypes.string,
style: PropTypes.string,
text: PropTypes.string,
};
WysiwygInlineControls.defaultProps = {
buttons: [],
disabled: false,
onToggle: () => {},
onToggleBlock: () => {},
};
WysiwygInlineControls.propTypes = {
buttons: PropTypes.array,
disabled: PropTypes.bool,
editorState: PropTypes.object.isRequired,
handlers: PropTypes.object.isRequired,
onToggle: PropTypes.func,
onToggleBlock: PropTypes.func,
};
export default WysiwygInlineControls;

View File

@ -0,0 +1,123 @@
.active {
background-color: red;
}
.controlsWrapper {
display: flex;
user-select: none;
> div:nth-child(even) {
border-left: 0;
border-right: 0;
}
}
.styleButton {
height: 32px;
min-width: 32px;
background-color: #FFFFFF;
border: 1px solid rgba(16,22,34,0.10);
font-size: 13px;
font-weight: 500;
line-height: 32px;
text-align: center;
cursor: pointer;
&:hover {
background-color: #F3F4F4;
}
}
.styleButtonActive {
border: 0;
background: rgba(16,22,34,0.00);
box-shadow: inset 0 -1px 0 0 rgba(16,22,34,0.04), inset 0 1px 0 0 rgba(16,22,34,0.04);
}
.styleButtonBold {
background-image: url('../../assets/icons/icon_bold.svg');
background-position: center;
background-repeat: no-repeat;
}
.styleButtonDisabled {
opacity: 0.7;
cursor: not-allowed;
}
.styleButtonItalic {
background-image: url('../../assets/icons/icon_italic.svg');
background-position: center;
background-repeat: no-repeat;
}
.styleButtonUnderline {
background-image: url('../../assets/icons/icon_underline.svg');
background-position: center 11px;
background-repeat: no-repeat;
}
.styleButtonUL {
background-image: url('../../assets/icons/icon_bullet-list.svg');
background-position: center;
background-repeat: no-repeat;
}
.styleButtonOL {
background-image: url('../../assets/icons/icon_numbered-list.svg');
background-position: center 12px;
background-repeat: no-repeat;
}
.styleButtonBlockQuote {
background-image: url('../../assets/icons/icon_quote-block.svg');
background-position: center;
background-repeat: no-repeat;
}
.styleButtonCodeBlock {
background-image: url('../../assets/icons/icon_code-block.svg');
background-position: center;
background-repeat: no-repeat;
}
.styleButtonLink {
background-image: url('../../assets/icons/icon_link.svg');
background-position: center;
background-repeat: no-repeat;
}
.styleButtonImg {
background-image: url('../../assets/icons/icon_media.svg');
background-position: center;
}
.styleButtonStrikedOut {
background-image: url('../../assets/icons/icon_barred.svg');
background-position: center;
background-repeat: no-repeat;
}
.wysiwygInlineControls {
height: 49px;
display: flex;
padding: 8px 3px 0 10px;
background-color: #F3F4F4;
user-select: none;
> div:nth-child(even) {
border-left: 0;
border-right: 0;
}
> div:first-child {
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
> div:last-child {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
border-right: 1px solid rgba(16,22,34,0.10);
}
}
.wysiwygInlineControlsFocus {
border-color: #78caff;
}

View File

@ -0,0 +1,7 @@
import Loadable from 'react-loadable';
import Loader from './Loader';
export default Loadable({
loader: () => import('./index'),
loading: Loader,
});

View File

@ -0,0 +1,11 @@
import React from 'react';
import styles from './styles.scss';
const Loader = () => (
<div className="col-md-12">
<div className={styles.wysLoader}><div /></div>
</div>
);
export default Loader;

View File

@ -0,0 +1,222 @@
/**
*
* WysiwygWithErrors
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import { isEmpty, isFunction } from 'lodash';
import cn from 'classnames';
// Design
import Label from 'components/Label';
import InputDescription from 'components/InputDescription';
import InputErrors from 'components/InputErrors';
import InputSpacer from 'components/InputSpacer';
import Wysiwyg from 'components/Wysiwyg';
// Utils
import validateInput from 'utils/inputsValidations';
import styles from './styles.scss';
class WysiwygWithErrors extends React.Component { // eslint-disable-line react/prefer-stateless-function
state = { errors: [], hasInitialValue: false };
componentDidMount() {
const { value, errors } = this.props;
// Prevent the input from displaying an error when the user enters and leaves without filling it
if (!isEmpty(value)) {
this.setState({ hasInitialValue: true });
}
// Display input error if it already has some
if (!isEmpty(errors)) {
this.setState({ errors });
}
}
componentWillReceiveProps(nextProps) {
// Show required error if the input's value is received after the compo is mounted
if (!isEmpty(nextProps.value) && !this.state.hasInitialValue) {
this.setState({ hasInitialValue: true });
}
// Check if errors have been updated during validations
if (nextProps.didCheckErrors !== this.props.didCheckErrors) {
// Remove from the state the errors that have already been set
const errors = isEmpty(nextProps.errors) ? [] : nextProps.errors;
this.setState({ errors });
}
}
/**
* Set the errors depending on the validations given to the input
* @param {Object} target
*/
handleBlur = ({ target }) => {
// Prevent from displaying error if the input is initially isEmpty
if (!isEmpty(target.value) || this.state.hasInitialValue) {
const errors = validateInput(target.value, this.props.validations);
this.setState({ errors, hasInitialValue: true });
}
}
render() {
const {
autoFocus,
className,
customBootstrapClass,
deactivateErrorHighlight,
disabled,
errorsClassName,
errorsStyle,
inputClassName,
inputDescription,
inputDescriptionClassName,
inputDescriptionStyle,
inputStyle,
label,
labelClassName,
labelStyle,
name,
noErrorsDescription,
onBlur,
onChange,
placeholder,
resetProps,
style,
tabIndex,
value,
} = this.props;
const handleBlur = isFunction(onBlur) ? onBlur : this.handleBlur;
let spacer = !isEmpty(inputDescription) ? <InputSpacer /> : <div />;
if (!noErrorsDescription && !isEmpty(this.state.errors)) {
spacer = <div />;
}
return (
<div
className={cn(
styles.containerWysiwyg,
customBootstrapClass,
!isEmpty(className) && className,
)}
style={style}
>
<Label
className={labelClassName}
htmlFor={name}
message={label}
style={labelStyle}
/>
<Wysiwyg
autoFocus={autoFocus}
className={inputClassName}
disabled={disabled}
deactivateErrorHighlight={deactivateErrorHighlight}
error={!isEmpty(this.state.errors)}
name={name}
onBlur={handleBlur}
onChange={onChange}
placeholder={placeholder}
resetProps={resetProps}
style={inputStyle}
tabIndex={tabIndex}
value={value}
/>
<InputDescription
className={inputDescriptionClassName}
message={inputDescription}
style={inputDescriptionStyle}
/>
<InputErrors
className={errorsClassName}
errors={!noErrorsDescription && this.state.errors || []}
style={errorsStyle}
/>
{spacer}
</div>
);
}
}
WysiwygWithErrors.defaultProps = {
autoFocus: false,
className: '',
customBootstrapClass: 'col-md-12',
deactivateErrorHighlight: false,
didCheckErrors: false,
disabled: false,
errors: [],
errorsClassName: '',
errorsStyle: {},
inputClassName: '',
inputDescription: '',
inputDescriptionClassName: '',
inputDescriptionStyle: {},
inputStyle: {},
label: '',
labelClassName: '',
labelStyle: {},
noErrorsDescription: false,
onBlur: false,
placeholder: '',
resetProps: false,
style: {},
tabIndex: '0',
validations: {},
};
WysiwygWithErrors.propTypes = {
autoFocus: PropTypes.bool,
className: PropTypes.string,
customBootstrapClass: PropTypes.string,
deactivateErrorHighlight: PropTypes.bool,
didCheckErrors: PropTypes.bool,
disabled: PropTypes.bool,
errors: PropTypes.array,
errorsClassName: PropTypes.string,
errorsStyle: PropTypes.object,
inputClassName: PropTypes.string,
inputDescription: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func,
PropTypes.shape({
id: PropTypes.string,
params: PropTypes.object,
}),
]),
inputDescriptionClassName: PropTypes.string,
inputDescriptionStyle: PropTypes.object,
inputStyle: PropTypes.object,
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func,
PropTypes.shape({
id: PropTypes.string,
params: PropTypes.object,
}),
]),
labelClassName: PropTypes.string,
labelStyle: PropTypes.object,
name: PropTypes.string.isRequired,
noErrorsDescription: PropTypes.bool,
onBlur: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.func,
]),
onChange: PropTypes.func.isRequired,
placeholder: PropTypes.string,
resetProps: PropTypes.bool,
style: PropTypes.object,
tabIndex: PropTypes.string,
validations: PropTypes.object,
value: PropTypes.string.isRequired,
};
export default WysiwygWithErrors;

View File

@ -0,0 +1,24 @@
.containerWysiwyg {
margin-bottom: 1.6rem;
font-size: 1.3rem;
font-family: 'Lato';
}
.wysLoader {
display: flex;
justify-content: space-around;
width: 100%;
> div {
border: 4px solid #f3f3f3; /* Light grey */
border-top: 4px solid #3498db; /* Blue */
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 2s linear infinite;
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

View File

@ -47,6 +47,7 @@ export function* getModels() {
method: 'GET',
});
// console.log('getModels', response);
yield put(loadedModels(response));
} catch (err) {
strapi.notification.error('content-manager.error.model.fetch');
@ -56,7 +57,7 @@ export function* getModels() {
export function* modelsLoaded() {
const models = yield select(makeSelectModels());
let schema;
// console.log('modelsLoaded', models);
try {
schema = generateSchema(models);
} catch (err) {

View File

@ -0,0 +1 @@
export default [];

View File

@ -49,4 +49,4 @@
"npm": ">= 5.0.0"
},
"license": "MIT"
}
}

View File

@ -0,0 +1 @@
export default [];

View File

@ -0,0 +1 @@
export default [];

View File

@ -0,0 +1 @@
export default [];

View File

@ -0,0 +1 @@
export default [];

View File

@ -0,0 +1 @@
export default [];