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.
Files changed (37) hide show
  1. package/CHANGELOG +9 -0
  2. package/dist/react-bootstrap-front.js +14 -14
  3. package/doc/editor/feuille-de-route.md +36 -0
  4. package/package.json +42 -8
  5. package/src/features/advanced/DropdownWrapper.js +12 -2
  6. package/src/features/basic/Dropdown.js +112 -31
  7. package/src/features/basic/computePlacement.js +66 -0
  8. package/src/features/editor/HtmlEditor.js +681 -0
  9. package/src/features/editor/HtmlEditor.scss +189 -0
  10. package/src/features/editor/ImageDialog.js +195 -0
  11. package/src/features/editor/LinkDialog.js +148 -0
  12. package/src/features/editor/MergeMenu.js +120 -0
  13. package/src/features/editor/SourceView.js +61 -0
  14. package/src/features/editor/Toolbar.js +355 -0
  15. package/src/features/editor/Toolbar.scss +130 -0
  16. package/src/features/editor/ToolbarButton.js +59 -0
  17. package/src/features/editor/ToolbarDropdown.js +46 -0
  18. package/src/features/editor/ToolbarGroup.js +27 -0
  19. package/src/features/editor/buildExtensions.js +77 -0
  20. package/src/features/editor/defaultIcons.js +45 -0
  21. package/src/features/editor/extensions/CleanPaste.js +44 -0
  22. package/src/features/editor/extensions/FontSize.js +60 -0
  23. package/src/features/editor/extensions/LegacyImageAttrs.js +43 -0
  24. package/src/features/editor/extensions/MergeVariable.js +104 -0
  25. package/src/features/editor/extensions/index.js +4 -0
  26. package/src/features/editor/html/cleanHtml.constants.js +279 -0
  27. package/src/features/editor/html/cleanHtml.js +691 -0
  28. package/src/features/editor/html/index.js +4 -0
  29. package/src/features/editor/html/normalizeLegacyHtml.js +196 -0
  30. package/src/features/editor/html/serialize.js +130 -0
  31. package/src/features/editor/html/sourceHelpers.js +86 -0
  32. package/src/features/editor/index.js +20 -0
  33. package/src/features/editor/style.scss +2 -0
  34. package/src/features/editor/toolbarPresets.js +74 -0
  35. package/src/features/page/DefaultHeader.jsx +1 -1
  36. package/src/index.js +8 -0
  37. package/src/styles/index.scss +1 -0
@@ -0,0 +1,36 @@
1
+ # Éditeur HTML : feuille de route
2
+
3
+ Remplacement de l'éditeur draft-js (`src/features/draft`) par TipTap 2
4
+ (`src/features/editor`).
5
+
6
+ Les commits de septembre 2026 sont numérotés « phase n/8 », mais aucun plan écrit
7
+ ne détaille les phases 6 à 8. **Cette page fait foi** pour ce qui reste à faire.
8
+
9
+ ## Fait : version 3.11.0
10
+
11
+ | Étape | Commit | Contenu |
12
+ |---|---|---|
13
+ | 1 | `36b9ce8` | Noyau de nettoyage : `cleanHtml` (profils LOAD, PASTE, STRICT), `normalizeLegacyHtml`, `serialize` |
14
+ | 2 | `ccfbef6` | Composant `HtmlEditor`, contrat d'appel identique à l'ancien `InputHtml` |
15
+ | 3 | `69d7e68` | Barre d'outils, dialogues lien, image et variables, vue source |
16
+ | 4-5 | `1178cbc` | Tests de montage, aperçu autonome `demo/`, `sync-to.sh` |
17
+ | | `abd989f`, `8e4d49c` | Placement des menus déroulants, dont profite la barre d'outils |
18
+
19
+ Première application intégrée : l'administration Kalaweit (dépôt `admin`, commit
20
+ `a5dc76a`).
21
+
22
+ ## Reste à faire
23
+
24
+ 1. **Publier la 3.11.0 sur npm.** Une application ne doit jamais dépendre d'une copie
25
+ manuelle : `sync-to.sh` sert au développement, pas à la livraison.
26
+ 2. **Valider en production**, avec l'administration Kalaweit 2.23.1.
27
+ 3. **Version 4.0.0**, une fois la validation faite :
28
+ - supprimer `src/features/draft` et son `export *` dans `src/index.js` ;
29
+ - retirer les dépendances `draft-js`, `draft-convert`, `html-to-draftjs`,
30
+ `react-draft-wysiwyg` et `@draft-js-plugins/image`, qui ne servent nulle part
31
+ ailleurs ;
32
+ - ré-exporter `HtmlEditor` sous le nom `InputHtml`, voir
33
+ `src/features/editor/index.js` ;
34
+ - le bundle doit repasser sous sa taille d'avant TipTap, environ 300 Ko de moins.
35
+ 4. **Quand le catalogue des variables de fusion viendra de l'API**, activer
36
+ `strictMergeVars` dans les applications, pour signaler les variables inconnues.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-bootstrap-front",
3
- "version": "3.10.2",
3
+ "version": "3.11.0",
4
4
  "description": "React Bootstrap Components",
