@shower/core 3.0.0-2 → 3.3.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/dist/shower.js CHANGED
@@ -1,51 +1,15 @@
1
1
  /**
2
2
  * Core for Shower HTML presentation engine
3
- * @shower/core v3.0.0-1, https://github.com/shower/core
4
- * @copyright 2010–2019 Vadim Makeev, https://pepelsbey.net
3
+ * @shower/core v3.2.0, https://github.com/shower/core
4
+ * @copyright 2010–2021 Vadim Makeev, https://pepelsbey.net
5
5
  * @license MIT
6
6
  */
7
7
  (function () {
8
8
  'use strict';
9
9
 
10
- const EVENT_TARGET = Symbol('EventTarget');
10
+ const isInteractiveElement = (element) => element.tabIndex !== -1;
11
11
 
12
- class EventTarget {
13
- constructor() {
14
- this[EVENT_TARGET] = document.createElement('div');
15
- }
16
-
17
- addEventListener(...args) {
18
- this[EVENT_TARGET].addEventListener(...args);
19
- }
20
-
21
- removeEventListener(...args) {
22
- this[EVENT_TARGET].removeEventListener(...args);
23
- }
24
-
25
- dispatchEvent(event) {
26
- Object.defineProperties(event, {
27
- target: { value: this },
28
- currentTarget: { value: this },
29
- });
30
-
31
- return this[EVENT_TARGET].dispatchEvent(event);
32
- }
33
- }
34
-
35
- const isInteractiveElement = element => element.tabIndex !== -1;
36
- const freezeHistory = callback => {
37
- history.pushState = () => {};
38
- history.replaceState = () => {};
39
-
40
- try {
41
- callback();
42
- } finally {
43
- delete history.pushState;
44
- delete history.replaceState;
45
- }
46
- };
47
-
48
- const contentLoaded = callback => {
12
+ const contentLoaded = (callback) => {
49
13
  if (document.currentScript.async) {
50
14
  callback();
51
15
  } else {
@@ -53,12 +17,27 @@
53
17
  }
54
18
  };
55
19
 
20
+ const defineReadOnly = (target, props) => {
21
+ for (const [key, value] of Object.entries(props)) {
22
+ Object.defineProperty(target, key, {
23
+ value,
24
+ writable: false,
25
+ enumerable: true,
26
+ configurable: true,
27
+ });
28
+ }
29
+ };
30
+
31
+ class ShowerError extends Error {}
32
+
56
33
  var defaultOptions = {
57
34
  containerSelector: '.shower',
58
35
  progressSelector: '.progress',
59
36
  stepSelector: '.next',
60
37
  fullModeClass: 'full',
61
38
  listModeClass: 'list',
39
+ mouseHiddenClass: 'pointless',
40
+ mouseInactivityTimeout: 5000,
62
41
 
63
42
  slideSelector: '.slide',
64
43
  slideTitleSelector: 'h2',
@@ -66,22 +45,32 @@
66
45
  visitedSlideClass: 'visited',
67
46
  };
68
47
 
69
- /**
70
- * @param {HTMLElement} element
71
- * @param {object} options
72
- */
73
48
  class Slide extends EventTarget {
74
- constructor(element, options) {
49
+ /**
50
+ * @param {Shower} shower
51
+ * @param {HTMLElement} element
52
+ */
53
+ constructor(shower, element) {
75
54
  super();
76
55
 
77
- this.element = element;
78
- this.options = options;
56
+ defineReadOnly(this, {
57
+ shower,
58
+ element,
59
+ state: {
60
+ visitCount: 0,
61
+ innerStepCount: 0,
62
+ },
63
+ });
79
64
 
80
65
  this._isActive = false;
81
- this.state = {
82
- visitsCount: 0,
83
- innerStepsCount: 0,
84
- };
66
+ this._options = this.shower.options;
67
+
68
+ this.element.addEventListener('click', (event) => {
69
+ if (event.defaultPrevented) return;
70
+
71
+ this.activate();
72
+ this.shower.enterFullMode();
73
+ });
85
74
  }
86
75
 
87
76
  get isActive() {
@@ -89,7 +78,7 @@
89
78
  }
90
79
 
91
80
  get isVisited() {
92
- return this.state.visitsCount > 0;
81
+ return this.state.visitCount > 0;
93
82
  }
94
83
 
95
84
  get id() {
@@ -97,27 +86,57 @@
97
86
  }
98
87
 
99
88
  get title() {
100
- const titleElement = this.element.querySelector(this.options.slideTitleSelector);
89
+ const titleElement = this.element.querySelector(this._options.slideTitleSelector);
101
90
  return titleElement ? titleElement.innerText : '';
102
91
  }
103
92
 
93
+ /**
94
+ * Deactivates currently active slide (if any) and activates itself.
95
+ * @emits Slide#deactivate
96
+ * @emits Slide#activate
97
+ * @emits Shower#slidechange
98
+ */
104
99
  activate() {
105
100
  if (this._isActive) return;
106
101
 
102
+ const prev = this.shower.activeSlide;
103
+ if (prev) {
104
+ prev._deactivate();
105
+ }
106
+
107
+ this.state.visitCount++;
108
+ this.element.classList.add(this._options.activeSlideClass);
109
+
107
110
  this._isActive = true;
108
- this.state.visitsCount++;
109
- this.element.classList.add(this.options.activeSlideClass);
110
111
  this.dispatchEvent(new Event('activate'));
112
+ this.shower.dispatchEvent(
113
+ new CustomEvent('slidechange', {
114
+ detail: { prev },
115
+ }),
116
+ );
111
117
  }
112
118
 
119
+ /**
120
+ * @throws {ShowerError}
121
+ * @emits Slide#deactivate
122
+ */
113
123
  deactivate() {
114
- if (!this._isActive) return;
124
+ if (this.shower.isFullMode) {
125
+ throw new ShowerError('In full mode, another slide should be activated instead.');
126
+ }
115
127
 
116
- this._isActive = false;
128
+ if (this._isActive) {
129
+ this._deactivate();
130
+ }
131
+ }
132
+
133
+ _deactivate() {
117
134
  this.element.classList.replace(
118
- this.options.activeSlideClass,
119
- this.options.visitedSlideClass,
135
+ this._options.activeSlideClass,
136
+ this._options.visitedSlideClass,
120
137
  );
138
+
139
+ this._isActive = false;
121
140
  this.dispatchEvent(new Event('deactivate'));
122
141
  }
123
142
  }
@@ -132,7 +151,7 @@
132
151
  return liveRegion;
133
152
  };
134
153
 
135
- var a11y = shower => {
154
+ var a11y = (shower) => {
136
155
  const { container } = shower;
137
156
  const liveRegion = createLiveRegion();
138
157
  container.appendChild(liveRegion);
@@ -152,12 +171,17 @@
152
171
  }
153
172
  };
154
173
 
174
+ shower.addEventListener('start', () => {
175
+ updateDocumentRole();
176
+ updateLiveRegion();
177
+ });
178
+
155
179
  shower.addEventListener('modechange', updateDocumentRole);
156
180
  shower.addEventListener('slidechange', updateLiveRegion);
157
181
  };
158
182
 
159
- var keys = shower => {
160
- const doSlideActions = event => {
183
+ var keys = (shower) => {
184
+ const doSlideActions = (event) => {
161
185
  const isShowerAction = !(event.ctrlKey || event.altKey || event.metaKey);
162
186
 
163
187
  switch (event.key.toUpperCase()) {
@@ -179,6 +203,7 @@
179
203
  }
180
204
  break;
181
205
 
206
+ case 'BACKSPACE':
182
207
  case 'PAGEUP':
183
208
  case 'ARROWUP':
184
209
  case 'ARROWLEFT':
@@ -226,7 +251,7 @@
226
251
  }
227
252
  };
228
253
 
229
- const doModeActions = event => {
254
+ const doModeActions = (event) => {
230
255
  switch (event.key.toUpperCase()) {
231
256
  case 'ESCAPE':
232
257
  if (shower.isFullMode) {
@@ -258,7 +283,7 @@
258
283
  }
259
284
  };
260
285
 
261
- shower.container.addEventListener('keydown', event => {
286
+ shower.container.addEventListener('keydown', (event) => {
262
287
  if (event.defaultPrevented) return;
263
288
  if (isInteractiveElement(event.target)) return;
264
289
 
@@ -267,8 +292,8 @@
267
292
  });
268
293
  };
269
294
 
270
- var location$1 = shower => {
271
- const getURL = () => {
295
+ var location$1 = (shower) => {
296
+ const composeURL = () => {
272
297
  const search = shower.isFullMode ? '?full' : '';
273
298
  const slide = shower.activeSlide;
274
299
  const hash = slide ? `#${slide.id}` : '';
@@ -276,11 +301,20 @@
276
301
  return location.pathname + search + hash; // path is required to clear search params
277
302
  };
278
303
 
279
- const changeSlide = () => {
304
+ const applyURLMode = () => {
305
+ const isFull = new URLSearchParams(location.search).has('full');
306
+ if (isFull) {
307
+ shower.enterFullMode();
308
+ } else {
309
+ shower.exitFullMode();
310
+ }
311
+ };
312
+
313
+ const applyURLSlide = () => {
280
314
  const id = location.hash.slice(1);
281
315
  if (!id) return;
282
316
 
283
- const target = shower.slides.find(slide => slide.id === id);
317
+ const target = shower.slides.find((slide) => slide.id === id);
284
318
  if (target) {
285
319
  target.activate();
286
320
  } else if (!shower.activeSlide) {
@@ -288,86 +322,86 @@
288
322
  }
289
323
  };
290
324
 
291
- shower.addEventListener('modechange', () => {
292
- history.replaceState(null, document.title, getURL());
293
- });
325
+ const applyURL = () => {
326
+ applyURLMode();
327
+ applyURLSlide();
328
+ };
294
329
 
295
- shower.addEventListener('slidechange', () => {
296
- history.pushState(null, document.title, getURL());
297
- });
330
+ applyURL();
331
+ window.addEventListener('popstate', applyURL);
298
332
 
299
333
  shower.addEventListener('start', () => {
300
- const isFull = new URL(location).searchParams.has('full');
301
- changeSlide();
302
- if (isFull) {
303
- shower.enterFullMode();
304
- }
334
+ history.replaceState(null, document.title, composeURL());
305
335
  });
306
336
 
307
- window.addEventListener('popstate', () => {
308
- freezeHistory(() => {
309
- changeSlide();
310
- const isFull = new URL(location).searchParams.has('full');
311
- if (isFull) {
312
- shower.enterFullMode();
313
- } else {
314
- shower.exitFullMode();
315
- }
316
- });
337
+ shower.addEventListener('modechange', () => {
338
+ history.replaceState(null, document.title, composeURL());
339
+ });
340
+
341
+ shower.addEventListener('slidechange', () => {
342
+ const url = composeURL();
343
+ if (!location.href.endsWith(url)) {
344
+ history.pushState(null, document.title, url);
345
+ }
317
346
  });
318
347
  };
319
348
 
320
- var next = shower => {
321
- const { stepSelector, activeSlideClass } = shower.options;
349
+ var next = (shower) => {
350
+ const { stepSelector, activeSlideClass, visitedSlideClass } = shower.options;
322
351
 
323
352
  let innerSteps;
324
- let innerAt;
353
+ let activeIndex;
325
354
 
326
- const getInnerSteps = () => {
327
- const { element } = shower.activeSlide;
328
- return [...element.querySelectorAll(stepSelector)];
329
- };
355
+ const isActive = (step) => step.classList.contains(activeSlideClass);
356
+ const isVisited = (step) => step.classList.contains(visitedSlideClass);
330
357
 
331
- const getInnerAt = () => {
332
- return innerSteps.filter(step => {
333
- return step.classList.contains(activeSlideClass);
334
- }).length;
335
- };
358
+ const setInnerStepsState = () => {
359
+ if (shower.isListMode) return;
336
360
 
337
- const toggleActive = () => {
338
- innerSteps.forEach((step, index) => {
339
- step.classList.toggle(activeSlideClass, index < innerAt);
340
- });
361
+ const slide = shower.activeSlide;
362
+
363
+ innerSteps = [...slide.element.querySelectorAll(stepSelector)];
364
+ activeIndex =
365
+ innerSteps.length && innerSteps.every(isVisited)
366
+ ? innerSteps.length
367
+ : innerSteps.filter(isActive).length - 1;
368
+
369
+ slide.state.innerStepCount = innerSteps.length;
341
370
  };
342
371
 
343
- shower.addEventListener('slidechange', () => {
344
- innerSteps = getInnerSteps();
345
- innerAt = getInnerAt();
372
+ shower.addEventListener('start', setInnerStepsState);
373
+ shower.addEventListener('modechange', setInnerStepsState);
374
+ shower.addEventListener('slidechange', setInnerStepsState);
346
375
 
347
- const slide = shower.activeSlide;
348
- slide.state.innerStepsCount = innerSteps.length;
349
- });
376
+ shower.addEventListener('next', (event) => {
377
+ if (shower.isListMode || event.defaultPrevented || !event.cancelable) return;
350
378
 
351
- shower.addEventListener('next', event => {
352
- if (event.defaultPrevented || !event.cancelable) return;
353
- if (shower.isListMode || innerAt === innerSteps.length) return;
379
+ activeIndex++;
380
+ innerSteps.forEach((step, index) => {
381
+ step.classList.toggle(visitedSlideClass, index < activeIndex);
382
+ step.classList.toggle(activeSlideClass, index === activeIndex);
383
+ });
354
384
 
355
- event.preventDefault();
356
- innerAt++;
357
- toggleActive();
385
+ if (activeIndex < innerSteps.length) {
386
+ event.preventDefault();
387
+ }
358
388
  });
359
389
 
360
- shower.addEventListener('prev', event => {
361
- if (event.defaultPrevented || !event.cancelable) return;
362
- if (shower.isListMode || innerAt === innerSteps.length || !innerAt) return;
390
+ shower.addEventListener('prev', (event) => {
391
+ if (shower.isListMode || event.defaultPrevented || !event.cancelable) return;
392
+ if (activeIndex === -1 || activeIndex === innerSteps.length) return;
393
+
394
+ activeIndex--;
395
+ innerSteps.forEach((step, index) => {
396
+ step.classList.toggle(visitedSlideClass, index < activeIndex + 1);
397
+ step.classList.toggle(activeSlideClass, index === activeIndex);
398
+ });
363
399
 
364
400
  event.preventDefault();
365
- innerAt--;
366
- toggleActive();
367
401
  });
368
402
  };
369
403
 
370
- var progress = shower => {
404
+ var progress = (shower) => {
371
405
  const { progressSelector } = shower.options;
372
406
  const bar = shower.container.querySelector(progressSelector);
373
407
  if (!bar) return;
@@ -386,46 +420,27 @@
386
420
  bar.setAttribute('aria-valuetext', `Slideshow progress: ${progress}%`);
387
421
  };
388
422
 
423
+ shower.addEventListener('start', updateProgress);
389
424
  shower.addEventListener('slidechange', updateProgress);
390
425
  };
391
426
 
392
- var scale = shower => {
393
- const { container } = shower;
394
- const getScale = () => {
395
- const maxRatio = Math.max(
396
- container.offsetWidth / window.innerWidth,
397
- container.offsetHeight / window.innerHeight,
398
- );
399
-
400
- return `scale(${1 / maxRatio})`;
401
- };
402
-
403
- const updateStyle = () => {
404
- container.style.transform = shower.isFullMode ? getScale() : '';
405
- };
406
-
407
- shower.addEventListener('modechange', updateStyle);
408
- window.addEventListener('resize', updateStyle);
409
- window.addEventListener('load', updateStyle);
410
- };
411
-
412
427
  const units = ['s', 'm', 'h'];
413
- const hasUnits = timing => {
414
- return units.some(unit => timing.includes(unit));
428
+ const hasUnits = (timing) => {
429
+ return units.some((unit) => timing.includes(unit));
415
430
  };
416
431
 
417
- const parseUnits = timing => {
418
- return units.map(unit => timing.match(`(\\S+)${unit}`)).map(match => match && match[1]);
432
+ const parseUnits = (timing) => {
433
+ return units.map((unit) => timing.match(`(\\S+)${unit}`)).map((match) => match && match[1]);
419
434
  };
420
435
 
421
- const parseColons = timing => {
436
+ const parseColons = (timing) => {
422
437
  return `::${timing}`.split(':').reverse();
423
438
  };
424
439
 
425
440
  const SEC_IN_MIN = 60;
426
441
  const SEC_IN_HOUR = SEC_IN_MIN * 60;
427
442
 
428
- var parseTiming = timing => {
443
+ var parseTiming = (timing) => {
429
444
  if (!timing) return 0;
430
445
 
431
446
  const parsed = hasUnits(timing) ? parseUnits(timing) : parseColons(timing);
@@ -438,32 +453,33 @@
438
453
  return Math.max(sec * 1000, 0);
439
454
  };
440
455
 
441
- var timer = shower => {
456
+ var timer = (shower) => {
442
457
  let id;
443
458
 
444
- const setTimer = () => {
459
+ const resetTimer = () => {
445
460
  clearTimeout(id);
446
461
  if (shower.isListMode) return;
447
462
 
448
463
  const slide = shower.activeSlide;
449
- const { visitsCount, innerStepsCount } = slide.state;
450
- if (visitsCount > 1) return;
464
+ const { visitCount, innerStepCount } = slide.state;
465
+ if (visitCount > 1) return;
451
466
 
452
467
  const timing = parseTiming(slide.element.dataset.timing);
453
468
  if (!timing) return;
454
469
 
455
- if (innerStepsCount) {
456
- const stepTiming = timing / (innerStepsCount + 1);
470
+ if (innerStepCount) {
471
+ const stepTiming = timing / (innerStepCount + 1);
457
472
  id = setInterval(() => shower.next(), stepTiming);
458
473
  } else {
459
474
  id = setTimeout(() => shower.next(), timing);
460
475
  }
461
476
  };
462
477
 
463
- shower.addEventListener('modechange', setTimer);
464
- shower.addEventListener('slidechange', setTimer);
478
+ shower.addEventListener('start', resetTimer);
479
+ shower.addEventListener('modechange', resetTimer);
480
+ shower.addEventListener('slidechange', resetTimer);
465
481
 
466
- shower.container.addEventListener('keydown', event => {
482
+ shower.container.addEventListener('keydown', (event) => {
467
483
  if (!event.defaultPrevented) {
468
484
  clearTimeout(id);
469
485
  }
@@ -472,7 +488,7 @@
472
488
 
473
489
  const mdash = '\u2014';
474
490
 
475
- var title = shower => {
491
+ var title = (shower) => {
476
492
  const { title } = document;
477
493
  const updateTitle = () => {
478
494
  if (shower.isFullMode) {
@@ -487,134 +503,242 @@
487
503
  document.title = title;
488
504
  };
489
505
 
506
+ shower.addEventListener('start', updateTitle);
490
507
  shower.addEventListener('modechange', updateTitle);
491
508
  shower.addEventListener('slidechange', updateTitle);
492
509
  };
493
510
 
494
- var view = shower => {
511
+ var view = (shower) => {
495
512
  const { container } = shower;
496
513
  const { fullModeClass, listModeClass } = shower.options;
497
514
 
498
- shower.addEventListener('modechange', () => {
515
+ if (container.classList.contains(fullModeClass)) {
516
+ shower.enterFullMode();
517
+ } else {
518
+ container.classList.add(listModeClass);
519
+ }
520
+
521
+ const updateScale = () => {
522
+ const firstSlide = shower.slides[0];
523
+ if (!firstSlide) return;
524
+
525
+ const { innerWidth, innerHeight } = window;
526
+ const { offsetWidth, offsetHeight } = firstSlide.element;
527
+
528
+ const listScale = 1 / (offsetWidth / innerWidth);
529
+ const fullScale = 1 / Math.max(offsetWidth / innerWidth, offsetHeight / innerHeight);
530
+
531
+ container.style.setProperty('--shower-list-scale', listScale);
532
+ container.style.setProperty('--shower-full-scale', fullScale);
533
+ };
534
+
535
+ const updateModeView = () => {
499
536
  if (shower.isFullMode) {
500
537
  container.classList.remove(listModeClass);
501
538
  container.classList.add(fullModeClass);
502
- return;
539
+ } else {
540
+ container.classList.remove(fullModeClass);
541
+ container.classList.add(listModeClass);
503
542
  }
504
543
 
505
- container.classList.remove(fullModeClass);
506
- container.classList.add(listModeClass);
544
+ updateScale();
545
+
546
+ if (shower.isFullMode) return;
507
547
 
508
548
  const slide = shower.activeSlide;
509
549
  if (slide) {
510
550
  slide.element.scrollIntoView({ block: 'center' });
511
551
  }
512
- });
552
+ };
513
553
 
554
+ shower.addEventListener('start', updateModeView);
555
+ shower.addEventListener('modechange', updateModeView);
514
556
  shower.addEventListener('slidechange', () => {
557
+ if (shower.isFullMode) return;
558
+
515
559
  const slide = shower.activeSlide;
516
560
  slide.element.scrollIntoView({ block: 'nearest' });
517
561
  });
518
562
 
519
- shower.addEventListener('start', () => {
520
- if (container.classList.contains(fullModeClass)) {
521
- shower.enterFullMode();
522
- } else {
523
- container.classList.add(listModeClass);
563
+ window.addEventListener('resize', updateScale);
564
+ };
565
+
566
+ var touch = (shower) => {
567
+ let exitFullScreen = false;
568
+ let clickable = false;
569
+
570
+ document.addEventListener('touchstart', (event) => {
571
+ if (event.touches.length === 1) {
572
+ const touch = event.touches[0];
573
+ const x = touch.clientX;
574
+ const { target } = touch;
575
+ clickable = target.tabIndex !== -1;
576
+ if (!clickable) {
577
+ if (shower.isFullMode) {
578
+ if (event.cancelable) event.preventDefault();
579
+ if (window.innerWidth / 2 < x) {
580
+ shower.next();
581
+ } else {
582
+ shower.prev();
583
+ }
584
+ }
585
+ }
586
+ } else if (event.touches.length === 3) {
587
+ exitFullScreen = true;
524
588
  }
525
589
  });
590
+
591
+ shower.container.addEventListener('touchend', (event) => {
592
+ if (exitFullScreen) {
593
+ event.preventDefault();
594
+ exitFullScreen = false;
595
+ shower.exitFullMode();
596
+ } else if (event.touches.length === 1 && !clickable && shower.isFullMode)
597
+ event.preventDefault();
598
+ });
599
+ };
600
+
601
+ var mouse = (shower) => {
602
+ const { mouseHiddenClass, mouseInactivityTimeout } = shower.options;
603
+
604
+ let hideMouseTimeoutId = null;
605
+
606
+ const cleanUp = () => {
607
+ shower.container.classList.remove(mouseHiddenClass);
608
+ clearTimeout(hideMouseTimeoutId);
609
+ hideMouseTimeoutId = null;
610
+ };
611
+
612
+ const hideMouseIfInactive = () => {
613
+ if (hideMouseTimeoutId !== null) {
614
+ cleanUp();
615
+ }
616
+
617
+ hideMouseTimeoutId = setTimeout(() => {
618
+ shower.container.classList.add(mouseHiddenClass);
619
+ }, mouseInactivityTimeout);
620
+ };
621
+
622
+ const initHideMouseIfInactiveModule = () => {
623
+ shower.container.addEventListener('mousemove', hideMouseIfInactive);
624
+ };
625
+
626
+ const destroyHideMouseIfInactiveModule = () => {
627
+ shower.container.removeEventListener('mousemove', hideMouseIfInactive);
628
+ cleanUp();
629
+ };
630
+
631
+ const handleModeChange = () => {
632
+ if (shower.isFullMode) {
633
+ initHideMouseIfInactiveModule();
634
+ } else {
635
+ destroyHideMouseIfInactiveModule();
636
+ }
637
+ };
638
+
639
+ shower.addEventListener('start', handleModeChange);
640
+ shower.addEventListener('modechange', handleModeChange);
526
641
  };
527
642
 
528
- var installModules = shower => {
643
+ var installModules = (shower) => {
529
644
  a11y(shower);
530
- keys(shower); // should come before `timer`
531
645
  progress(shower);
532
- next(shower); // should come before `timer`
533
- timer(shower);
534
- title(shower); // should come before `location`
535
- location$1(shower);
646
+ keys(shower);
647
+ next(shower);
648
+ timer(shower); // should come after `keys` and `next`
649
+ title(shower);
650
+ location$1(shower); // should come after `title`
536
651
  view(shower);
537
- scale(shower);
538
- };
652
+ touch(shower);
653
+ mouse(shower);
539
654
 
540
- const ensureSlideId = (slideElement, index) => {
541
- if (!slideElement.id) {
542
- slideElement.id = index + 1;
655
+ // maintains invariant: active slide always exists in `full` mode
656
+ if (shower.isFullMode && !shower.activeSlide) {
657
+ shower.first();
543
658
  }
544
659
  };
545
660
 
546
661
  class Shower extends EventTarget {
662
+ /**
663
+ * @param {object=} options
664
+ */
547
665
  constructor(options) {
548
666
  super();
549
667
 
668
+ defineReadOnly(this, {
669
+ options: { ...defaultOptions, ...options },
670
+ });
671
+
550
672
  this._mode = 'list';
551
673
  this._isStarted = false;
552
- this.options = Object.assign({}, defaultOptions, options);
674
+ this._container = null;
553
675
  }
554
676
 
555
677
  /**
556
- * @param {object} options
678
+ * @param {object=} options
679
+ * @throws {ShowerError}
557
680
  */
558
681
  configure(options) {
682
+ if (this._isStarted) {
683
+ throw new ShowerError('Shower should be configured before it is started.');
684
+ }
685
+
559
686
  Object.assign(this.options, options);
560
687
  }
561
688
 
689
+ /**
690
+ * @throws {ShowerError}
691
+ * @emits Shower#start
692
+ */
562
693
  start() {
563
694
  if (this._isStarted) return;
564
695
 
565
696
  const { containerSelector } = this.options;
566
- this.container = document.querySelector(containerSelector);
567
- if (!this.container) {
568
- throw new Error(`Shower container with selector '${containerSelector}' not found.`);
697
+ this._container = document.querySelector(containerSelector);
698
+ if (!this._container) {
699
+ throw new ShowerError(
700
+ `Shower container with selector '${containerSelector}' was not found.`,
701
+ );
569
702
  }
570
703
 
571
- this._isStarted = true;
572
704
  this._initSlides();
573
-
574
- // maintains invariant: active slide always exists in `full` mode
575
- this.addEventListener('modechange', () => {
576
- if (this.isFullMode && !this.activeSlide) {
577
- this.first();
578
- }
579
- });
580
-
581
705
  installModules(this);
706
+
707
+ this._isStarted = true;
582
708
  this.dispatchEvent(new Event('start'));
583
709
  }
584
710
 
585
711
  _initSlides() {
586
- const slideElements = [
587
- ...this.container.querySelectorAll(this.options.slideSelector),
588
- ].filter(slideElement => !slideElement.hidden);
589
-
590
- slideElements.forEach(ensureSlideId);
591
- this.slides = slideElements.map(slideElement => {
592
- const slide = new Slide(slideElement, this.options);
593
-
594
- slide.addEventListener('activate', () => {
595
- this._toggleActiveSlide(slide);
596
- });
597
-
598
- slide.element.addEventListener('click', () => {
599
- if (this.isListMode) {
600
- slide.activate();
601
- this.enterFullMode();
602
- }
603
- });
712
+ const visibleSlideSelector = `${this.options.slideSelector}:not([hidden])`;
713
+ const visibleSlideElements = this._container.querySelectorAll(visibleSlideSelector);
604
714
 
605
- return slide;
715
+ this.slides = Array.from(visibleSlideElements, (slideElement, index) => {
716
+ if (!slideElement.id) {
717
+ slideElement.id = index + 1;
718
+ }
719
+
720
+ return new Slide(this, slideElement);
606
721
  });
607
722
  }
608
723
 
609
- _toggleActiveSlide(target) {
610
- // at this point, there can be two active slides
611
- this.slides.forEach(slide => {
612
- if (slide !== target) {
613
- slide.deactivate();
614
- }
615
- });
724
+ _setMode(mode) {
725
+ if (mode === this._mode) return;
616
726
 
617
- this.dispatchEvent(new Event('slidechange'));
727
+ this._mode = mode;
728
+ this.dispatchEvent(new Event('modechange'));
729
+ }
730
+
731
+ /**
732
+ * @param {Event} event
733
+ */
734
+ dispatchEvent(event) {
735
+ if (!this._isStarted) return false;
736
+
737
+ return super.dispatchEvent(event);
738
+ }
739
+
740
+ get container() {
741
+ return this._container;
618
742
  }
619
743
 
620
744
  get isFullMode() {
@@ -626,31 +750,27 @@
626
750
  }
627
751
 
628
752
  get activeSlide() {
629
- return this.slides.find(slide => slide.isActive);
753
+ return this.slides.find((slide) => slide.isActive);
630
754
  }
631
755
 
632
756
  get activeSlideIndex() {
633
- return this.slides.findIndex(slide => slide.isActive);
757
+ return this.slides.findIndex((slide) => slide.isActive);
634
758
  }
635
759
 
636
760
  /**
637
761
  * Slide fills the maximum area.
762
+ * @emits Shower#modechange
638
763
  */
639
764
  enterFullMode() {
640
- if (!this.isFullMode) {
641
- this._mode = 'full';
642
- this.dispatchEvent(new Event('modechange'));
643
- }
765
+ this._setMode('full');
644
766
  }
645
767
 
646
768
  /**
647
769
  * Shower returns into list mode.
770
+ * @emits Shower#modechange
648
771
  */
649
772
  exitFullMode() {
650
- if (!this.isListMode) {
651
- this._mode = 'list';
652
- this.dispatchEvent(new Event('modechange'));
653
- }
773
+ this._setMode('list');
654
774
  }
655
775
 
656
776
  /**
@@ -666,27 +786,29 @@
666
786
  /**
667
787
  * @param {number} delta
668
788
  */
669
- go(delta) {
789
+ goBy(delta) {
670
790
  this.goTo(this.activeSlideIndex + delta);
671
791
  }
672
792
 
673
793
  /**
674
- * @param {boolean=} isForce
794
+ * @param {boolean} [isForce=false]
795
+ * @emits Shower#prev
675
796
  */
676
797
  prev(isForce) {
677
798
  const prev = new Event('prev', { cancelable: !isForce });
678
799
  if (this.dispatchEvent(prev)) {
679
- this.go(-1);
800
+ this.goBy(-1);
680
801
  }
681
802
  }
682
803
 
683
804
  /**
684
- * @param {boolean=} isForce
805
+ * @param {boolean} [isForce=false]
806
+ * @emits Shower#next
685
807
  */
686
808
  next(isForce) {
687
809
  const next = new Event('next', { cancelable: !isForce });
688
810
  if (this.dispatchEvent(next)) {
689
- this.go(1);
811
+ this.goBy(1);
690
812
  }
691
813
  }
692
814
 
@@ -711,4 +833,4 @@
711
833
  shower.start();
712
834
  });
713
835
 
714
- }());
836
+ })();