1
|
|
|
import DeviceDetection from 'src/helper/device-detection.helper'; |
2
|
|
|
import NativeEventEmitter from 'src/helper/emitter.helper'; |
3
|
|
|
import Backdrop, { BACKDROP_EVENT } from 'src/utility/backdrop/backdrop.util'; |
4
|
|
|
import Iterator from 'src/helper/iterator.helper'; |
5
|
|
|
|
6
|
|
|
const OFF_CANVAS_CLASS = 'offcanvas'; |
7
|
|
|
const OFF_CANVAS_OPEN_CLASS = 'is-open'; |
8
|
|
|
const OFF_CANVAS_FULLWIDTH_CLASS = 'is-fullwidth'; |
9
|
|
|
const OFF_CANVAS_CLOSE_TRIGGER_CLASS = 'js-offcanvas-close'; |
10
|
|
|
const REMOVE_OFF_CANVAS_DELAY = 350; |
11
|
|
|
|
12
|
|
|
class OffCanvasSingleton { |
13
|
|
|
|
14
|
|
|
constructor() { |
15
|
|
|
this.$emitter = new NativeEventEmitter(); |
16
|
|
|
} |
17
|
|
|
|
18
|
|
|
/** |
19
|
|
|
* Open the offcanvas and its backdrop |
20
|
|
|
* @param {string} content |
21
|
|
|
* @param {function|null} callback |
22
|
|
|
* @param {'left'|'right'|'bottom'} position |
23
|
|
|
* @param {boolean} closable |
24
|
|
|
* @param {number} delay |
25
|
|
|
* @param {boolean} fullwidth |
26
|
|
|
* @param {array|string} cssClass |
27
|
|
|
*/ |
28
|
|
|
open(content, callback, position, closable, delay, fullwidth, cssClass) { |
29
|
|
|
// avoid multiple backdrops |
30
|
|
|
this._removeExistingOffCanvas(); |
31
|
|
|
|
32
|
|
|
const offCanvas = this._createOffCanvas(position, fullwidth, cssClass); |
33
|
|
|
this.setContent(content, closable, delay); |
34
|
|
|
this._openOffcanvas(offCanvas, callback); |
35
|
|
|
} |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* Method to change the content of the already visible OffCanvas |
39
|
|
|
* @param {string} content |
40
|
|
|
* @param {boolean} closable |
41
|
|
|
* @param {number} delay |
42
|
|
|
*/ |
43
|
|
|
setContent(content, closable, delay) { |
44
|
|
|
const offCanvas = this.getOffCanvas(); |
45
|
|
|
|
46
|
|
|
if (!offCanvas[0]) { |
47
|
|
|
return; |
48
|
|
|
} |
49
|
|
|
|
50
|
|
|
offCanvas[0].innerHTML = content; |
51
|
|
|
|
52
|
|
|
//register events again |
53
|
|
|
this._registerEvents(closable, delay); |
54
|
|
|
} |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* adds an additional class to the offcanvas |
58
|
|
|
* |
59
|
|
|
* @param {string} className |
60
|
|
|
*/ |
61
|
|
|
setAdditionalClassName(className) { |
62
|
|
|
const offCanvas = this.getOffCanvas(); |
63
|
|
|
offCanvas[0].classList.add(className); |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* Determine list of existing offcanvas |
68
|
|
|
* @returns {NodeListOf<Element>} |
69
|
|
|
* @private |
70
|
|
|
*/ |
71
|
|
|
getOffCanvas() { |
72
|
|
|
return document.querySelectorAll(`.${OFF_CANVAS_CLASS}`); |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* Close the offcanvas and its backdrop when the browser goes back in history |
77
|
|
|
* @param {number} delay |
78
|
|
|
*/ |
79
|
|
|
close(delay) { |
80
|
|
|
// remove open class to make any css animation effects possible |
81
|
|
|
const OffCanvasElements = this.getOffCanvas(); |
82
|
|
|
Iterator.iterate(OffCanvasElements, backdrop => backdrop.classList.remove(OFF_CANVAS_OPEN_CLASS)); |
83
|
|
|
|
84
|
|
|
// wait before removing backdrop to let css animation effects take place |
85
|
|
|
setTimeout(this._removeExistingOffCanvas.bind(this), delay); |
86
|
|
|
|
87
|
|
|
Backdrop.remove(delay); |
88
|
|
|
|
89
|
|
|
setTimeout(() => { |
90
|
|
|
this.$emitter.publish('onCloseOffcanvas', { |
91
|
|
|
offCanvasContent: OffCanvasElements |
92
|
|
|
}); |
93
|
|
|
}, delay); |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* Callback for close button, goes back in browser history to trigger close |
98
|
|
|
* @returns {void} |
99
|
|
|
*/ |
100
|
|
|
goBackInHistory() { |
101
|
|
|
window.history.back(); |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* Returns whether any OffCanvas exists or not |
106
|
|
|
* @returns {boolean} |
107
|
|
|
*/ |
108
|
|
|
exists() { |
109
|
|
|
return (this.getOffCanvas().length > 0); |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
/** |
113
|
|
|
* Opens the offcanvas and its backdrop |
114
|
|
|
* |
115
|
|
|
* @param {HTMLElement} offCanvas |
116
|
|
|
* @param {function} callback |
117
|
|
|
* |
118
|
|
|
* @private |
119
|
|
|
*/ |
120
|
|
|
_openOffcanvas(offCanvas, callback) { |
121
|
|
|
// the timeout allows to apply the animation effects |
122
|
|
|
setTimeout(() => { |
123
|
|
|
Backdrop.create(() => { |
124
|
|
|
offCanvas.classList.add(OFF_CANVAS_OPEN_CLASS); |
125
|
|
|
window.history.pushState('offcanvas-open', ''); |
126
|
|
|
|
127
|
|
|
// if a callback function is being injected execute it after opening the OffCanvas |
128
|
|
|
if (typeof callback === 'function') { |
129
|
|
|
callback(); |
130
|
|
|
} |
131
|
|
|
}); |
132
|
|
|
}, 75); |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
/** |
136
|
|
|
* Register events |
137
|
|
|
* @param {boolean} closable |
138
|
|
|
* @param {number} delay |
139
|
|
|
* @private |
140
|
|
|
*/ |
141
|
|
|
_registerEvents(closable, delay) { |
142
|
|
|
const event = (DeviceDetection.isTouchDevice()) ? 'touchstart' : 'click'; |
143
|
|
|
|
144
|
|
|
if (closable) { |
145
|
|
|
const onBackdropClick = () => { |
146
|
|
|
this.close(delay); |
147
|
|
|
// remove the event listener immediately to avoid multiple listeners |
148
|
|
|
document.removeEventListener(BACKDROP_EVENT.ON_CLICK, onBackdropClick); |
149
|
|
|
}; |
150
|
|
|
|
151
|
|
|
document.addEventListener(BACKDROP_EVENT.ON_CLICK, onBackdropClick); |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
window.addEventListener('popstate', this.close.bind(this, delay), { once: true }); |
155
|
|
|
const closeTriggers = document.querySelectorAll(`.${OFF_CANVAS_CLOSE_TRIGGER_CLASS}`); |
156
|
|
|
Iterator.iterate(closeTriggers, trigger => trigger.addEventListener(event, this.goBackInHistory.bind(this))); |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
/** |
160
|
|
|
* Remove all existing offcanvas from DOM |
161
|
|
|
* @private |
162
|
|
|
*/ |
163
|
|
|
_removeExistingOffCanvas() { |
164
|
|
|
const offCanvasElements = this.getOffCanvas(); |
165
|
|
|
return Iterator.iterate(offCanvasElements, offCanvas => offCanvas.remove()); |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
/** |
169
|
|
|
* Defines the position of the offcanvas by setting css class |
170
|
|
|
* @param {'left'|'right'|'bottom'} position |
171
|
|
|
* @returns {string} |
172
|
|
|
* @private |
173
|
|
|
*/ |
174
|
|
|
_getPositionClass(position) { |
175
|
|
|
return `is-${position}`; |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
/** |
179
|
|
|
* Creates the offcanvas element prototype including all relevant settings, |
180
|
|
|
* appends it to the DOM and returns the HTMLElement for further processing |
181
|
|
|
* @param {'left'|'right'|'bottom'} position |
182
|
|
|
* @param {boolean} fullwidth |
183
|
|
|
* @param {array|string} cssClass |
184
|
|
|
* @returns {HTMLElement} |
185
|
|
|
* @private |
186
|
|
|
*/ |
187
|
|
|
_createOffCanvas(position, fullwidth, cssClass) { |
188
|
|
|
const offCanvas = document.createElement('div'); |
189
|
|
|
offCanvas.classList.add(OFF_CANVAS_CLASS); |
190
|
|
|
offCanvas.classList.add(this._getPositionClass(position)); |
191
|
|
|
|
192
|
|
|
if (fullwidth === true) { |
193
|
|
|
offCanvas.classList.add(OFF_CANVAS_FULLWIDTH_CLASS); |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
if (cssClass) { |
197
|
|
|
const type = typeof cssClass; |
198
|
|
|
|
199
|
|
|
if (type === 'string') { |
200
|
|
|
offCanvas.classList.add(cssClass); |
201
|
|
|
} else if (Array.isArray(cssClass)) { |
202
|
|
|
cssClass.forEach((value) => { |
203
|
|
|
offCanvas.classList.add(value); |
204
|
|
|
}); |
205
|
|
|
} else { |
206
|
|
|
throw new Error(`The type "${type}" is not supported. Please pass an array or a string.`); |
207
|
|
|
} |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
document.body.appendChild(offCanvas); |
211
|
|
|
|
212
|
|
|
return offCanvas; |
213
|
|
|
} |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
|
217
|
|
|
/** |
218
|
|
|
* Create the OffCanvas instance. |
219
|
|
|
* @type {Readonly<OffCanvasSingleton>} |
220
|
|
|
*/ |
221
|
|
|
export const OffCanvasInstance = Object.freeze(new OffCanvasSingleton()); |
222
|
|
|
|
223
|
|
|
export default class OffCanvas { |
224
|
|
|
|
225
|
|
|
/** |
226
|
|
|
* Open the OffCanvas |
227
|
|
|
* @param {string} content |
228
|
|
|
* @param {function|null} callback |
229
|
|
|
* @param {'left'|'right'|'bottom'} position |
230
|
|
|
* @param {boolean} closable |
231
|
|
|
* @param {number} delay |
232
|
|
|
* @param {boolean} fullwidth |
233
|
|
|
* @param {array|string} cssClass |
234
|
|
|
*/ |
235
|
|
|
static open(content, callback = null, position = 'left', closable = true, delay = REMOVE_OFF_CANVAS_DELAY, fullwidth = false, cssClass = '') { |
236
|
|
|
OffCanvasInstance.open(content, callback, position, closable, delay, fullwidth, cssClass); |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
/** |
240
|
|
|
* Change content of visible OffCanvas |
241
|
|
|
* @param {string} content |
242
|
|
|
* @param {boolean} closable |
243
|
|
|
* @param {number} delay |
244
|
|
|
*/ |
245
|
|
|
static setContent(content, closable = true, delay = REMOVE_OFF_CANVAS_DELAY) { |
246
|
|
|
OffCanvasInstance.setContent(content, closable, delay); |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
/** |
250
|
|
|
* adds an additional class to the offcanvas |
251
|
|
|
* |
252
|
|
|
* @param {string} className |
253
|
|
|
*/ |
254
|
|
|
static setAdditionalClassName(className) { |
255
|
|
|
OffCanvasInstance.setAdditionalClassName(className); |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
/** |
259
|
|
|
* Close the OffCanvas |
260
|
|
|
* @param {number} delay |
261
|
|
|
*/ |
262
|
|
|
static close(delay = REMOVE_OFF_CANVAS_DELAY) { |
263
|
|
|
OffCanvasInstance.close(delay); |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* Returns whether any OffCanvas exists or not |
268
|
|
|
* @returns {boolean} |
269
|
|
|
*/ |
270
|
|
|
static exists() { |
271
|
|
|
return OffCanvasInstance.exists(); |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
/** |
275
|
|
|
* returns all existing offcanvas elements |
276
|
|
|
* |
277
|
|
|
* @returns {NodeListOf<Element>} |
278
|
|
|
*/ |
279
|
|
|
static getOffCanvas() { |
280
|
|
|
return OffCanvasInstance.getOffCanvas(); |
281
|
|
|
} |
282
|
|
|
|
283
|
|
|
/** |
284
|
|
|
* Expose constant |
285
|
|
|
* @returns {number} |
286
|
|
|
*/ |
287
|
|
|
static REMOVE_OFF_CANVAS_DELAY() { |
288
|
|
|
return REMOVE_OFF_CANVAS_DELAY; |
289
|
|
|
} |
290
|
|
|
} |
291
|
|
|
|