5
5
  "keywords": [
6
6
  "Bootstrap 4",
@@ -21,7 +21,7 @@
21
21
  "scripts": {
22
22
  "build": "webpack --config webpack.config.js",
23
23
  "test": "jest --silent=false",
24
- "prepublish": "npm run build",
24
+ "prepublishOnly": "npm run build",
25
25
  "studio": "rekit-studio -p 8477",
26
26
  "doc:prepare": "cross-env NODE_ENV=development gitbook install",
27
27
  "doc:serve": "npm run doc:prepare && gitbook serve",
@@ -37,6 +37,19 @@
37
37
  "@draft-js-plugins/image": "^4.1.2",
38
38
  "@ericblade/quagga2": "^1.4.1",
39
39
  "@popperjs/core": "^2.5.4",
40
+ "@tiptap/core": "2.27.3",
41
+ "@tiptap/extension-color": "2.27.3",
42
+ "@tiptap/extension-font-family": "2.27.3",
43
+ "@tiptap/extension-image": "2.27.3",
44
+ "@tiptap/extension-link": "2.27.3",
45
+ "@tiptap/extension-placeholder": "2.27.3",
46
+ "@tiptap/extension-subscript": "2.27.3",
47
+ "@tiptap/extension-superscript": "2.27.3",
48
+ "@tiptap/extension-text-align": "2.27.3",
49
+ "@tiptap/extension-text-style": "2.27.3",
50
+ "@tiptap/extension-underline": "2.27.3",
51
+ "@tiptap/pm": "2.27.3",
52
+ "@tiptap/starter-kit": "2.27.3",
40
53
  "autosize": "^4.0.2",
41
54
  "babel-runtime": "^6.26.0",
42
55
  "classnames": "^2.2.6",
@@ -91,7 +104,7 @@
91
104
  "@babel/preset-env": "^7.12.1",
92
105
  "@babel/preset-react": "^7.12.1",
93
106
  "babel-core": "7.0.0-bridge.0",
94
- "babel-jest": "^29.4.1",
107
+ "babel-jest": "^26.6.3",
95
108
  "babel-loader": "8.0.4",
96
109
  "babel-plugin-module-resolver": "3.1.1",
97
110
  "babel-plugin-transform-runtime": "^6.23.0",
@@ -146,14 +159,13 @@
146
159
  "testEnvironment": "jsdom",
147
160
  "testURL": "http://localhost",
148
161
  "transform": {
149
- "^.+\\.(js|jsx|ts|tsx)$": "<rootDir>/node_modules/babel-jest",
162
+ "^.+\\.(js|jsx|ts|tsx|cjs|mjs)$": "<rootDir>/node_modules/babel-jest",
150
163
  "^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
151
- "^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
164
+ "^(?!.*\\.(js|jsx|ts|tsx|cjs|mjs|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
152
165
  },
153
166
  "transformIgnorePatterns": [
154
- "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$",
155
- "^.+\\.module\\.(css|sass|scss)$",
156
- "node_modules/(?!imask)"
167
+ "[/\\\\]node_modules[/\\\\](?!(@tiptap|prosemirror-|linkifyjs|orderedmap|rope-sequence|w3c-keyname|crelt|markdown-it|entities|uc\\.micro|punycode\\.js|mdurl|imask)).+\\.(js|jsx|ts|tsx)$",
168
+ "^.+\\.module\\.(css|sass|scss)$"
157
169
  ],
158
170
  "moduleNameMapper": {
159
171
  "^react-native$": "react-native-web",
@@ -162,6 +174,8 @@
162
174
  "moduleFileExtensions": [
163
175
  "web.js",
164
176
  "js",
177
+ "cjs",
178
+ "mjs",
165
179
  "web.ts",
166
180
  "ts",
167
181
  "web.tsx",
@@ -171,5 +185,25 @@
171
185
  "jsx",
172
186
  "node"
173
187
  ]
188
+ },
189
+ "overrides": {
190
+ "prosemirror-changeset": "2.4.2",
191
+ "prosemirror-collab": "1.3.1",
192
+ "prosemirror-commands": "1.7.2",
193
+ "prosemirror-dropcursor": "1.8.3",
194
+ "prosemirror-gapcursor": "1.4.1",
195
+ "prosemirror-history": "1.5.0",
196
+ "prosemirror-inputrules": "1.5.1",
197
+ "prosemirror-keymap": "1.2.3",
198
+ "prosemirror-markdown": "1.13.7",
199
+ "prosemirror-menu": "1.3.2",
200
+ "prosemirror-model": "1.25.11",
201
+ "prosemirror-schema-basic": "1.2.4",
202
+ "prosemirror-schema-list": "1.5.1",
203
+ "prosemirror-state": "1.4.4",
204
+ "prosemirror-tables": "1.8.5",
205
+ "prosemirror-trailing-node": "3.0.0",
206
+ "prosemirror-transform": "1.12.1",
207
+ "prosemirror-view": "1.42.3"
174
208
  }
175
209
  }
@@ -22,8 +22,18 @@ export default class DropdownWrapper extends Component {
22
22
  tooltip: '',
23
23
  };
24
24
 
25
- static getDerivedStateFromProps(state, props) {
26
- if (state.origOpen !== props.open && state.open !== props.open) {
25
+ /**
26
+ * React appelle cette méthode avec (props, state) : les deux paramètres
27
+ * étaient nommés à l'envers, ce qui rendait la prop `open` inopérante — elle
28
+ * se comparait à elle-même. Aucun appelant ne la passe aujourd'hui, le
29
+ * comportement observable est donc inchangé.
30
+ *
31
+ * `open` est une valeur initiale, pas un contrôle : on ne la reprend que
32
+ * lorsqu'elle change réellement côté appelant, sinon le menu se refermerait
33
+ * à chaque rendu du parent.
34
+ */
35
+ static getDerivedStateFromProps(props, state) {
36
+ if (props.open !== state.origOpen) {
27
37
  return { open: props.open, origOpen: props.open };
28
38
  }
29
39
  return null;
@@ -3,17 +3,23 @@ import classnames from 'classnames';
3
3
  import ReactDOM from 'react-dom';
4
4
  import PropTypes from 'prop-types';
5
5
  import { Portal } from '../advanced';
6
+ import { computePlacement, DROPDOWN_MARGIN } from './computePlacement';
6
7
 
7
- const getCoords = (props) => {
8
+ const getCoords = props => {
8
9
  if (props.myRef && props.myRef.current) {
9
10
  try {
10
11
  return props.myRef.current.getBoundingClientRect();
11
12
  } catch (ex) {
12
- console.log("FK pas de coordonées",ex);
13
+ return null;
13
14
  }
14
15
  }
15
16
  return null;
16
- }
17
+ };
18
+
19
+ const getViewport = () => ({
20
+ width: typeof window !== 'undefined' ? window.innerWidth : 0,
21
+ height: typeof window !== 'undefined' ? window.innerHeight : 0,
22
+ });
17
23
 
18
24
  export default class Dropdown extends Component {
19
25
  static propTypes = {
@@ -21,7 +27,7 @@ export default class Dropdown extends Component {
21
27
  children: PropTypes.element.isRequired,
22
28
  className: PropTypes.string,
23
29
  maxHeight: PropTypes.string,
24
- myRef: PropTypes.element.isRequired,
30
+ myRef: PropTypes.object.isRequired,
25
31
  onClose: PropTypes.func,
26
32
  };
27
33
 
@@ -29,31 +35,86 @@ export default class Dropdown extends Component {
29
35
  align: 'bottom-left',
30
36
  className: '',
31
37
  maxHeight: 'auto',
32
- onClose: null
38
+ onClose: null,
33
39
  };
34
40
 
35
41
  constructor(props) {
36
42
  super(props);
43
+ this.nodeRef = React.createRef();
44
+ this.scrollRef = React.createRef();
45
+ /* Première position calculée sur la seule ancre : la taille du menu n'est
46
+ pas encore connue. Elle est affinée dès le montage, ce qui évite
47
+ d'afficher le menu au mauvais endroit le temps d'une image. */
37
48
  this.state = {
38
- coords: getCoords(this.props),
49
+ placement: this.getPlacement(null),
39
50
  };
40
51
  this.handleClickOutside = this.handleClickOutside.bind(this);
41
- this.handleScroll = this.handleScroll.bind(this);
52
+ this.reposition = this.reposition.bind(this);
42
53
  }
43
54
 
44
55
  componentDidMount() {
45
56
  document.addEventListener('click', this.handleClickOutside, true);
46
- document.addEventListener('scroll', this.handleScroll, true);
57
+ /* En capture : le menu doit suivre un défilement de n'importe quel
58
+ conteneur, pas seulement de la page. */
59
+ document.addEventListener('scroll', this.reposition, true);
60
+ window.addEventListener('resize', this.reposition);
61
+ this.reposition();
47
62
  }
48
63
 
49
64
  componentWillUnmount() {
50
65
  document.removeEventListener('click', this.handleClickOutside, true);
51
- document.removeEventListener('scroll', this.handleScroll, true);
66
+ document.removeEventListener('scroll', this.reposition, true);
67
+ window.removeEventListener('resize', this.reposition);
68
+ }
69
+
70
+ /**
71
+ * Taille du menu, une fois rendu. Nulle au premier calcul.
72
+ *
73
+ * La hauteur lue est celle du CONTENU (`scrollHeight` du conteneur
74
+ * défilant), pas celle de l'élément : ce dernier porte déjà le `max-height`
75
+ * calculé au tour précédent, si bien que sa hauteur rendue ne dépasse jamais
76
+ * l'espace disponible. S'y fier rendrait tout débordement invisible et le
77
+ * menu ne se retournerait jamais — il resterait écrasé sous le bas de page.
78
+ */
79
+ getMenuSize() {
80
+ const node = this.nodeRef.current;
81
+ if (!node) {
82
+ return null;
83
+ }
84
+ const scroller = this.scrollRef.current;
85
+ const height = scroller ? scroller.scrollHeight : node.scrollHeight;
86
+ return { width: node.offsetWidth, height: height };
87
+ }
88
+
89
+ getPlacement(menuSize) {
90
+ const anchor = getCoords(this.props);
91
+ if (!anchor) {
92
+ return null;
93
+ }
94
+ return computePlacement(anchor, menuSize, this.props.align, getViewport());
95
+ }
96
+
97
+ reposition() {
98
+ const placement = this.getPlacement(this.getMenuSize());
99
+ if (!placement) {
100
+ return;
101
+ }
102
+ const current = this.state.placement;
103
+ if (
104
+ current &&
105
+ current.top === placement.top &&
106
+ current.left === placement.left &&
107
+ current.right === placement.right &&
108
+ current.maxHeight === placement.maxHeight
109
+ ) {
110
+ return;
111
+ }
112
+ this.setState({ placement: placement });
52
113
  }
53
114
 
54
115
  handleClickOutside(event) {
55
116
  const domNode = ReactDOM.findDOMNode(this);
56
- if (!domNode || !domNode.contains(event.target)) {
117
+ if (!domNode || !domNode.contains(event.target)) {
57
118
  if (this.props.myRef && this.props.myRef.current && this.props.myRef.current.contains(event.target)) {
58
119
  event.preventDefault();
59
120
  event.stopPropagation();
@@ -62,35 +123,53 @@ export default class Dropdown extends Component {
62
123
  }
63
124
  }
64
125
 
65
- handleScroll(event) {
66
- this.setState({coords: getCoords(this.props)});
126
+ /**
127
+ * Hauteur retenue : la plus contraignante entre celle demandée et la place
128
+ * réellement disponible dans la fenêtre.
129
+ */
130
+ getMaxHeight() {
131
+ const { placement } = this.state;
132
+ const available = placement ? Math.max(0, placement.maxHeight) : null;
133
+ const asked = this.props.maxHeight;
134
+ if (asked && asked !== 'auto') {
135
+ const value = parseFloat(asked);
136
+ if (!isNaN(value) && available !== null && available < value) {
137
+ return available + 'px';
138
+ }
139
+ return asked;
140
+ }
141
+ return available !== null ? available + 'px' : 'auto';
67
142
  }
68
143
 
69
144
  render() {
70
- let divStyle = {
71
- position: 'absolute',
72
- maxHeight: this.props.maxHeight,
145
+ const { placement } = this.state;
146
+ const maxHeight = this.getMaxHeight();
147
+ /* `fixed` et non `absolute` : les coordonnées de l'ancre viennent de
148
+ getBoundingClientRect, donc relatives à la fenêtre. En `absolute` dans
149
+ un portail accroché au body, le menu dérivait de toute la hauteur
150
+ défilée — c'était le défaut visible. */
151
+ const divStyle = {
152
+ position: 'fixed',
153
+ maxHeight: maxHeight,
73
154
  overflow: 'hidden',
74
155
  };
75
- try {
76
- if (this.state.coords) {
77
- const { coords } = this.state;
78
- switch (this.props.align) {
79
- case 'bottom-right':
80
- divStyle.right = -1 * (coords.left + coords.width);
81
- divStyle.top = coords.top + coords.height;
82
- break;
83
- default:
84
- divStyle.left = coords.left;
85
- divStyle.top = coords.top + coords.height;
86
- break;
87
- }
156
+ if (placement) {
157
+ divStyle.top = placement.top;
158
+ if (placement.left !== undefined) {
159
+ divStyle.left = placement.left;
160
+ }
161
+ if (placement.right !== undefined) {
162
+ divStyle.right = placement.right;
88
163
  }
89
- } catch (ex) { console.log(ex); }
164
+ }
90
165
  return (
91
166
  <Portal>
92
- <div className={classnames('rbf-dropdown', this.props.className)} style={divStyle}>
93
- <div className="custom-scrollbar" style={{ maxHeight: this.props.maxHeight, overflowX: 'hidden', overflowY: 'auto' }}>
167
+ <div ref={this.nodeRef} className={classnames('rbf-dropdown', this.props.className)} style={divStyle}>
168
+ <div
169
+ ref={this.scrollRef}
170
+ className="custom-scrollbar"
171
+ style={{ maxHeight: maxHeight, overflowX: 'hidden', overflowY: 'auto' }}
172
+ >
94
173
  {this.props.children}
95
174
  </div>
96
175
  </div>
@@ -98,3 +177,5 @@ export default class Dropdown extends Component {
98
177
  );
99
178
  }
100
179
  }
180
+
181
+ export { DROPDOWN_MARGIN };
@@ -0,0 +1,66 @@
1
+ /** Marge minimale conservée entre le menu et le bord de la fenêtre. */
2
+ export const DROPDOWN_MARGIN = 8;
3
+
4
+ /**
5
+ * Calcule la position d'un menu déroulant.
6
+ *
7
+ * Fonction pure, volontairement séparée du composant : c'est la seule partie
8
+ * délicate, et elle se teste sans DOM.
9
+ *
10
+ * Les coordonnées de l'ancre viennent de `getBoundingClientRect()`, donc
11
+ * relatives à la FENÊTRE. Le menu doit être positionné en `fixed`, sinon il
12
+ * dérive de toute la hauteur défilée.
13
+ *
14
+ * @param {object} anchor { left, top, width, height, right, bottom }
15
+ * @param {object} menu { width, height } — 0 tant que le menu n'est pas mesuré
16
+ * @param {string} align 'bottom-right' aligne les bords droits, sinon les gauches
17
+ * @param {object} viewport { width, height }
18
+ * @param {number} margin
19
+ * @returns {object} style partiel : top, plus left ou right, plus maxHeight
20
+ */
21
+ export function computePlacement(anchor, menu, align, viewport, margin = DROPDOWN_MARGIN) {
22
+ const menuWidth = (menu && menu.width) || 0;
23
+ const menuHeight = (menu && menu.height) || 0;
24
+ const anchorRight = anchor.right !== undefined ? anchor.right : anchor.left + anchor.width;
25
+ const anchorBottom = anchor.bottom !== undefined ? anchor.bottom : anchor.top + anchor.height;
26
+
27
+ const spaceBelow = viewport.height - anchorBottom;
28
+ const spaceAbove = anchor.top;
29
+
30
+ /* Le menu s'ouvre vers le bas, sauf s'il n'y tient pas et qu'il y a
31
+ davantage de place au-dessus.
32
+ `menuHeight` doit être la hauteur NATURELLE du contenu : la hauteur rendue
33
+ est déjà écrêtée par le max-height posé au calcul précédent, et s'y fier
34
+ rendrait tout débordement invisible. */
35
+ const flip = menuHeight > 0 && spaceBelow < menuHeight + margin && spaceAbove > spaceBelow;
36
+ const available = Math.max(0, (flip ? spaceAbove : spaceBelow) - margin);
37
+ const effectiveHeight = menuHeight > 0 ? Math.min(menuHeight, available) : 0;
38
+
39
+ /* Retourné, c'est le BAS du menu qui se cale sur le déclencheur : partir de
40
+ la hauteur naturelle laisserait un trou dès que le menu est écrêté. */
41
+ let top = flip ? anchor.top - effectiveHeight : anchorBottom;
42
+ top = Math.min(top, viewport.height - margin - effectiveHeight);
43
+ top = Math.max(margin, top);
44
+
45
+ const placement = { top: top, maxHeight: available };
46
+
47
+ if (align === 'bottom-right') {
48
+ /* Bords droits alignés. En `right`, cela vaut la distance entre le bord
49
+ droit de la fenêtre et celui de l'ancre. */
50
+ let right = viewport.width - anchorRight;
51
+ if (menuWidth > 0) {
52
+ /* Ne pas déborder par la gauche. */
53
+ right = Math.min(right, viewport.width - menuWidth - margin);
54
+ }
55
+ placement.right = Math.max(margin, right);
56
+ } else {
57
+ let left = anchor.left;
58
+ if (menuWidth > 0) {
59
+ left = Math.min(left, viewport.width - menuWidth - margin);
60
+ }
61
+ placement.left = Math.max(margin, left);
62
+ }
63
+ return placement;
64
+ }
65
+
66
+ export default computePlacement;