@pie-lib/editable-html 7.22.5 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/editor.jsx CHANGED
@@ -1,16 +1,17 @@
1
- import { Editor as SlateEditor, findNode } from 'slate-react';
1
+ import { Editor as SlateEditor, findNode, getEventRange, getEventTransfer } from 'slate-react';
2
2
  import SlateTypes from 'slate-prop-types';
3
3
 
4
4
  import isEqual from 'lodash/isEqual';
5
5
  import * as serialization from './serialization';
6
6
  import PropTypes from 'prop-types';
7
7
  import React from 'react';
8
- import { Value, Block } from 'slate';
8
+ import { Value, Block, Inline } from 'slate';
9
9
  import { buildPlugins, ALL_PLUGINS, DEFAULT_PLUGINS } from './plugins';
10
10
  import debug from 'debug';
11
11
  import { withStyles } from '@material-ui/core/styles';
12
12
  import classNames from 'classnames';
13
13
  import { color } from '@pie-lib/render-ui';
14
+ import { getBase64 } from './serialization';
14
15
 
15
16
  export { ALL_PLUGINS, DEFAULT_PLUGINS, serialization };
16
17
 
@@ -30,6 +31,8 @@ const defaultResponseAreaProps = {
30
31
  onHandleAreaChange: () => {}
31
32
  };
32
33
 
34
+ const defaultLanguageCharactersProps = [];
35
+
33
36
  const createToolbarOpts = toolbarOpts => {
34
37
  return {
35
38
  ...defaultToolbarOpts,
@@ -49,6 +52,7 @@ export class Editor extends React.Component {
49
52
  focus: PropTypes.func.isRequired,
50
53
  value: SlateTypes.value.isRequired,
51
54
  imageSupport: PropTypes.object,
55
+ charactersLimit: PropTypes.number,
52
56
  width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
53
57
  height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
54
58
  minHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
@@ -72,6 +76,13 @@ export class Editor extends React.Component {
72
76
  respAreaToolbar: PropTypes.func,
73
77
  onHandleAreaChange: PropTypes.func
74
78
  }),
79
+ languageCharactersProps: PropTypes.arrayOf(
80
+ PropTypes.shape({
81
+ language: PropTypes.string,
82
+ characterIcon: PropTypes.string,
83
+ characters: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string))
84
+ })
85
+ ),
75
86
  toolbarOpts: PropTypes.shape({
76
87
  position: PropTypes.oneOf(['bottom', 'top']),
77
88
  alignment: PropTypes.oneOf(['left', 'right']),
@@ -87,7 +98,9 @@ export class Editor extends React.Component {
87
98
  new Error(`Invalid values: ${values}, values must be one of [${ALL_PLUGINS.join(',')}]`)
88
99
  );
89
100
  }),
90
- className: PropTypes.string
101
+ className: PropTypes.string,
102
+ maxImageWidth: PropTypes.number,
103
+ maxImageHeight: PropTypes.number
91
104
  };
92
105
 
