File : contextMenus/baseContextMenu/BaseContextMenu.js

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