@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/lib/shower.js CHANGED
@@ -1,86 +1,89 @@
1
1
  import defaultOptions from './default-options';
2
2
  import Slide from './slide';
3
- import { EventTarget } from './utils';
4
- import installModules from './modules';
5
-
6
- const ensureSlideId = (slideElement, index) => {
7
- if (!slideElement.id) {
8
- slideElement.id = index + 1;
9
- }
10
- };
3
+ import { defineReadOnly, ShowerError } from './utils';
4
+ import installModules from './modules/install';
11
5
 
12
6
  class Shower extends EventTarget {
7
+ /**
8
+ * @param {object=} options
9
+ */
13
10
  constructor(options) {
14
11
  super();
15
12
 
13
+ defineReadOnly(this, {
14
+ options: { ...defaultOptions, ...options },
15
+ });
16
+
16
17
  this._mode = 'list';
17
18
  this._isStarted = false;
18
- this.options = Object.assign({}, defaultOptions, options);
19
+ this._container = null;
19
20
  }
20
21
 
21
22
  /**
22
- * @param {object} options
23
+ * @param {object=} options
24
+ * @throws {ShowerError}
23
25
  */
24
26
  configure(options) {
27
+ if (this._isStarted) {
28
+ throw new ShowerError('Shower should be configured before it is started.');
29
+ }
30
+
25
31
  Object.assign(this.options, options);
26
32
  }
27
33
 
34
+ /**
35
+ * @throws {ShowerError}
36
+ * @emits Shower#start
37
+ */
28
38
  start() {
29
39
  if (this._isStarted) return;
30
40
 
31
41
  const { containerSelector } = this.options;
32
- this.container = document.querySelector(containerSelector);
33
- if (!this.container) {
34
- throw new Error(`Shower container with selector '${containerSelector}' not found.`);
42
+ this._container = document.querySelector(containerSelector);
43
+ if (!this._container) {
44
+ throw new ShowerError(
45
+ `Shower container with selector '${containerSelector}' was not found.`,
46
+ );
35
47
  }
36
48
 
37
- this._isStarted = true;
38
49
  this._initSlides();
39
-
40
- // maintains invariant: active slide always exists in `full` mode
41
- this.addEventListener('modechange', () => {
42
- if (this.isFullMode && !this.activeSlide) {
43
- this.first();
44
- }
45
- });
46
-
47
50
  installModules(this);
51
+
52
+ this._isStarted = true;
48
53
  this.dispatchEvent(new Event('start'));
49
54
  }
50
55
 
51
56
  _initSlides() {
52
- const slideElements = [
53
- ...this.container.querySelectorAll(this.options.slideSelector),
54
- ].filter(slideElement => !slideElement.hidden);
55
-
56
- slideElements.forEach(ensureSlideId);
57
- this.slides = slideElements.map(slideElement => {
58
- const slide = new Slide(slideElement, this.options);
59
-
60
- slide.addEventListener('activate', () => {
61
- this._toggleActiveSlide(slide);
62
- });
63
-
64
- slide.element.addEventListener('click', () => {
65
- if (this.isListMode) {
66
- slide.activate();
67
- this.enterFullMode();
68
- }
69
- });
70
-
71
- return slide;
72
- });
73
- }
57
+ const visibleSlideSelector = `${this.options.slideSelector}:not([hidden])`;
58
+ const visibleSlideElements = this._container.querySelectorAll(visibleSlideSelector);
74
59
 
75
- _toggleActiveSlide(target) {
76
- // at this point, there can be two active slides
77
- this.slides.forEach(slide => {
78
- if (slide !== target) {
79
- slide.deactivate();
60
+ this.slides = Array.from(visibleSlideElements, (slideElement, index) => {
61
+ if (!slideElement.id) {
62
+ slideElement.id = index + 1;
80
63
  }
64
+
65
+ return new Slide(this, slideElement);
81
66
  });
67
+ }
68
+
69
+ _setMode(mode) {
70
+ if (mode === this._mode) return;
82
71
 
83
- this.dispatchEvent(new Event('slidechange'));
72
+ this._mode = mode;
73
+ this.dispatchEvent(new Event('modechange'));
74
+ }
75
+
76
+ /**
77
+ * @param {Event} event
78
+ */
79
+ dispatchEvent(event) {
80
+ if (!this._isStarted) return false;
81
+
82
+ return super.dispatchEvent(event);
83
+ }
84
+
85
+ get container() {
86
+ return this._container;
84
87
  }
85
88
 
86
89
  get isFullMode() {
@@ -92,31 +95,27 @@ class Shower extends EventTarget {
92
95
  }
93
96
 
94
97
  get activeSlide() {
95
- return this.slides.find(slide => slide.isActive);
98
+ return this.slides.find((slide) => slide.isActive);
96
99
  }
97
100
 
98
101
  get activeSlideIndex() {
99
- return this.slides.findIndex(slide => slide.isActive);
102
+ return this.slides.findIndex((slide) => slide.isActive);
100
103
  }
101
104
 
102
105
  /**
103
106
  * Slide fills the maximum area.
107
+ * @emits Shower#modechange
104
108
  */
105
109
  enterFullMode() {
106
- if (!this.isFullMode) {
107
- this._mode = 'full';
108
- this.dispatchEvent(new Event('modechange'));
109
- }
110
+ this._setMode('full');
110
111
  }
111
112
 
112
113
  /**
113
114
  * Shower returns into list mode.
115
+ * @emits Shower#modechange
114
116
  */
115
117
  exitFullMode() {
116
- if (!this.isListMode) {
117
- this._mode = 'list';
118
- this.dispatchEvent(new Event('modechange'));
119
- }
118
+ this._setMode('list');
120
119
  }
121
120
 
122
121
  /**
@@ -132,27 +131,29 @@ class Shower extends EventTarget {
132
131
  /**
133
132
  * @param {number} delta
134
133
  */
135
- go(delta) {
134
+ goBy(delta) {
136
135
  this.goTo(this.activeSlideIndex + delta);
137
136
  }
138
137
 
139
138
  /**
140
- * @param {boolean=} isForce
139
+ * @param {boolean} [isForce=false]
140
+ * @emits Shower#prev
141
141
  */
142
142
  prev(isForce) {
143
143
  const prev = new Event('prev', { cancelable: !isForce });
144
144
  if (this.dispatchEvent(prev)) {
145
- this.go(-1);
145
+ this.goBy(-1);
146
146
  }
147
147
  }
148
148
 
149
149
  /**
150
- * @param {boolean=} isForce
150
+ * @param {boolean} [isForce=false]
151
+ * @emits Shower#next
151
152
  */
152
153
  next(isForce) {
153
154
  const next = new Event('next', { cancelable: !isForce });
154
155
  if (this.dispatchEvent(next)) {
155
- this.go(1);
156
+ this.goBy(1);
156
157
  }
157
158
  }
158
159
 
package/lib/slide.js ADDED
@@ -0,0 +1,99 @@
1
+ import { defineReadOnly, ShowerError } from './utils';
2
+
3
+ class Slide extends EventTarget {
4
+ /**
5
+ * @param {Shower} shower
6
+ * @param {HTMLElement} element
7
+ */
8
+ constructor(shower, element) {
9
+ super();
10
+
11
+ defineReadOnly(this, {
12
+ shower,
13
+ element,
14
+ state: {
15
+ visitCount: 0,
16
+ innerStepCount: 0,
17
+ },
18
+ });
19
+
20
+ this._isActive = false;
21
+ this._options = this.shower.options;
22
+
23
+ this.element.addEventListener('click', (event) => {
24
+ if (event.defaultPrevented) return;
25
+
26
+ this.activate();
27
+ this.shower.enterFullMode();
28
+ });
29
+ }
30
+
31
+ get isActive() {
32
+ return this._isActive;
33
+ }
34
+
35
+ get isVisited() {
36
+ return this.state.visitCount > 0;
37
+ }
38
+
39
+ get id() {
40
+ return this.element.id;
41
+ }
42
+
43
+ get title() {
44
+ const titleElement = this.element.querySelector(this._options.slideTitleSelector);
45
+ return titleElement ? titleElement.innerText : '';
46
+ }
47
+
48
+ /**
49
+ * Deactivates currently active slide (if any) and activates itself.
50
+ * @emits Slide#deactivate
51
+ * @emits Slide#activate
52
+ * @emits Shower#slidechange
53
+ */
54
+ activate() {
55
+ if (this._isActive) return;
56
+
57
+ const prev = this.shower.activeSlide;
58
+ if (prev) {
59
+ prev._deactivate();
60
+ }
61
+
62
+ this.state.visitCount++;
63
+ this.element.classList.add(this._options.activeSlideClass);
64
+
65
+ this._isActive = true;
66
+ this.dispatchEvent(new Event('activate'));
67
+ this.shower.dispatchEvent(
68
+ new CustomEvent('slidechange', {
69
+ detail: { prev },
70
+ }),
71
+ );
72
+ }
73
+
74
+ /**
75
+ * @throws {ShowerError}
76
+ * @emits Slide#deactivate
77
+ */
78
+ deactivate() {
79
+ if (this.shower.isFullMode) {
80
+ throw new ShowerError('In full mode, another slide should be activated instead.');
81
+ }
82
+
83
+ if (this._isActive) {
84
+ this._deactivate();
85
+ }
86
+ }
87
+
88
+ _deactivate() {
89
+ this.element.classList.replace(
90
+ this._options.activeSlideClass,
91
+ this._options.visitedSlideClass,
92
+ );
93
+
94
+ this._isActive = false;
95
+ this.dispatchEvent(new Event('deactivate'));
96
+ }
97
+ }
98
+
99
+ export default Slide;
package/lib/start.js ADDED
@@ -0,0 +1,14 @@
1
+ import { contentLoaded } from './utils';
2
+ import Shower from './shower';
3
+
4
+ const options = document.currentScript.dataset;
5
+ const shower = new Shower(options);
6
+
7
+ Object.defineProperty(window, 'shower', {
8
+ value: shower,
9
+ configurable: true,
10
+ });
11
+
12
+ contentLoaded(() => {
13
+ shower.start();
14
+ });
package/lib/utils.js ADDED
@@ -0,0 +1,22 @@
1
+ export const isInteractiveElement = (element) => element.tabIndex !== -1;
2
+
3
+ export const contentLoaded = (callback) => {
4
+ if (document.currentScript.async) {
5
+ callback();
6
+ } else {
7
+ document.addEventListener('DOMContentLoaded', callback);
8
+ }
9
+ };
10
+
11
+ export const defineReadOnly = (target, props) => {
12
+ for (const [key, value] of Object.entries(props)) {
13
+ Object.defineProperty(target, key, {
14
+ value,
15
+ writable: false,
16
+ enumerable: true,
17
+ configurable: true,
18
+ });
19
+ }
20
+ };
21
+
22
+ export class ShowerError extends Error {}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shower/core",
3
3
  "description": "Core for Shower HTML presentation engine",
4
- "version": "3.0.0-2",
4
+ "version": "3.3.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -21,43 +21,43 @@
21
21
  ],
22
22
  "main": "lib/shower.js",
23
23
  "files": [
24
+ "lib",
24
25
  "dist/shower.js"
25
26
  ],
26
27
  "devDependencies": {
27
- "chai": "^4.2.0",
28
- "chai-dom": "^1.8.1",
29
- "chromedriver": "^2.46.0",
30
- "eslint": "^5.16.0",
31
- "eslint-config-airbnb-base": "^13.1.0",
32
- "eslint-config-prettier": "^4.2.0",
33
- "eslint-plugin-import": "^2.17.2",
34
- "eslint-plugin-prettier": "^3.1.0",
35
- "esm": "^3.2.22",
36
- "husky": "^2.2.0",
37
- "lint-staged": "^8.1.6",
38
- "mocha": "^6.1.4",
39
- "nightwatch": "^1.0.19",
40
- "prettier": "^1.17.1",
41
- "puppeteer": "^1.15.0",
42
- "rollup": "^1.11.3",
43
- "rollup-plugin-commonjs": "^9.3.4",
44
- "rollup-plugin-node-resolve": "^4.2.4",
45
- "sauce-connect-launcher": "^1.2.6",
46
- "serve": "^11.0.0",
47
- "serve-handler": "^6.0.0",
48
- "yn": "^3.1.0"
28
+ "chai": "^4.3.4",
29
+ "chromedriver": "^83.0.1",
30
+ "eslint": "^7.29.0",
31
+ "eslint-config-airbnb-base": "^14.2.1",
32
+ "eslint-config-prettier": "^6.15.0",
33
+ "eslint-plugin-import": "^2.25.2",
34
+ "eslint-plugin-prettier": "^3.4.1",
35
+ "husky": "^4.3.8",
36
+ "lint-staged": "^10.5.4",
37
+ "mocha": "^7.2.0",
38
+ "nightwatch": "^1.7.6",
39
+ "prettier": "^2.3.1",
40
+ "puppeteer": "^3.3.0",
41
+ "rollup": "^2.58.0",
42
+ "sauce-connect-launcher": "^1.3.2",
43
+ "serve": "^11.3.2",
44
+ "serve-handler": "^6.1.3"
49
45
  },
50
46
  "scripts": {
51
47
  "build": "rollup --config",
48
+ "build:watch": "rollup --config --watch",
52
49
  "serve": "serve dist",
53
- "lint": "eslint '**/*.js'",
54
- "lint:fix": "eslint '**/*.js' --fix",
50
+ "lint": "eslint '**/*.{js,mjs}'",
51
+ "lint:fix": "eslint '**/*.{js,mjs}' --fix",
55
52
  "format": "npm run format:js && npm run format:etc",
56
- "format:js": "prettier '**/*.js' --write",
53
+ "format:js": "prettier '**/*.{js,mjs}' --write",
57
54
  "format:etc": "prettier '**/*.{json,md,yml}' --write",
58
- "test": "npm run lint && npm run test:unit && npm run test:sauce",
59
- "test:unit": "mocha test/unit --require esm --require chai/register-should",
60
- "test:local": "nightwatch --env chrome-local",
61
- "test:sauce": "nightwatch --env chrome,firefox,safari,edge"
55
+ "test": "npm run lint && npm run test:unit && npm run test:local",
56
+ "test:unit": "mocha test/unit --require chai/register-should",
57
+ "test:local": "nightwatch --env chrome-local --timeout 500",
58
+ "test:sauce": "nightwatch --env chrome,firefox,safari"
59
+ },
60
+ "config": {
61
+ "test_port": 8031
62
62
  }
63
63
  }