1 | /* |
2 | Copyright - 2017 2023 - wwwouaiebe - Contact: https://www.ouaie.be/ |
3 | |
4 | This program is free software; |
5 | you can redistribute it and/or modify it under the terms of the |
6 | GNU General Public License as published by the Free Software Foundation; |
7 | either version 3 of the License, or any later version. |
8 | |
9 | This program is distributed in the hope that it will be useful, |
10 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 | GNU General Public License for more details. |
13 | |
14 | You should have received a copy of the GNU General Public License |
15 | along with this program; if not, write to the Free Software |
16 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
17 | */ |
18 | /* |
19 | Changes: |
20 | - v4.0.0: |
21 | - created from v3.6.0 |
22 | Doc reviewed 202208 |
23 | */ |
24 | |
25 | import theTranslator from '../../core/uiLib/Translator.js'; |
26 | import theHTMLElementsFactory from '../../core/uiLib/HTMLElementsFactory.js'; |
27 | import BaseContextMenuOperator from './BaseContextMenuOperator.js'; |
28 | import theDevice from '../../core/lib/Device.js'; |
29 | import { ZERO, INVALID_OBJ_ID, LAT_LNG } from '../../main/Constants.js'; |
30 | |
31 | /* ------------------------------------------------------------------------------------------------------------------------- */ |
32 | /** |
33 | Base class used to create context menus |
34 | */ |
35 | /* ------------------------------------------------------------------------------------------------------------------------- */ |
36 | |
37 | class BaseContextMenu { |
38 | |
39 | /** |
40 | The X screen coordinate of the mouse event that have triggered the menu |
41 | @type {Number} |
42 | */ |
43 | |
44 | #clientX; |
45 | |
46 | /** |
47 | The Y screen coordinate of the mouse event that have triggered the menu |
48 | @type {Number} |
49 | */ |
50 | |
51 | #clientY; |
52 | |
53 | /** |
54 | The lat an lng at the mouse position for events triggered by the map |
55 | @type {Array.<Number>} |
56 | */ |
57 | |
58 | #latLng; |
59 | |
60 | /** |
61 | The ObjId of the TravelObject on witch the mouse is positionned if any |
62 | @type {Number} |
63 | */ |
64 | |
65 | #targetObjId; |
66 | |
67 | /** |
68 | A flag indicating when the menu must have a parent node. Menus triggered from leaflet objects don't have |
69 | parentNode |
70 | @type {Boolean} |
71 | */ |
72 | |
73 | #haveParentNode; |
74 | |
75 | /** |
76 | The active BaseContextMenu instance. Needed to close the menu when a second menu is loaded |
77 | @type {BaseContextMenu} |
78 | */ |
79 | |
80 | static #currentMenu = null; |
81 | |
82 | /** |
83 | The promise ok handler |
84 | @type {function} |
85 | */ |
86 | |
87 | #onPromiseOk; |
88 | |
89 | /** |
90 | The promise error handler |
91 | @type {function} |
92 | */ |
93 | |
94 | #onPromiseError; |
95 | |
96 | /** |
97 | The root HTMLElement of the menu |
98 | @type {HTMLElement} |
99 | */ |
100 | |
101 | #contextMenuHTMLElement; |
102 | |
103 | /** |
104 | The cancel button HTMLElement |
105 | @type {HTMLElement} |
106 | */ |
107 | |
108 | #cancelButton; |
109 | |
110 | /** |
111 | The HTMLElement of the menu items |
112 | @type {Array.<HTMLElement>} |
113 | */ |
114 | |
115 | #menuItemHTMLElements; |
116 | |
117 | /** |
118 | The associated BaseContextMenuOperator object |
119 | @type {BaseContextMenuOperator} |
120 | */ |
121 | |
122 | #menuOperator; |
123 | |
124 | /** |
125 | The min margin between the screen borders and the menu in pixels |
126 | @type {Number} |
127 | */ |
128 | |
129 | // eslint-disable-next-line no-magic-numbers |
130 | static get #menuMargin ( ) { return 20; } |
131 | |
132 | /** |
133 | A flag indicating if a menu is active |
134 | @type {Boolean} |
135 | */ |
136 | |
137 | static get isActive ( ) { return Boolean ( BaseContextMenu.#currentMenu ); } |
138 | |
139 | /** |
140 | Build the menu container and add event listeners |
141 | */ |
142 | |
143 | #createContextMenuHTMLElement ( ) { |
144 | this.#contextMenuHTMLElement = theHTMLElementsFactory.create ( |
145 | 'div', |
146 | { |
147 | className : 'TravelNotes-ContextMenu-ContextMenuHTMLElement' |
148 | }, |
149 | document.body |
150 | ); |
151 | } |
152 | |
153 | /** |
154 | Create the cancel button and it's event listener to the menu |
155 | */ |
156 | |
157 | #createCancelButton ( ) { |
158 | this.#cancelButton = theHTMLElementsFactory.create ( |
159 | 'div', |
160 | { |
161 | textContent : '❌', |
162 | className : 'TravelNotes-ContextMenu-CancelButton', |
163 | title : theTranslator.getText ( 'BaseContextMenu - Close' ) |
164 | }, |
165 | this.#contextMenuHTMLElement |
166 | ); |
167 | } |
168 | |
169 | /** |
170 | Create the menuItems html elements |
171 | */ |
172 | |
173 | #createMenuItemsHTMLElements ( ) { |
174 | this.#menuItemHTMLElements = []; |
175 | this.menuItems.forEach ( |
176 | ( menuItem, index ) => { |
177 | const menuItemHTMLElement = theHTMLElementsFactory.create ( |
178 | 'div', |
179 | { |
180 | textContent : menuItem.itemText, |
181 | className : 'TravelNotes-ContextMenu-MenuItem', |
182 | dataset : { ObjId : String ( index ) } |
183 | }, |
184 | this.#contextMenuHTMLElement |
185 | ); |
186 | if ( ! menuItem.isActive ) { |
187 | menuItemHTMLElement.classList.add ( 'TravelNotes-ContextMenu-MenuItemDisabled' ); |
188 | } |
189 | this.#menuItemHTMLElements.push ( menuItemHTMLElement ); |
190 | } |
191 | ); |
192 | } |
193 | |
194 | /** |
195 | Move the container, so the top of the container is near the mouse |
196 | */ |
197 | |
198 | #moveContextMenu ( ) { |
199 | const screenAvailable = theDevice.screenAvailable; |
200 | |
201 | // the menu is positionned ( = top left where the user have clicked but the menu must be completely in the window... |
202 | const menuTop = Math.min ( |
203 | this.#clientY, |
204 | screenAvailable.height - this.#contextMenuHTMLElement.clientHeight - BaseContextMenu.#menuMargin |
205 | ); |
206 | this.#contextMenuHTMLElement.style.top = String ( menuTop ) + 'px'; |
207 | const menuLeft = Math.min ( |
208 | this.#clientX, |
209 | screenAvailable.width - this.#contextMenuHTMLElement.clientWidth - BaseContextMenu.#menuMargin |
210 | ); |
211 | this.#contextMenuHTMLElement.style.left = String ( menuLeft ) + 'px'; |
212 | } |
213 | |
214 | /** |
215 | Create and show the menu. This method is called by the Promise |
216 | @param {function} onPromiseOk The Promise Ok handler |
217 | @param {function} onPromiseError The Promise Error handler |
218 | */ |
219 | |
220 | #createMenu ( onPromiseOk, onPromiseError ) { |
221 | this.#onPromiseOk = onPromiseOk; |
222 | this.#onPromiseError = onPromiseError; |
223 | this.#createContextMenuHTMLElement ( ); |
224 | this.#createCancelButton ( ); |
225 | this.#createMenuItemsHTMLElements ( ); |
226 | this.#moveContextMenu ( ); |
227 | this.#menuOperator = new BaseContextMenuOperator ( this ); |
228 | |
229 | } |
230 | |
231 | /** |
232 | The constructor |
233 | @param {Event} contextMenuEvent The event that have triggered the menu |
234 | @param {?HTMLElement} parentNode The parent node of the menu. Can be null for leaflet objects |
235 | */ |
236 | |
237 | constructor ( contextMenuEvent, parentNode ) { |
238 | |
239 | Object.freeze ( this ); |
240 | |
241 | if ( BaseContextMenu.#currentMenu ) { |
242 | |
243 | // the menu is already opened, so we suppose the user will close the menu by clicking outside... |
244 | BaseContextMenu.#currentMenu.onCancel ( ); |
245 | return; |
246 | } |
247 | |
248 | // Saving data from the contextMenuEvent and parentNode |
249 | this.#clientX = contextMenuEvent.clientX || contextMenuEvent.originalEvent.clientX || ZERO; |
250 | this.#clientY = contextMenuEvent.clientY || contextMenuEvent.originalEvent.clientY || ZERO; |
251 | this.#latLng = [ |
252 | contextMenuEvent.latlng ? contextMenuEvent.latlng.lat : LAT_LNG.defaultValue, |
253 | contextMenuEvent.latlng ? contextMenuEvent.latlng.lng : LAT_LNG.defaultValue |
254 | ]; |
255 | this.#targetObjId = |
256 | contextMenuEvent.target?.objId |
257 | ?? |
258 | ( Number.parseInt ( contextMenuEvent?.currentTarget?.dataset?.tanObjId ) || INVALID_OBJ_ID ); |
259 | this.#haveParentNode = Boolean ( parentNode ); |
260 | BaseContextMenu.#currentMenu = this; |
261 | } |
262 | |
263 | /** |
264 | onOk method used by the menu operator. Clean the variables and call the Promise Ok handler |
265 | @param {Number} selectedItemObjId The id of the item selected by the user |
266 | */ |
267 | |
268 | onOk ( selectedItemObjId ) { |
269 | this.#menuOperator.destructor ( ); |
270 | BaseContextMenu.#currentMenu = null; |
271 | this.#onPromiseOk ( selectedItemObjId ); |
272 | } |
273 | |
274 | /** |
275 | onCancel method used by the menu operator. Clean the variables and call the Promise Error handler |
276 | */ |
277 | |
278 | onCancel ( ) { |
279 | this.#menuOperator.destructor ( ); |
280 | BaseContextMenu.#currentMenu = null; |
281 | this.#onPromiseError ( ); |
282 | } |
283 | |
284 | /** |
285 | Show the menu on the screen and perform the correct operation when an item is selected |
286 | */ |
287 | |
288 | show ( ) { |
289 | if ( ! BaseContextMenu.#currentMenu ) { |
290 | return; |
291 | } |
292 | new Promise ( |
293 | ( onPromiseOk, onPromiseError ) => { this.#createMenu ( onPromiseOk, onPromiseError ); } |
294 | ) |
295 | .then ( selectedItemObjId => this.menuItems [ selectedItemObjId ].doAction ( ) ) |
296 | .catch ( |
297 | err => { |
298 | if ( err ) { |
299 | console.error ( err ); |
300 | } |
301 | } |
302 | ); |
303 | } |
304 | |
305 | /** |
306 | The list of menu items to use. Must be implemented in the derived classes |
307 | @type {Array.<MenuItem>} |
308 | */ |
309 | |
310 | get menuItems ( ) { return []; } |
311 | |
312 | /** |
313 | The root HTMLElement of the menu |
314 | @type {HTMLElement} |
315 | */ |
316 | |
317 | get contextMenuHTMLElement ( ) { return this.#contextMenuHTMLElement; } |
318 | |
319 | /** |
320 | The cancel button HTMLElement |
321 | @type {HTMLElement} |
322 | */ |
323 | |
324 | get cancelButton ( ) { return this.#cancelButton; } |
325 | |
326 | /** |
327 | The HTMLElement of the menu items |
328 | @type {Array.<HTMLElement>} |
329 | */ |
330 | |
331 | get menuItemHTMLElements ( ) { return this.#menuItemHTMLElements; } |
332 | |
333 | /** |
334 | The X screen coordinate of the mouse event that have triggered the menu |
335 | @type {Number} |
336 | */ |
337 | |
338 | get clientX ( ) { return this.#clientX; } |
339 | |
340 | /** |
341 | The Y screen coordinate of the mouse event that have triggered the menu |
342 | @type {Number} |
343 | */ |
344 | |
345 | get clientY ( ) { return this.#clientY; } |
346 | |
347 | /** |
348 | The lat an lng at the mouse position for events triggered by the map |
349 | @type {Array.<Number>} |
350 | */ |
351 | |
352 | get latLng ( ) { return this.#latLng; } |
353 | |
354 | /** |
355 | The ObjId of the TravelObject on witch the mouse is positionned if any |
356 | @type {Number} |
357 | */ |
358 | |
359 | get targetObjId ( ) { return this.#targetObjId; } |
360 | |
361 | /** |
362 | A flag indicating when the menu must have a parent node. Menus triggered from leaflet objects don't have |
363 | parentNode |
364 | @type {Boolean} |
365 | */ |
366 | |
367 | get haveParentNode ( ) { return this.#haveParentNode; } |
368 | |
369 | } |
370 | |
371 | export default BaseContextMenu; |
372 | |
373 | /* --- End of file --------------------------------------------------------------------------------------------------------- */ |
374 |