93
106
  static defaultProps = {
@@ -96,7 +109,8 @@ export class Editor extends React.Component {
96
109
  onBlur: () => {},
97
110
  onKeyDown: () => {},
98
111
  toolbarOpts: defaultToolbarOpts,
99
- responseAreaProps: defaultResponseAreaProps
112
+ responseAreaProps: defaultResponseAreaProps,
113
+ languageCharactersProps: defaultLanguageCharactersProps
100
114
  };
101
115
 
102
116
  constructor(props) {
@@ -106,15 +120,19 @@ export class Editor extends React.Component {
106
120
  toolbarOpts: createToolbarOpts(props.toolbarOpts)
107
121
  };
108
122
 
123
+ this.onResize = () => {
124
+ props.onChange(this.state.value, true);
125
+ };
126
+
127
+ this.handlePlugins(this.props);
128
+ }
129
+
130
+ handlePlugins = props => {
109
131
  const normalizedResponseAreaProps = {
110
132
  ...defaultResponseAreaProps,
111
133
  ...props.responseAreaProps
112
134
  };
113
135
 
114
- this.onResize = () => {
115
- props.onChange(this.state.value, true);
116
- };
117
-
118
136
  this.plugins = buildPlugins(props.activePlugins, {
119
137
  math: {
120
138
  onClick: this.onMathClick,
@@ -123,25 +141,27 @@ export class Editor extends React.Component {
123
141
  },
124
142
  image: {
125
143
  onDelete:
126
- this.props.imageSupport &&
127
- this.props.imageSupport.delete &&
144
+ props.imageSupport &&
145
+ props.imageSupport.delete &&
128
146
  ((src, done) => {
129
- this.props.imageSupport.delete(src, e => {
147
+ props.imageSupport.delete(src, e => {
130
148
  done(e, this.state.value);
131
149
  });
132
150
  }),
133
151
  insertImageRequested:
134
- this.props.imageSupport &&
152
+ props.imageSupport &&
135
153
  (getHandler => {
136
154
  /**
137
155
  * The handler is the object through which the outer context
138
156
  * communicates file upload events like: fileChosen, cancel, progress
139
157
  */
140
158
  const handler = getHandler(() => this.state.value);
141
- this.props.imageSupport.add(handler);
159
+ props.imageSupport.add(handler);
142
160
  }),
143
161
  onFocus: this.onPluginFocus,
144
- onBlur: this.onPluginBlur
162
+ onBlur: this.onPluginBlur,
163
+ maxImageWidth: this.props.maxImageWidth,
164
+ maxImageHeight: this.props.maxImageHeight
145
165
  },
146
166
  toolbar: {
147
167
  /**
@@ -151,7 +171,7 @@ export class Editor extends React.Component {
151
171
  disableUnderline: props.disableUnderline,
152
172
  autoWidth: props.autoWidthToolbar,
153
173
  onDone: () => {
154
- const { nonEmpty } = this.props;
174
+ const { nonEmpty } = props;
155
175
 
156
176
  log('[onDone]');
157
177
  this.setState({ toolbarInFocus: false, focusedNode: null });
@@ -192,13 +212,14 @@ export class Editor extends React.Component {
192
212
  this.onPluginBlur();
193
213
  }
194
214
  },
215
+ languageCharacters: props.languageCharactersProps,
195
216
  media: {
196
217
  focus: this.focus,
197
218
  createChange: () => this.state.value.change(),
198
219
  onChange: this.onChange
199
220
  }
200
221
  });
201
- }
222
+ };
202
223
 
203
224
  componentDidMount() {
204
225
  // onRef is needed to get the ref of the component because we export it using withStyles
@@ -232,6 +253,10 @@ export class Editor extends React.Component {
232
253
  toolbarOpts: newToolbarOpts
233
254
  });
234
255
  }
256
+
257
+ if (!isEqual(nextProps.languageCharactersProps, this.props.languageCharactersProps)) {
258
+ this.handlePlugins(nextProps);
259
+ }
235
260
  }
236
261
 
237
262
  componentDidUpdate() {
@@ -440,7 +465,20 @@ export class Editor extends React.Component {
440
465
 
441
466
  onChange = (change, done) => {
442
467
  log('[onChange]');
443
- this.setState({ value: change.value }, () => {
468
+
469
+ const { value } = change;
470
+ const { charactersLimit } = this.props;
471
+
472
+ if (
473
+ value &&
474
+ value.document &&
475
+ value.document.text &&
476
+ value.document.text.length > charactersLimit
477
+ ) {
478
+ return;
479
+ }
480
+
481
+ this.setState({ value }, () => {
444
482
  log('[onChange], call done()');
445
483
 
446
484
  if (done) {
@@ -533,6 +571,42 @@ export class Editor extends React.Component {
533
571
  this.props.focus(position, node);
534
572
  };
535
573
 
574
+ onDropPaste = async (event, change, dropContext) => {
575
+ if (!this.props.imageSupport) {
576
+ return;
577
+ }
578
+ const editor = change.editor;
579
+ const transfer = getEventTransfer(event);
580
+ const file = transfer.files[0];
581
+
582
+ if (file.type === 'image/jpeg' || file.type === 'image/jpg' || file.type === 'image/png') {
583
+ try {
584
+ log('[onDropPaste]');
585
+ const src = await getBase64(file);
586
+ const inline = Inline.create({
587
+ type: 'image',
588
+ isVoid: true,
589
+ data: {
590
+ loading: false,
591
+ src
592
+ }
593
+ });
594
+ if (dropContext) {
595
+ this.focus();
596
+ } else {
597
+ const range = getEventRange(event, editor);
598
+ if (range) {
599
+ change.select(range);
600
+ }
601
+ }
602
+ const ch = change.insertInline(inline);
603
+ this.onChange(ch);
604
+ } catch (err) {
605
+ log('[onDropPaste] error: ', err);
606
+ }
607
+ }
608
+ };
609
+
536
610
  render() {
537
611
  const {
538
612
  disabled,
@@ -581,6 +655,8 @@ export class Editor extends React.Component {
581
655
  onKeyDown={onKeyDown}
582
656
  onChange={this.onChange}
583
657
  onBlur={this.onBlur}
658
+ onDrop={(event, editor) => this.onDropPaste(event, editor, true)}
659
+ onPaste={(event, editor) => this.onDropPaste(event, editor)}
584
660
  onFocus={this.onFocus}
585
661
  onEditingDone={this.onEditingDone}
586
662
  focusedNode={focusedNode}
@@ -0,0 +1,45 @@
1
+ import React from 'react';
2
+ import { withStyles } from '@material-ui/core/styles';
3
+ import Popover from '@material-ui/core/Popover';
4
+ import Typography from '@material-ui/core/Typography';
5
+
6
+ const styles = () => ({
7
+ popover: {
8
+ pointerEvents: 'none',
9
+ zIndex: 99999
10
+ },
11
+ paper: {
12
+ padding: 20,
13
+ height: 'auto',
14
+ width: 'auto'
15
+ },
16
+ typography: {
17
+ fontSize: 50,
18
+ textAlign: 'center'
19
+ }
20
+ });
21
+
22
+ const CustomPopOver = withStyles(styles)(({ classes, children, ...props }) => (
23
+ <Popover
24
+ id="mouse-over-popover"
25
+ open
26
+ className={classes.popover}
27
+ classes={{
28
+ paper: classes.paper
29
+ }}
30
+ anchorOrigin={{
31
+ vertical: 'bottom',
32
+ horizontal: 'left'
33
+ }}
34
+ transformOrigin={{
35
+ vertical: 'top',
36
+ horizontal: 'left'
37
+ }}
38
+ disableRestoreFocus
39
+ {...props}
40
+ >
41
+ <Typography classes={{ root: classes.typography }}>{children}</Typography>
42
+ </Popover>
43
+ ));
44
+
45
+ export default CustomPopOver;
@@ -0,0 +1,237 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom';
3
+ import debug from 'debug';
4
+ import get from 'lodash/get';
5
+
6
+ import { PureToolbar } from '@pie-lib/math-toolbar';
7
+
8
+ import CustomPopOver from './custom-popover';
9
+ import { insertSnackBar } from '../respArea/utils';
10
+ import { characterIcons, spanishConfig, specialConfig } from './utils';
11
+ const log = debug('@pie-lib:editable-html:plugins:characters');
12
+
13
+ const removeDialogs = () => {
14
+ const prevDialogs = document.querySelectorAll('.insert-character-dialog');
15
+
16
+ log('[characters:removeDialogs]');
17
+ prevDialogs.forEach(s => s.remove());
18
+ };
19
+
20
+ const removePopOvers = () => {
21
+ const prevPopOvers = document.querySelectorAll('#mouse-over-popover');
22
+
23
+ log('[characters:removePopOvers]');
24
+ prevPopOvers.forEach(s => s.remove());
25
+ };
26
+
27
+ const insertDialog = ({ value, callback, opts }) => {
28
+ const newEl = document.createElement('div');
29
+ const initialBodyOverflow = document.body.style.overflow;
30
+
31
+ log('[characters:insertDialog]');
32
+
33
+ removeDialogs();
34
+
35
+ newEl.className = 'insert-character-dialog';
36
+ document.body.style.overflow = 'hidden';
37
+
38
+ let configToUse;
39
+
40
+ switch (true) {
41
+ case opts.language === 'spanish':
42
+ configToUse = spanishConfig;
43
+ break;
44
+ case opts.language === 'special':
45
+ configToUse = specialConfig;
46
+ break;
47
+ default:
48
+ configToUse = opts;
49
+ }
50
+
51
+ if (!configToUse.characters) {
52
+ insertSnackBar('No characters provided or language not recognized');
53
+ return;
54
+ }
55
+
56
+ const layoutForCharacters = configToUse.characters.reduce(
57
+ (obj, arr) => {
58
+ if (arr.length >= obj.columns) {
59
+ obj.columns = arr.length;
60
+ }
61
+
62
+ return obj;
63
+ },
64
+ { rows: configToUse.characters.length, columns: 0 }
65
+ );
66
+
67
+ let popoverEl;
68
+
69
+ const closePopOver = () => {
70
+ if (popoverEl) {
71
+ popoverEl.remove();
72
+ }
73
+
74
+ removePopOvers();
75
+ };
76
+
77
+ const renderPopOver = (event, el) => {
78
+ if (!event) {
79
+ return;
80
+ }
81
+
82
+ const infoStyle = { fontSize: '20px', lineHeight: '20px' };
83
+
84
+ closePopOver();
85
+
86
+ popoverEl = document.createElement('div');
87
+ ReactDOM.render(
88
+ <CustomPopOver onClose={closePopOver} anchorEl={event.currentTarget}>
89
+ <div>{el.label}</div>
90
+
91
+ <div style={infoStyle}>{el.description}</div>
92
+
93
+ <div style={infoStyle}>{el.unicode}</div>
94
+ </CustomPopOver>,
95
+ popoverEl
96
+ );
97
+
98
+ document.body.appendChild(newEl);
99
+ };
100
+
101
+ const handleClose = () => {
102
+ newEl.remove();
103
+ closePopOver();
104
+ document.body.style.overflow = initialBodyOverflow;
105
+ callback(undefined, true);
106
+ };
107
+
108
+ const handleChange = val => {
109
+ if (typeof val === 'string') {
110
+ callback(val);
111
+ }
112
+
113
+ if (configToUse.autoClose) {
114
+ handleClose();
115
+ }
116
+ };
117
+
118
+ const el = (
119
+ <PureToolbar
120
+ autoFocus
121
+ noDecimal
122
+ hideInput
123
+ noLatexHandling
124
+ layoutForKeyPad={layoutForCharacters}
125
+ additionalKeys={configToUse.characters.reduce((arr, n) => {
126
+ arr = [
127
+ ...arr,
128
+ ...n.map(k => ({
129
+ name: get(k, 'name') || k,
130
+ write: get(k, 'write') || k,
131
+ label: get(k, 'label') || k,
132
+ category: 'character',
133
+ extraClass: 'character',
134
+ ...(configToUse.hasPreview
135
+ ? {
136
+ actions: { onMouseEnter: ev => renderPopOver(ev, k), onMouseLeave: closePopOver }
137
+ }
138
+ : {})
139
+ }))
140
+ ];
141
+
142
+ return arr;
143
+ }, [])}
144
+ keypadMode="language"
145
+ onChange={handleChange}
146
+ onDone={handleClose}
147
+ />
148
+ );
149
+
150
+ ReactDOM.render(el, newEl, () => {
151
+ const cursorItem = document.querySelector(`[data-key="${value.anchorKey}"]`);
152
+
153
+ if (cursorItem) {
154
+ const boundRect = cursorItem.getBoundingClientRect();
155
+
156
+ document.body.appendChild(newEl);
157
+ newEl.style.position = 'fixed';
158
+ newEl.style.top = `${boundRect.top - newEl.offsetHeight - 10}px`;
159
+ newEl.style.left = `${boundRect.left + cursorItem.offsetWidth + 10}px`;
160
+ newEl.style.zIndex = 99999;
161
+
162
+ let firstCallMade = false;
163
+
164
+ const listener = () => {
165
+ // this will be triggered right after setting it because
166
+ // this toolbar is added on the mousedown event
167
+ // so right after mouseup, the click will be triggered
168
+ if (firstCallMade) {
169
+ document.body.removeEventListener('click', listener);
170
+ handleClose();
171
+ } else {
172
+ firstCallMade = true;
173
+ }
174
+ };
175
+
176
+ if (configToUse.autoClose) {
177
+ document.body.addEventListener('click', listener);
178
+ }
179
+ }
180
+ });
181
+ };
182
+
183
+ const CharacterIcon = ({ letter }) => (
184
+ <div
185
+ style={{
186
+ fontSize: '25px',
187
+ lineHeight: '15px'
188
+ }}
189
+ >
190
+ {letter}
191
+ </div>
192
+ );
193
+
194
+ export default function CharactersPlugin(opts) {
195
+ removeDialogs();
196
+ return {
197
+ name: 'math',
198
+ toolbar: {
199
+ icon: <CharacterIcon letter={opts.characterIcon || characterIcons[opts.language] || 'ñ'} />,
200
+ onClick: (value, onChange) => {
201
+ let valueToUse = value;
202
+ const callback = (char, focus) => {
203
+ if (char) {
204
+ const change = valueToUse
205
+ .change()
206
+ .insertTextByKey(valueToUse.anchorKey, valueToUse.anchorOffset, char);
207
+
208
+ valueToUse = change.value;
209
+ log('[characters:insert]: ', value);
210
+ onChange(change);
211
+ }
212
+
213
+ log('[characters:click]');
214
+
215
+ if (focus) {
216
+ const editorDOM = document.querySelector(`[data-key="${valueToUse.document.key}"]`);
217
+
218
+ if (editorDOM) {
219
+ editorDOM.focus();
220
+ }
221
+ }
222
+ };
223
+
224
+ insertDialog({ value: valueToUse, callback, opts });
225
+ }
226
+ },
227
+
228
+ pluginStyles: (node, parentNode, p) => {
229
+ if (p) {
230
+ return {
231
+ position: 'absolute',
232
+ top: 'initial'
233
+ };
234
+ }
235
+ }
236
+ };
237
+ }