react-bootstrap-front 3.10.2 → 3.11.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/CHANGELOG +9 -0
- package/dist/react-bootstrap-front.js +14 -14
- package/doc/editor/feuille-de-route.md +36 -0
- package/package.json +42 -8
- package/src/features/advanced/DropdownWrapper.js +12 -2
- package/src/features/basic/Dropdown.js +112 -31
- package/src/features/basic/computePlacement.js +66 -0
- package/src/features/editor/HtmlEditor.js +681 -0
- package/src/features/editor/HtmlEditor.scss +189 -0
- package/src/features/editor/ImageDialog.js +195 -0
- package/src/features/editor/LinkDialog.js +148 -0
- package/src/features/editor/MergeMenu.js +120 -0
- package/src/features/editor/SourceView.js +61 -0
- package/src/features/editor/Toolbar.js +355 -0
- package/src/features/editor/Toolbar.scss +130 -0
- package/src/features/editor/ToolbarButton.js +59 -0
- package/src/features/editor/ToolbarDropdown.js +46 -0
- package/src/features/editor/ToolbarGroup.js +27 -0
- package/src/features/editor/buildExtensions.js +77 -0
- package/src/features/editor/defaultIcons.js +45 -0
- package/src/features/editor/extensions/CleanPaste.js +44 -0
- package/src/features/editor/extensions/FontSize.js +60 -0
- package/src/features/editor/extensions/LegacyImageAttrs.js +43 -0
- package/src/features/editor/extensions/MergeVariable.js +104 -0
- package/src/features/editor/extensions/index.js +4 -0
- package/src/features/editor/html/cleanHtml.constants.js +279 -0
- package/src/features/editor/html/cleanHtml.js +691 -0
- package/src/features/editor/html/index.js +4 -0
- package/src/features/editor/html/normalizeLegacyHtml.js +196 -0
- package/src/features/editor/html/serialize.js +130 -0
- package/src/features/editor/html/sourceHelpers.js +86 -0
- package/src/features/editor/index.js +20 -0
- package/src/features/editor/style.scss +2 -0
- package/src/features/editor/toolbarPresets.js +74 -0
- package/src/features/page/DefaultHeader.jsx +1 -1
- package/src/index.js +8 -0
- package/src/styles/index.scss +1 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Édition du HTML brut.
|
|
6
|
+
*
|
|
7
|
+
* Le champ remonte directement la saisie au formulaire : enregistrer depuis la
|
|
8
|
+
* vue source doit fonctionner, sans exiger un retour préalable au mode riche.
|
|
9
|
+
*/
|
|
10
|
+
export default function SourceView(props) {
|
|
11
|
+
const { value, onChange, warning, onRestore, onDismiss, style, t } = props;
|
|
12
|
+
return (
|
|
13
|
+
<div className="rbf-editor-source">
|
|
14
|
+
{warning && (
|
|
15
|
+
<div className="alert alert-warning alert-sm rbf-editor-source-warning" role="alert">
|
|
16
|
+
<span>
|
|
17
|
+
{warning === 'unbalanced'
|
|
18
|
+
? t({
|
|
19
|
+
id: 'ui.editor.source.unbalanced',
|
|
20
|
+
defaultMessage: 'The HTML was not balanced and has been corrected automatically.',
|
|
21
|
+
})
|
|
22
|
+
: t({
|
|
23
|
+
id: 'ui.editor.source.unsupported',
|
|
24
|
+
defaultMessage: 'Some tags are not supported by the editor and have been removed.',
|
|
25
|
+
})}
|
|
26
|
+
</span>
|
|
27
|
+
<button type="button" className="btn btn-sm btn-link" onClick={onRestore}>
|
|
28
|
+
{t({ id: 'ui.editor.source.back', defaultMessage: 'Back to the source' })}
|
|
29
|
+
</button>
|
|
30
|
+
<button type="button" className="btn btn-sm btn-link" onClick={onDismiss}>
|
|
31
|
+
{t({ id: 'ui.editor.source.dismiss', defaultMessage: 'Keep the correction' })}
|
|
32
|
+
</button>
|
|
33
|
+
</div>
|
|
34
|
+
)}
|
|
35
|
+
<textarea
|
|
36
|
+
className="form-control border-0 text-monospace rbf-editor-source-text"
|
|
37
|
+
style={style}
|
|
38
|
+
value={value}
|
|
39
|
+
spellCheck="false"
|
|
40
|
+
onChange={onChange}
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
SourceView.propTypes = {
|
|
47
|
+
value: PropTypes.string.isRequired,
|
|
48
|
+
onChange: PropTypes.func.isRequired,
|
|
49
|
+
warning: PropTypes.string,
|
|
50
|
+
onRestore: PropTypes.func,
|
|
51
|
+
onDismiss: PropTypes.func,
|
|
52
|
+
style: PropTypes.object,
|
|
53
|
+
t: PropTypes.func.isRequired,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
SourceView.defaultProps = {
|
|
57
|
+
warning: null,
|
|
58
|
+
onRestore: () => {},
|
|
59
|
+
onDismiss: () => {},
|
|
60
|
+
style: null,
|
|
61
|
+
};
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import React, { Component } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import ToolbarButton from './ToolbarButton';
|
|
4
|
+
import ToolbarGroup from './ToolbarGroup';
|
|
5
|
+
import ToolbarDropdown from './ToolbarDropdown';
|
|
6
|
+
import MergeMenu, { normalizeMergeVars } from './MergeMenu';
|
|
7
|
+
import { FONT_SIZES, FONT_FAMILIES, COLORS, OPTIONAL_KEYS } from './toolbarPresets';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Barre d'outils.
|
|
11
|
+
*
|
|
12
|
+
* Elle ne détient aucun état : l'état actif est lu directement dans l'éditeur
|
|
13
|
+
* (`editor.isActive(...)`), et le parent la re-rend à chaque transaction.
|
|
14
|
+
*
|
|
15
|
+
* Toute commande commence par `.focus()`, sans quoi un clic passé par un menu
|
|
16
|
+
* déroulant appliquerait la commande hors de la sélection.
|
|
17
|
+
*/
|
|
18
|
+
export default class Toolbar extends Component {
|
|
19
|
+
static propTypes = {
|
|
20
|
+
editor: PropTypes.object,
|
|
21
|
+
config: PropTypes.array.isRequired,
|
|
22
|
+
icons: PropTypes.object.isRequired,
|
|
23
|
+
t: PropTypes.func.isRequired,
|
|
24
|
+
size: PropTypes.string,
|
|
25
|
+
mergeVars: PropTypes.array,
|
|
26
|
+
sourceMode: PropTypes.bool,
|
|
27
|
+
/** Appelé après chaque commande, pour remonter la saisie au formulaire. */
|
|
28
|
+
onCommand: PropTypes.func.isRequired,
|
|
29
|
+
onLink: PropTypes.func.isRequired,
|
|
30
|
+
onImage: PropTypes.func.isRequired,
|
|
31
|
+
onClean: PropTypes.func.isRequired,
|
|
32
|
+
onSource: PropTypes.func.isRequired,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
static defaultProps = {
|
|
36
|
+
editor: null,
|
|
37
|
+
size: 'sm',
|
|
38
|
+
mergeVars: [],
|
|
39
|
+
sourceMode: false,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
constructor(props) {
|
|
43
|
+
super(props);
|
|
44
|
+
this.run = this.run.bind(this);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Exécute une commande puis remonte la saisie.
|
|
49
|
+
*/
|
|
50
|
+
run(build) {
|
|
51
|
+
const { editor } = this.props;
|
|
52
|
+
if (!editor) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
build(editor.chain().focus()).run();
|
|
56
|
+
this.props.onCommand();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
isActive(...args) {
|
|
60
|
+
const { editor } = this.props;
|
|
61
|
+
return !!editor && editor.isActive(...args);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
label(id, defaultMessage) {
|
|
65
|
+
return this.props.t({ id: 'ui.editor.toolbar.' + id, defaultMessage });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
renderSimple(key, title, icon, active, onClick, disabled) {
|
|
69
|
+
return (
|
|
70
|
+
<ToolbarButton
|
|
71
|
+
key={key}
|
|
72
|
+
icon={icon}
|
|
73
|
+
title={title}
|
|
74
|
+
active={active}
|
|
75
|
+
disabled={disabled}
|
|
76
|
+
size={this.props.size}
|
|
77
|
+
onClick={onClick}
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
renderColor(key) {
|
|
83
|
+
const { icons, editor } = this.props;
|
|
84
|
+
const current = editor ? editor.getAttributes('textStyle').color : null;
|
|
85
|
+
return (
|
|
86
|
+
<ToolbarDropdown
|
|
87
|
+
key={key}
|
|
88
|
+
icon={icons.color}
|
|
89
|
+
title={this.label('color', 'Text color')}
|
|
90
|
+
active={!!current}
|
|
91
|
+
size={this.props.size}
|
|
92
|
+
>
|
|
93
|
+
<div className="rbf-editor-colors">
|
|
94
|
+
<div className="rbf-editor-swatches">
|
|
95
|
+
{COLORS.map(color => (
|
|
96
|
+
<button
|
|
97
|
+
key={color}
|
|
98
|
+
type="button"
|
|
99
|
+
className="rbf-editor-swatch"
|
|
100
|
+
style={{ backgroundColor: color }}
|
|
101
|
+
title={color}
|
|
102
|
+
onMouseDown={ev => ev.preventDefault()}
|
|
103
|
+
onClick={() => this.run(chain => chain.setColor(color))}
|
|
104
|
+
/>
|
|
105
|
+
))}
|
|
106
|
+
</div>
|
|
107
|
+
<button
|
|
108
|
+
type="button"
|
|
109
|
+
className="dropdown-item"
|
|
110
|
+
onMouseDown={ev => ev.preventDefault()}
|
|
111
|
+
onClick={() => this.run(chain => chain.unsetColor())}
|
|
112
|
+
>
|
|
113
|
+
{this.label('colorReset', 'Default color')}
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
</ToolbarDropdown>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
renderFontSize(key) {
|
|
121
|
+
const { icons, editor } = this.props;
|
|
122
|
+
const current = editor ? editor.getAttributes('textStyle').fontSize : null;
|
|
123
|
+
return (
|
|
124
|
+
<ToolbarDropdown
|
|
125
|
+
key={key}
|
|
126
|
+
icon={icons.fontSize}
|
|
127
|
+
title={this.label('fontSize', 'Font size')}
|
|
128
|
+
active={!!current}
|
|
129
|
+
size={this.props.size}
|
|
130
|
+
>
|
|
131
|
+
<div className="rbf-editor-sizes">
|
|
132
|
+
{FONT_SIZES.map(size => (
|
|
133
|
+
<button
|
|
134
|
+
key={size}
|
|
135
|
+
type="button"
|
|
136
|
+
className="dropdown-item"
|
|
137
|
+
style={{ fontSize: size }}
|
|
138
|
+
onMouseDown={ev => ev.preventDefault()}
|
|
139
|
+
onClick={() => this.run(chain => chain.setFontSize(size))}
|
|
140
|
+
>
|
|
141
|
+
{size}
|
|
142
|
+
</button>
|
|
143
|
+
))}
|
|
144
|
+
<button
|
|
145
|
+
type="button"
|
|
146
|
+
className="dropdown-item"
|
|
147
|
+
onMouseDown={ev => ev.preventDefault()}
|
|
148
|
+
onClick={() => this.run(chain => chain.unsetFontSize())}
|
|
149
|
+
>
|
|
150
|
+
{this.label('fontSizeReset', 'Default size')}
|
|
151
|
+
</button>
|
|
152
|
+
</div>
|
|
153
|
+
</ToolbarDropdown>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
renderFontFamily(key) {
|
|
158
|
+
const { icons, editor } = this.props;
|
|
159
|
+
const current = editor ? editor.getAttributes('textStyle').fontFamily : null;
|
|
160
|
+
return (
|
|
161
|
+
<ToolbarDropdown
|
|
162
|
+
key={key}
|
|
163
|
+
icon={icons.fontFamily}
|
|
164
|
+
title={this.label('fontFamily', 'Font')}
|
|
165
|
+
active={!!current}
|
|
166
|
+
size={this.props.size}
|
|
167
|
+
>
|
|
168
|
+
<div className="rbf-editor-fonts">
|
|
169
|
+
{FONT_FAMILIES.map(font => (
|
|
170
|
+
<button
|
|
171
|
+
key={font}
|
|
172
|
+
type="button"
|
|
173
|
+
className="dropdown-item"
|
|
174
|
+
style={{ fontFamily: font }}
|
|
175
|
+
onMouseDown={ev => ev.preventDefault()}
|
|
176
|
+
onClick={() => this.run(chain => chain.setFontFamily(font))}
|
|
177
|
+
>
|
|
178
|
+
{font}
|
|
179
|
+
</button>
|
|
180
|
+
))}
|
|
181
|
+
<button
|
|
182
|
+
type="button"
|
|
183
|
+
className="dropdown-item"
|
|
184
|
+
onMouseDown={ev => ev.preventDefault()}
|
|
185
|
+
onClick={() => this.run(chain => chain.unsetFontFamily())}
|
|
186
|
+
>
|
|
187
|
+
{this.label('fontFamilyReset', 'Default font')}
|
|
188
|
+
</button>
|
|
189
|
+
</div>
|
|
190
|
+
</ToolbarDropdown>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
renderMergeVar(key) {
|
|
195
|
+
const { icons, t, mergeVars } = this.props;
|
|
196
|
+
const groups = normalizeMergeVars(mergeVars, t({ id: 'ui.editor.mergevar.title', defaultMessage: 'Variables' }));
|
|
197
|
+
if (groups.length === 0) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
return (
|
|
201
|
+
<ToolbarDropdown
|
|
202
|
+
key={key}
|
|
203
|
+
icon={icons.mergeVar}
|
|
204
|
+
title={this.label('mergeVar', 'Insert a merge variable')}
|
|
205
|
+
size={this.props.size}
|
|
206
|
+
className="rbf-editor-dropdown-wide"
|
|
207
|
+
>
|
|
208
|
+
<MergeMenu
|
|
209
|
+
groups={groups}
|
|
210
|
+
t={t}
|
|
211
|
+
onSelect={item =>
|
|
212
|
+
/* Insertion en texte brut : le HTML produit reste identique à celui
|
|
213
|
+
de l'ancien éditeur, aucune migration ni changement côté serveur. */
|
|
214
|
+
this.run(chain => chain.insertContent({ type: 'text', text: item.code }))
|
|
215
|
+
}
|
|
216
|
+
/>
|
|
217
|
+
</ToolbarDropdown>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
renderKey(key) {
|
|
222
|
+
const { icons, editor } = this.props;
|
|
223
|
+
const can = editor ? editor.can() : null;
|
|
224
|
+
|
|
225
|
+
switch (key) {
|
|
226
|
+
case 'undo':
|
|
227
|
+
return this.renderSimple(key, this.label('undo', 'Undo'), icons.undo, false, () => this.run(c => c.undo()), !can || !can.undo());
|
|
228
|
+
case 'redo':
|
|
229
|
+
return this.renderSimple(key, this.label('redo', 'Redo'), icons.redo, false, () => this.run(c => c.redo()), !can || !can.redo());
|
|
230
|
+
case 'paragraph':
|
|
231
|
+
return this.renderSimple(key, this.label('paragraph', 'Paragraph'), icons.paragraph, this.isActive('paragraph'), () =>
|
|
232
|
+
this.run(c => c.setParagraph()),
|
|
233
|
+
);
|
|
234
|
+
case 'heading1':
|
|
235
|
+
case 'heading2':
|
|
236
|
+
case 'heading3': {
|
|
237
|
+
const level = parseInt(key.slice(-1), 10);
|
|
238
|
+
return this.renderSimple(
|
|
239
|
+
key,
|
|
240
|
+
this.label(key, 'Heading ' + level),
|
|
241
|
+
icons[key],
|
|
242
|
+
this.isActive('heading', { level }),
|
|
243
|
+
() => this.run(c => c.toggleHeading({ level })),
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
case 'bold':
|
|
247
|
+
return this.renderSimple(key, this.label('bold', 'Bold'), icons.bold, this.isActive('bold'), () => this.run(c => c.toggleBold()));
|
|
248
|
+
case 'italic':
|
|
249
|
+
return this.renderSimple(key, this.label('italic', 'Italic'), icons.italic, this.isActive('italic'), () =>
|
|
250
|
+
this.run(c => c.toggleItalic()),
|
|
251
|
+
);
|
|
252
|
+
case 'underline':
|
|
253
|
+
return this.renderSimple(key, this.label('underline', 'Underline'), icons.underline, this.isActive('underline'), () =>
|
|
254
|
+
this.run(c => c.toggleUnderline()),
|
|
255
|
+
);
|
|
256
|
+
case 'strike':
|
|
257
|
+
return this.renderSimple(key, this.label('strike', 'Strikethrough'), icons.strike, this.isActive('strike'), () =>
|
|
258
|
+
this.run(c => c.toggleStrike()),
|
|
259
|
+
);
|
|
260
|
+
case 'subscript':
|
|
261
|
+
return this.renderSimple(key, this.label('subscript', 'Subscript'), icons.subscript, this.isActive('subscript'), () =>
|
|
262
|
+
this.run(c => c.toggleSubscript()),
|
|
263
|
+
);
|
|
264
|
+
case 'superscript':
|
|
265
|
+
return this.renderSimple(key, this.label('superscript', 'Superscript'), icons.superscript, this.isActive('superscript'), () =>
|
|
266
|
+
this.run(c => c.toggleSuperscript()),
|
|
267
|
+
);
|
|
268
|
+
case 'bulletList':
|
|
269
|
+
return this.renderSimple(key, this.label('bulletList', 'Bullet list'), icons.bulletList, this.isActive('bulletList'), () =>
|
|
270
|
+
this.run(c => c.toggleBulletList()),
|
|
271
|
+
);
|
|
272
|
+
case 'orderedList':
|
|
273
|
+
return this.renderSimple(key, this.label('orderedList', 'Numbered list'), icons.orderedList, this.isActive('orderedList'), () =>
|
|
274
|
+
this.run(c => c.toggleOrderedList()),
|
|
275
|
+
);
|
|
276
|
+
case 'blockquote':
|
|
277
|
+
return this.renderSimple(key, this.label('blockquote', 'Quote'), icons.blockquote, this.isActive('blockquote'), () =>
|
|
278
|
+
this.run(c => c.toggleBlockquote()),
|
|
279
|
+
);
|
|
280
|
+
case 'horizontalRule':
|
|
281
|
+
return this.renderSimple(key, this.label('horizontalRule', 'Horizontal rule'), icons.horizontalRule, false, () =>
|
|
282
|
+
this.run(c => c.setHorizontalRule()),
|
|
283
|
+
);
|
|
284
|
+
case 'alignLeft':
|
|
285
|
+
case 'alignCenter':
|
|
286
|
+
case 'alignRight':
|
|
287
|
+
case 'alignJustify': {
|
|
288
|
+
const align = key.slice(5).toLowerCase();
|
|
289
|
+
return this.renderSimple(key, this.label(key, 'Align ' + align), icons[key], this.isActive({ textAlign: align }), () =>
|
|
290
|
+
this.run(c => c.setTextAlign(align)),
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
case 'color':
|
|
294
|
+
return this.renderColor(key);
|
|
295
|
+
case 'fontSize':
|
|
296
|
+
return this.renderFontSize(key);
|
|
297
|
+
case 'fontFamily':
|
|
298
|
+
return this.renderFontFamily(key);
|
|
299
|
+
case 'link':
|
|
300
|
+
return this.renderSimple(key, this.label('link', 'Insert a link'), icons.link, this.isActive('link'), this.props.onLink);
|
|
301
|
+
case 'unlink':
|
|
302
|
+
return this.renderSimple(
|
|
303
|
+
key,
|
|
304
|
+
this.label('unlink', 'Remove the link'),
|
|
305
|
+
icons.unlink,
|
|
306
|
+
false,
|
|
307
|
+
() => this.run(c => c.unsetLink()),
|
|
308
|
+
!this.isActive('link'),
|
|
309
|
+
);
|
|
310
|
+
case 'image':
|
|
311
|
+
return this.renderSimple(key, this.label('image', 'Insert an image'), icons.image, false, this.props.onImage);
|
|
312
|
+
case 'mergeVar':
|
|
313
|
+
return this.renderMergeVar(key);
|
|
314
|
+
case 'clean':
|
|
315
|
+
return this.renderSimple(key, this.label('clean', 'Clean up formatting'), icons.clean, false, () => this.props.onClean('soft'));
|
|
316
|
+
case 'formatClear':
|
|
317
|
+
return this.renderSimple(key, this.label('formatClear', 'Remove all formatting'), icons.formatClear, false, () =>
|
|
318
|
+
this.props.onClean('all'),
|
|
319
|
+
);
|
|
320
|
+
case 'source':
|
|
321
|
+
return this.renderSimple(
|
|
322
|
+
key,
|
|
323
|
+
this.label('source', 'HTML source'),
|
|
324
|
+
icons.source,
|
|
325
|
+
this.props.sourceMode,
|
|
326
|
+
this.props.onSource,
|
|
327
|
+
);
|
|
328
|
+
default:
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
render() {
|
|
334
|
+
const { config, sourceMode } = this.props;
|
|
335
|
+
return (
|
|
336
|
+
<div className="rbf-editor-toolbar d-flex flex-wrap align-items-center" role="toolbar">
|
|
337
|
+
{config.map((group, index) => {
|
|
338
|
+
/* En vue source, seuls le nettoyage et le retour au mode riche ont
|
|
339
|
+
un sens : les commandes de mise en forme n'ont pas de sélection. */
|
|
340
|
+
const keys = sourceMode ? group.filter(key => key === 'source' || key === 'clean') : group;
|
|
341
|
+
const buttons = keys.map(key => this.renderKey(key)).filter(Boolean);
|
|
342
|
+
if (buttons.length === 0) {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
const optional = keys.every(key => OPTIONAL_KEYS.indexOf(key) !== -1);
|
|
346
|
+
return (
|
|
347
|
+
<ToolbarGroup key={index} optional={optional}>
|
|
348
|
+
{buttons}
|
|
349
|
+
</ToolbarGroup>
|
|
350
|
+
);
|
|
351
|
+
})}
|
|
352
|
+
</div>
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
.rbf-editor-toolbar {
|
|
2
|
+
padding: 2px;
|
|
3
|
+
border-bottom: 1px solid theme-color('secondary-light');
|
|
4
|
+
background-color: $gray-100;
|
|
5
|
+
|
|
6
|
+
.rbf-editor-group {
|
|
7
|
+
margin-right: 4px;
|
|
8
|
+
margin-bottom: 2px;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.rbf-editor-button {
|
|
12
|
+
line-height: 1;
|
|
13
|
+
min-width: 1.9rem;
|
|
14
|
+
border: 1px solid transparent;
|
|
15
|
+
|
|
16
|
+
&:hover {
|
|
17
|
+
background-color: $white;
|
|
18
|
+
border-color: theme-color('secondary-light');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
&.active {
|
|
22
|
+
background-color: theme-color('secondary-light');
|
|
23
|
+
border-color: theme-color('secondary');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
&:disabled {
|
|
27
|
+
opacity: 0.35;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.rbf-editor-dropdown {
|
|
33
|
+
max-height: 20rem;
|
|
34
|
+
overflow-y: auto;
|
|
35
|
+
|
|
36
|
+
&.rbf-editor-dropdown-wide {
|
|
37
|
+
min-width: 18rem;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Palette volontairement courte : dans un courriel, une charte tient en une
|
|
42
|
+
poignée de couleurs. */
|
|
43
|
+
.rbf-editor-swatches {
|
|
44
|
+
display: grid;
|
|
45
|
+
grid-template-columns: repeat(6, 1.25rem);
|
|
46
|
+
gap: 3px;
|
|
47
|
+
padding: 0.4rem 0.6rem;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.rbf-editor-swatch {
|
|
51
|
+
width: 1.25rem;
|
|
52
|
+
height: 1.25rem;
|
|
53
|
+
padding: 0;
|
|
54
|
+
border: 1px solid rgba(0, 0, 0, 0.2);
|
|
55
|
+
border-radius: 2px;
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
|
|
58
|
+
&:hover {
|
|
59
|
+
outline: 2px solid theme-color('primary');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.rbf-editor-mergemenu {
|
|
64
|
+
min-width: 18rem;
|
|
65
|
+
|
|
66
|
+
.rbf-editor-mergemenu-search {
|
|
67
|
+
padding: 0.4rem 0.6rem;
|
|
68
|
+
position: sticky;
|
|
69
|
+
top: 0;
|
|
70
|
+
background: $white;
|
|
71
|
+
z-index: 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.rbf-editor-mergemenu-list {
|
|
75
|
+
max-height: 16rem;
|
|
76
|
+
overflow-y: auto;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.rbf-editor-mergemenu-item {
|
|
80
|
+
white-space: normal;
|
|
81
|
+
|
|
82
|
+
.rbf-editor-mergemenu-label {
|
|
83
|
+
font-weight: 500;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
small {
|
|
87
|
+
font-family: $font-family-monospace;
|
|
88
|
+
font-size: 0.7rem;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.rbf-editor-dialog {
|
|
94
|
+
padding: 0.5rem;
|
|
95
|
+
border-bottom: 1px solid theme-color('secondary-light');
|
|
96
|
+
background-color: $gray-100;
|
|
97
|
+
|
|
98
|
+
.rbf-editor-dialog-label {
|
|
99
|
+
font-size: 0.7rem;
|
|
100
|
+
margin-bottom: 0.1rem;
|
|
101
|
+
color: theme-color('secondary');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.nav-tabs-sm .nav-link {
|
|
105
|
+
padding: 0.15rem 0.6rem;
|
|
106
|
+
font-size: 0.75rem;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.rbf-editor-source {
|
|
111
|
+
.rbf-editor-source-text {
|
|
112
|
+
font-size: 0.75rem;
|
|
113
|
+
resize: none;
|
|
114
|
+
height: 100%;
|
|
115
|
+
overflow-y: auto;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.rbf-editor-source-warning {
|
|
120
|
+
margin: 0;
|
|
121
|
+
padding: 0.25rem 0.5rem;
|
|
122
|
+
border-radius: 0;
|
|
123
|
+
font-size: 0.75rem;
|
|
124
|
+
|
|
125
|
+
.btn-link {
|
|
126
|
+
padding: 0 0.35rem;
|
|
127
|
+
font-size: 0.75rem;
|
|
128
|
+
vertical-align: baseline;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import classnames from 'classnames';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Bouton de la barre d'outils.
|
|
7
|
+
*
|
|
8
|
+
* Deux détails sans lesquels la barre ne fonctionne pas dans un formulaire :
|
|
9
|
+
*
|
|
10
|
+
* - `type="button"` : sans lui, le bouton soumet le formulaire qui l'entoure ;
|
|
11
|
+
* - `onMouseDown` avec `preventDefault` : sans lui, le clic vole le focus à
|
|
12
|
+
* l'éditeur, la sélection est perdue et la commande s'applique ailleurs.
|
|
13
|
+
*
|
|
14
|
+
* Le composant est un `forwardRef` parce que `DropdownWrapper` clone son
|
|
15
|
+
* déclencheur en lui posant une ref.
|
|
16
|
+
*/
|
|
17
|
+
const ToolbarButton = React.forwardRef((props, ref) => {
|
|
18
|
+
const { icon, title, active, disabled, onClick, size, className, ...rest } = props;
|
|
19
|
+
return (
|
|
20
|
+
<button
|
|
21
|
+
{...rest}
|
|
22
|
+
ref={ref}
|
|
23
|
+
type="button"
|
|
24
|
+
title={title}
|
|
25
|
+
aria-label={title}
|
|
26
|
+
aria-pressed={active ? 'true' : 'false'}
|
|
27
|
+
disabled={disabled}
|
|
28
|
+
className={classnames('btn btn-light rbf-editor-button', size === 'sm' && 'btn-sm', active && 'active', className)}
|
|
29
|
+
onMouseDown={ev => ev.preventDefault()}
|
|
30
|
+
onClick={onClick}
|
|
31
|
+
>
|
|
32
|
+
{icon}
|
|
33
|
+
</button>
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
ToolbarButton.displayName = 'ToolbarButton';
|
|
38
|
+
|
|
39
|
+
ToolbarButton.propTypes = {
|
|
40
|
+
icon: PropTypes.node,
|
|
41
|
+
title: PropTypes.string,
|
|
42
|
+
active: PropTypes.bool,
|
|
43
|
+
disabled: PropTypes.bool,
|
|
44
|
+
onClick: PropTypes.func,
|
|
45
|
+
size: PropTypes.string,
|
|
46
|
+
className: PropTypes.string,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
ToolbarButton.defaultProps = {
|
|
50
|
+
icon: null,
|
|
51
|
+
title: '',
|
|
52
|
+
active: false,
|
|
53
|
+
disabled: false,
|
|
54
|
+
onClick: () => {},
|
|
55
|
+
size: 'sm',
|
|
56
|
+
className: '',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export default ToolbarButton;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import classnames from 'classnames';
|
|
4
|
+
import DropdownWrapper from '../advanced/DropdownWrapper';
|
|
5
|
+
import DropdownMenu from '../basic/DropdownMenu';
|
|
6
|
+
import ToolbarButton from './ToolbarButton';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Menu déroulant de la barre d'outils.
|
|
10
|
+
* S'appuie sur les briques déjà présentes dans la librairie (portail,
|
|
11
|
+
* fermeture automatique, positionnement) : rien de nouveau à écrire.
|
|
12
|
+
*/
|
|
13
|
+
export default function ToolbarDropdown(props) {
|
|
14
|
+
const { icon, title, active, size, align, autoClose, className, children } = props;
|
|
15
|
+
return (
|
|
16
|
+
<DropdownWrapper
|
|
17
|
+
align={align}
|
|
18
|
+
autoClose={autoClose}
|
|
19
|
+
tooltip={title}
|
|
20
|
+
trigger={<ToolbarButton icon={icon} title={title} active={active} size={size} />}
|
|
21
|
+
>
|
|
22
|
+
<DropdownMenu className={classnames('rbf-editor-dropdown', className)}>{children}</DropdownMenu>
|
|
23
|
+
</DropdownWrapper>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
ToolbarDropdown.propTypes = {
|
|
28
|
+
icon: PropTypes.node,
|
|
29
|
+
title: PropTypes.string,
|
|
30
|
+
active: PropTypes.bool,
|
|
31
|
+
size: PropTypes.string,
|
|
32
|
+
align: PropTypes.string,
|
|
33
|
+
autoClose: PropTypes.bool,
|
|
34
|
+
className: PropTypes.string,
|
|
35
|
+
children: PropTypes.element.isRequired,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
ToolbarDropdown.defaultProps = {
|
|
39
|
+
icon: null,
|
|
40
|
+
title: '',
|
|
41
|
+
active: false,
|
|
42
|
+
size: 'sm',
|
|
43
|
+
align: 'bottom-left',
|
|
44
|
+
autoClose: true,
|
|
45
|
+
className: '',
|
|
46
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import classnames from 'classnames';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Groupe de boutons de la barre d'outils.
|
|
7
|
+
* Les groupes marqués optionnels disparaissent sous md et sont rebasculés dans
|
|
8
|
+
* le menu « … » : c'est du pur CSS Bootstrap, aucun calcul de largeur.
|
|
9
|
+
*/
|
|
10
|
+
export default function ToolbarGroup(props) {
|
|
11
|
+
return (
|
|
12
|
+
<div
|
|
13
|
+
className={classnames('btn-group btn-group-sm rbf-editor-group', props.optional && 'd-none d-md-inline-flex')}
|
|
14
|
+
role="group"
|
|
15
|
+
>
|
|
16
|
+
{props.children}
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
ToolbarGroup.propTypes = {
|
|
22
|
+
optional: PropTypes.bool,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
ToolbarGroup.defaultProps = {
|
|
26
|
+
optional: false,
|
|
27
|
+
};
|