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,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.
|
|
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
|
-
"
|
|
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": "^
|
|
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
|
-
|
|
26
|
-
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
49
|
+
placement: this.getPlacement(null),
|
|
39
50
|
};
|
|
40
51
|
this.handleClickOutside = this.handleClickOutside.bind(this);
|
|
41
|
-
this.
|
|
52
|
+
this.reposition = this.reposition.bind(this);
|
|
42
53
|
}
|
|
43
54
|
|
|
44
55
|
componentDidMount() {
|
|
45
56
|
document.addEventListener('click', this.handleClickOutside, true);
|
|
46
|
-
|
|
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.
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
}
|
|
164
|
+
}
|
|
90
165
|
return (
|
|
91
166
|
<Portal>
|
|
92
|
-
<div className={classnames('rbf-dropdown', this.props.className)} style={divStyle}>
|
|
93
|
-
<div
|
|
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;
|