Passed
Push — master ( 928287...7bd6be )
by Christian
12:03 queued 11s
created

OffCanvasSingleton.goBackInHistory   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
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