-
Notifications
You must be signed in to change notification settings - Fork 382
/
edit.js
159 lines (144 loc) · 4.84 KB
/
edit.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
/**
* External dependencies
*/
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { Editor, EditorState, SelectionState } from 'draft-js';
import { stateFromHTML } from 'draft-js-import-html';
import { stateToHTML } from 'draft-js-export-html';
/**
* WordPress dependencies
*/
import { useState, useEffect, useLayoutEffect, useRef, useCallback } from '@wordpress/element';
/**
* Internal dependencies
*/
import { useStory, useFont } from '../../app';
import { useCanvas } from '../../components/canvas';
import {
ElementFillContent,
ElementWithFont,
ElementWithBackgroundColor,
ElementWithFontColor,
} from '../shared';
import { getFilteredState, getHandleKeyCommand } from './util';
const Element = styled.div`
margin: 0;
${ ElementFillContent }
${ ElementWithFont }
${ ElementWithBackgroundColor }
${ ElementWithFontColor }
&::after {
content: '';
display: block;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
border: 1px solid ${ ( { theme } ) => theme.colors.mg.v1 }70;
pointer-events: none;
}
`;
function TextEdit( { id, content, color, backgroundColor, width, height, fontFamily, fontFallback, fontSize, fontWeight, fontStyle } ) {
const props = {
color,
backgroundColor,
fontFamily,
fontFallback,
fontStyle,
fontSize,
fontWeight,
width,
height,
};
const editorRef = useRef( null );
const { actions: { maybeEnqueueFontStyle } } = useFont();
const { actions: { updateElementById } } = useStory();
const { state: { editingElementState } } = useCanvas();
const { offset, clearContent } = editingElementState || {};
// To clear content, we can't just use createEmpty() or even pure white-space.
// The editor needs some content to insert the first character in,
// so we use a non-breaking space instead and trim it on save if still present.
const EMPTY_VALUE = '\u00A0';
const initialState = (
clearContent ?
EditorState.createWithContent( stateFromHTML( EMPTY_VALUE ) ) :
EditorState.createWithContent( stateFromHTML( content ) )
);
const [ editorState, setEditorState ] = useState( initialState );
const mustAddOffset = useRef( offset ? 2 : 0 );
// This is to allow the finalizing useEffect to *not* depend on editorState,
// as would otherwise be a lint error.
const lastKnownState = useRef( null );
// This filters out illegal content (see `getFilteredState`)
// on paste and updates state accordingly.
// Furthermore it also sets initial selection if relevant.
const updateEditorState = useCallback( ( newEditorState ) => {
let filteredState = getFilteredState( newEditorState, editorState );
if ( mustAddOffset.current ) {
// For some reason forced selection only sticks the second time around?
// Several other checks have been attempted here without success.
// Optimize at your own perril!
mustAddOffset.current--;
const key = filteredState.getCurrentContent().getFirstBlock().getKey();
const selectionState = new SelectionState( { anchorKey: key, anchorOffset: offset } );
filteredState = EditorState.forceSelection( filteredState, selectionState );
}
lastKnownState.current = filteredState.getCurrentContent();
setEditorState( filteredState );
}, [ editorState, offset ] );
// Finally update content for element on unmount.
useEffect( () => () => {
if ( lastKnownState.current ) {
// Remember to trim any trailing non-breaking space.
const properties = {
content: stateToHTML( lastKnownState.current, { defaultBlockTag: null } )
.replace( / $/, '' ),
};
updateElementById( { elementId: id, properties } );
}
}, [ id, updateElementById ] );
// Make sure to allow the user to click in the text box while working on the text.
const onClick = ( evt ) => {
const editor = editorRef.current;
// Refocus the editor if the container outside it is clicked.
if ( ! editor.editorContainer.contains( evt.target ) ) {
editor.focus();
}
evt.stopPropagation();
};
// Handle basic key commands such as bold, italic and underscore.
const handleKeyCommand = getHandleKeyCommand( setEditorState );
// Set focus when initially rendered
useLayoutEffect( () => {
editorRef.current.focus();
}, [] );
useEffect( () => {
maybeEnqueueFontStyle( fontFamily );
}, [ fontFamily, maybeEnqueueFontStyle ] );
return (
<Element { ...props } onClick={ onClick }>
<Editor
ref={ editorRef }
onChange={ updateEditorState }
editorState={ editorState }
handleKeyCommand={ handleKeyCommand }
/>
</Element>
);
}
TextEdit.propTypes = {
id: PropTypes.string.isRequired,
content: PropTypes.string,
color: PropTypes.string,
backgroundColor: PropTypes.string,
fontFamily: PropTypes.string,
fontFallback: PropTypes.array,
fontSize: PropTypes.number,
fontWeight: PropTypes.number,
fontStyle: PropTypes.string,
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
};
export default TextEdit;