Remove webpack warnings and react-intl errors
@ -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,
|
||||
|
||||
@ -115,4 +115,4 @@
|
||||
"webpack-hot-middleware": "^2.18.2",
|
||||
"whatwg-fetch": "^2.0.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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;
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
@ -0,0 +1,5 @@
|
||||
.containerJSON {
|
||||
margin-bottom: 1.6rem;
|
||||
font-size: 1.3rem;
|
||||
font-family: 'Lato';
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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: '',
|
||||
},
|
||||
{
|
||||
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 = ' ';
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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: '',
|
||||
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 };
|
||||
}
|
||||
@ -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;
|
||||
@ -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 = ``;
|
||||
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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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 };
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
@ -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'
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
import Loadable from 'react-loadable';
|
||||
import Loader from './Loader';
|
||||
|
||||
export default Loadable({
|
||||
loader: () => import('./index'),
|
||||
loading: Loader,
|
||||
});
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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); }
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export default [];
|
||||
@ -49,4 +49,4 @@
|
||||
"npm": ">= 5.0.0"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export default [];
|
||||
@ -0,0 +1 @@
|
||||
export default [];
|
||||
@ -0,0 +1 @@
|
||||
export default [];
|
||||
@ -0,0 +1 @@
|
||||
export default [];
|
||||
@ -0,0 +1 @@
|
||||
export default [];
|
||||
@ -0,0 +1 @@
|
||||
{}
|
||||