draftail-text-utils 0.1.7__py3-none-any.whl
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.
- draftail_text_utils/__init__.py +4 -0
- draftail_text_utils/apps.py +7 -0
- draftail_text_utils/conf.py +482 -0
- draftail_text_utils/migrations/__init__.py +0 -0
- draftail_text_utils/rich_text/__init__.py +0 -0
- draftail_text_utils/rich_text/base.py +267 -0
- draftail_text_utils/rich_text/font_family.py +61 -0
- draftail_text_utils/rich_text/font_size.py +94 -0
- draftail_text_utils/rich_text/highlight_color.py +99 -0
- draftail_text_utils/rich_text/text_alignment.py +70 -0
- draftail_text_utils/rich_text/text_color.py +95 -0
- draftail_text_utils/static/draftail_text_utils/css/draftail_text_utils.css +5 -0
- draftail_text_utils/static/draftail_text_utils/css/font_family.css +57 -0
- draftail_text_utils/static/draftail_text_utils/css/font_size.css +102 -0
- draftail_text_utils/static/draftail_text_utils/css/highlight_color.css +117 -0
- draftail_text_utils/static/draftail_text_utils/css/text_alignment.css +72 -0
- draftail_text_utils/static/draftail_text_utils/css/text_color.css +117 -0
- draftail_text_utils/static/draftail_text_utils/js/font_family.js +149 -0
- draftail_text_utils/static/draftail_text_utils/js/font_size.js +408 -0
- draftail_text_utils/static/draftail_text_utils/js/font_size_entity.js +39 -0
- draftail_text_utils/static/draftail_text_utils/js/highlight_color.js +350 -0
- draftail_text_utils/static/draftail_text_utils/js/highlight_color_entity.js +43 -0
- draftail_text_utils/static/draftail_text_utils/js/text_alignment.js +139 -0
- draftail_text_utils/static/draftail_text_utils/js/text_color.js +343 -0
- draftail_text_utils/static/draftail_text_utils/js/text_color_entity.js +42 -0
- draftail_text_utils/templates/draftail_text_utils/icons/align-center.svg +1 -0
- draftail_text_utils/templates/draftail_text_utils/icons/align-justify.svg +1 -0
- draftail_text_utils/templates/draftail_text_utils/icons/align-left.svg +1 -0
- draftail_text_utils/templates/draftail_text_utils/icons/align-right.svg +1 -0
- draftail_text_utils/templates/draftail_text_utils/icons/font.svg +1 -0
- draftail_text_utils/templates/draftail_text_utils/icons/highlighter.svg +1 -0
- draftail_text_utils/templates/draftail_text_utils/icons/palette.svg +1 -0
- draftail_text_utils/templates/draftail_text_utils/icons/text-height.svg +1 -0
- draftail_text_utils/templatetags/__init__.py +0 -0
- draftail_text_utils/templatetags/draftail_text_utils_tags.py +29 -0
- draftail_text_utils/wagtail_hooks.py +266 -0
- draftail_text_utils-0.1.7.dist-info/METADATA +63 -0
- draftail_text_utils-0.1.7.dist-info/RECORD +39 -0
- draftail_text_utils-0.1.7.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
// https://github.com/wagtail/draftail/blob/main/src/components/Toolbar/ToolbarButton.tsx
|
|
2
|
+
// export interface ToolbarButtonProps {
|
|
3
|
+
// name?: string;
|
|
4
|
+
// active?: boolean;
|
|
5
|
+
// label?: string | null;
|
|
6
|
+
// title?: string | null;
|
|
7
|
+
// icon?: IconProp | null;
|
|
8
|
+
// className?: string | null;
|
|
9
|
+
// tooltipDirection?: "up" | "down";
|
|
10
|
+
// onClick?: ((name: string) => void) | null;
|
|
11
|
+
// }
|
|
12
|
+
|
|
13
|
+
// CSS Color Component
|
|
14
|
+
// https://github.com/argyleink/css-color-component
|
|
15
|
+
|
|
16
|
+
(function () {
|
|
17
|
+
var React = window.React;
|
|
18
|
+
var RichUtils = window.DraftJS.RichUtils;
|
|
19
|
+
var Modifier = window.DraftJS.Modifier;
|
|
20
|
+
var EditorState = window.DraftJS.EditorState;
|
|
21
|
+
var ToolbarButton = window.Draftail.ToolbarButton;
|
|
22
|
+
var data = window.draftailTextUtils || {};
|
|
23
|
+
|
|
24
|
+
// Build options from config
|
|
25
|
+
var options = data.customHighlightColors || [];
|
|
26
|
+
var optMap = data.customHighlightColorStyleMap || {};
|
|
27
|
+
|
|
28
|
+
var savedSelection = null;
|
|
29
|
+
|
|
30
|
+
const entityType = 'HIGHLIGHT_COLOR';
|
|
31
|
+
const controlType = 'highlight-color';
|
|
32
|
+
const control = JSON.parse(
|
|
33
|
+
document.getElementById(`draftail-plugin-control-${controlType}`)
|
|
34
|
+
.textContent,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
function removeHighlightColorEntity(editorState) {
|
|
38
|
+
var selection = editorState.getSelection();
|
|
39
|
+
if (!selection.getHasFocus()) return editorState;
|
|
40
|
+
|
|
41
|
+
var contentWithoutEntity = Modifier.applyEntity(
|
|
42
|
+
editorState.getCurrentContent(),
|
|
43
|
+
selection,
|
|
44
|
+
null, // null removes entity
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return EditorState.push(editorState, contentWithoutEntity, 'apply-entity');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function saveSelection(getEditorState) {
|
|
51
|
+
const selection = getEditorState().getSelection();
|
|
52
|
+
if (selection && !selection.isCollapsed()) {
|
|
53
|
+
savedSelection = selection;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function restoreSelection(editorState) {
|
|
58
|
+
if (savedSelection && !savedSelection.isCollapsed()) {
|
|
59
|
+
return EditorState.forceSelection(editorState, savedSelection);
|
|
60
|
+
}
|
|
61
|
+
return editorState;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Helper to find active color
|
|
65
|
+
function getActiveHighlightColor(editorState) {
|
|
66
|
+
var selection = editorState.getSelection();
|
|
67
|
+
if (!selection.getHasFocus()) return null;
|
|
68
|
+
|
|
69
|
+
var contentState = editorState.getCurrentContent();
|
|
70
|
+
var startKey = selection.getStartKey();
|
|
71
|
+
var startOffset = selection.getStartOffset();
|
|
72
|
+
var blockWithEntityAt = contentState.getBlockForKey(startKey);
|
|
73
|
+
|
|
74
|
+
// Check entity at cursor position
|
|
75
|
+
var entityKey = blockWithEntityAt.getEntityAt(startOffset);
|
|
76
|
+
if (entityKey) {
|
|
77
|
+
var entity = contentState.getEntity(entityKey);
|
|
78
|
+
if (entity.getType() === entityType) {
|
|
79
|
+
return entity.getData().backgroundColor;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// If selection is collapsed, also check character before (so styling after typing works)
|
|
84
|
+
if (selection.isCollapsed() && startOffset > 0) {
|
|
85
|
+
entityKey = blockWithEntityAt.getEntityAt(startOffset - 1);
|
|
86
|
+
if (entityKey) {
|
|
87
|
+
var entity = contentState.getEntity(entityKey);
|
|
88
|
+
if (entity.getType() === entityType) {
|
|
89
|
+
return entity.getData().backgroundColor;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// The control component
|
|
98
|
+
var HighlightColorControl = class HighlightColorControl
|
|
99
|
+
extends React.Component
|
|
100
|
+
{
|
|
101
|
+
constructor(props) {
|
|
102
|
+
super(props); // getEditorState, onChange
|
|
103
|
+
this.state = {
|
|
104
|
+
isDropdownOpen: false,
|
|
105
|
+
inputValue: '',
|
|
106
|
+
isInputFocused: false,
|
|
107
|
+
};
|
|
108
|
+
this.controlRef = React.createRef();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// lifecycle
|
|
112
|
+
componentDidMount() {
|
|
113
|
+
this.syncInputValue();
|
|
114
|
+
document.addEventListener('mousedown', this.handleClickOutside);
|
|
115
|
+
|
|
116
|
+
// Listen to CSS Color Component events
|
|
117
|
+
var inputEl = document.querySelector('#Draftail--highlight-color-input');
|
|
118
|
+
if (inputEl) {
|
|
119
|
+
inputEl.addEventListener('open', this.handleInputFocus);
|
|
120
|
+
inputEl.addEventListener('close', this.handleInputBlur);
|
|
121
|
+
inputEl.addEventListener('change', this.handleInputChange);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
componentWillUnmount() {
|
|
126
|
+
document.removeEventListener('mousedown', this.handleClickOutside);
|
|
127
|
+
|
|
128
|
+
// Unsubscribe from CSS Color Component events
|
|
129
|
+
var inputEl = document.querySelector('#Draftail--highlight-color-input');
|
|
130
|
+
if (inputEl) {
|
|
131
|
+
inputEl.removeEventListener('open', this.handleInputFocus);
|
|
132
|
+
inputEl.removeEventListener('close', this.handleInputBlur);
|
|
133
|
+
inputEl.removeEventListener('change', this.handleInputChange);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
componentDidUpdate() {
|
|
138
|
+
this.syncInputValue();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// helpers
|
|
142
|
+
getActiveHighlightColor() {
|
|
143
|
+
return getActiveHighlightColor(this.props.getEditorState());
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
syncInputValue() {
|
|
147
|
+
// While the user is editing, never overwrite their input
|
|
148
|
+
if (this.state.isInputFocused || this.state.isDropdownOpen) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
var activeHighlightColor = this.getActiveHighlightColor();
|
|
153
|
+
var newValue = activeHighlightColor || '';
|
|
154
|
+
|
|
155
|
+
if (this.state.inputValue !== newValue) {
|
|
156
|
+
this.setState({ inputValue: newValue });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
toggleDropdown(force) {
|
|
161
|
+
const willBeOpen =
|
|
162
|
+
typeof force === 'boolean' ? force : !this.state.isDropdownOpen;
|
|
163
|
+
if (willBeOpen && !this.state.isDropdownOpen) {
|
|
164
|
+
saveSelection(this.props.getEditorState);
|
|
165
|
+
}
|
|
166
|
+
this.setState({ isDropdownOpen: willBeOpen });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
applyHighlightColor(newColor) {
|
|
170
|
+
var backgroundColor = newColor === 'transparent' ? '#00000000' : newColor;
|
|
171
|
+
var editorState = restoreSelection(this.props.getEditorState());
|
|
172
|
+
var currentColor = this.getActiveHighlightColor(editorState);
|
|
173
|
+
editorState = removeHighlightColorEntity(editorState);
|
|
174
|
+
|
|
175
|
+
if (backgroundColor === currentColor) {
|
|
176
|
+
// Re-sync the input value
|
|
177
|
+
this.props.onChange(editorState);
|
|
178
|
+
this.setState({ isDropdownOpen: false, inputValue: '' });
|
|
179
|
+
this.syncInputValue();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
var selection = editorState.getSelection();
|
|
184
|
+
|
|
185
|
+
// Apply the entity to the selection
|
|
186
|
+
var contentWithEntity = editorState
|
|
187
|
+
.getCurrentContent()
|
|
188
|
+
.createEntity(entityType, 'MUTABLE', {
|
|
189
|
+
backgroundColor,
|
|
190
|
+
});
|
|
191
|
+
var contentStateWithEntity = Modifier.applyEntity(
|
|
192
|
+
contentWithEntity,
|
|
193
|
+
selection,
|
|
194
|
+
contentWithEntity.getLastCreatedEntityKey(),
|
|
195
|
+
);
|
|
196
|
+
var newContent = EditorState.push(
|
|
197
|
+
editorState,
|
|
198
|
+
contentStateWithEntity,
|
|
199
|
+
'apply-entity',
|
|
200
|
+
);
|
|
201
|
+
this.props.onChange(newContent);
|
|
202
|
+
|
|
203
|
+
// Re-sync the input value
|
|
204
|
+
this.setState({
|
|
205
|
+
isDropdownOpen: false,
|
|
206
|
+
inputValue: String(backgroundColor),
|
|
207
|
+
});
|
|
208
|
+
this.syncInputValue();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
handleInputFocus = () => {
|
|
212
|
+
saveSelection(this.props.getEditorState);
|
|
213
|
+
this.setState({ isInputFocused: true });
|
|
214
|
+
if (!this.state.isDropdownOpen) {
|
|
215
|
+
this.setState({ isDropdownOpen: true });
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
handleInputBlur = () => {
|
|
220
|
+
// When leaving the field, clear the focus flag and re‑sync (will clear if no style)
|
|
221
|
+
this.setState({ isInputFocused: false }, () => {
|
|
222
|
+
this.syncInputValue();
|
|
223
|
+
});
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
handleInputChange = (e) => {
|
|
227
|
+
this.applyHighlightColor(e.detail.value);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
handleInputKeyDown = (e) => {
|
|
231
|
+
if (e.key === 'Escape') {
|
|
232
|
+
this.toggleDropdown(false);
|
|
233
|
+
this.syncInputValue();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (e.key === 'Enter') {
|
|
238
|
+
e.preventDefault();
|
|
239
|
+
this.applyHighlightColor(this.state.inputValue);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
handleClickOutside = (event) => {
|
|
245
|
+
if (
|
|
246
|
+
this.controlRef.current &&
|
|
247
|
+
!this.controlRef.current.contains(event.target)
|
|
248
|
+
) {
|
|
249
|
+
this.setState({ isDropdownOpen: false, isInputFocused: false });
|
|
250
|
+
var inputEl = document.querySelector(
|
|
251
|
+
'#Draftail--highlight-color-input',
|
|
252
|
+
);
|
|
253
|
+
if (inputEl) {
|
|
254
|
+
inputEl.close();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
createOptElement = (opt) => {
|
|
260
|
+
return React.createElement(
|
|
261
|
+
'li',
|
|
262
|
+
{
|
|
263
|
+
key: `HIGHLIGHT_COLOR_OPT_${opt.key.toUpperCase()}`,
|
|
264
|
+
className: 'Draftail--highlight-color-option-wrapper',
|
|
265
|
+
},
|
|
266
|
+
React.createElement(ToolbarButton, {
|
|
267
|
+
name: opt.value === 'transparent' ? '#00000000' : opt.value,
|
|
268
|
+
active: this.getActiveHighlightColor() === opt.value,
|
|
269
|
+
title: opt.label,
|
|
270
|
+
label: '\u2713',
|
|
271
|
+
tooltipDirection: 'up',
|
|
272
|
+
className: 'Draftail--highlight-color-option',
|
|
273
|
+
onClick: () => this.applyHighlightColor(opt.value),
|
|
274
|
+
}),
|
|
275
|
+
);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// render
|
|
279
|
+
render() {
|
|
280
|
+
const icon = `#icon-${control.icon}`;
|
|
281
|
+
|
|
282
|
+
var activeHighlightColor = this.getActiveHighlightColor();
|
|
283
|
+
var inputDisplay = this.state.inputValue;
|
|
284
|
+
var isDropdownExpanded = this.state.isDropdownOpen;
|
|
285
|
+
|
|
286
|
+
var input = React.createElement(
|
|
287
|
+
'li',
|
|
288
|
+
{
|
|
289
|
+
'key': 'HIGHLIGHT_COLOR_CTRL_INPUT_WRAPPER',
|
|
290
|
+
'className': 'Draftail--highlight-color-input-wrapper',
|
|
291
|
+
'data-draftail-balloon': 'down',
|
|
292
|
+
'aria-label': 'Select custom highlight color',
|
|
293
|
+
},
|
|
294
|
+
React.createElement('color-input', {
|
|
295
|
+
key: 'HIGHLIGHT_COLOR_CTRL_INPUT',
|
|
296
|
+
id: 'Draftail--highlight-color-input',
|
|
297
|
+
className: 'Draftail--highlight-color-input',
|
|
298
|
+
defaultValue: activeHighlightColor || '#000000',
|
|
299
|
+
value: inputDisplay || '#000000',
|
|
300
|
+
onKeyDown: this.handleInputKeyDown,
|
|
301
|
+
}),
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
var hr = React.createElement('hr', {
|
|
305
|
+
key: 'HIGHLIGHT_COLOR_CTRL_HR',
|
|
306
|
+
className: 'Draftail--highlight-color-hr',
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
var dropdown = React.createElement(
|
|
310
|
+
'ul',
|
|
311
|
+
{
|
|
312
|
+
'id': 'Draftail--highlight-color-dropdown',
|
|
313
|
+
'className': 'Draftail--highlight-color-dropdown',
|
|
314
|
+
'aria-expanded': isDropdownExpanded,
|
|
315
|
+
},
|
|
316
|
+
options.map(this.createOptElement),
|
|
317
|
+
hr,
|
|
318
|
+
input,
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
// ── Toolbar button ──
|
|
322
|
+
var button = React.createElement(ToolbarButton, {
|
|
323
|
+
name: control.type.toUpperCase(),
|
|
324
|
+
active: !!activeHighlightColor,
|
|
325
|
+
title: control.label,
|
|
326
|
+
icon: icon,
|
|
327
|
+
tooltipDirection: 'up',
|
|
328
|
+
onClick: () => this.toggleDropdown(),
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
return React.createElement(
|
|
332
|
+
'div',
|
|
333
|
+
{
|
|
334
|
+
key: 'HIGHLIGHT_COLOR_CTRL',
|
|
335
|
+
className: 'Draftail--highlight-color-control',
|
|
336
|
+
ref: this.controlRef,
|
|
337
|
+
},
|
|
338
|
+
dropdown,
|
|
339
|
+
button,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
window.draftail.registerPlugin(
|
|
344
|
+
{
|
|
345
|
+
type: control.type,
|
|
346
|
+
inline: HighlightColorControl,
|
|
347
|
+
},
|
|
348
|
+
'controls',
|
|
349
|
+
);
|
|
350
|
+
})();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Highlight color entity for Draftail.
|
|
3
|
+
* Registers an entity type (HIGHLIGHT_COLOR) that stores an arbitrary highlight color.
|
|
4
|
+
* The entity is applied by `highlight-color` control when a custom highlight color is entered.
|
|
5
|
+
* See:`src/draftail_text_utils/static/draftail_text_utils/js/highlight_color.js`
|
|
6
|
+
*/
|
|
7
|
+
(function () {
|
|
8
|
+
const React = window.React;
|
|
9
|
+
const ENTITY_TYPE = 'HIGHLIGHT_COLOR';
|
|
10
|
+
const STYLE_KEY = 'style';
|
|
11
|
+
const DATASET_KEY = 'data-entity-type';
|
|
12
|
+
|
|
13
|
+
const HighlightColorDecorator = ({ children, contentState, entityKey }) => {
|
|
14
|
+
const entity = contentState.getEntity(entityKey);
|
|
15
|
+
const { backgroundColor: entityColor } = entity.getData();
|
|
16
|
+
if (!entityColor) return children;
|
|
17
|
+
|
|
18
|
+
const backgroundColor =
|
|
19
|
+
entityColor == 'transparent' ? '#00000000' : entityColor;
|
|
20
|
+
return React.createElement(
|
|
21
|
+
'span',
|
|
22
|
+
{
|
|
23
|
+
[STYLE_KEY]: { backgroundColor },
|
|
24
|
+
[DATASET_KEY]: ENTITY_TYPE,
|
|
25
|
+
},
|
|
26
|
+
children,
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const HighlightColorSource = ({ onClose }) => {
|
|
31
|
+
onClose();
|
|
32
|
+
return null;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
window.draftail.registerPlugin(
|
|
36
|
+
{
|
|
37
|
+
type: ENTITY_TYPE,
|
|
38
|
+
source: HighlightColorSource,
|
|
39
|
+
decorator: HighlightColorDecorator,
|
|
40
|
+
},
|
|
41
|
+
'entityTypes',
|
|
42
|
+
);
|
|
43
|
+
})();
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// https://github.com/wagtail/draftail/blob/main/src/components/Toolbar/ToolbarButton.tsx
|
|
2
|
+
// export interface ToolbarButtonProps {
|
|
3
|
+
// name?: string;
|
|
4
|
+
// active?: boolean;
|
|
5
|
+
// label?: string | null;
|
|
6
|
+
// title?: string | null;
|
|
7
|
+
// icon?: IconProp | null;
|
|
8
|
+
// className?: string | null;
|
|
9
|
+
// tooltipDirection?: "up" | "down";
|
|
10
|
+
// onClick?: ((name: string) => void) | null;
|
|
11
|
+
// }
|
|
12
|
+
|
|
13
|
+
(function () {
|
|
14
|
+
var React = window.React;
|
|
15
|
+
var RichUtils = window.DraftJS.RichUtils;
|
|
16
|
+
var ToolbarButton = window.Draftail.ToolbarButton;
|
|
17
|
+
var data = window.draftailTextUtils || {};
|
|
18
|
+
const type_ = 'text-alignment';
|
|
19
|
+
const control = JSON.parse(
|
|
20
|
+
document.getElementById(`draftail-plugin-control-${type_}`).textContent,
|
|
21
|
+
);
|
|
22
|
+
const options = control.data || [];
|
|
23
|
+
|
|
24
|
+
// ── Helper to find active text alignment ──
|
|
25
|
+
function getActiveOption(editorState) {
|
|
26
|
+
const selection = editorState.getSelection();
|
|
27
|
+
const block = editorState
|
|
28
|
+
.getCurrentContent()
|
|
29
|
+
.getBlockForKey(selection.getStartKey());
|
|
30
|
+
const blockType = block.getType();
|
|
31
|
+
return options.find((opt) => opt.type === blockType) || null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class TextAlignmentControl extends React.Component {
|
|
35
|
+
constructor(props) {
|
|
36
|
+
super(props);
|
|
37
|
+
this.state = {
|
|
38
|
+
isOpen: false,
|
|
39
|
+
};
|
|
40
|
+
this.controlRef = React.createRef();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
componentDidMount() {
|
|
44
|
+
document.addEventListener('mousedown', this.handleClickOutside);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
componentWillUnmount() {
|
|
48
|
+
document.removeEventListener('mousedown', this.handleClickOutside);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── close dropdown when clicking outside ──
|
|
52
|
+
handleClickOutside = (event) => {
|
|
53
|
+
if (
|
|
54
|
+
this.controlRef.current &&
|
|
55
|
+
!this.controlRef.current.contains(event.target)
|
|
56
|
+
) {
|
|
57
|
+
this.setState({ isOpen: false });
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// ── toggle dropdown (used by button) ──
|
|
62
|
+
toggleDropdown = (force) => {
|
|
63
|
+
this.setState((prevState) => ({
|
|
64
|
+
isOpen: typeof force === 'boolean' ? force : !prevState.isOpen,
|
|
65
|
+
}));
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// ── apply a text alignment ──
|
|
69
|
+
handleOptionClick = (opt) => {
|
|
70
|
+
const { getEditorState, onChange } = this.props;
|
|
71
|
+
const editorState = getEditorState();
|
|
72
|
+
const newState = RichUtils.toggleBlockType(editorState, opt.type);
|
|
73
|
+
onChange(newState);
|
|
74
|
+
this.setState({ isOpen: false });
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
render() {
|
|
78
|
+
const { getEditorState } = this.props;
|
|
79
|
+
const editorState = getEditorState();
|
|
80
|
+
const activeOption = getActiveOption(editorState);
|
|
81
|
+
const isOpen = this.state.isOpen;
|
|
82
|
+
const icon = `#icon-${activeOption ? activeOption.icon : control.icon}`;
|
|
83
|
+
|
|
84
|
+
// ── Dropdown list ──
|
|
85
|
+
const dropdown = React.createElement(
|
|
86
|
+
'ul',
|
|
87
|
+
{
|
|
88
|
+
'className': 'Draftail--text-alignment-dropdown',
|
|
89
|
+
'aria-expanded': isOpen,
|
|
90
|
+
},
|
|
91
|
+
options.map((opt) =>
|
|
92
|
+
React.createElement(
|
|
93
|
+
'li',
|
|
94
|
+
{
|
|
95
|
+
key: opt.type,
|
|
96
|
+
},
|
|
97
|
+
React.createElement(ToolbarButton, {
|
|
98
|
+
name: opt.type.toUpperCase(),
|
|
99
|
+
active: activeOption ? activeOption.type === opt.type : false,
|
|
100
|
+
title: opt.label,
|
|
101
|
+
className: 'Draftail--text-alignment-option',
|
|
102
|
+
icon: `#icon-${opt.icon}`,
|
|
103
|
+
tooltipDirection: 'up',
|
|
104
|
+
onClick: () => this.handleOptionClick(opt),
|
|
105
|
+
}),
|
|
106
|
+
),
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// ── Toolbar button ──
|
|
111
|
+
const button = React.createElement(ToolbarButton, {
|
|
112
|
+
name: control.type.toUpperCase(),
|
|
113
|
+
active: !!activeOption,
|
|
114
|
+
title: control.label,
|
|
115
|
+
icon: icon,
|
|
116
|
+
tooltipDirection: 'up',
|
|
117
|
+
onClick: () => this.toggleDropdown(),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return React.createElement(
|
|
121
|
+
'div',
|
|
122
|
+
{
|
|
123
|
+
className: 'Draftail--text-alignment-control',
|
|
124
|
+
ref: this.controlRef,
|
|
125
|
+
},
|
|
126
|
+
dropdown,
|
|
127
|
+
button,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
window.draftail.registerPlugin(
|
|
133
|
+
{
|
|
134
|
+
type: control.type,
|
|
135
|
+
inline: TextAlignmentControl,
|
|
136
|
+
},
|
|
137
|
+
'controls',
|
|
138
|
+
);
|
|
139
|
+
})();
|