├── .gitignore
├── .stylelintrc
├── test
├── slow.gif
├── spec
│ ├── .eslintrc.json
│ ├── display.html
│ ├── common.html
│ ├── blockingDisabled.html
│ ├── dragHandle.js
│ ├── display.js
│ ├── closeButton.js
│ ├── makeState.js
│ ├── options.js
│ ├── closeByOverlay.js
│ └── closeByEscKey.js
├── index-limit.html
├── index.html
├── closeByEscKey
│ ├── browser.html
│ └── event.html
├── utils.js
└── httpd.js
├── .eslintignore
├── .eslintrc.json
├── src
├── .eslintrc.json
├── default.scss
├── plain-modal-limit.proc.js
└── plain-modal.proc.js
├── bower.json
├── LICENSE
├── package.json
├── webpack.config.js
├── README.md
├── plain-modal-limit.esm.js
└── plain-modal.esm.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode
2 | node_modules
3 |
--------------------------------------------------------------------------------
/.stylelintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../_common/files/stylelintrc.json"
3 | }
4 |
--------------------------------------------------------------------------------
/test/slow.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anseki/plain-modal/HEAD/test/slow.gif
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | *.min.js
2 | *.esm.js
3 | test/plain-modal-limit.js
4 | test/plain-modal.js
5 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "extends": "../../_common/files/eslintrc.json"
4 | }
5 |
--------------------------------------------------------------------------------
/src/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {"browser": true, "es6": true},
3 | "parserOptions": {"sourceType": "module"},
4 | "rules": {
5 | "no-underscore-dangle": [2, {"allow": ["_id"]}]
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/test/spec/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {"browser": true, "jasmine": true},
3 | "globals": {"loadPage": false},
4 | "rules": {
5 | "no-var": "off",
6 | "prefer-arrow-callback": "off",
7 | "no-underscore-dangle": [2, {"allow": ["_id"]}]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/test/spec/display.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | "
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 anseki
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/test/spec/common.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
44 |
45 |
46 |
47 |
48 |
handle11
49 |
handle12
50 |
button11
51 |
button12
52 | elm1
53 |
54 |
55 |
56 |
handle21
57 | elm2
58 |
59 |
60 | elm3
61 | elm4
62 | elm5
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/src/default.scss:
--------------------------------------------------------------------------------
1 | @import "common";
2 |
3 | $media: "";
4 | %anim-init#{$media} { @include anim-init; }
5 |
6 | $app-id: plainmodal;
7 | $app-content: #{$app-id}-content;
8 | $app-overlay: #{$app-id}-overlay;
9 | $app-overlay-hide: #{$app-overlay}-hide;
10 | $app-overlay-force: #{$app-overlay}-force;
11 |
12 | $plainoverlay-app-id: plainoverlay; // COPY from PlainOverlay
13 |
14 | $duration: 200ms; // COPY from PlainOverlay
15 | $overlay-bg: rgba(136, 136, 136, 0.6);
16 |
17 | .#{$app-id}.#{$plainoverlay-app-id} {
18 | // Disable PlainOverlay style
19 | background-color: transparent;
20 | cursor: auto;
21 | }
22 |
23 | .#{$app-id} .#{$app-content} {
24 | z-index: 9000;
25 | }
26 |
27 | .#{$app-id} .#{$app-overlay} {
28 | width: 100%;
29 | height: 100%;
30 | position: absolute;
31 | left: 0;
32 | top: 0;
33 | background-color: $overlay-bg;
34 |
35 | @extend %anim-init#{$media};
36 | transition-property: opacity;
37 | transition-duration: $duration;
38 | transition-timing-function: linear;
39 | opacity: 1;
40 |
41 | &.#{$app-overlay-hide} {
42 | opacity: 0;
43 | }
44 |
45 | &.#{$app-overlay-force} {
46 | transition-property: none; // Disable animation
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/test/index-limit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Jasmine Spec Runner
6 |
7 |
8 |
9 |
10 |
11 |
16 |
17 |
18 |
19 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Jasmine Spec Runner
6 |
7 |
8 |
9 |
10 |
11 |
16 |
17 |
18 |
19 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/test/closeByEscKey/browser.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | elm1
36 | Open 2
37 |
38 |
39 | elm2
40 | Open 3
41 |
42 |
43 | elm3
44 | Log
45 |
46 |
47 |
48 |
49 | Open 1
50 |
51 |
66 |
67 | Test
68 |
69 | "Escape" key closes 3 modals and cancel loading image.
70 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "plain-modal",
3 | "version": "1.0.34",
4 | "title": "PlainModal",
5 | "description": "The simple library for customizable modal window.",
6 | "keywords": [
7 | "modal",
8 | "popup",
9 | "dialog",
10 | "draggable",
11 | "drag",
12 | "ui",
13 | "simple",
14 | "customizable",
15 | "window"
16 | ],
17 | "main": "plain-modal.min.js",
18 | "module": "plain-modal.esm.js",
19 | "jsnext:main": "plain-modal.esm.js",
20 | "files": [
21 | "plain-modal?(-limit)?(-debug).@(min.js|esm.js)",
22 | "bower.json"
23 | ],
24 | "devDependencies": {
25 | "@babel/core": "^7.14.3",
26 | "@babel/preset-env": "^7.14.2",
27 | "anim-event": "^1.0.17",
28 | "autoprefixer": "^10.2.6",
29 | "babel-core": "^7.0.0-bridge.0",
30 | "babel-loader": "^7.1.5",
31 | "cross-env": "^7.0.3",
32 | "cssprefix": "^2.0.17",
33 | "fibers": "^5.0.0",
34 | "jasmine-core": "^3.7.1",
35 | "log4js": "^6.4.0",
36 | "m-class-list": "^1.1.10",
37 | "node-static-alias": "^1.1.2",
38 | "plain-draggable": "^2.5.14",
39 | "plain-overlay": "^1.4.17",
40 | "pointer-event": "^1.0.2",
41 | "post-compile-webpack-plugin": "^0.1.2",
42 | "postcss": "^8.3.0",
43 | "postcss-loader": "^4.3.0",
44 | "pre-proc": "^1.0.2",
45 | "pre-proc-loader": "^3.0.3",
46 | "sass": "^1.34.0",
47 | "sass-loader": "^10.2.0",
48 | "skeleton-loader": "^2.0.0",
49 | "stats-filelist": "^1.0.1",
50 | "test-page-loader": "^1.0.8",
51 | "timed-transition": "^1.5.4",
52 | "webpack": "^4.46.0",
53 | "webpack-cli": "^3.3.12"
54 | },
55 | "scripts": {
56 | "build": "cross-env NODE_ENV=production webpack --verbose",
57 | "dev": "webpack --verbose",
58 | "build-limit": "cross-env EDITION=limit NODE_ENV=production webpack --verbose",
59 | "dev-limit": "cross-env EDITION=limit webpack --verbose",
60 | "test": "node ./test/httpd"
61 | },
62 | "homepage": "https://anseki.github.io/plain-modal/",
63 | "repository": {
64 | "type": "git",
65 | "url": "git://github.com/anseki/plain-modal.git"
66 | },
67 | "bugs": "https://github.com/anseki/plain-modal/issues",
68 | "license": "MIT",
69 | "author": {
70 | "name": "anseki",
71 | "url": "https://github.com/anseki"
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/test/spec/blockingDisabled.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
18 |
19 |
20 |
21 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
22 |
23 | L0rem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
24 |
25 |
26 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
27 |
28 |
L1rem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
29 |
30 |
31 |
32 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
33 |
34 |
L2rem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
35 |
36 |
37 |
38 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
39 |
40 |
L3rem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/test/spec/dragHandle.js:
--------------------------------------------------------------------------------
1 | describe('dragHandle', function() {
2 | 'use strict';
3 |
4 | var window, document, PlainModal, insProps, traceLog, pageDone,
5 | modal, handle1, handle2;
6 |
7 | beforeAll(function(beforeDone) {
8 | loadPage('spec/common.html', function(pageWindow, pageDocument, pageBody, done) {
9 | window = pageWindow;
10 | document = pageDocument;
11 | PlainModal = window.PlainModal;
12 | insProps = PlainModal.insProps;
13 | traceLog = PlainModal.traceLog;
14 |
15 | modal = new PlainModal(document.getElementById('elm1'));
16 | handle1 = document.getElementById('handle11');
17 | handle2 = document.getElementById('handle12');
18 |
19 | pageDone = done;
20 | beforeDone();
21 | });
22 | });
23 |
24 | afterAll(function() {
25 | pageDone();
26 | });
27 |
28 | it('By default, dragging is disabled', function(done) {
29 | expect(typeof modal.dragHandle).toBe('undefined');
30 | expect(insProps[modal._id].plainDraggable == null).toBe(true);
31 |
32 | done();
33 | });
34 |
35 | it('Remove the option, nothing is changed', function(done) {
36 | traceLog.length = 0;
37 | modal.dragHandle = null;
38 | expect(typeof modal.dragHandle).toBe('undefined');
39 | expect(traceLog).toEqual([]);
40 | expect(insProps[modal._id].plainDraggable == null).toBe(true);
41 |
42 | done();
43 | });
44 |
45 | it('dragHandle is element, PlainDraggable is added', function(done) {
46 | traceLog.length = 0;
47 | modal.dragHandle = handle1;
48 | expect(modal.dragHandle).toBe(handle1);
49 | expect(traceLog).toEqual([
50 | '', '_id:' + modal._id, 'state:STATE_CLOSED',
51 | 'plainDraggable.disabled:true',
52 | ' '
53 | ]);
54 | expect(insProps[modal._id].plainDraggable == null).toBe(false);
55 | expect(insProps[modal._id].plainDraggable.handle).toBe(handle1);
56 |
57 | done();
58 | });
59 |
60 | it('dragHandle is another element', function(done) {
61 | traceLog.length = 0;
62 | modal.dragHandle = handle2;
63 | expect(modal.dragHandle).toBe(handle2);
64 | expect(traceLog).toEqual([
65 | '', '_id:' + modal._id, 'state:STATE_CLOSED',
66 | 'plainDraggable.disabled:true',
67 | ' '
68 | ]);
69 | expect(insProps[modal._id].plainDraggable == null).toBe(false);
70 | expect(insProps[modal._id].plainDraggable.handle).toBe(handle2);
71 |
72 | done();
73 | });
74 |
75 | it('Remove the option', function(done) {
76 | traceLog.length = 0;
77 | modal.dragHandle = null;
78 | expect(typeof modal.dragHandle).toBe('undefined');
79 | expect(traceLog).toEqual([
80 | '', '_id:' + modal._id, 'state:STATE_CLOSED',
81 | 'plainDraggable.disabled:true',
82 | ' '
83 | ]);
84 | expect(insProps[modal._id].plainDraggable == null).toBe(false);
85 | expect(insProps[modal._id].plainDraggable.handle).toBe(handle2); // Not changed
86 |
87 | done();
88 | });
89 |
90 | });
91 |
--------------------------------------------------------------------------------
/test/spec/display.js:
--------------------------------------------------------------------------------
1 | describe('display', function() {
2 | 'use strict';
3 |
4 | var window, document, PlainModal, pageDone;
5 |
6 | beforeAll(function(beforeDone) {
7 | loadPage('spec/display.html', function(pageWindow, pageDocument, pageBody, done) {
8 | window = pageWindow;
9 | document = pageDocument;
10 | PlainModal = window.PlainModal;
11 |
12 | pageDone = done;
13 | beforeDone();
14 | });
15 | });
16 |
17 | afterAll(function() {
18 | pageDone();
19 | });
20 |
21 | it('Check Edition (to be LIMIT: ' + !!self.top.LIMIT + ')', function() {
22 | expect(!!window.PlainModal.limit).toBe(!!self.top.LIMIT);
23 | });
24 |
25 | it('No display', function() {
26 | var div = document.getElementById('div'),
27 | span = document.getElementById('span');
28 |
29 | expect(div.style.display).toBe('');
30 | expect(span.style.display).toBe('');
31 | expect(window.getComputedStyle(div, '').display).toBe('block');
32 | expect(window.getComputedStyle(span, '').display).toBe('inline');
33 |
34 | new PlainModal(div); // eslint-disable-line no-new
35 | new PlainModal(span); // eslint-disable-line no-new
36 |
37 | expect(div.style.display).toBe('');
38 | expect(span.style.display).toBe('');
39 | expect(window.getComputedStyle(div, '').display).toBe('block');
40 | // This should be `block` because it's flex-item. Trident bug
41 | expect(window.getComputedStyle(span, '').display).toBe(PlainModal.IS_TRIDENT ? 'inline' : 'block');
42 | });
43 |
44 | it('display by stylesheet', function() {
45 | var div = document.getElementById('div-stylesheet'),
46 | span = document.getElementById('span-stylesheet');
47 |
48 | expect(div.style.display).toBe('');
49 | expect(span.style.display).toBe('');
50 | expect(window.getComputedStyle(div, '').display).toBe('none');
51 | expect(window.getComputedStyle(span, '').display).toBe('none');
52 |
53 | new PlainModal(div); // eslint-disable-line no-new
54 | new PlainModal(span); // eslint-disable-line no-new
55 |
56 | expect(div.style.display).toBe('block');
57 | expect(span.style.display).toBe('block');
58 | expect(window.getComputedStyle(div, '').display).toBe('block');
59 | expect(window.getComputedStyle(span, '').display).toBe('block');
60 | });
61 |
62 | it('display by style', function() {
63 | var div = document.getElementById('div-style'),
64 | span = document.getElementById('span-style');
65 |
66 | expect(div.style.display).toBe('none');
67 | expect(span.style.display).toBe('none');
68 | expect(window.getComputedStyle(div, '').display).toBe('none');
69 | expect(window.getComputedStyle(span, '').display).toBe('none');
70 |
71 | new PlainModal(div); // eslint-disable-line no-new
72 | new PlainModal(span); // eslint-disable-line no-new
73 |
74 | expect(div.style.display).toBe('block');
75 | expect(span.style.display).toBe('block');
76 | expect(window.getComputedStyle(div, '').display).toBe('block');
77 | expect(window.getComputedStyle(span, '').display).toBe('block');
78 | });
79 |
80 | });
81 |
--------------------------------------------------------------------------------
/test/spec/closeButton.js:
--------------------------------------------------------------------------------
1 | describe('closeButton', function() {
2 | 'use strict';
3 |
4 | var window, document, PlainModal, traceLog, pageDone,
5 | modal, button1, button2;
6 |
7 | function clickElement(element) {
8 | var event;
9 | try {
10 | event = new window.MouseEvent('click');
11 | } catch (error) {
12 | event = document.createEvent('MouseEvent');
13 | event.initMouseEvent('click', true, true, document.defaultView, 1,
14 | 0, 0, 0, 0, false, false, false, false, 0, null);
15 | }
16 | element.dispatchEvent(event);
17 | }
18 |
19 | beforeAll(function(beforeDone) {
20 | loadPage('spec/common.html', function(pageWindow, pageDocument, pageBody, done) {
21 | window = pageWindow;
22 | document = pageDocument;
23 | PlainModal = window.PlainModal;
24 | traceLog = PlainModal.traceLog;
25 |
26 | modal = new PlainModal(document.getElementById('elm1'));
27 | button1 = document.getElementById('button11');
28 | button2 = document.getElementById('button12');
29 |
30 | pageDone = done;
31 | beforeDone();
32 | });
33 | });
34 |
35 | afterAll(function() {
36 | pageDone();
37 | });
38 |
39 | it('Check Edition (to be LIMIT: ' + !!self.top.LIMIT + ')', function() {
40 | expect(!!window.PlainModal.limit).toBe(!!self.top.LIMIT);
41 | });
42 |
43 | it('By default, nothing is called', function(done) {
44 | expect(typeof modal.closeButton).toBe('undefined');
45 |
46 | traceLog.length = 0;
47 | clickElement(button1);
48 | expect(traceLog).toEqual([]);
49 |
50 | traceLog.length = 0;
51 | clickElement(button2);
52 | expect(traceLog).toEqual([]);
53 |
54 | done();
55 | });
56 |
57 | it('Remove the option, nothing is changed', function(done) {
58 | modal.closeButton = null;
59 | expect(typeof modal.closeButton).toBe('undefined');
60 |
61 | traceLog.length = 0;
62 | clickElement(button1);
63 | expect(traceLog).toEqual([]);
64 |
65 | traceLog.length = 0;
66 | clickElement(button2);
67 | expect(traceLog).toEqual([]);
68 |
69 | done();
70 | });
71 |
72 | it('`close` is attached to element', function(done) {
73 | modal.closeButton = button1;
74 | expect(modal.closeButton).toBe(button1);
75 |
76 | traceLog.length = 0;
77 | clickElement(button1);
78 | expect(traceLog).toEqual([
79 | '', '_id:' + modal._id, 'state:STATE_CLOSED', 'CANCEL', ' '
80 | ]);
81 |
82 | traceLog.length = 0;
83 | clickElement(button2);
84 | expect(traceLog).toEqual([]);
85 |
86 | done();
87 | });
88 |
89 | it('`close` is attached to another element', function(done) {
90 | modal.closeButton = button2;
91 | expect(modal.closeButton).toBe(button2);
92 |
93 | traceLog.length = 0;
94 | clickElement(button1);
95 | expect(traceLog).toEqual([]);
96 |
97 | traceLog.length = 0;
98 | clickElement(button2);
99 | expect(traceLog).toEqual([
100 | '', '_id:' + modal._id, 'state:STATE_CLOSED', 'CANCEL', ' '
101 | ]);
102 |
103 | done();
104 | });
105 |
106 | it('Remove the option', function(done) {
107 | modal.closeButton = null;
108 | expect(typeof modal.closeButton).toBe('undefined');
109 |
110 | traceLog.length = 0;
111 | clickElement(button1);
112 | expect(traceLog).toEqual([]);
113 |
114 | traceLog.length = 0;
115 | clickElement(button2);
116 | expect(traceLog).toEqual([]);
117 |
118 | done();
119 | });
120 |
121 | });
122 |
--------------------------------------------------------------------------------
/test/utils.js:
--------------------------------------------------------------------------------
1 | /* exported utils */
2 | /* eslint-env browser */
3 | /* eslint no-var: "off", prefer-arrow-callback: "off", object-shorthand: "off" */
4 |
5 | var utils = (function() {
6 | 'use strict';
7 |
8 | var DEFAULT_INTERVAL = 10;
9 |
10 | function intervalExec(list) {
11 | var interval = 1, // default value for first
12 | index = -1;
13 |
14 | function execNext() {
15 | var fnc;
16 | while (++index <= list.length - 1) {
17 | if (typeof list[index] === 'number') {
18 | interval = list[index];
19 | } else if (typeof list[index] === 'function') {
20 | fnc = list[index];
21 | break;
22 | }
23 | }
24 | if (fnc) {
25 | setTimeout(function() {
26 | fnc();
27 | interval = DEFAULT_INTERVAL;
28 | execNext();
29 | }, interval);
30 | }
31 | }
32 |
33 | execNext();
34 | }
35 |
36 | /**
37 | * @param {(Object|Object[])} instances - A instance or an Array that contains instances.
38 | * @param {(number|number[])} states - Wanted state. Last one is copied if there is a shortage of elements.
39 | * @param {function} cbChange - It is called to change the state. It is not called again if this returned `true`.
40 | * @param {function} cbReady - It is called when all instances have each wanted state.
41 | * @returns {void}
42 | */
43 | function makeState(instances, states, cbChange, cbReady) {
44 | var SAVE_PROP_NAMES = ['onOpen', 'onClose', 'onBeforeOpen', 'onBeforeClose'],
45 | waitCount = 0,
46 | changed = [],
47 | saveProps = [],
48 | nomoreChange, timer, instancesLen;
49 |
50 | function doFnc() {
51 | clearTimeout(timer);
52 |
53 | var readyCount = 0;
54 | instances.forEach(function(instance, i) {
55 | if (instance.state === states[i]) {
56 | if (changed[i]) {
57 | // Restore props
58 | SAVE_PROP_NAMES.forEach(function(propName) {
59 | instance[propName] = saveProps[i][propName];
60 | });
61 | }
62 | readyCount++;
63 |
64 | } else {
65 | setTimeout(function() { // setTimeout for separation
66 | if (!nomoreChange && !changed[i]) {
67 | // Save props
68 | saveProps[i] = {};
69 | SAVE_PROP_NAMES.forEach(function(propName) {
70 | saveProps[i][propName] = instance[propName];
71 | instance[propName] = null;
72 | });
73 |
74 | changed[i] = true;
75 | nomoreChange = cbChange(instance);
76 | }
77 | }, 0);
78 | }
79 | });
80 |
81 | if (readyCount >= instancesLen) {
82 | cbReady();
83 | } else {
84 | waitCount++;
85 | if (waitCount > makeState.MAX_WAIT_COUNT) {
86 | throw new Error('`state` can not become ' + states + '.');
87 | }
88 | timer = setTimeout(doFnc, 10);
89 | }
90 | }
91 |
92 | if (!Array.isArray(instances)) { instances = [instances]; }
93 | instancesLen = instances.length;
94 |
95 | if (!Array.isArray(states)) { states = [states]; }
96 | var statesLen = states.length;
97 | if (statesLen < instancesLen) { // Repeat last value
98 | var lastValue = states[statesLen - 1];
99 | for (var i = statesLen; i < instancesLen; i++) {
100 | states[i] = lastValue;
101 | }
102 | }
103 |
104 | doFnc();
105 | }
106 |
107 | makeState.MAX_WAIT_COUNT = 500;
108 |
109 | return {
110 | intervalExec: intervalExec,
111 | makeState: makeState
112 | };
113 | })();
114 |
--------------------------------------------------------------------------------
/test/httpd.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node, es6 */
2 |
3 | 'use strict';
4 |
5 | const
6 | nodeStaticAlias = require('node-static-alias'),
7 | log4js = require('log4js'),
8 | http = require('http'),
9 | pathUtil = require('path'),
10 | fs = require('fs'),
11 | filelist = require('stats-filelist'),
12 |
13 | DOC_ROOT = __dirname,
14 | PORT = 8080,
15 | SLOW_RESPONSE = 10000,
16 |
17 | MODULE_PACKAGES = [
18 | 'jasmine-core',
19 | 'test-page-loader'
20 | ],
21 |
22 | EXT_DIR = pathUtil.resolve(__dirname, '../../test-ext'),
23 |
24 | logger = (() => {
25 | log4js.configure({ // Super simple format
26 | appenders: {out: {type: 'stdout', layout: {type: 'pattern', pattern: '%[[%r]%] %m'}}},
27 | categories: {default: {appenders: ['out'], level: 'info'}}
28 | });
29 | return log4js.getLogger('node-static-alias');
30 | })(),
31 |
32 | staticAlias = new nodeStaticAlias.Server(DOC_ROOT, {
33 | cache: false,
34 | headers: {'Cache-Control': 'no-cache, must-revalidate'},
35 | alias:
36 | MODULE_PACKAGES.map(packageName =>
37 | ({ // node_modules
38 | match: new RegExp(`^/${packageName}/.+`),
39 | serve: `${require.resolve(packageName).replace(
40 | // Include `packageName` for nested `node_modules`
41 | new RegExp(`^(.*[/\\\\]node_modules)[/\\\\]${packageName}[/\\\\].*$`), '$1')}<% reqPath %>`,
42 | allowOutside: true
43 | })
44 | ).concat([
45 | // limited-function script
46 | {
47 | match: /^\/plain-modal\.js$/,
48 | serve: params =>
49 | (/\bLIMIT=true\b/.test(params.cookie)
50 | ? params.absPath.replace(/\.js$/, '-limit.js') : params.absPath)
51 | },
52 |
53 | // test-ext
54 | {
55 | match: /^\/ext\/.+/,
56 | serve: params => params.reqPath.replace(/^\/ext/, EXT_DIR),
57 | allowOutside: true
58 | },
59 | // test-ext index
60 | {
61 | match: /^\/ext\/?$/,
62 | serve: () => {
63 | const indexPath = pathUtil.join(EXT_DIR, '.index.html');
64 | fs.writeFileSync(indexPath,
65 | `${
66 | filelist.getSync(EXT_DIR, {
67 | filter: stats => /^[^.].*\.html$/.test(stats.name),
68 | listOf: 'fullPath'
69 | }).sort().map(fullPath => { // abs URL for '/ext' (no trailing slash)
70 | const htmlPath = `/ext/${pathUtil.relative(EXT_DIR, fullPath).replace(/\\/g, '/')}`;
71 | return `${htmlPath} `;
72 | }).join('')
73 | } `);
74 | return indexPath;
75 | },
76 | allowOutside: true
77 | }
78 | ]),
79 | logger
80 | });
81 |
82 | http.createServer((request, response) => {
83 | request.addListener('end', () => {
84 |
85 | function serve() {
86 | staticAlias.serve(request, response, error => {
87 | if (error) {
88 | response.writeHead(error.status, error.headers);
89 | logger.error('(%s) %s', request.url, response.statusCode);
90 | if (error.status === 404) {
91 | response.end('Not Found');
92 | }
93 | } else {
94 | logger.info('(%s) %s', request.url, response.statusCode);
95 | }
96 | });
97 | }
98 |
99 | if (/^\/slow\.gif/.test(request.url)) { // slow response
100 | logger.info('(%s) SLOW RESPONSE %dms', request.url, SLOW_RESPONSE);
101 | setTimeout(serve, SLOW_RESPONSE);
102 | } else {
103 | serve();
104 | }
105 |
106 | }).resume();
107 | }).listen(PORT);
108 |
109 | console.log(`START: http://localhost:${PORT}/\nROOT: ${DOC_ROOT}`);
110 | console.log('(^C to stop)');
111 |
--------------------------------------------------------------------------------
/test/closeByEscKey/event.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
39 |
40 |
41 |
42 | How browser fire key events
43 | Browser compatibility
44 |
45 |
46 |
47 |
48 | LOAD
49 | useEsc
50 |
51 |
116 |
117 |
118 | Some browsers don't fire `keypress` when "Escape" key is pressed.
119 | In some browsers, the "Escape" key returns `Esc` instead of `Escape` as `key`.
120 |
121 |
122 | Test
123 |
124 | `keydown` event is fired when "Escape" key pressed.
125 | When "Escape" key is pressed before the image is loaded, `keydown` -> `error` or `abort` -> `keyup` events are fired.
126 | When the checkbox that the key event does `preventDefault` is enabled, "Escape" key can't cancel the loading the image. Second clicking works.
127 |
128 |
129 |
130 |
131 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node, es6 */
2 |
3 | 'use strict';
4 |
5 | const
6 | BASE_NAME = 'plain-modal',
7 | OBJECT_NAME = 'PlainModal',
8 | LIMIT_TAGS = ['DRAG'],
9 | BUILD_MODE = process.env.NODE_ENV === 'production',
10 | LIMIT = process.env.EDITION === 'limit',
11 | BUILD_BASE_NAME = `${BASE_NAME}${LIMIT ? '-limit' : ''}`,
12 | PREPROC_REMOVE_TAGS = (BUILD_MODE ? ['DEBUG'] : []).concat(LIMIT ? LIMIT_TAGS : []),
13 |
14 | webpack = require('webpack'),
15 | preProc = require('pre-proc'),
16 | pathUtil = require('path'),
17 | fs = require('fs'),
18 | PKG = require('./package'),
19 |
20 | SRC_DIR_PATH = pathUtil.resolve(__dirname, 'src'),
21 | BUILD_DIR_PATH = BUILD_MODE ? __dirname : pathUtil.resolve(__dirname, 'test'),
22 | ESM_DIR_PATH = __dirname,
23 | ENTRY_PATH = pathUtil.join(SRC_DIR_PATH, `${BASE_NAME}.js`),
24 |
25 | STATIC_ESM_FILES = [], // [{fileName, content}]
26 | STATIC_ESM_CONTENTS = [], // [{path, re, content}]
27 | PostCompile = require('post-compile-webpack-plugin');
28 |
29 | function writeFile(filePath, content, messageClass) {
30 | const HL = '='.repeat(48);
31 | fs.writeFileSync(filePath,
32 | `/* ${HL}\n DON'T MANUALLY EDIT THIS FILE\n${HL} */\n\n${content}`);
33 | console.log(`Output (${messageClass}): ${filePath}`);
34 | }
35 |
36 | module.exports = {
37 | // optimization: {minimize: false},
38 | mode: BUILD_MODE ? 'production' : 'development',
39 | entry: ENTRY_PATH,
40 | output: {
41 | path: BUILD_DIR_PATH,
42 | filename: `${BUILD_BASE_NAME}${BUILD_MODE ? '.min' : ''}.js`,
43 | library: OBJECT_NAME,
44 | libraryTarget: 'var',
45 | libraryExport: 'default'
46 | },
47 | resolve: {
48 | alias: {
49 | // {EDITION: 'limit', SYNC: 'yes'}
50 | 'plain-overlay': `plain-overlay/plain-overlay-limit-sync${BUILD_MODE ? '' : '-debug'}.esm.js`,
51 | // {EDITION: 'limit'}
52 | 'plain-draggable': 'plain-draggable/plain-draggable-limit.esm.js'
53 | }
54 | },
55 | module: {
56 | rules: [
57 | {
58 | resource: {and: [SRC_DIR_PATH, /\.js$/]},
59 | use: [
60 | // ================================ Static ESM
61 | {
62 | loader: 'skeleton-loader',
63 | options: {
64 | procedure(content) {
65 | if (this.resourcePath === ENTRY_PATH) {
66 | STATIC_ESM_FILES.push(
67 | {fileName: `${BUILD_BASE_NAME}${BUILD_MODE ? '' : '-debug'}.esm.js`, content});
68 | }
69 | return content;
70 | }
71 | }
72 | },
73 | // ================================ Babel
74 | {
75 | loader: 'babel-loader',
76 | options: {presets: [['@babel/preset-env', {targets: 'defaults', modules: false}]]}
77 | },
78 | // ================================ Preprocess
79 | PREPROC_REMOVE_TAGS.length ? {
80 | loader: 'skeleton-loader',
81 | options: {
82 | procedure(content) {
83 | content = preProc.removeTag(PREPROC_REMOVE_TAGS, content);
84 | if (BUILD_MODE && this.resourcePath === ENTRY_PATH) {
85 | writeFile(pathUtil.join(SRC_DIR_PATH, `${BUILD_BASE_NAME}.proc.js`), content, 'PROC');
86 | }
87 | return content;
88 | }
89 | }
90 | } : null
91 | ].filter(loader => !!loader)
92 | },
93 | {
94 | resource: {and: [SRC_DIR_PATH, /\.scss$/]},
95 | use: [
96 | // ================================ Static ESM
97 | {
98 | loader: 'skeleton-loader',
99 | options: {
100 | procedure(content) {
101 | if (!this._module.rawRequest) { throw new Error('Can\'t get `rawRequest`'); }
102 | STATIC_ESM_CONTENTS.push({path: this._module.rawRequest, content});
103 | return content;
104 | },
105 | toCode: true
106 | }
107 | },
108 | // ================================ Autoprefixer
109 | {
110 | loader: 'postcss-loader',
111 | options: {postcssOptions: {plugins: [['autoprefixer']]}}
112 | },
113 | // ================================ SASS
114 | {
115 | loader: 'sass-loader',
116 | options: {
117 | implementation: require('sass'),
118 | sassOptions: {
119 | fiber: require('fibers'),
120 | includePaths: [pathUtil.resolve(__dirname, '../../_common/files')],
121 | outputStyle: 'compressed'
122 | }
123 | }
124 | },
125 | // ================================ Preprocess
126 | PREPROC_REMOVE_TAGS.length ? {
127 | loader: 'pre-proc-loader',
128 | options: {removeTag: {tag: PREPROC_REMOVE_TAGS}}
129 | } : null
130 | ].filter(loader => !!loader)
131 | }
132 | ]
133 | },
134 | devtool: BUILD_MODE ? false : 'source-map',
135 | plugins: [
136 | BUILD_MODE ? new webpack.BannerPlugin(
137 | `${PKG.title || PKG.name} v${PKG.version} (c) ${PKG.author.name} ${PKG.homepage}`) : null,
138 |
139 | // Static ESM
140 | new PostCompile(() => {
141 | // Fix STATIC_ESM_CONTENTS
142 | STATIC_ESM_CONTENTS.forEach(content => {
143 | // Member Import is not supported
144 | content.re = new RegExp(`\\bimport\\s+(\\w+)\\s+from\\s+(?:'|")${
145 | content.path.replace(/[\x00-\x7f]/g, // eslint-disable-line no-control-regex
146 | s => `\\x${('00' + s.charCodeAt().toString(16)).substr(-2)}`)}(?:'|")`, 'g');
147 | content.content = JSON.stringify(content.content);
148 | });
149 |
150 | STATIC_ESM_FILES.forEach(file => {
151 | STATIC_ESM_CONTENTS.forEach(content => {
152 | file.content = file.content.replace(content.re,
153 | (s, varName) => `/* Static ESM */ /* ${s} */ var ${varName} = ${content.content}`);
154 | });
155 | // Save ESM file
156 | writeFile(pathUtil.join(ESM_DIR_PATH, file.fileName), file.content, 'ESM');
157 | });
158 | })
159 | ].filter(plugin => !!plugin)
160 | };
161 |
--------------------------------------------------------------------------------
/test/spec/makeState.js:
--------------------------------------------------------------------------------
1 | describe('makeState', function() {
2 | 'use strict';
3 |
4 | var window, utils, pageDone,
5 | ins1, ins2, ins3;
6 |
7 | function initIns() {
8 | ins1 = {
9 | id: 1, state: 0,
10 | onOpen: 'ins1-onOpen', onClose: 'ins1-onClose',
11 | onBeforeOpen: 'ins1-onBeforeOpen', onBeforeClose: 'ins1-onBeforeClose'
12 | };
13 | ins2 = {
14 | id: 2, state: 0,
15 | onOpen: 'ins2-onOpen', onClose: 'ins2-onClose',
16 | onBeforeOpen: 'ins2-onBeforeOpen', onBeforeClose: 'ins2-onBeforeClose'
17 | };
18 | ins3 = {
19 | id: 3, state: 0,
20 | onOpen: 'ins3-onOpen', onClose: 'ins3-onClose',
21 | onBeforeOpen: 'ins3-onBeforeOpen', onBeforeClose: 'ins3-onBeforeClose'
22 | };
23 | }
24 |
25 | beforeAll(function(beforeDone) {
26 | loadPage('spec/common.html', function(pageWindow, pageDocument, pageBody, done) {
27 | window = pageWindow;
28 | utils = window.utils;
29 |
30 | pageDone = done;
31 | beforeDone();
32 | });
33 | });
34 |
35 | afterAll(function() {
36 | pageDone();
37 | });
38 |
39 | it('Normal flow', function(done) {
40 | var log = [];
41 | initIns();
42 |
43 | utils.makeState(
44 | [ins1, ins2, ins3],
45 | [1, 2, 4],
46 | function(ins) {
47 | expect(ins.onOpen == null).toBe(true);
48 | expect(ins.onClose == null).toBe(true);
49 | expect(ins.onBeforeOpen == null).toBe(true);
50 | expect(ins.onBeforeClose == null).toBe(true);
51 |
52 | log.push('cbChange:' + ins.id);
53 | ins.state =
54 | ins.id === 1 ? 1 :
55 | ins.id === 2 ? 2 :
56 | ins.id === 3 ? 4 : null;
57 | },
58 | function() {
59 |
60 | expect(ins1.onOpen).toBe('ins1-onOpen');
61 | expect(ins1.onClose).toBe('ins1-onClose');
62 | expect(ins1.onBeforeOpen).toBe('ins1-onBeforeOpen');
63 | expect(ins1.onBeforeClose).toBe('ins1-onBeforeClose');
64 | expect(ins2.onOpen).toBe('ins2-onOpen');
65 | expect(ins2.onClose).toBe('ins2-onClose');
66 | expect(ins2.onBeforeOpen).toBe('ins2-onBeforeOpen');
67 | expect(ins2.onBeforeClose).toBe('ins2-onBeforeClose');
68 | expect(ins3.onOpen).toBe('ins3-onOpen');
69 | expect(ins3.onClose).toBe('ins3-onClose');
70 | expect(ins3.onBeforeOpen).toBe('ins3-onBeforeOpen');
71 | expect(ins3.onBeforeClose).toBe('ins3-onBeforeClose');
72 |
73 | expect(ins1.state).toBe(1);
74 | expect(ins2.state).toBe(2);
75 | expect(ins3.state).toBe(4);
76 | expect(log).toEqual([
77 | 'cbChange:1',
78 | 'cbChange:2',
79 | 'cbChange:3'
80 | ]);
81 |
82 | done();
83 | }
84 | );
85 | });
86 |
87 | it('should throw an error when it can not change state', function(done) {
88 | var log = [];
89 | initIns();
90 |
91 | // To catch an error that is thrown asynchronously (`toThrowError` can't it).
92 | window.Error = function(message) {
93 | expect(ins1.state).toBe(0);
94 | expect(ins2.state).toBe(0);
95 | expect(ins3.state).toBe(0);
96 | expect(log).toEqual([
97 | 'cbChange:1',
98 | 'cbChange:2',
99 | 'cbChange:3'
100 | ]);
101 | expect(message).toBe('`state` can not become 1,2,4.');
102 |
103 | done();
104 | };
105 |
106 | utils.makeState.MAX_WAIT_COUNT = 50;
107 |
108 | utils.makeState(
109 | [ins1, ins2, ins3],
110 | [1, 2, 4],
111 | function(ins) {
112 | expect(ins.onOpen == null).toBe(true);
113 | expect(ins.onClose == null).toBe(true);
114 | expect(ins.onBeforeOpen == null).toBe(true);
115 | expect(ins.onBeforeClose == null).toBe(true);
116 |
117 | log.push('cbChange:' + ins.id);
118 | },
119 | function() {
120 | log.push('cbReady'); // This is not executed
121 | }
122 | );
123 |
124 | });
125 |
126 | it('should copy last state', function(done) {
127 | var log = [];
128 | initIns();
129 |
130 | utils.makeState(
131 | [ins1, ins2, ins3],
132 | 5, // means [5, 5, 5]
133 | function(ins) {
134 | expect(ins.onOpen == null).toBe(true);
135 | expect(ins.onClose == null).toBe(true);
136 | expect(ins.onBeforeOpen == null).toBe(true);
137 | expect(ins.onBeforeClose == null).toBe(true);
138 |
139 | log.push('cbChange:' + ins.id);
140 | ins.state = 5;
141 | },
142 | function() {
143 |
144 | expect(ins1.onOpen).toBe('ins1-onOpen');
145 | expect(ins1.onClose).toBe('ins1-onClose');
146 | expect(ins1.onBeforeOpen).toBe('ins1-onBeforeOpen');
147 | expect(ins1.onBeforeClose).toBe('ins1-onBeforeClose');
148 | expect(ins2.onOpen).toBe('ins2-onOpen');
149 | expect(ins2.onClose).toBe('ins2-onClose');
150 | expect(ins2.onBeforeOpen).toBe('ins2-onBeforeOpen');
151 | expect(ins2.onBeforeClose).toBe('ins2-onBeforeClose');
152 | expect(ins3.onOpen).toBe('ins3-onOpen');
153 | expect(ins3.onClose).toBe('ins3-onClose');
154 | expect(ins3.onBeforeOpen).toBe('ins3-onBeforeOpen');
155 | expect(ins3.onBeforeClose).toBe('ins3-onBeforeClose');
156 |
157 | expect(ins1.state).toBe(5);
158 | expect(ins2.state).toBe(5);
159 | expect(ins3.state).toBe(5);
160 | expect(log).toEqual([
161 | 'cbChange:1',
162 | 'cbChange:2',
163 | 'cbChange:3'
164 | ]);
165 |
166 | done();
167 | }
168 | );
169 | });
170 |
171 | it('should call cbChange only once at each instance', function(done) {
172 | var log = [];
173 | initIns();
174 |
175 | utils.makeState(
176 | [ins1, ins2, ins3],
177 | [1, 2, 4],
178 | function(ins) {
179 | expect(ins.onOpen == null).toBe(true);
180 | expect(ins.onClose == null).toBe(true);
181 | expect(ins.onBeforeOpen == null).toBe(true);
182 | expect(ins.onBeforeClose == null).toBe(true);
183 |
184 | log.push('cbChange:' + ins.id);
185 |
186 | setTimeout(function() {
187 | ins.state =
188 | ins.id === 1 ? 1 :
189 | ins.id === 2 ? 2 :
190 | ins.id === 3 ? 4 : null;
191 | }, 100);
192 | },
193 | function() {
194 |
195 | expect(ins1.onOpen).toBe('ins1-onOpen');
196 | expect(ins1.onClose).toBe('ins1-onClose');
197 | expect(ins1.onBeforeOpen).toBe('ins1-onBeforeOpen');
198 | expect(ins1.onBeforeClose).toBe('ins1-onBeforeClose');
199 | expect(ins2.onOpen).toBe('ins2-onOpen');
200 | expect(ins2.onClose).toBe('ins2-onClose');
201 | expect(ins2.onBeforeOpen).toBe('ins2-onBeforeOpen');
202 | expect(ins2.onBeforeClose).toBe('ins2-onBeforeClose');
203 | expect(ins3.onOpen).toBe('ins3-onOpen');
204 | expect(ins3.onClose).toBe('ins3-onClose');
205 | expect(ins3.onBeforeOpen).toBe('ins3-onBeforeOpen');
206 | expect(ins3.onBeforeClose).toBe('ins3-onBeforeClose');
207 |
208 | expect(ins1.state).toBe(1);
209 | expect(ins2.state).toBe(2);
210 | expect(ins3.state).toBe(4);
211 | expect(log).toEqual([
212 | 'cbChange:1',
213 | 'cbChange:2',
214 | 'cbChange:3'
215 | ]);
216 |
217 | done();
218 | }
219 | );
220 | });
221 |
222 | it('should call cbChange only once if that returned true', function(done) {
223 | var log = [];
224 | initIns();
225 |
226 | utils.makeState(
227 | [ins1, ins2, ins3],
228 | [1, 2, 4],
229 | function(ins) {
230 | expect(ins.onOpen == null).toBe(true);
231 | expect(ins.onClose == null).toBe(true);
232 | expect(ins.onBeforeOpen == null).toBe(true);
233 | expect(ins.onBeforeClose == null).toBe(true);
234 |
235 | log.push('cbChange:' + ins.id);
236 |
237 | setTimeout(function() {
238 | ins1.state = 1;
239 | ins2.state = 2;
240 | ins3.state = 4;
241 | }, 200);
242 |
243 | return true;
244 | },
245 | function() {
246 |
247 | expect(ins1.onOpen).toBe('ins1-onOpen');
248 | expect(ins1.onClose).toBe('ins1-onClose');
249 | expect(ins1.onBeforeOpen).toBe('ins1-onBeforeOpen');
250 | expect(ins1.onBeforeClose).toBe('ins1-onBeforeClose');
251 | expect(ins2.onOpen).toBe('ins2-onOpen');
252 | expect(ins2.onClose).toBe('ins2-onClose');
253 | expect(ins2.onBeforeOpen).toBe('ins2-onBeforeOpen');
254 | expect(ins2.onBeforeClose).toBe('ins2-onBeforeClose');
255 | expect(ins3.onOpen).toBe('ins3-onOpen');
256 | expect(ins3.onClose).toBe('ins3-onClose');
257 | expect(ins3.onBeforeOpen).toBe('ins3-onBeforeOpen');
258 | expect(ins3.onBeforeClose).toBe('ins3-onBeforeClose');
259 |
260 | expect(ins1.state).toBe(1);
261 | expect(ins2.state).toBe(2);
262 | expect(ins3.state).toBe(4);
263 | expect(log).toEqual([
264 | 'cbChange:1'
265 | ]);
266 |
267 | done();
268 | }
269 | );
270 | });
271 |
272 | });
273 |
--------------------------------------------------------------------------------
/test/spec/options.js:
--------------------------------------------------------------------------------
1 | describe('options', function() {
2 | 'use strict';
3 |
4 | var window, document, PlainModal, insProps, pageDone;
5 |
6 | beforeAll(function(beforeDone) {
7 | loadPage('spec/common.html', function(pageWindow, pageDocument, pageBody, done) {
8 | window = pageWindow;
9 | document = pageDocument;
10 | PlainModal = window.PlainModal;
11 | insProps = PlainModal.insProps;
12 |
13 | pageDone = done;
14 | beforeDone();
15 | });
16 | });
17 |
18 | afterAll(function() {
19 | pageDone();
20 | });
21 |
22 | it('Check Edition (to be LIMIT: ' + !!self.top.LIMIT + ')', function() {
23 | expect(!!window.PlainModal.limit).toBe(!!self.top.LIMIT);
24 | });
25 |
26 | describe('closeButton', function() {
27 | var modal, button1, button2;
28 |
29 | beforeAll(function(done) {
30 | modal = new PlainModal(document.getElementById('elm1'));
31 | button1 = document.getElementById('button11');
32 | button2 = document.getElementById('button12');
33 | done();
34 | });
35 |
36 | it('default', function(done) {
37 | expect(typeof modal.closeButton).toBe('undefined');
38 |
39 | done();
40 | });
41 |
42 | it('Update - element', function(done) {
43 | modal.closeButton = button1;
44 | expect(modal.closeButton).toBe(button1);
45 |
46 | done();
47 | });
48 |
49 | it('Update - another element', function(done) {
50 | modal.closeButton = button2;
51 | expect(modal.closeButton).toBe(button2);
52 |
53 | done();
54 | });
55 |
56 | it('Update - default', function(done) {
57 | modal.closeButton = null;
58 | expect(typeof modal.closeButton).toBe('undefined');
59 |
60 | done();
61 | });
62 |
63 | it('Update - Invalid value -> ignored', function(done) {
64 | modal.closeButton = button1;
65 | expect(modal.closeButton).toBe(button1);
66 |
67 | modal.closeButton = 5;
68 | expect(modal.closeButton).toBe(button1);
69 |
70 | done();
71 | });
72 |
73 | it('Update another option -> ignored', function(done) {
74 | modal.duration = 5;
75 | expect(modal.closeButton).toBe(button1);
76 |
77 | done();
78 | });
79 | });
80 |
81 | describe('duration', function() {
82 | var modal;
83 |
84 | beforeAll(function(done) {
85 | modal = new PlainModal(document.getElementById('elm1'));
86 | done();
87 | });
88 |
89 | it('default', function(done) {
90 | expect(modal.duration).toBe(200);
91 | expect(insProps[modal._id].plainOverlay.duration).toBe(200); // Passed value
92 |
93 | done();
94 | });
95 |
96 | it('Update - number', function(done) {
97 | modal.duration = 255;
98 | expect(modal.duration).toBe(255);
99 | expect(insProps[modal._id].plainOverlay.duration).toBe(255); // Passed value
100 |
101 | done();
102 | });
103 |
104 | it('Update - another number', function(done) {
105 | modal.duration = 64;
106 | expect(modal.duration).toBe(64);
107 | expect(insProps[modal._id].plainOverlay.duration).toBe(64); // Passed value
108 |
109 | done();
110 | });
111 |
112 | it('Update - default', function(done) {
113 | modal.duration = 200;
114 | expect(modal.duration).toBe(200);
115 | expect(insProps[modal._id].plainOverlay.duration).toBe(200); // Passed value
116 |
117 | done();
118 | });
119 |
120 | it('Update - Invalid value -> ignored', function(done) {
121 | modal.duration = 400;
122 | expect(modal.duration).toBe(400);
123 | expect(insProps[modal._id].plainOverlay.duration).toBe(400); // Passed value
124 |
125 | modal.duration = false;
126 | expect(modal.duration).toBe(400);
127 | expect(insProps[modal._id].plainOverlay.duration).toBe(400); // Passed value
128 |
129 | done();
130 | });
131 |
132 | it('Update another option -> ignored', function(done) {
133 | modal.overlayBlur = 5;
134 | expect(modal.duration).toBe(400);
135 | expect(insProps[modal._id].plainOverlay.duration).toBe(400); // Passed value
136 |
137 | done();
138 | });
139 | });
140 |
141 | describe('overlayBlur', function() {
142 | var modal;
143 |
144 | beforeAll(function(done) {
145 | modal = new PlainModal(document.getElementById('elm1'));
146 | done();
147 | });
148 |
149 | it('default', function(done) {
150 | expect(modal.overlayBlur).toBe(false);
151 | expect(insProps[modal._id].plainOverlay.blur).toBe(false); // Passed value
152 |
153 | done();
154 | });
155 |
156 | it('Update - number', function(done) {
157 | modal.overlayBlur = 2;
158 | expect(modal.overlayBlur).toBe(2);
159 | expect(insProps[modal._id].plainOverlay.blur).toBe(2); // Passed value
160 |
161 | done();
162 | });
163 |
164 | it('Update - another number', function(done) {
165 | modal.overlayBlur = 5;
166 | expect(modal.overlayBlur).toBe(5);
167 | expect(insProps[modal._id].plainOverlay.blur).toBe(5); // Passed value
168 |
169 | done();
170 | });
171 |
172 | it('Update - default', function(done) {
173 | modal.overlayBlur = false;
174 | expect(modal.overlayBlur).toBe(false);
175 | expect(insProps[modal._id].plainOverlay.blur).toBe(false); // Passed value
176 |
177 | done();
178 | });
179 |
180 | it('Update - Invalid value -> ignored', function(done) {
181 | modal.overlayBlur = 3;
182 | expect(modal.overlayBlur).toBe(3);
183 | expect(insProps[modal._id].plainOverlay.blur).toBe(3); // Passed value
184 |
185 | modal.overlayBlur = 'x';
186 | expect(modal.overlayBlur).toBe(3);
187 | expect(insProps[modal._id].plainOverlay.blur).toBe(3); // Passed value
188 |
189 | done();
190 | });
191 |
192 | it('Update another option -> ignored', function(done) {
193 | modal.duration = 5;
194 | expect(modal.overlayBlur).toBe(3);
195 | expect(insProps[modal._id].plainOverlay.blur).toBe(3); // Passed value
196 |
197 | done();
198 | });
199 | });
200 |
201 | describe('dragHandle', function() {
202 | var modal, handle1, handle2;
203 |
204 | if (self.top.LIMIT) { return; }
205 |
206 | beforeAll(function(done) {
207 | modal = new PlainModal(document.getElementById('elm1'));
208 | handle1 = document.getElementById('handle11');
209 | handle2 = document.getElementById('handle12');
210 | done();
211 | });
212 |
213 | it('default', function(done) {
214 | expect(typeof modal.dragHandle).toBe('undefined');
215 |
216 | done();
217 | });
218 |
219 | it('Update - element', function(done) {
220 | modal.dragHandle = handle1;
221 | expect(modal.dragHandle).toBe(handle1);
222 |
223 | done();
224 | });
225 |
226 | it('Update - another element', function(done) {
227 | modal.dragHandle = handle2;
228 | expect(modal.dragHandle).toBe(handle2);
229 |
230 | done();
231 | });
232 |
233 | it('Update - default', function(done) {
234 | modal.dragHandle = null;
235 | expect(typeof modal.dragHandle).toBe('undefined');
236 |
237 | done();
238 | });
239 |
240 | it('Update - Invalid value -> ignored', function(done) {
241 | modal.dragHandle = handle1;
242 | expect(modal.dragHandle).toBe(handle1);
243 |
244 | modal.dragHandle = 5;
245 | expect(modal.dragHandle).toBe(handle1);
246 |
247 | done();
248 | });
249 |
250 | it('Update another option -> ignored', function(done) {
251 | modal.duration = 5;
252 | expect(modal.dragHandle).toBe(handle1);
253 |
254 | done();
255 | });
256 | });
257 |
258 | describe('openEffect', function() {
259 | var modal;
260 | function fnc1() {}
261 | function fnc2() {}
262 |
263 | beforeAll(function(done) {
264 | modal = new PlainModal(document.getElementById('elm1'));
265 | done();
266 | });
267 |
268 | it('default', function(done) {
269 | expect(typeof modal.openEffect).toBe('undefined');
270 |
271 | done();
272 | });
273 |
274 | it('Update - function', function(done) {
275 | modal.openEffect = fnc1;
276 | expect(modal.openEffect).toBe(fnc1);
277 |
278 | done();
279 | });
280 |
281 | it('Update - another function', function(done) {
282 | modal.openEffect = fnc2;
283 | expect(modal.openEffect).toBe(fnc2);
284 |
285 | done();
286 | });
287 |
288 | it('Update - default', function(done) {
289 | modal.openEffect = null;
290 | expect(typeof modal.openEffect).toBe('undefined');
291 |
292 | done();
293 | });
294 |
295 | it('Update - Invalid value -> ignored', function(done) {
296 | modal.openEffect = fnc1;
297 | expect(modal.openEffect).toBe(fnc1);
298 |
299 | modal.openEffect = 5;
300 | expect(modal.openEffect).toBe(fnc1);
301 |
302 | done();
303 | });
304 |
305 | it('Update another option -> ignored', function(done) {
306 | modal.duration = 5;
307 | expect(modal.openEffect).toBe(fnc1);
308 |
309 | done();
310 | });
311 | });
312 |
313 | describe('closeEffect', function() {
314 | var modal;
315 | function fnc1() {}
316 | function fnc2() {}
317 |
318 | beforeAll(function(done) {
319 | modal = new PlainModal(document.getElementById('elm1'));
320 | done();
321 | });
322 |
323 | it('default', function(done) {
324 | expect(typeof modal.closeEffect).toBe('undefined');
325 |
326 | done();
327 | });
328 |
329 | it('Update - function', function(done) {
330 | modal.closeEffect = fnc1;
331 | expect(modal.closeEffect).toBe(fnc1);
332 |
333 | done();
334 | });
335 |
336 | it('Update - another function', function(done) {
337 | modal.closeEffect = fnc2;
338 | expect(modal.closeEffect).toBe(fnc2);
339 |
340 | done();
341 | });
342 |
343 | it('Update - default', function(done) {
344 | modal.closeEffect = null;
345 | expect(typeof modal.closeEffect).toBe('undefined');
346 |
347 | done();
348 | });
349 |
350 | it('Update - Invalid value -> ignored', function(done) {
351 | modal.closeEffect = fnc1;
352 | expect(modal.closeEffect).toBe(fnc1);
353 |
354 | modal.closeEffect = 5;
355 | expect(modal.closeEffect).toBe(fnc1);
356 |
357 | done();
358 | });
359 |
360 | it('Update another option -> ignored', function(done) {
361 | modal.duration = 5;
362 | expect(modal.closeEffect).toBe(fnc1);
363 |
364 | done();
365 | });
366 | });
367 |
368 | describe('onOpen', function() {
369 | var modal;
370 | function fnc1() {}
371 | function fnc2() {}
372 |
373 | beforeAll(function(done) {
374 | modal = new PlainModal(document.getElementById('elm1'));
375 | done();
376 | });
377 |
378 | it('default', function(done) {
379 | expect(typeof modal.onOpen).toBe('undefined');
380 |
381 | done();
382 | });
383 |
384 | it('Update - function', function(done) {
385 | modal.onOpen = fnc1;
386 | expect(modal.onOpen).toBe(fnc1);
387 |
388 | done();
389 | });
390 |
391 | it('Update - another function', function(done) {
392 | modal.onOpen = fnc2;
393 | expect(modal.onOpen).toBe(fnc2);
394 |
395 | done();
396 | });
397 |
398 | it('Update - default', function(done) {
399 | modal.onOpen = null;
400 | expect(typeof modal.onOpen).toBe('undefined');
401 |
402 | done();
403 | });
404 |
405 | it('Update - Invalid value -> ignored', function(done) {
406 | modal.onOpen = fnc1;
407 | expect(modal.onOpen).toBe(fnc1);
408 |
409 | modal.onOpen = 5;
410 | expect(modal.onOpen).toBe(fnc1);
411 |
412 | done();
413 | });
414 |
415 | it('Update another option -> ignored', function(done) {
416 | modal.duration = 5;
417 | expect(modal.onOpen).toBe(fnc1);
418 |
419 | done();
420 | });
421 | });
422 |
423 | describe('onClose', function() {
424 | var modal;
425 | function fnc1() {}
426 | function fnc2() {}
427 |
428 | beforeAll(function(done) {
429 | modal = new PlainModal(document.getElementById('elm1'));
430 | done();
431 | });
432 |
433 | it('default', function(done) {
434 | expect(typeof modal.onClose).toBe('undefined');
435 |
436 | done();
437 | });
438 |
439 | it('Update - function', function(done) {
440 | modal.onClose = fnc1;
441 | expect(modal.onClose).toBe(fnc1);
442 |
443 | done();
444 | });
445 |
446 | it('Update - another function', function(done) {
447 | modal.onClose = fnc2;
448 | expect(modal.onClose).toBe(fnc2);
449 |
450 | done();
451 | });
452 |
453 | it('Update - default', function(done) {
454 | modal.onClose = null;
455 | expect(typeof modal.onClose).toBe('undefined');
456 |
457 | done();
458 | });
459 |
460 | it('Update - Invalid value -> ignored', function(done) {
461 | modal.onClose = fnc1;
462 | expect(modal.onClose).toBe(fnc1);
463 |
464 | modal.onClose = 5;
465 | expect(modal.onClose).toBe(fnc1);
466 |
467 | done();
468 | });
469 |
470 | it('Update another option -> ignored', function(done) {
471 | modal.duration = 5;
472 | expect(modal.onClose).toBe(fnc1);
473 |
474 | done();
475 | });
476 | });
477 |
478 | describe('onBeforeOpen', function() {
479 | var modal;
480 | function fnc1() {}
481 | function fnc2() {}
482 |
483 | beforeAll(function(done) {
484 | modal = new PlainModal(document.getElementById('elm1'));
485 | done();
486 | });
487 |
488 | it('default', function(done) {
489 | expect(typeof modal.onBeforeOpen).toBe('undefined');
490 |
491 | done();
492 | });
493 |
494 | it('Update - function', function(done) {
495 | modal.onBeforeOpen = fnc1;
496 | expect(modal.onBeforeOpen).toBe(fnc1);
497 |
498 | done();
499 | });
500 |
501 | it('Update - another function', function(done) {
502 | modal.onBeforeOpen = fnc2;
503 | expect(modal.onBeforeOpen).toBe(fnc2);
504 |
505 | done();
506 | });
507 |
508 | it('Update - default', function(done) {
509 | modal.onBeforeOpen = null;
510 | expect(typeof modal.onBeforeOpen).toBe('undefined');
511 |
512 | done();
513 | });
514 |
515 | it('Update - Invalid value -> ignored', function(done) {
516 | modal.onBeforeOpen = fnc1;
517 | expect(modal.onBeforeOpen).toBe(fnc1);
518 |
519 | modal.onBeforeOpen = 5;
520 | expect(modal.onBeforeOpen).toBe(fnc1);
521 |
522 | done();
523 | });
524 |
525 | it('Update another option -> ignored', function(done) {
526 | modal.duration = 5;
527 | expect(modal.onBeforeOpen).toBe(fnc1);
528 |
529 | done();
530 | });
531 | });
532 |
533 | describe('onBeforeClose', function() {
534 | var modal;
535 | function fnc1() {}
536 | function fnc2() {}
537 |
538 | beforeAll(function(done) {
539 | modal = new PlainModal(document.getElementById('elm1'));
540 | done();
541 | });
542 |
543 | it('default', function(done) {
544 | expect(typeof modal.onBeforeClose).toBe('undefined');
545 |
546 | done();
547 | });
548 |
549 | it('Update - function', function(done) {
550 | modal.onBeforeClose = fnc1;
551 | expect(modal.onBeforeClose).toBe(fnc1);
552 |
553 | done();
554 | });
555 |
556 | it('Update - another function', function(done) {
557 | modal.onBeforeClose = fnc2;
558 | expect(modal.onBeforeClose).toBe(fnc2);
559 |
560 | done();
561 | });
562 |
563 | it('Update - default', function(done) {
564 | modal.onBeforeClose = null;
565 | expect(typeof modal.onBeforeClose).toBe('undefined');
566 |
567 | done();
568 | });
569 |
570 | it('Update - Invalid value -> ignored', function(done) {
571 | modal.onBeforeClose = fnc1;
572 | expect(modal.onBeforeClose).toBe(fnc1);
573 |
574 | modal.onBeforeClose = 5;
575 | expect(modal.onBeforeClose).toBe(fnc1);
576 |
577 | done();
578 | });
579 |
580 | it('Update another option -> ignored', function(done) {
581 | modal.duration = 5;
582 | expect(modal.onBeforeClose).toBe(fnc1);
583 |
584 | done();
585 | });
586 | });
587 |
588 | });
589 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PlainModal
2 |
3 | [](https://www.npmjs.com/package/plain-modal) [](https://github.com/anseki/plain-modal/issues) [](package.json) [](LICENSE)
4 |
5 | The simple library for fully customizable modal window in a web page.
6 |
7 | **Document and Examples https://anseki.github.io/plain-modal/ **
8 |
9 | PlainModal has basic functions only, and it does nothing about visual style of the modal window. It means that you can free style it to perfect match for your web site or your app. It has no image files and no CSS files, it is just one small file only.
10 |
11 | **Features:**
12 |
13 | - Make a specified element become a modal window, and control the opening and closing it.
14 | - Cover a web page with an overlay, and block scrolling, focusing and accessing anything under the overlay by a mouse or keys.
15 | - No dependency.
16 | - Single file.
17 | - Modern browsers are supported. (If you want to support legacy browsers such as IE 9-, see [jQuery-plainModal](https://anseki.github.io/jquery-plainmodal/).)
18 |
19 | ## Usage
20 |
21 | Load PlainModal into your web page.
22 |
23 | ```html
24 |
25 | ```
26 |
27 | This is simplest case:
28 |
29 | ```html
30 |
31 | Hello, world!
32 |
33 | ```
34 |
35 | ```js
36 | var modal = new PlainModal(document.getElementById('modal-content'));
37 | modal.open();
38 | ```
39 |
40 | Now, new modal window is opened.
41 | You will see that the modal window has no style except for `background-color` to improve the visibility of this example. Therefore you can free style it. In other words, you have to do that for the visual design you want.
42 |
43 | For options and more details, refer to the following.
44 |
45 | ## Constructor
46 |
47 | ```js
48 | modal = new PlainModal(content[, options])
49 | ```
50 |
51 | The `content` argument is an element that is shown as a modal window.
52 | The modal window is initially closed. That is, the `content` element of the constructed PlainModal is being hidden.
53 | To hide the `content` element until the web page is ready, you can apply `display: none` to the `content` element before the constructing PlainModal. PlainModal updates the `display` if it is `none`.
54 |
55 | The `options` argument is an Object that can have properties as [options](#options). You can also change the options by [`setOptions`](#setoptions) or [`open`](#open) methods or [properties](#properties) of the PlainModal instance.
56 |
57 | For example:
58 |
59 | ```js
60 | // Construct new modal window, with `duration` option.
61 | var modal = new PlainModal(document.getElementById('modal-content'), {duration: 400});
62 | ```
63 |
64 | ## Methods
65 |
66 | ### `open`
67 |
68 | ```js
69 | self = modal.open([force][, options])
70 | ```
71 |
72 | Open the modal window.
73 | If `true` is specified for `force` argument, open it immediately without an effect. (As to the effect, see [`duration`](#options-duration) option.)
74 | If `options` argument is specified, call [`setOptions`](#setoptions) method and open the modal window. It works the same as:
75 |
76 | ```js
77 | modal.setOptions(options).open();
78 | ```
79 |
80 | ### `close`
81 |
82 | ```js
83 | self = modal.close([force])
84 | ```
85 |
86 | Close the modal window.
87 | If `true` is specified for `force` argument, close it immediately without an effect. (As to the effect, see [`duration`](#options-duration) option.)
88 |
89 | ### `setOptions`
90 |
91 | ```js
92 | self = modal.setOptions(options)
93 | ```
94 |
95 | Set one or more options.
96 | The `options` argument is an Object that can have properties as [options](#options).
97 |
98 | ## Options
99 |
100 | ### `closeButton`
101 |
102 | *Type:* Element or `undefined`
103 | *Default:* `undefined`
104 |
105 | Bind [`close`](#close) method to specified element, and the modal window is closed when the user clicks the element.
106 |
107 | For example:
108 |
109 | ```html
110 |
111 | Hello, world!
112 |
CLOSE
113 |
114 | ```
115 |
116 | ```js
117 | modal.closeButton = document.getElementById('close-button');
118 | ```
119 |
120 | ### `duration`
121 |
122 | *Type:* number
123 | *Default:* `200`
124 |
125 | A number determining how long (milliseconds) the effect (fade-in/out) animation for opening and closing the modal window will run.
126 |
127 | ### `overlayBlur`
128 |
129 | *Type:* number or boolean
130 | *Default:* `false`
131 |
132 | Applies a Gaussian blur to the web page while the overlay is shown. Note that the current browser might not support it.
133 | It is not applied if `false` is specified.
134 |
135 | For example:
136 |
137 | ```js
138 | modal.overlayBlur = 3;
139 | ```
140 |
141 | ### `dragHandle`
142 |
143 | *Type:* Element or `undefined`
144 | *Default:* `undefined`
145 |
146 | To make the modal window be draggable, specify a part element of the `content` element (or the `content` element itself) that receives mouse operations. A user seizes and drags this element to move the modal window.
147 | The `content` element itself can be specified, and all of the modal window can be seized and dragged.
148 |
149 | For example:
150 |
151 | ```js
152 | modal.dragHandle = document.getElementById('title-bar');
153 | ```
154 |
155 | ### `openEffect`, `closeEffect`
156 |
157 | *Type:* function or `undefined`
158 | *Default:* `undefined`
159 |
160 | By default, the modal window is opened and closed with the fade-in effect and fade-out effect animation. You can specify additional effect for `openEffect` and `closeEffect` option. The default fade-in/out effect also still runs, specify `0` for [`duration`](#options-duration) option if you want to disable the default effect.
161 |
162 | Each effect is a function that is called when the modal window is opened and closed. The function do something to open/close the modal window. It usually starts an animation that the modal window appears or it vanishes. For example, it adds or removes a CSS class that specifies CSS Animation.
163 |
164 | The function may be passed `done` argument that is callback function.
165 | When the `done` is passed, the current opening or closing runs asynchronously, and the function must call the `done` when the effect finished.
166 | On the other hand, when the `done` is not passed, the current opening or closing runs synchronously, and the function must make the modal window appear or vanish immediately without effect. The opening or closing runs synchronously when [`open`](#open) method or [`close`](#close) method is called with `force` argument, or a child modal window is closed by closing parent modal window.
167 |
168 | In the functions, `this` refers to the current PlainModal instance.
169 |
170 | For example:
171 |
172 | ```js
173 | var content = document.getElementById('modal-content'),
174 | modal = new PlainModal(content, {
175 | openEffect: function(done) {
176 | if (done) { // It is running asynchronously.
177 | startOpenAnim(); // Show your animation 3 sec.
178 | setTimeout(function() {
179 | stopOpenAnim();
180 | content.style.display = 'block';
181 | done(); // Tell the finished to PlainModal.
182 | }, 3000);
183 |
184 | } else { // It is running synchronously.
185 | stopOpenAnim(); // It might be called when it is already running.
186 | content.style.display = 'block'; // Finish it immediately.
187 | }
188 | },
189 |
190 | closeEffect: function(done) {
191 | if (done) { // It is running asynchronously.
192 | startCloseAnim(); // Show your animation 1 sec.
193 | setTimeout(function() {
194 | stopCloseAnim();
195 | content.style.display = 'none';
196 | done(); // Tell the finished to PlainModal.
197 | }, 1000);
198 |
199 | } else { // It is running synchronously.
200 | stopCloseAnim(); // It might be called when it is already running.
201 | content.style.display = 'none'; // Finish it immediately.
202 | }
203 | }
204 | });
205 | ```
206 |
207 | You might want to use CSS animation such as [CSS Transitions](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions) for the effect, and you might want to use DOM events such as [`transitionend`](https://developer.mozilla.org/en-US/docs/Web/Events/transitionend) event to get the finishing the effect. (You should consider cross-browser compatibility when you use those. It will be described later.)
208 | Note that you should add an event listener only once because the DOM events allow adding multiple listeners for an event. If you will use `addEventListener` in the `openEffect` or `closeEffect` function, you need something like a flag to avoid multiple adding.
209 | Or, you can get the callback function via `effectDone` property instead of `done` argument. This is useful for using effects with DOM events. You can get it at outer of the `openEffect` and `closeEffect` functions also. And it is a property, not a method. Therefore you can pass it to `addEventListener` directly without wrapping by a function. And also, it can be used for both the `openEffect` and `closeEffect` functions.
210 |
211 | For example:
212 |
213 | ```css
214 | .modal {
215 | margin-top: -600px; /* Default position: out of view */
216 | transition: margin-top 1s;
217 | }
218 |
219 | .opened {
220 | margin-top: 0; /* Move to the center position */
221 | }
222 |
223 | .force {
224 | transition-property: none; /* Disable the animation */
225 | }
226 | ```
227 |
228 | ```js
229 | var content = document.getElementById('modal-content'),
230 | modal = new PlainModal(content, {
231 | openEffect: function(done) {
232 | content.classList.toggle('force', !done); // Switch by async/sync.
233 | content.classList.add('opened');
234 | },
235 | closeEffect: function(done) {
236 | content.classList.toggle('force', !done); // Switch by async/sync.
237 | content.classList.remove('opened');
238 | }
239 | // You will probably collect 2 functions into single function
240 | // with `state` and `toggle`.
241 | });
242 |
243 | // Tell the finished to PlainModal when the event is fired.
244 | content.addEventListener('transitionend', modal.effectDone, true); // Add only once.
245 | ```
246 |
247 | Note that you should consider cross-browser compatibility if you will use CSS Transitions or `element.classList`.
248 | These may help you for cross-browser compatibility.
249 |
250 | - [TimedTransition](https://github.com/anseki/timed-transition) for CSS Transitions
251 | - [mClassList](https://github.com/anseki/m-class-list) for `classList`
252 | - [CSSPrefix](https://github.com/anseki/cssprefix) for CSS properties
253 |
254 | In addition, if you will use other transitions also to do something, the listener will be code like:
255 |
256 | ```js
257 | // `timedTransitionEnd` instead of `transitionend`, for cross-browser compatibility
258 | content.addEventListener('timedTransitionEnd', function(event) {
259 | if (event.target === content && event.propertyName === 'margin-top') {
260 | modal.effectDone();
261 | }
262 | }, true);
263 | ```
264 |
265 | ### `onOpen`, `onClose`, `onBeforeOpen`, `onBeforeClose`
266 |
267 | *Type:* function or `undefined`
268 | *Default:* `undefined`
269 |
270 | Event listeners:
271 |
272 | - `onBeforeOpen` is called when the modal window is about to be opened. If `false` is returned, the opening is canceled.
273 | - `onOpen` is called when an opening effect of the modal window is finished.
274 | - `onBeforeClose` is called when the modal window is about to be closed. If `false` is returned, the closing is canceled.
275 | - `onClose` is called when a closing effect of the modal window is finished.
276 |
277 | In the functions, `this` refers to the current PlainModal instance.
278 |
279 | For example:
280 |
281 | ```js
282 | var modal = new PlainModal({
283 | onOpen: function() {
284 | this.closeButton.style.display = 'block';
285 | name.focus(); // Activate the first input box.
286 | },
287 | onBeforeClose: function() {
288 | if (!name.value) {
289 | alert('Please input your name');
290 | return false; // Cancel the closing the modal window.
291 | }
292 | }
293 | });
294 | ```
295 |
296 | ## Properties
297 |
298 | ### `state`
299 |
300 | *Type:* number
301 | *Read-only*
302 |
303 | A number to indicate current state of the modal window.
304 | It is one of the following static constant values:
305 |
306 | - `PlainModal.STATE_CLOSED` (`0`): The modal window is being closing fully.
307 | - `PlainModal.STATE_OPENING` (`1`): An opening effect of the modal window is running.
308 | - `PlainModal.STATE_OPENED` (`2`): The modal window is being opening fully.
309 | - `PlainModal.STATE_CLOSING` (`3`): A closing effect of the modal window is running.
310 | - `PlainModal.STATE_INACTIVATING` (`4`): An inactivating effect of the modal window is running.
311 | - `PlainModal.STATE_INACTIVATED` (`5`): The modal window is being inactivating fully.
312 | - `PlainModal.STATE_ACTIVATING` (`6`): An activating effect of the modal window is running.
313 |
314 | A modal window is inactivated and activated by a child modal window. For details, see "[Child and Descendants](#child-and-descendants)".
315 |
316 | For example:
317 |
318 | ```js
319 | openButton.addEventListener('click', function() {
320 | if (modal.state === PlainModal.STATE_CLOSED ||
321 | modal.state === PlainModal.STATE_CLOSING) {
322 | modal.open();
323 | }
324 | }, false);
325 | ```
326 |
327 | ### `closeButton`
328 |
329 | Get or set [`closeButton`](#options-closebutton) option.
330 |
331 | ### `duration`
332 |
333 | Get or set [`duration`](#options-duration) option.
334 |
335 | ### `overlayBlur`
336 |
337 | Get or set [`overlayBlur`](#options-overlayblur) option.
338 |
339 | ### `dragHandle`
340 |
341 | Get or set [`dragHandle`](#options-draghandle) option.
342 |
343 | ### `openEffect`, `closeEffect`
344 |
345 | Get or set [`openEffect`, `closeEffect`](#options-openeffect-closeeffect) options.
346 |
347 | ### `effectDone`
348 |
349 | *Type:* function
350 | *Read-only*
351 |
352 | See [`openEffect`, `closeEffect`](#options-openeffect-closeeffect) options.
353 |
354 | ### `onOpen`, `onClose`, `onBeforeOpen`, `onBeforeClose`
355 |
356 | Get or set [`onOpen`, `onClose`, `onBeforeOpen`, `onBeforeClose`](#options-onopen-onclose-onbeforeopen-onbeforeclose) options.
357 |
358 | ## Child and Descendants
359 |
360 | When a modal window was already opened and another modal window is opened, now, the new one is the first one's "child modal window" and the first one is the new one's "parent modal window". Also, more modal windows can be opened, and those are descendants modal windows.
361 |
362 | When a child modal window is opened, its parent modal window is moved to under the overlay that the user can't touch, then that is inactivated. And the child modal window is put on the foreground, then that is activated.
363 | When the child modal window is closed, its parent modal window is activated again.
364 | Descendants modal windows also work in the same way. That is, only one modal window can be active one that the user can touch. The active modal window is one that was last opened or a parent modal window that was last activated by closing its child modal window.
365 |
366 | When a parent modal window is closed, its child modal window is closed before. The child modal window is closed with `force`.
367 | Descendants modal windows also work in the same way. That is, when a modal window is closed, all its descendants modal windows are closed.
368 |
369 | ## `PlainModal.closeByEscKey`
370 |
371 | By default, when the user presses Escape key, the current active modal window is closed.
372 | If you want, set `PlainModal.closeByEscKey = false` to disable this behavior.
373 |
374 | ## `PlainModal.closeByOverlay`
375 |
376 | By default, when the user clicks an overlay, the current active modal window is closed.
377 | If you want, set `PlainModal.closeByOverlay = false` to disable this behavior.
378 |
379 | ## Style of overlay
380 |
381 | If you want to change style of the overlay, you can define style rules with `.plainmodal .plainmodal-overlay` selector in your style-sheet.
382 | Note that some properties that affect the layout (e.g. `width`, `border`, etc.) might not work or those might break the overlay.
383 |
384 | For example, CSS rule-definition for whity overlay:
385 |
386 | ```css
387 | .plainmodal .plainmodal-overlay {
388 | background-color: rgba(255, 255, 255, 0.6);
389 | }
390 | ```
391 |
392 | For example, for background image:
393 |
394 | ```css
395 | .plainmodal .plainmodal-overlay {
396 | background-image: url(bg.png);
397 | }
398 | ```
399 |
400 | ## See Also
401 |
402 | - [PlainOverlay](https://anseki.github.io/plain-overlay/) : The simple library for customizable overlay which covers all or part of a web page.
403 | - [PlainDraggable](https://anseki.github.io/plain-draggable/) : The simple and high performance library to allow HTML/SVG element to be dragged.
404 |
405 | ---
406 |
407 | Thanks for images: [GitHub](https://github.com/), [The Pattern Library](http://thepatternlibrary.com/)
408 |
--------------------------------------------------------------------------------
/src/plain-modal-limit.proc.js:
--------------------------------------------------------------------------------
1 | /* ================================================
2 | DON'T MANUALLY EDIT THIS FILE
3 | ================================================ */
4 |
5 | /*
6 | * PlainModal
7 | * https://anseki.github.io/plain-modal/
8 | *
9 | * Copyright (c) 2021 anseki
10 | * Licensed under the MIT license.
11 | */
12 |
13 | import CSSPrefix from 'cssprefix';
14 | import mClassList from 'm-class-list';
15 | import PlainOverlay from 'plain-overlay';
16 | import CSS_TEXT from './default.scss';
17 | mClassList.ignoreNative = true;
18 |
19 | const
20 | APP_ID = 'plainmodal',
21 | STYLE_ELEMENT_ID = `${APP_ID}-style`,
22 | STYLE_CLASS = APP_ID,
23 | STYLE_CLASS_CONTENT = `${APP_ID}-content`,
24 | STYLE_CLASS_OVERLAY = `${APP_ID}-overlay`,
25 | STYLE_CLASS_OVERLAY_HIDE = `${STYLE_CLASS_OVERLAY}-hide`,
26 | STYLE_CLASS_OVERLAY_FORCE = `${STYLE_CLASS_OVERLAY}-force`,
27 |
28 | STATE_CLOSED = 0,
29 | STATE_OPENING = 1,
30 | STATE_OPENED = 2,
31 | STATE_CLOSING = 3,
32 | STATE_INACTIVATING = 4,
33 | STATE_INACTIVATED = 5,
34 | STATE_ACTIVATING = 6,
35 | DURATION = 200, // COPY from PlainOverlay
36 |
37 | IS_EDGE = '-ms-scroll-limit' in document.documentElement.style &&
38 | '-ms-ime-align' in document.documentElement.style && !window.navigator.msPointerEnabled,
39 | IS_TRIDENT = !IS_EDGE && !!document.uniqueID, // Future Edge might support `document.uniqueID`.
40 |
41 | isObject = (() => {
42 | const toString = {}.toString,
43 | fnToString = {}.hasOwnProperty.toString,
44 | objFnString = fnToString.call(Object);
45 | return obj => {
46 | let proto, constr;
47 | return obj && toString.call(obj) === '[object Object]' &&
48 | (!(proto = Object.getPrototypeOf(obj)) ||
49 | (constr = proto.hasOwnProperty('constructor') && proto.constructor) &&
50 | typeof constr === 'function' && fnToString.call(constr) === objFnString);
51 | };
52 | })(),
53 |
54 | /**
55 | * An object that has properties of instance.
56 | * @typedef {Object} props
57 | * @property {Element} elmContent - Content element.
58 | * @property {Element} elmOverlay - Overlay element. (Not PlainOverlay)
59 | * @property {PlainOverlay} plainOverlay - PlainOverlay instance.
60 | * @property {PlainDraggable} plainDraggable - PlainDraggable instance.
61 | * @property {number} state - Current state.
62 | * @property {Object} options - Options.
63 | * @property {props} parentProps - props that is effected with current props.
64 | * @property {{plainOverlay: boolean, option: boolean}} effectFinished - The effect finished.
65 | */
66 |
67 | /** @type {Object.<_id: number, props>} */
68 | insProps = {},
69 |
70 | /**
71 | * A `props` list, it have a `state` other than `STATE_CLOSED`.
72 | * A `props` is pushed to the end of this array, `shownProps[shownProps.length - 1]` can be active.
73 | * @type {Array.}
74 | */
75 | shownProps = [];
76 |
77 | let
78 | closeByEscKey = true,
79 | closeByOverlay = true,
80 | insId = 0,
81 | openCloseEffectProps; // A `props` that is running the "open/close" effect now.
82 |
83 |
84 | function forceReflow(target) {
85 | // Trident and Blink bug (reflow like `offsetWidth` can't update)
86 | setTimeout(() => {
87 | const parent = target.parentNode,
88 | next = target.nextSibling;
89 | // It has to be removed first for Blink.
90 | parent.insertBefore(parent.removeChild(target), next);
91 | }, 0);
92 | }
93 |
94 | /**
95 | * @param {Element} element - A target element.
96 | * @returns {boolean} `true` if connected element.
97 | */
98 | function isElement(element) {
99 | return !!(element &&
100 | element.nodeType === Node.ELEMENT_NODE &&
101 | // element instanceof HTMLElement &&
102 | typeof element.getBoundingClientRect === 'function' &&
103 | !(element.compareDocumentPosition(document) & Node.DOCUMENT_POSITION_DISCONNECTED));
104 | }
105 |
106 |
107 | function finishOpening(props) {
108 | openCloseEffectProps = null;
109 | props.state = STATE_OPENED;
110 | if (props.parentProps) {
111 | props.parentProps.state = STATE_INACTIVATED;
112 | }
113 | if (props.options.onOpen) { props.options.onOpen.call(props.ins); }
114 | }
115 |
116 | function finishClosing(props) {
117 | shownProps.pop();
118 | openCloseEffectProps = null;
119 | props.state = STATE_CLOSED;
120 | if (props.parentProps) {
121 | props.parentProps.state = STATE_OPENED;
122 | props.parentProps = null;
123 | }
124 | if (props.options.onClose) { props.options.onClose.call(props.ins); }
125 | }
126 |
127 | /**
128 | * @param {props} props - `props` of instance.
129 | * @param {string} effectKey - `plainOverlay' or 'option`
130 | * @returns {void}
131 | */
132 | function finishOpenEffect(props, effectKey) {
133 | if (props.state !== STATE_OPENING) {
134 | return;
135 | }
136 | props.effectFinished[effectKey] = true;
137 | if (props.effectFinished.plainOverlay &&
138 | (!props.options.openEffect || props.effectFinished.option)) {
139 | finishOpening(props);
140 | }
141 | }
142 |
143 | /**
144 | * @param {props} props - `props` of instance.
145 | * @param {string} effectKey - `plainOverlay' or 'option`
146 | * @returns {void}
147 | */
148 | function finishCloseEffect(props, effectKey) {
149 | if (props.state !== STATE_CLOSING) {
150 | return;
151 | }
152 | props.effectFinished[effectKey] = true;
153 | if (props.effectFinished.plainOverlay &&
154 | (!props.options.closeEffect || props.effectFinished.option)) {
155 | finishClosing(props);
156 | }
157 | }
158 |
159 | /**
160 | * Process after preparing data and adjusting style.
161 | * @param {props} props - `props` of instance.
162 | * @param {boolean} [force] - Skip effect.
163 | * @returns {void}
164 | */
165 | function execOpening(props, force) {
166 | if (props.parentProps) { // inactivate parentProps
167 | /*
168 | Cases:
169 | - STATE_OPENED or STATE_ACTIVATING, regardless of force
170 | - STATE_INACTIVATING and force
171 | */
172 | const parentProps = props.parentProps,
173 | elmOverlay = parentProps.elmOverlay;
174 | if (parentProps.state === STATE_OPENED) {
175 | elmOverlay.style[CSSPrefix.getName('transitionDuration')] =
176 | props.options.duration === DURATION ? '' : `${props.options.duration}ms`;
177 | }
178 | const elmOverlayClassList = mClassList(elmOverlay);
179 | elmOverlayClassList.toggle(STYLE_CLASS_OVERLAY_FORCE, !!force);
180 | elmOverlayClassList.add(STYLE_CLASS_OVERLAY_HIDE);
181 | // Update `state` regardless of force, for switchDraggable.
182 | parentProps.state = STATE_INACTIVATING;
183 | parentProps.plainOverlay.blockingDisabled = true;
184 | }
185 |
186 | props.state = STATE_OPENING;
187 | props.plainOverlay.blockingDisabled = false;
188 | props.effectFinished.plainOverlay = props.effectFinished.option = false;
189 | props.plainOverlay.show(force);
190 | if (props.options.openEffect) {
191 | if (force) {
192 | props.options.openEffect.call(props.ins);
193 | finishOpenEffect(props, 'option');
194 | } else {
195 | props.options.openEffect.call(props.ins, props.openEffectDone);
196 | }
197 | }
198 | }
199 |
200 | /**
201 | * Process after preparing data and adjusting style.
202 | * @param {props} props - `props` of instance.
203 | * @param {boolean} [force] - Skip effect.
204 | * @param {boolean} [sync] - `force` with sync-mode. (Skip restoring active element)
205 | * @returns {void}
206 | */
207 | function execClosing(props, force, sync) {
208 | if (props.parentProps) { // activate parentProps
209 | /*
210 | Cases:
211 | - STATE_INACTIVATED or STATE_INACTIVATING, regardless of `force`
212 | - STATE_ACTIVATING and `force`
213 | */
214 | const parentProps = props.parentProps,
215 | elmOverlay = parentProps.elmOverlay;
216 | if (parentProps.state === STATE_INACTIVATED) {
217 | elmOverlay.style[CSSPrefix.getName('transitionDuration')] =
218 | props.options.duration === DURATION ? '' : `${props.options.duration}ms`;
219 | }
220 | const elmOverlayClassList = mClassList(elmOverlay);
221 | elmOverlayClassList.toggle(STYLE_CLASS_OVERLAY_FORCE, !!force);
222 | elmOverlayClassList.remove(STYLE_CLASS_OVERLAY_HIDE);
223 | // same condition as props
224 | parentProps.state = STATE_ACTIVATING;
225 | parentProps.plainOverlay.blockingDisabled = false;
226 | }
227 |
228 | props.state = STATE_CLOSING;
229 | props.effectFinished.plainOverlay = props.effectFinished.option = false;
230 | props.plainOverlay.hide(force, sync);
231 | if (props.options.closeEffect) {
232 | if (force) {
233 | props.options.closeEffect.call(props.ins);
234 | finishCloseEffect(props, 'option');
235 | } else {
236 | props.options.closeEffect.call(props.ins, props.closeEffectDone);
237 | }
238 | }
239 | }
240 |
241 | /**
242 | * Finish the "open/close" effect immediately with sync-mode.
243 | * @param {props} props - `props` of instance.
244 | * @returns {void}
245 | */
246 | function fixOpenClose(props) {
247 | if (props.state === STATE_OPENING) {
248 | execOpening(props, true);
249 | } else if (props.state === STATE_CLOSING) {
250 | execClosing(props, true, true);
251 | }
252 | }
253 |
254 | /**
255 | * @param {props} props - `props` of instance.
256 | * @param {boolean} [force] - Skip effect.
257 | * @returns {void}
258 | */
259 | function open(props, force) {
260 | if (props.state !== STATE_CLOSED &&
261 | props.state !== STATE_CLOSING && props.state !== STATE_OPENING ||
262 | props.state === STATE_OPENING && !force ||
263 | props.state !== STATE_OPENING &&
264 | props.options.onBeforeOpen && props.options.onBeforeOpen.call(props.ins) === false) {
265 | return false;
266 | }
267 | /*
268 | Cases:
269 | - STATE_CLOSED or STATE_CLOSING, regardless of `force`
270 | - STATE_OPENING and `force`
271 | */
272 |
273 | if (props.state === STATE_CLOSED) {
274 | if (openCloseEffectProps) { fixOpenClose(openCloseEffectProps); }
275 | openCloseEffectProps = props;
276 |
277 | if (shownProps.length) {
278 | props.parentProps = shownProps[shownProps.length - 1];
279 | }
280 | shownProps.push(props);
281 |
282 | mClassList(props.elmOverlay).add(STYLE_CLASS_OVERLAY_FORCE).remove(STYLE_CLASS_OVERLAY_HIDE);
283 | }
284 |
285 | execOpening(props, force);
286 | return true;
287 | }
288 |
289 | /**
290 | * @param {props} props - `props` of instance.
291 | * @param {boolean} [force] - Skip effect.
292 | * @returns {void}
293 | */
294 | function close(props, force) {
295 | if (props.state === STATE_CLOSED ||
296 | props.state === STATE_CLOSING && !force ||
297 | props.state !== STATE_CLOSING &&
298 | props.options.onBeforeClose && props.options.onBeforeClose.call(props.ins) === false) {
299 | return false;
300 | }
301 | /*
302 | Cases:
303 | - Other than STATE_CLOSED and STATE_CLOSING, regardless of `force`
304 | - STATE_CLOSING and `force`
305 | */
306 |
307 | if (openCloseEffectProps && openCloseEffectProps !== props) {
308 | fixOpenClose(openCloseEffectProps);
309 | openCloseEffectProps = null;
310 | }
311 | /*
312 | Cases:
313 | - STATE_OPENED, STATE_OPENING or STATE_INACTIVATED, regardless of `force`
314 | - STATE_CLOSING and `force`
315 | */
316 | if (props.state === STATE_INACTIVATED) { // -> STATE_OPENED
317 | let topProps;
318 | while ((topProps = shownProps[shownProps.length - 1]) !== props) {
319 | execClosing(topProps, true, true);
320 | }
321 | }
322 | /*
323 | Cases:
324 | - STATE_OPENED or STATE_OPENING, regardless of `force`
325 | - STATE_CLOSING and `force`
326 | */
327 |
328 | if (props.state === STATE_OPENED) {
329 | openCloseEffectProps = props;
330 | }
331 |
332 | execClosing(props, force);
333 | return true;
334 | }
335 |
336 | /**
337 | * @param {props} props - `props` of instance.
338 | * @param {Object} newOptions - New options.
339 | * @returns {void}
340 | */
341 | function setOptions(props, newOptions) {
342 | const options = props.options,
343 | plainOverlay = props.plainOverlay;
344 |
345 | // closeButton
346 | if (newOptions.hasOwnProperty('closeButton') &&
347 | (newOptions.closeButton = isElement(newOptions.closeButton) ? newOptions.closeButton :
348 | newOptions.closeButton == null ? void 0 : false) !== false &&
349 | newOptions.closeButton !== options.closeButton) {
350 | if (options.closeButton) { // Remove
351 | options.closeButton.removeEventListener('click', props.handleClose, false);
352 | }
353 | options.closeButton = newOptions.closeButton;
354 | if (options.closeButton) { // Add
355 | options.closeButton.addEventListener('click', props.handleClose, false);
356 | }
357 | }
358 |
359 | // duration
360 | // Check by PlainOverlay
361 | plainOverlay.duration = newOptions.duration;
362 | options.duration = plainOverlay.duration;
363 |
364 | // overlayBlur
365 | // Check by PlainOverlay
366 | plainOverlay.blur = newOptions.overlayBlur;
367 | options.overlayBlur = plainOverlay.blur;
368 |
369 |
370 | // effect functions and event listeners
371 | ['openEffect', 'closeEffect', 'onOpen', 'onClose', 'onBeforeOpen', 'onBeforeClose']
372 | .forEach(option => {
373 | if (typeof newOptions[option] === 'function') {
374 | options[option] = newOptions[option];
375 | } else if (newOptions.hasOwnProperty(option) && newOptions[option] == null) {
376 | options[option] = void 0;
377 | }
378 | });
379 | }
380 |
381 | class PlainModal {
382 | /**
383 | * Create a `PlainModal` instance.
384 | * @param {Element} content - An element that is shown as the content of the modal window.
385 | * @param {Object} [options] - Options.
386 | */
387 | constructor(content, options) {
388 | const props = {
389 | ins: this,
390 | options: { // Initial options (not default)
391 | closeButton: void 0,
392 | duration: DURATION,
393 | overlayBlur: false
394 | },
395 | state: STATE_CLOSED,
396 | effectFinished: {plainOverlay: false, option: false}
397 | };
398 |
399 | Object.defineProperty(this, '_id', {value: ++insId});
400 | props._id = this._id;
401 | insProps[this._id] = props;
402 |
403 | if (!content.nodeType || content.nodeType !== Node.ELEMENT_NODE ||
404 | content.ownerDocument.defaultView !== window) {
405 | throw new Error('This `content` is not accepted.');
406 | }
407 | props.elmContent = content;
408 | if (!options) {
409 | options = {};
410 | } else if (!isObject(options)) {
411 | throw new Error('Invalid options.');
412 | }
413 |
414 | // Setup window
415 | if (!document.getElementById(STYLE_ELEMENT_ID)) {
416 | const head = document.getElementsByTagName('head')[0] || document.documentElement,
417 | sheet = head.insertBefore(document.createElement('style'), head.firstChild);
418 | sheet.type = 'text/css';
419 | sheet.id = STYLE_ELEMENT_ID;
420 | sheet.textContent = CSS_TEXT;
421 | if (IS_TRIDENT || IS_EDGE) { forceReflow(sheet); } // Trident bug
422 |
423 | // for closeByEscKey
424 | window.addEventListener('keydown', event => {
425 | let key, topProps;
426 | if (closeByEscKey &&
427 | ((key = event.key.toLowerCase()) === 'escape' || key === 'esc') &&
428 | (topProps = shownProps.length && shownProps[shownProps.length - 1]) &&
429 | close(topProps)) {
430 | event.preventDefault();
431 | event.stopImmediatePropagation(); // preventDefault stops other listeners, maybe.
432 | event.stopPropagation();
433 | }
434 | }, true);
435 | }
436 |
437 | mClassList(content).add(STYLE_CLASS_CONTENT);
438 | // Overlay
439 | props.plainOverlay = new PlainOverlay({
440 | face: content,
441 | onShow: () => { finishOpenEffect(props, 'plainOverlay'); },
442 | onHide: () => { finishCloseEffect(props, 'plainOverlay'); }
443 | });
444 | // The `content` is now contained into PlainOverlay, and update `display`.
445 | if (window.getComputedStyle(content, '').display === 'none') {
446 | content.style.display = 'block';
447 | }
448 | // Trident can not get parent of SVG by parentElement.
449 | const elmPlainOverlayBody = content.parentNode; // elmOverlayBody of PlainOverlay
450 | mClassList(elmPlainOverlayBody.parentNode).add(STYLE_CLASS); // elmOverlay of PlainOverlay
451 |
452 | // elmOverlay (own overlay)
453 | const elmOverlay =
454 | props.elmOverlay = elmPlainOverlayBody.appendChild(document.createElement('div'));
455 | elmOverlay.className = STYLE_CLASS_OVERLAY;
456 | // for closeByOverlay
457 | elmOverlay.addEventListener('click', event => {
458 | if (event.target === elmOverlay && closeByOverlay) {
459 | close(props);
460 | }
461 | }, true);
462 |
463 | // Prepare removable event listeners for each instance.
464 | props.handleClose = () => { close(props); };
465 | // Callback functions for additional effects, prepare these to allow to be used as listener.
466 | props.openEffectDone = () => { finishOpenEffect(props, 'option'); };
467 | props.closeEffectDone = () => { finishCloseEffect(props, 'option'); };
468 | props.effectDone = () => {
469 | if (props.state === STATE_OPENING) {
470 | finishOpenEffect(props, 'option');
471 | } else if (props.state === STATE_CLOSING) {
472 | finishCloseEffect(props, 'option');
473 | }
474 | };
475 |
476 | setOptions(props, options);
477 | }
478 |
479 | /**
480 | * @param {Object} options - New options.
481 | * @returns {PlainModal} Current instance itself.
482 | */
483 | setOptions(options) {
484 | if (isObject(options)) {
485 | setOptions(insProps[this._id], options);
486 | }
487 | return this;
488 | }
489 |
490 | /**
491 | * Open the modal window.
492 | * @param {boolean} [force] - Show it immediately without effect.
493 | * @param {Object} [options] - New options.
494 | * @returns {PlainModal} Current instance itself.
495 | */
496 | open(force, options) {
497 | if (arguments.length < 2 && typeof force !== 'boolean') {
498 | options = force;
499 | force = false;
500 | }
501 |
502 | this.setOptions(options);
503 | open(insProps[this._id], force);
504 | return this;
505 | }
506 |
507 | /**
508 | * Close the modal window.
509 | * @param {boolean} [force] - Close it immediately without effect.
510 | * @returns {PlainModal} Current instance itself.
511 | */
512 | close(force) {
513 | close(insProps[this._id], force);
514 | return this;
515 | }
516 |
517 | get state() { return insProps[this._id].state; }
518 |
519 | get closeButton() { return insProps[this._id].options.closeButton; }
520 | set closeButton(value) { setOptions(insProps[this._id], {closeButton: value}); }
521 |
522 | get duration() { return insProps[this._id].options.duration; }
523 | set duration(value) { setOptions(insProps[this._id], {duration: value}); }
524 |
525 | get overlayBlur() { return insProps[this._id].options.overlayBlur; }
526 | set overlayBlur(value) { setOptions(insProps[this._id], {overlayBlur: value}); }
527 |
528 |
529 | get openEffect() { return insProps[this._id].options.openEffect; }
530 | set openEffect(value) { setOptions(insProps[this._id], {openEffect: value}); }
531 |
532 | get closeEffect() { return insProps[this._id].options.closeEffect; }
533 | set closeEffect(value) { setOptions(insProps[this._id], {closeEffect: value}); }
534 |
535 | get effectDone() { return insProps[this._id].effectDone; }
536 |
537 | get onOpen() { return insProps[this._id].options.onOpen; }
538 | set onOpen(value) { setOptions(insProps[this._id], {onOpen: value}); }
539 |
540 | get onClose() { return insProps[this._id].options.onClose; }
541 | set onClose(value) { setOptions(insProps[this._id], {onClose: value}); }
542 |
543 | get onBeforeOpen() { return insProps[this._id].options.onBeforeOpen; }
544 | set onBeforeOpen(value) { setOptions(insProps[this._id], {onBeforeOpen: value}); }
545 |
546 | get onBeforeClose() { return insProps[this._id].options.onBeforeClose; }
547 | set onBeforeClose(value) { setOptions(insProps[this._id], {onBeforeClose: value}); }
548 |
549 | static get closeByEscKey() { return closeByEscKey; }
550 | static set closeByEscKey(value) { if (typeof value === 'boolean') { closeByEscKey = value; } }
551 |
552 | static get closeByOverlay() { return closeByOverlay; }
553 | static set closeByOverlay(value) { if (typeof value === 'boolean') { closeByOverlay = value; } }
554 |
555 | static get STATE_CLOSED() { return STATE_CLOSED; }
556 | static get STATE_OPENING() { return STATE_OPENING; }
557 | static get STATE_OPENED() { return STATE_OPENED; }
558 | static get STATE_CLOSING() { return STATE_CLOSING; }
559 | static get STATE_INACTIVATING() { return STATE_INACTIVATING; }
560 | static get STATE_INACTIVATED() { return STATE_INACTIVATED; }
561 | static get STATE_ACTIVATING() { return STATE_ACTIVATING; }
562 | }
563 |
564 | PlainModal.limit = true;
565 |
566 |
567 | export default PlainModal;
568 |
--------------------------------------------------------------------------------
/src/plain-modal.proc.js:
--------------------------------------------------------------------------------
1 | /* ================================================
2 | DON'T MANUALLY EDIT THIS FILE
3 | ================================================ */
4 |
5 | /*
6 | * PlainModal
7 | * https://anseki.github.io/plain-modal/
8 | *
9 | * Copyright (c) 2021 anseki
10 | * Licensed under the MIT license.
11 | */
12 |
13 | import CSSPrefix from 'cssprefix';
14 | import mClassList from 'm-class-list';
15 | import PlainOverlay from 'plain-overlay';
16 | import CSS_TEXT from './default.scss';
17 | // [DRAG]
18 | import PlainDraggable from 'plain-draggable';
19 | // [/DRAG]
20 | mClassList.ignoreNative = true;
21 |
22 | const
23 | APP_ID = 'plainmodal',
24 | STYLE_ELEMENT_ID = `${APP_ID}-style`,
25 | STYLE_CLASS = APP_ID,
26 | STYLE_CLASS_CONTENT = `${APP_ID}-content`,
27 | STYLE_CLASS_OVERLAY = `${APP_ID}-overlay`,
28 | STYLE_CLASS_OVERLAY_HIDE = `${STYLE_CLASS_OVERLAY}-hide`,
29 | STYLE_CLASS_OVERLAY_FORCE = `${STYLE_CLASS_OVERLAY}-force`,
30 |
31 | STATE_CLOSED = 0,
32 | STATE_OPENING = 1,
33 | STATE_OPENED = 2,
34 | STATE_CLOSING = 3,
35 | STATE_INACTIVATING = 4,
36 | STATE_INACTIVATED = 5,
37 | STATE_ACTIVATING = 6,
38 | DURATION = 200, // COPY from PlainOverlay
39 |
40 | IS_EDGE = '-ms-scroll-limit' in document.documentElement.style &&
41 | '-ms-ime-align' in document.documentElement.style && !window.navigator.msPointerEnabled,
42 | IS_TRIDENT = !IS_EDGE && !!document.uniqueID, // Future Edge might support `document.uniqueID`.
43 |
44 | isObject = (() => {
45 | const toString = {}.toString,
46 | fnToString = {}.hasOwnProperty.toString,
47 | objFnString = fnToString.call(Object);
48 | return obj => {
49 | let proto, constr;
50 | return obj && toString.call(obj) === '[object Object]' &&
51 | (!(proto = Object.getPrototypeOf(obj)) ||
52 | (constr = proto.hasOwnProperty('constructor') && proto.constructor) &&
53 | typeof constr === 'function' && fnToString.call(constr) === objFnString);
54 | };
55 | })(),
56 |
57 | /**
58 | * An object that has properties of instance.
59 | * @typedef {Object} props
60 | * @property {Element} elmContent - Content element.
61 | * @property {Element} elmOverlay - Overlay element. (Not PlainOverlay)
62 | * @property {PlainOverlay} plainOverlay - PlainOverlay instance.
63 | * @property {PlainDraggable} plainDraggable - PlainDraggable instance.
64 | * @property {number} state - Current state.
65 | * @property {Object} options - Options.
66 | * @property {props} parentProps - props that is effected with current props.
67 | * @property {{plainOverlay: boolean, option: boolean}} effectFinished - The effect finished.
68 | */
69 |
70 | /** @type {Object.<_id: number, props>} */
71 | insProps = {},
72 |
73 | /**
74 | * A `props` list, it have a `state` other than `STATE_CLOSED`.
75 | * A `props` is pushed to the end of this array, `shownProps[shownProps.length - 1]` can be active.
76 | * @type {Array.}
77 | */
78 | shownProps = [];
79 |
80 | let
81 | closeByEscKey = true,
82 | closeByOverlay = true,
83 | insId = 0,
84 | openCloseEffectProps; // A `props` that is running the "open/close" effect now.
85 |
86 |
87 | function forceReflow(target) {
88 | // Trident and Blink bug (reflow like `offsetWidth` can't update)
89 | setTimeout(() => {
90 | const parent = target.parentNode,
91 | next = target.nextSibling;
92 | // It has to be removed first for Blink.
93 | parent.insertBefore(parent.removeChild(target), next);
94 | }, 0);
95 | }
96 |
97 | /**
98 | * @param {Element} element - A target element.
99 | * @returns {boolean} `true` if connected element.
100 | */
101 | function isElement(element) {
102 | return !!(element &&
103 | element.nodeType === Node.ELEMENT_NODE &&
104 | // element instanceof HTMLElement &&
105 | typeof element.getBoundingClientRect === 'function' &&
106 | !(element.compareDocumentPosition(document) & Node.DOCUMENT_POSITION_DISCONNECTED));
107 | }
108 |
109 | // [DRAG]
110 | function switchDraggable(props) {
111 | if (props.plainDraggable) {
112 | const disabled = !(props.options.dragHandle && props.state === STATE_OPENED);
113 | props.plainDraggable.disabled = disabled;
114 | if (!disabled) { props.plainDraggable.position(); }
115 | }
116 | }
117 | // [/DRAG]
118 |
119 | function finishOpening(props) {
120 | openCloseEffectProps = null;
121 | props.state = STATE_OPENED;
122 | switchDraggable(props); // [DRAG/]
123 | if (props.parentProps) {
124 | props.parentProps.state = STATE_INACTIVATED;
125 | }
126 | if (props.options.onOpen) { props.options.onOpen.call(props.ins); }
127 | }
128 |
129 | function finishClosing(props) {
130 | shownProps.pop();
131 | openCloseEffectProps = null;
132 | props.state = STATE_CLOSED;
133 | if (props.parentProps) {
134 | props.parentProps.state = STATE_OPENED;
135 | switchDraggable(props.parentProps); // [DRAG/]
136 | props.parentProps = null;
137 | }
138 | if (props.options.onClose) { props.options.onClose.call(props.ins); }
139 | }
140 |
141 | /**
142 | * @param {props} props - `props` of instance.
143 | * @param {string} effectKey - `plainOverlay' or 'option`
144 | * @returns {void}
145 | */
146 | function finishOpenEffect(props, effectKey) {
147 | if (props.state !== STATE_OPENING) {
148 | return;
149 | }
150 | props.effectFinished[effectKey] = true;
151 | if (props.effectFinished.plainOverlay &&
152 | (!props.options.openEffect || props.effectFinished.option)) {
153 | finishOpening(props);
154 | }
155 | }
156 |
157 | /**
158 | * @param {props} props - `props` of instance.
159 | * @param {string} effectKey - `plainOverlay' or 'option`
160 | * @returns {void}
161 | */
162 | function finishCloseEffect(props, effectKey) {
163 | if (props.state !== STATE_CLOSING) {
164 | return;
165 | }
166 | props.effectFinished[effectKey] = true;
167 | if (props.effectFinished.plainOverlay &&
168 | (!props.options.closeEffect || props.effectFinished.option)) {
169 | finishClosing(props);
170 | }
171 | }
172 |
173 | /**
174 | * Process after preparing data and adjusting style.
175 | * @param {props} props - `props` of instance.
176 | * @param {boolean} [force] - Skip effect.
177 | * @returns {void}
178 | */
179 | function execOpening(props, force) {
180 | if (props.parentProps) { // inactivate parentProps
181 | /*
182 | Cases:
183 | - STATE_OPENED or STATE_ACTIVATING, regardless of force
184 | - STATE_INACTIVATING and force
185 | */
186 | const parentProps = props.parentProps,
187 | elmOverlay = parentProps.elmOverlay;
188 | if (parentProps.state === STATE_OPENED) {
189 | elmOverlay.style[CSSPrefix.getName('transitionDuration')] =
190 | props.options.duration === DURATION ? '' : `${props.options.duration}ms`;
191 | }
192 | const elmOverlayClassList = mClassList(elmOverlay);
193 | elmOverlayClassList.toggle(STYLE_CLASS_OVERLAY_FORCE, !!force);
194 | elmOverlayClassList.add(STYLE_CLASS_OVERLAY_HIDE);
195 | // Update `state` regardless of force, for switchDraggable.
196 | parentProps.state = STATE_INACTIVATING;
197 | parentProps.plainOverlay.blockingDisabled = true;
198 | switchDraggable(parentProps); // [DRAG/]
199 | }
200 |
201 | props.state = STATE_OPENING;
202 | props.plainOverlay.blockingDisabled = false;
203 | props.effectFinished.plainOverlay = props.effectFinished.option = false;
204 | props.plainOverlay.show(force);
205 | if (props.options.openEffect) {
206 | if (force) {
207 | props.options.openEffect.call(props.ins);
208 | finishOpenEffect(props, 'option');
209 | } else {
210 | props.options.openEffect.call(props.ins, props.openEffectDone);
211 | }
212 | }
213 | }
214 |
215 | /**
216 | * Process after preparing data and adjusting style.
217 | * @param {props} props - `props` of instance.
218 | * @param {boolean} [force] - Skip effect.
219 | * @param {boolean} [sync] - `force` with sync-mode. (Skip restoring active element)
220 | * @returns {void}
221 | */
222 | function execClosing(props, force, sync) {
223 | if (props.parentProps) { // activate parentProps
224 | /*
225 | Cases:
226 | - STATE_INACTIVATED or STATE_INACTIVATING, regardless of `force`
227 | - STATE_ACTIVATING and `force`
228 | */
229 | const parentProps = props.parentProps,
230 | elmOverlay = parentProps.elmOverlay;
231 | if (parentProps.state === STATE_INACTIVATED) {
232 | elmOverlay.style[CSSPrefix.getName('transitionDuration')] =
233 | props.options.duration === DURATION ? '' : `${props.options.duration}ms`;
234 | }
235 | const elmOverlayClassList = mClassList(elmOverlay);
236 | elmOverlayClassList.toggle(STYLE_CLASS_OVERLAY_FORCE, !!force);
237 | elmOverlayClassList.remove(STYLE_CLASS_OVERLAY_HIDE);
238 | // same condition as props
239 | parentProps.state = STATE_ACTIVATING;
240 | parentProps.plainOverlay.blockingDisabled = false;
241 | }
242 |
243 | props.state = STATE_CLOSING;
244 | switchDraggable(props); // [DRAG/]
245 | props.effectFinished.plainOverlay = props.effectFinished.option = false;
246 | props.plainOverlay.hide(force, sync);
247 | if (props.options.closeEffect) {
248 | if (force) {
249 | props.options.closeEffect.call(props.ins);
250 | finishCloseEffect(props, 'option');
251 | } else {
252 | props.options.closeEffect.call(props.ins, props.closeEffectDone);
253 | }
254 | }
255 | }
256 |
257 | /**
258 | * Finish the "open/close" effect immediately with sync-mode.
259 | * @param {props} props - `props` of instance.
260 | * @returns {void}
261 | */
262 | function fixOpenClose(props) {
263 | if (props.state === STATE_OPENING) {
264 | execOpening(props, true);
265 | } else if (props.state === STATE_CLOSING) {
266 | execClosing(props, true, true);
267 | }
268 | }
269 |
270 | /**
271 | * @param {props} props - `props` of instance.
272 | * @param {boolean} [force] - Skip effect.
273 | * @returns {void}
274 | */
275 | function open(props, force) {
276 | if (props.state !== STATE_CLOSED &&
277 | props.state !== STATE_CLOSING && props.state !== STATE_OPENING ||
278 | props.state === STATE_OPENING && !force ||
279 | props.state !== STATE_OPENING &&
280 | props.options.onBeforeOpen && props.options.onBeforeOpen.call(props.ins) === false) {
281 | return false;
282 | }
283 | /*
284 | Cases:
285 | - STATE_CLOSED or STATE_CLOSING, regardless of `force`
286 | - STATE_OPENING and `force`
287 | */
288 |
289 | if (props.state === STATE_CLOSED) {
290 | if (openCloseEffectProps) { fixOpenClose(openCloseEffectProps); }
291 | openCloseEffectProps = props;
292 |
293 | if (shownProps.length) {
294 | props.parentProps = shownProps[shownProps.length - 1];
295 | }
296 | shownProps.push(props);
297 |
298 | mClassList(props.elmOverlay).add(STYLE_CLASS_OVERLAY_FORCE).remove(STYLE_CLASS_OVERLAY_HIDE);
299 | }
300 |
301 | execOpening(props, force);
302 | return true;
303 | }
304 |
305 | /**
306 | * @param {props} props - `props` of instance.
307 | * @param {boolean} [force] - Skip effect.
308 | * @returns {void}
309 | */
310 | function close(props, force) {
311 | if (props.state === STATE_CLOSED ||
312 | props.state === STATE_CLOSING && !force ||
313 | props.state !== STATE_CLOSING &&
314 | props.options.onBeforeClose && props.options.onBeforeClose.call(props.ins) === false) {
315 | return false;
316 | }
317 | /*
318 | Cases:
319 | - Other than STATE_CLOSED and STATE_CLOSING, regardless of `force`
320 | - STATE_CLOSING and `force`
321 | */
322 |
323 | if (openCloseEffectProps && openCloseEffectProps !== props) {
324 | fixOpenClose(openCloseEffectProps);
325 | openCloseEffectProps = null;
326 | }
327 | /*
328 | Cases:
329 | - STATE_OPENED, STATE_OPENING or STATE_INACTIVATED, regardless of `force`
330 | - STATE_CLOSING and `force`
331 | */
332 | if (props.state === STATE_INACTIVATED) { // -> STATE_OPENED
333 | let topProps;
334 | while ((topProps = shownProps[shownProps.length - 1]) !== props) {
335 | execClosing(topProps, true, true);
336 | }
337 | }
338 | /*
339 | Cases:
340 | - STATE_OPENED or STATE_OPENING, regardless of `force`
341 | - STATE_CLOSING and `force`
342 | */
343 |
344 | if (props.state === STATE_OPENED) {
345 | openCloseEffectProps = props;
346 | }
347 |
348 | execClosing(props, force);
349 | return true;
350 | }
351 |
352 | /**
353 | * @param {props} props - `props` of instance.
354 | * @param {Object} newOptions - New options.
355 | * @returns {void}
356 | */
357 | function setOptions(props, newOptions) {
358 | const options = props.options,
359 | plainOverlay = props.plainOverlay;
360 |
361 | // closeButton
362 | if (newOptions.hasOwnProperty('closeButton') &&
363 | (newOptions.closeButton = isElement(newOptions.closeButton) ? newOptions.closeButton :
364 | newOptions.closeButton == null ? void 0 : false) !== false &&
365 | newOptions.closeButton !== options.closeButton) {
366 | if (options.closeButton) { // Remove
367 | options.closeButton.removeEventListener('click', props.handleClose, false);
368 | }
369 | options.closeButton = newOptions.closeButton;
370 | if (options.closeButton) { // Add
371 | options.closeButton.addEventListener('click', props.handleClose, false);
372 | }
373 | }
374 |
375 | // duration
376 | // Check by PlainOverlay
377 | plainOverlay.duration = newOptions.duration;
378 | options.duration = plainOverlay.duration;
379 |
380 | // overlayBlur
381 | // Check by PlainOverlay
382 | plainOverlay.blur = newOptions.overlayBlur;
383 | options.overlayBlur = plainOverlay.blur;
384 |
385 | // [DRAG]
386 | // dragHandle
387 | if (newOptions.hasOwnProperty('dragHandle') &&
388 | (newOptions.dragHandle = isElement(newOptions.dragHandle) ? newOptions.dragHandle :
389 | newOptions.dragHandle == null ? void 0 : false) !== false &&
390 | newOptions.dragHandle !== options.dragHandle) {
391 | options.dragHandle = newOptions.dragHandle;
392 | if (options.dragHandle) {
393 | if (!props.plainDraggable) { props.plainDraggable = new PlainDraggable(props.elmContent); }
394 | props.plainDraggable.handle = options.dragHandle;
395 | }
396 | switchDraggable(props);
397 | }
398 | // [/DRAG]
399 |
400 | // effect functions and event listeners
401 | ['openEffect', 'closeEffect', 'onOpen', 'onClose', 'onBeforeOpen', 'onBeforeClose']
402 | .forEach(option => {
403 | if (typeof newOptions[option] === 'function') {
404 | options[option] = newOptions[option];
405 | } else if (newOptions.hasOwnProperty(option) && newOptions[option] == null) {
406 | options[option] = void 0;
407 | }
408 | });
409 | }
410 |
411 | class PlainModal {
412 | /**
413 | * Create a `PlainModal` instance.
414 | * @param {Element} content - An element that is shown as the content of the modal window.
415 | * @param {Object} [options] - Options.
416 | */
417 | constructor(content, options) {
418 | const props = {
419 | ins: this,
420 | options: { // Initial options (not default)
421 | closeButton: void 0,
422 | duration: DURATION,
423 | dragHandle: void 0, // [DRAG/]
424 | overlayBlur: false
425 | },
426 | state: STATE_CLOSED,
427 | effectFinished: {plainOverlay: false, option: false}
428 | };
429 |
430 | Object.defineProperty(this, '_id', {value: ++insId});
431 | props._id = this._id;
432 | insProps[this._id] = props;
433 |
434 | if (!content.nodeType || content.nodeType !== Node.ELEMENT_NODE ||
435 | content.ownerDocument.defaultView !== window) {
436 | throw new Error('This `content` is not accepted.');
437 | }
438 | props.elmContent = content;
439 | if (!options) {
440 | options = {};
441 | } else if (!isObject(options)) {
442 | throw new Error('Invalid options.');
443 | }
444 |
445 | // Setup window
446 | if (!document.getElementById(STYLE_ELEMENT_ID)) {
447 | const head = document.getElementsByTagName('head')[0] || document.documentElement,
448 | sheet = head.insertBefore(document.createElement('style'), head.firstChild);
449 | sheet.type = 'text/css';
450 | sheet.id = STYLE_ELEMENT_ID;
451 | sheet.textContent = CSS_TEXT;
452 | if (IS_TRIDENT || IS_EDGE) { forceReflow(sheet); } // Trident bug
453 |
454 | // for closeByEscKey
455 | window.addEventListener('keydown', event => {
456 | let key, topProps;
457 | if (closeByEscKey &&
458 | ((key = event.key.toLowerCase()) === 'escape' || key === 'esc') &&
459 | (topProps = shownProps.length && shownProps[shownProps.length - 1]) &&
460 | close(topProps)) {
461 | event.preventDefault();
462 | event.stopImmediatePropagation(); // preventDefault stops other listeners, maybe.
463 | event.stopPropagation();
464 | }
465 | }, true);
466 | }
467 |
468 | mClassList(content).add(STYLE_CLASS_CONTENT);
469 | // Overlay
470 | props.plainOverlay = new PlainOverlay({
471 | face: content,
472 | onShow: () => { finishOpenEffect(props, 'plainOverlay'); },
473 | onHide: () => { finishCloseEffect(props, 'plainOverlay'); }
474 | });
475 | // The `content` is now contained into PlainOverlay, and update `display`.
476 | if (window.getComputedStyle(content, '').display === 'none') {
477 | content.style.display = 'block';
478 | }
479 | // Trident can not get parent of SVG by parentElement.
480 | const elmPlainOverlayBody = content.parentNode; // elmOverlayBody of PlainOverlay
481 | mClassList(elmPlainOverlayBody.parentNode).add(STYLE_CLASS); // elmOverlay of PlainOverlay
482 |
483 | // elmOverlay (own overlay)
484 | const elmOverlay =
485 | props.elmOverlay = elmPlainOverlayBody.appendChild(document.createElement('div'));
486 | elmOverlay.className = STYLE_CLASS_OVERLAY;
487 | // for closeByOverlay
488 | elmOverlay.addEventListener('click', event => {
489 | if (event.target === elmOverlay && closeByOverlay) {
490 | close(props);
491 | }
492 | }, true);
493 |
494 | // Prepare removable event listeners for each instance.
495 | props.handleClose = () => { close(props); };
496 | // Callback functions for additional effects, prepare these to allow to be used as listener.
497 | props.openEffectDone = () => { finishOpenEffect(props, 'option'); };
498 | props.closeEffectDone = () => { finishCloseEffect(props, 'option'); };
499 | props.effectDone = () => {
500 | if (props.state === STATE_OPENING) {
501 | finishOpenEffect(props, 'option');
502 | } else if (props.state === STATE_CLOSING) {
503 | finishCloseEffect(props, 'option');
504 | }
505 | };
506 |
507 | setOptions(props, options);
508 | }
509 |
510 | /**
511 | * @param {Object} options - New options.
512 | * @returns {PlainModal} Current instance itself.
513 | */
514 | setOptions(options) {
515 | if (isObject(options)) {
516 | setOptions(insProps[this._id], options);
517 | }
518 | return this;
519 | }
520 |
521 | /**
522 | * Open the modal window.
523 | * @param {boolean} [force] - Show it immediately without effect.
524 | * @param {Object} [options] - New options.
525 | * @returns {PlainModal} Current instance itself.
526 | */
527 | open(force, options) {
528 | if (arguments.length < 2 && typeof force !== 'boolean') {
529 | options = force;
530 | force = false;
531 | }
532 |
533 | this.setOptions(options);
534 | open(insProps[this._id], force);
535 | return this;
536 | }
537 |
538 | /**
539 | * Close the modal window.
540 | * @param {boolean} [force] - Close it immediately without effect.
541 | * @returns {PlainModal} Current instance itself.
542 | */
543 | close(force) {
544 | close(insProps[this._id], force);
545 | return this;
546 | }
547 |
548 | get state() { return insProps[this._id].state; }
549 |
550 | get closeButton() { return insProps[this._id].options.closeButton; }
551 | set closeButton(value) { setOptions(insProps[this._id], {closeButton: value}); }
552 |
553 | get duration() { return insProps[this._id].options.duration; }
554 | set duration(value) { setOptions(insProps[this._id], {duration: value}); }
555 |
556 | get overlayBlur() { return insProps[this._id].options.overlayBlur; }
557 | set overlayBlur(value) { setOptions(insProps[this._id], {overlayBlur: value}); }
558 |
559 | // [DRAG]
560 | get dragHandle() { return insProps[this._id].options.dragHandle; }
561 | set dragHandle(value) { setOptions(insProps[this._id], {dragHandle: value}); }
562 | // [/DRAG]
563 |
564 | get openEffect() { return insProps[this._id].options.openEffect; }
565 | set openEffect(value) { setOptions(insProps[this._id], {openEffect: value}); }
566 |
567 | get closeEffect() { return insProps[this._id].options.closeEffect; }
568 | set closeEffect(value) { setOptions(insProps[this._id], {closeEffect: value}); }
569 |
570 | get effectDone() { return insProps[this._id].effectDone; }
571 |
572 | get onOpen() { return insProps[this._id].options.onOpen; }
573 | set onOpen(value) { setOptions(insProps[this._id], {onOpen: value}); }
574 |
575 | get onClose() { return insProps[this._id].options.onClose; }
576 | set onClose(value) { setOptions(insProps[this._id], {onClose: value}); }
577 |
578 | get onBeforeOpen() { return insProps[this._id].options.onBeforeOpen; }
579 | set onBeforeOpen(value) { setOptions(insProps[this._id], {onBeforeOpen: value}); }
580 |
581 | get onBeforeClose() { return insProps[this._id].options.onBeforeClose; }
582 | set onBeforeClose(value) { setOptions(insProps[this._id], {onBeforeClose: value}); }
583 |
584 | static get closeByEscKey() { return closeByEscKey; }
585 | static set closeByEscKey(value) { if (typeof value === 'boolean') { closeByEscKey = value; } }
586 |
587 | static get closeByOverlay() { return closeByOverlay; }
588 | static set closeByOverlay(value) { if (typeof value === 'boolean') { closeByOverlay = value; } }
589 |
590 | static get STATE_CLOSED() { return STATE_CLOSED; }
591 | static get STATE_OPENING() { return STATE_OPENING; }
592 | static get STATE_OPENED() { return STATE_OPENED; }
593 | static get STATE_CLOSING() { return STATE_CLOSING; }
594 | static get STATE_INACTIVATING() { return STATE_INACTIVATING; }
595 | static get STATE_INACTIVATED() { return STATE_INACTIVATED; }
596 | static get STATE_ACTIVATING() { return STATE_ACTIVATING; }
597 | }
598 |
599 | /* [DRAG/]
600 | PlainModal.limit = true;
601 | [DRAG/] */
602 |
603 |
604 | export default PlainModal;
605 |
--------------------------------------------------------------------------------
/test/spec/closeByOverlay.js:
--------------------------------------------------------------------------------
1 | describe('closeByOverlay', function() {
2 | 'use strict';
3 |
4 | var window, document, utils, PlainModal, traceLog, shownProps, pageDone,
5 | modal1, modal2, modal3, allModals;
6 |
7 | function clickTopOverlay() {
8 | var shownOverlays = document.querySelectorAll('.plainoverlay.plainmodal:not(.plainoverlay-hide)'),
9 | topElmOverlay = shownOverlays.length && shownOverlays[shownOverlays.length - 1]
10 | .querySelector('.plainmodal-overlay:not(.plainmodal-overlay-hide)'),
11 | event;
12 | if (!topElmOverlay) { return false; }
13 | try {
14 | event = new window.MouseEvent('click');
15 | } catch (error) {
16 | event = document.createEvent('MouseEvent');
17 | event.initMouseEvent('click', true, true, document.defaultView, 1,
18 | 0, 0, 0, 0, false, false, false, false, 0, null);
19 | }
20 | topElmOverlay.dispatchEvent(event);
21 | return true;
22 | }
23 |
24 | beforeAll(function(beforeDone) {
25 | loadPage('spec/common.html', function(pageWindow, pageDocument, pageBody, done) {
26 | window = pageWindow;
27 | document = pageDocument;
28 | utils = window.utils;
29 | PlainModal = window.PlainModal;
30 | traceLog = PlainModal.traceLog;
31 | shownProps = PlainModal.shownProps;
32 |
33 | modal1 = new PlainModal(document.getElementById('elm1'), {duration: 50});
34 | modal2 = new PlainModal(document.getElementById('elm2'), {duration: 50});
35 | modal3 = new PlainModal(document.getElementById('elm3'), {duration: 50});
36 | allModals = [modal1, modal2, modal3];
37 |
38 | pageDone = done;
39 | beforeDone();
40 | });
41 | });
42 |
43 | afterAll(function() {
44 | pageDone();
45 | });
46 |
47 | it('Check Edition (to be LIMIT: ' + !!self.top.LIMIT + ')', function() {
48 | expect(!!window.PlainModal.limit).toBe(!!self.top.LIMIT);
49 | });
50 |
51 | it('Normal flow - overlay click -> close()', function(done) {
52 | modal1.onOpen = modal1.onClose = modal1.onBeforeOpen = modal1.onBeforeClose =
53 | modal2.onOpen = modal2.onClose = modal2.onBeforeOpen = modal2.onBeforeClose =
54 | modal3.onOpen = modal3.onClose = modal3.onBeforeOpen = modal3.onBeforeClose = null;
55 |
56 | var timer1;
57 | utils.makeState(allModals,
58 | [PlainModal.STATE_OPENED, PlainModal.STATE_CLOSED],
59 | function() {
60 | modal1.close(true);
61 | modal2.close(true);
62 | modal3.close(true);
63 | timer1 = setTimeout(function() {
64 | modal1.open(true);
65 | }, 10);
66 | return true;
67 | },
68 | function() {
69 | clearTimeout(timer1);
70 |
71 | expect(modal1.state).toBe(PlainModal.STATE_OPENED);
72 | expect(shownProps.map(function(props) { return props.ins; }))
73 | .toEqual([modal1]);
74 |
75 | modal1.onClose = function() {
76 | setTimeout(function() {
77 | expect(modal1.state).toBe(PlainModal.STATE_CLOSED);
78 | expect(shownProps).toEqual([]);
79 |
80 | expect(traceLog).toEqual([
81 | ' ', 'CLOSE', '_id:' + modal1._id,
82 |
83 | // START: close
84 | '', '_id:' + modal1._id, 'state:STATE_OPENED',
85 | 'openCloseEffectProps:NONE',
86 |
87 | '', '_id:' + modal1._id, 'state:STATE_OPENED',
88 | 'force:false', 'sync:false',
89 | 'state:STATE_CLOSING',
90 |
91 | '', '_id:' + modal1._id, 'state:STATE_CLOSING',
92 | 'plainDraggable:NONE',
93 | ' ',
94 |
95 | // PlainOverlay.hide()
96 | '_id:' + modal1._id, ' ',
97 |
98 | '_id:' + modal1._id, ' ',
99 | // DONE: close
100 |
101 | '', '_id:' + modal1._id, 'state:STATE_CLOSING',
102 | 'effectKey:plainOverlay',
103 | 'effectFinished.plainOverlay:true',
104 | 'effectFinished.option:false', 'closeEffect:NO',
105 |
106 | '', '_id:' + modal1._id, 'state:STATE_CLOSING',
107 | 'shownProps:NONE',
108 | 'state:STATE_CLOSED',
109 | ' ',
110 |
111 | '_id:' + modal1._id, ' '
112 | ]);
113 |
114 | done();
115 | }, 0);
116 | };
117 |
118 | traceLog.length = 0;
119 | PlainModal.closeByOverlay = true;
120 | expect(clickTopOverlay()).toBe(true);
121 | }
122 | );
123 | });
124 |
125 | it('PlainModal.closeByOverlay = false, overlay click -> ignored', function(done) {
126 | modal1.onOpen = modal1.onClose = modal1.onBeforeOpen = modal1.onBeforeClose =
127 | modal2.onOpen = modal2.onClose = modal2.onBeforeOpen = modal2.onBeforeClose =
128 | modal3.onOpen = modal3.onClose = modal3.onBeforeOpen = modal3.onBeforeClose = null;
129 |
130 | var timer1;
131 | utils.makeState(allModals,
132 | [PlainModal.STATE_OPENED, PlainModal.STATE_CLOSED],
133 | function() {
134 | modal1.close(true);
135 | modal2.close(true);
136 | modal3.close(true);
137 | timer1 = setTimeout(function() {
138 | modal1.open(true);
139 | }, 10);
140 | return true;
141 | },
142 | function() {
143 | clearTimeout(timer1);
144 |
145 | expect(modal1.state).toBe(PlainModal.STATE_OPENED);
146 | expect(shownProps.map(function(props) { return props.ins; }))
147 | .toEqual([modal1]);
148 |
149 | setTimeout(function() {
150 | expect(modal1.state).toBe(PlainModal.STATE_OPENED);
151 | expect(shownProps.map(function(props) { return props.ins; }))
152 | .toEqual([modal1]);
153 |
154 | expect(traceLog).toEqual([]);
155 |
156 | done();
157 | }, 100);
158 |
159 | traceLog.length = 0;
160 | PlainModal.closeByOverlay = false;
161 | expect(clickTopOverlay()).toBe(true);
162 | }
163 | );
164 | });
165 |
166 | it('STATE_CLOSED, overlay click -> ignored', function(done) {
167 | modal1.onOpen = modal1.onClose = modal1.onBeforeOpen = modal1.onBeforeClose =
168 | modal2.onOpen = modal2.onClose = modal2.onBeforeOpen = modal2.onBeforeClose =
169 | modal3.onOpen = modal3.onClose = modal3.onBeforeOpen = modal3.onBeforeClose = null;
170 |
171 | utils.makeState(allModals,
172 | PlainModal.STATE_CLOSED,
173 | function() {
174 | modal1.close(true);
175 | modal2.close(true);
176 | modal3.close(true);
177 | return true;
178 | },
179 | function() {
180 |
181 | expect(modal1.state).toBe(PlainModal.STATE_CLOSED);
182 | expect(shownProps).toEqual([]);
183 |
184 | setTimeout(function() {
185 | expect(modal1.state).toBe(PlainModal.STATE_CLOSED);
186 | expect(shownProps).toEqual([]);
187 |
188 | expect(traceLog).toEqual([]);
189 |
190 | done();
191 | }, 100);
192 |
193 | traceLog.length = 0;
194 | PlainModal.closeByOverlay = true;
195 | expect(clickTopOverlay()).toBe(false);
196 | }
197 | );
198 | });
199 |
200 | it('STATE_OPENING, overlay click -> close()', function(done) {
201 | modal1.onOpen = modal1.onClose = modal1.onBeforeOpen = modal1.onBeforeClose =
202 | modal2.onOpen = modal2.onClose = modal2.onBeforeOpen = modal2.onBeforeClose =
203 | modal3.onOpen = modal3.onClose = modal3.onBeforeOpen = modal3.onBeforeClose = null;
204 |
205 | var timer1;
206 | utils.makeState(allModals,
207 | [PlainModal.STATE_OPENING, PlainModal.STATE_CLOSED],
208 | function() {
209 | modal1.close(true);
210 | modal2.close(true);
211 | modal3.close(true);
212 | timer1 = setTimeout(function() {
213 | modal1.open();
214 | }, 10);
215 | return true;
216 | },
217 | function() {
218 | clearTimeout(timer1);
219 |
220 | expect(modal1.state).toBe(PlainModal.STATE_OPENING);
221 | expect(shownProps.map(function(props) { return props.ins; }))
222 | .toEqual([modal1]);
223 |
224 | modal1.onClose = function() {
225 | setTimeout(function() {
226 | expect(modal1.state).toBe(PlainModal.STATE_CLOSED);
227 | expect(shownProps).toEqual([]);
228 |
229 | expect(traceLog).toEqual([
230 | ' ', 'CLOSE', '_id:' + modal1._id,
231 |
232 | // START: close
233 | '', '_id:' + modal1._id, 'state:STATE_OPENING',
234 | 'openCloseEffectProps:' + modal1._id,
235 |
236 | '', '_id:' + modal1._id, 'state:STATE_OPENING',
237 | 'force:false', 'sync:false',
238 | 'state:STATE_CLOSING',
239 |
240 | '', '_id:' + modal1._id, 'state:STATE_CLOSING',
241 | 'plainDraggable:NONE',
242 | ' ',
243 |
244 | // PlainOverlay.hide()
245 | '_id:' + modal1._id, ' ',
246 |
247 | '_id:' + modal1._id, ' ',
248 | // DONE: close
249 |
250 | '', '_id:' + modal1._id, 'state:STATE_CLOSING',
251 | 'effectKey:plainOverlay',
252 | 'effectFinished.plainOverlay:true',
253 | 'effectFinished.option:false', 'closeEffect:NO',
254 |
255 | '', '_id:' + modal1._id, 'state:STATE_CLOSING',
256 | 'shownProps:NONE',
257 | 'state:STATE_CLOSED',
258 | ' ',
259 |
260 | '_id:' + modal1._id, ' '
261 | ]);
262 |
263 | done();
264 | }, 0);
265 | };
266 |
267 | traceLog.length = 0;
268 | PlainModal.closeByOverlay = true;
269 | expect(clickTopOverlay()).toBe(true);
270 | }
271 | );
272 | });
273 |
274 | it('STATE_CLOSING, overlay click -> close() -> CANCEL', function(done) {
275 | modal1.onOpen = modal1.onClose = modal1.onBeforeOpen = modal1.onBeforeClose =
276 | modal2.onOpen = modal2.onClose = modal2.onBeforeOpen = modal2.onBeforeClose =
277 | modal3.onOpen = modal3.onClose = modal3.onBeforeOpen = modal3.onBeforeClose = null;
278 |
279 | var timer1;
280 | utils.makeState(allModals,
281 | [PlainModal.STATE_CLOSING, PlainModal.STATE_CLOSED],
282 | function() {
283 | modal1.close(true);
284 | modal2.close(true);
285 | modal3.close(true);
286 | timer1 = setTimeout(function() {
287 | modal1.open(true);
288 | timer1 = setTimeout(function() {
289 | modal1.close();
290 | }, 10);
291 | }, 10);
292 | return true;
293 | },
294 | function() {
295 | clearTimeout(timer1);
296 |
297 | expect(modal1.state).toBe(PlainModal.STATE_CLOSING);
298 | expect(shownProps.map(function(props) { return props.ins; }))
299 | .toEqual([modal1]);
300 |
301 | setTimeout(function() {
302 | expect(modal1.state).toBe(PlainModal.STATE_CLOSING);
303 | expect(shownProps.map(function(props) { return props.ins; }))
304 | .toEqual([modal1]);
305 |
306 | expect(traceLog).toEqual([
307 | ' ', 'CLOSE', '_id:' + modal1._id,
308 |
309 | '', '_id:' + modal1._id, 'state:STATE_CLOSING', 'CANCEL', ' '
310 | ]);
311 |
312 | done();
313 | }, 0);
314 |
315 | traceLog.length = 0;
316 | PlainModal.closeByOverlay = true;
317 | expect(clickTopOverlay()).toBe(true);
318 | }
319 | );
320 | });
321 |
322 | it('STATE_OPENED * 3, overlay click * 2 -> close() * 2', function(done) {
323 | modal1.onOpen = modal1.onClose = modal1.onBeforeOpen = modal1.onBeforeClose =
324 | modal2.onOpen = modal2.onClose = modal2.onBeforeOpen = modal2.onBeforeClose =
325 | modal3.onOpen = modal3.onClose = modal3.onBeforeOpen = modal3.onBeforeClose = null;
326 |
327 | var timer1, timer2, timer3;
328 | utils.makeState(allModals,
329 | [PlainModal.STATE_INACTIVATED, PlainModal.STATE_INACTIVATED, PlainModal.STATE_OPENED],
330 | function() {
331 | modal1.close(true);
332 | modal2.close(true);
333 | modal3.close(true);
334 | timer1 = setTimeout(function() { modal1.open(true); }, 10);
335 | timer2 = setTimeout(function() { modal2.open(true); }, 10);
336 | timer3 = setTimeout(function() { modal3.open(true); }, 10);
337 | return true;
338 | },
339 | function() {
340 | clearTimeout(timer1);
341 | clearTimeout(timer2);
342 | clearTimeout(timer3);
343 |
344 | expect(modal1.state).toBe(PlainModal.STATE_INACTIVATED);
345 | expect(modal2.state).toBe(PlainModal.STATE_INACTIVATED);
346 | expect(modal3.state).toBe(PlainModal.STATE_OPENED);
347 | expect(shownProps.map(function(props) { return props.ins; }))
348 | .toEqual([modal1, modal2, modal3]);
349 |
350 | modal2.onClose = function() {
351 | setTimeout(function() {
352 | expect(modal1.state).toBe(PlainModal.STATE_OPENED);
353 | expect(modal2.state).toBe(PlainModal.STATE_CLOSED);
354 | expect(modal3.state).toBe(PlainModal.STATE_CLOSED);
355 | expect(shownProps.map(function(props) { return props.ins; }))
356 | .toEqual([modal1]);
357 |
358 | expect(traceLog).toEqual([
359 | ' ', 'CLOSE', '_id:' + modal3._id,
360 |
361 | // 3 START: close
362 | '', '_id:' + modal3._id, 'state:STATE_OPENED',
363 | 'openCloseEffectProps:NONE',
364 |
365 | '', '_id:' + modal3._id, 'state:STATE_OPENED',
366 | 'force:false', 'sync:false',
367 |
368 | 'parentProps._id:' + modal2._id, 'parentProps.state:STATE_INACTIVATED',
369 | 'elmOverlay.duration:50ms',
370 | 'elmOverlay.CLASS_FORCE:false', 'elmOverlay.CLASS_HIDE:false',
371 | 'parentProps.state:STATE_ACTIVATING',
372 | 'state:STATE_CLOSING',
373 |
374 | '', '_id:' + modal3._id, 'state:STATE_CLOSING',
375 | 'plainDraggable:NONE',
376 | ' ',
377 |
378 | // PlainOverlay.hide()
379 | '_id:' + modal3._id, ' ',
380 |
381 | '_id:' + modal3._id, ' ',
382 | // DONE: close
383 |
384 | '', '_id:' + modal3._id, 'state:STATE_CLOSING',
385 | 'effectKey:plainOverlay',
386 | 'effectFinished.plainOverlay:true',
387 | 'effectFinished.option:false', 'closeEffect:NO',
388 |
389 | '', '_id:' + modal3._id, 'state:STATE_CLOSING',
390 | 'shownProps:' + modal1._id + ',' + modal2._id,
391 | 'state:STATE_CLOSED',
392 |
393 | 'parentProps._id:' + modal2._id, 'parentProps.state:STATE_ACTIVATING',
394 | 'parentProps.state:STATE_OPENED',
395 |
396 | '', '_id:' + modal2._id, 'state:STATE_OPENED',
397 | 'plainDraggable:NONE',
398 | ' ',
399 |
400 | 'parentProps(UNLINK):' + modal2._id,
401 |
402 | ' ',
403 |
404 | '_id:' + modal3._id, ' ',
405 |
406 | ' ', 'CLOSE', '_id:' + modal2._id,
407 |
408 | // 2 START: close
409 | '', '_id:' + modal2._id, 'state:STATE_OPENED',
410 | 'openCloseEffectProps:NONE',
411 |
412 | '', '_id:' + modal2._id, 'state:STATE_OPENED',
413 | 'force:false', 'sync:false',
414 |
415 | 'parentProps._id:' + modal1._id, 'parentProps.state:STATE_INACTIVATED',
416 | 'elmOverlay.duration:50ms',
417 | 'elmOverlay.CLASS_FORCE:false', 'elmOverlay.CLASS_HIDE:false',
418 | 'parentProps.state:STATE_ACTIVATING',
419 | 'state:STATE_CLOSING',
420 |
421 | '', '_id:' + modal2._id, 'state:STATE_CLOSING',
422 | 'plainDraggable:NONE',
423 | ' ',
424 |
425 | // PlainOverlay.hide()
426 | '_id:' + modal2._id, ' ',
427 |
428 | '_id:' + modal2._id, ' ',
429 | // DONE: close
430 |
431 | '', '_id:' + modal2._id, 'state:STATE_CLOSING',
432 | 'effectKey:plainOverlay',
433 | 'effectFinished.plainOverlay:true',
434 | 'effectFinished.option:false', 'closeEffect:NO',
435 |
436 | '', '_id:' + modal2._id, 'state:STATE_CLOSING',
437 | 'shownProps:' + modal1._id,
438 | 'state:STATE_CLOSED',
439 |
440 | 'parentProps._id:' + modal1._id, 'parentProps.state:STATE_ACTIVATING',
441 | 'parentProps.state:STATE_OPENED',
442 |
443 | '', '_id:' + modal1._id, 'state:STATE_OPENED',
444 | 'plainDraggable:NONE',
445 | ' ',
446 |
447 | 'parentProps(UNLINK):' + modal1._id,
448 |
449 | ' ',
450 |
451 | '_id:' + modal2._id, ' '
452 | ]);
453 |
454 | done();
455 | }, 0);
456 | };
457 |
458 | traceLog.length = 0;
459 | PlainModal.closeByOverlay = true;
460 | expect(clickTopOverlay()).toBe(true);
461 | setTimeout(function() {
462 | expect(clickTopOverlay()).toBe(true);
463 | }, 100);
464 | }
465 | );
466 | });
467 |
468 | it('STATE_OPENED * 2, overlay click * 4 -> close() * 2', function(done) {
469 | modal1.onOpen = modal1.onClose = modal1.onBeforeOpen = modal1.onBeforeClose =
470 | modal2.onOpen = modal2.onClose = modal2.onBeforeOpen = modal2.onBeforeClose =
471 | modal3.onOpen = modal3.onClose = modal3.onBeforeOpen = modal3.onBeforeClose = null;
472 |
473 | var timer1, timer2;
474 | utils.makeState(allModals,
475 | [PlainModal.STATE_OPENED, PlainModal.STATE_CLOSED, PlainModal.STATE_INACTIVATED],
476 | function() {
477 | modal1.close(true);
478 | modal2.close(true);
479 | modal3.close(true);
480 | // 3, 1
481 | timer1 = setTimeout(function() { modal3.open(true); }, 10);
482 | timer2 = setTimeout(function() { modal1.open(true); }, 10);
483 | return true;
484 | },
485 | function() {
486 | clearTimeout(timer1);
487 | clearTimeout(timer2);
488 |
489 | expect(modal3.state).toBe(PlainModal.STATE_INACTIVATED);
490 | expect(modal1.state).toBe(PlainModal.STATE_OPENED);
491 | expect(modal2.state).toBe(PlainModal.STATE_CLOSED);
492 | expect(shownProps.map(function(props) { return props.ins; }))
493 | .toEqual([modal3, modal1]);
494 |
495 | traceLog.length = 0;
496 | PlainModal.closeByOverlay = true;
497 |
498 | utils.intervalExec([
499 | // ====================================
500 | function() {
501 | expect(clickTopOverlay()).toBe(true); // 1
502 | },
503 | // ====================================
504 | 100, function() {
505 | expect(modal3.state).toBe(PlainModal.STATE_OPENED);
506 | expect(modal1.state).toBe(PlainModal.STATE_CLOSED);
507 | expect(modal2.state).toBe(PlainModal.STATE_CLOSED);
508 | expect(shownProps.map(function(props) { return props.ins; })).toEqual([modal3]);
509 |
510 | expect(clickTopOverlay()).toBe(true); // 3
511 | },
512 | // ====================================
513 | 100, function() {
514 | expect(modal3.state).toBe(PlainModal.STATE_CLOSED);
515 | expect(modal1.state).toBe(PlainModal.STATE_CLOSED);
516 | expect(modal2.state).toBe(PlainModal.STATE_CLOSED);
517 | expect(shownProps).toEqual([]);
518 |
519 | expect(clickTopOverlay()).toBe(false); // No modal
520 | },
521 | // ====================================
522 | 50, function() {
523 | expect(modal3.state).toBe(PlainModal.STATE_CLOSED);
524 | expect(modal1.state).toBe(PlainModal.STATE_CLOSED);
525 | expect(modal2.state).toBe(PlainModal.STATE_CLOSED);
526 | expect(shownProps).toEqual([]);
527 |
528 | expect(clickTopOverlay()).toBe(false); // No modal
529 | },
530 | // ====================================
531 | 50, function() {
532 | expect(modal3.state).toBe(PlainModal.STATE_CLOSED);
533 | expect(modal1.state).toBe(PlainModal.STATE_CLOSED);
534 | expect(modal2.state).toBe(PlainModal.STATE_CLOSED);
535 | expect(shownProps).toEqual([]);
536 |
537 | expect(traceLog).toEqual([
538 | ' ', 'CLOSE', '_id:' + modal1._id,
539 |
540 | // 1 START: close
541 | '', '_id:' + modal1._id, 'state:STATE_OPENED',
542 | 'openCloseEffectProps:NONE',
543 |
544 | '', '_id:' + modal1._id, 'state:STATE_OPENED',
545 | 'force:false', 'sync:false',
546 |
547 | 'parentProps._id:' + modal3._id, 'parentProps.state:STATE_INACTIVATED',
548 | 'elmOverlay.duration:50ms',
549 | 'elmOverlay.CLASS_FORCE:false', 'elmOverlay.CLASS_HIDE:false',
550 | 'parentProps.state:STATE_ACTIVATING',
551 | 'state:STATE_CLOSING',
552 |
553 | '', '_id:' + modal1._id, 'state:STATE_CLOSING',
554 | 'plainDraggable:NONE',
555 | ' ',
556 |
557 | // PlainOverlay.hide()
558 | '_id:' + modal1._id, ' ',
559 |
560 | '_id:' + modal1._id, ' ',
561 | // DONE: close
562 |
563 | '', '_id:' + modal1._id, 'state:STATE_CLOSING',
564 | 'effectKey:plainOverlay',
565 | 'effectFinished.plainOverlay:true',
566 | 'effectFinished.option:false', 'closeEffect:NO',
567 |
568 | '', '_id:' + modal1._id, 'state:STATE_CLOSING',
569 | 'shownProps:' + modal3._id,
570 | 'state:STATE_CLOSED',
571 |
572 | 'parentProps._id:' + modal3._id, 'parentProps.state:STATE_ACTIVATING',
573 | 'parentProps.state:STATE_OPENED',
574 |
575 | '', '_id:' + modal3._id, 'state:STATE_OPENED',
576 | 'plainDraggable:NONE',
577 | ' ',
578 |
579 | 'parentProps(UNLINK):' + modal3._id,
580 |
581 | ' ',
582 |
583 | '_id:' + modal1._id, ' ',
584 |
585 | ' ', 'CLOSE', '_id:' + modal3._id,
586 |
587 | // 3 START: close
588 | '', '_id:' + modal3._id, 'state:STATE_OPENED',
589 | 'openCloseEffectProps:NONE',
590 |
591 | '', '_id:' + modal3._id, 'state:STATE_OPENED',
592 | 'force:false', 'sync:false',
593 | 'state:STATE_CLOSING',
594 |
595 | '', '_id:' + modal3._id, 'state:STATE_CLOSING',
596 | 'plainDraggable:NONE',
597 | ' ',
598 |
599 | // PlainOverlay.hide()
600 | '_id:' + modal3._id, ' ',
601 |
602 | '_id:' + modal3._id, ' ',
603 | // DONE: close
604 |
605 | '', '_id:' + modal3._id, 'state:STATE_CLOSING',
606 | 'effectKey:plainOverlay',
607 | 'effectFinished.plainOverlay:true',
608 | 'effectFinished.option:false', 'closeEffect:NO',
609 |
610 | '', '_id:' + modal3._id, 'state:STATE_CLOSING',
611 | 'shownProps:NONE',
612 | 'state:STATE_CLOSED',
613 | ' ',
614 |
615 | '_id:' + modal3._id, ' '
616 | ]);
617 | },
618 | // ====================================
619 | 0, done
620 | ]);
621 | }
622 | );
623 | });
624 |
625 | });
626 |
--------------------------------------------------------------------------------
/plain-modal-limit.esm.js:
--------------------------------------------------------------------------------
1 | /* ================================================
2 | DON'T MANUALLY EDIT THIS FILE
3 | ================================================ */
4 |
5 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
6 |
7 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
8 |
9 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
10 |
11 | /*
12 | * PlainModal
13 | * https://anseki.github.io/plain-modal/
14 | *
15 | * Copyright (c) 2021 anseki
16 | * Licensed under the MIT license.
17 | */
18 | import CSSPrefix from 'cssprefix';
19 | import mClassList from 'm-class-list';
20 | import PlainOverlay from 'plain-overlay';
21 | /* Static ESM */ /* import CSS_TEXT from './default.scss' */ var CSS_TEXT = ".plainmodal .plainmodal-overlay{-webkit-tap-highlight-color:rgba(0,0,0,0);transform:translateZ(0);box-shadow:0 0 1px rgba(0,0,0,0)}.plainmodal.plainoverlay{background-color:transparent;cursor:auto}.plainmodal .plainmodal-content{z-index:9000}.plainmodal .plainmodal-overlay{width:100%;height:100%;position:absolute;left:0;top:0;background-color:rgba(136,136,136,.6);transition-property:opacity;transition-duration:200ms;transition-timing-function:linear;opacity:1}.plainmodal .plainmodal-overlay.plainmodal-overlay-hide{opacity:0}.plainmodal .plainmodal-overlay.plainmodal-overlay-force{transition-property:none}";
22 | mClassList.ignoreNative = true;
23 |
24 | var APP_ID = 'plainmodal',
25 | STYLE_ELEMENT_ID = "".concat(APP_ID, "-style"),
26 | STYLE_CLASS = APP_ID,
27 | STYLE_CLASS_CONTENT = "".concat(APP_ID, "-content"),
28 | STYLE_CLASS_OVERLAY = "".concat(APP_ID, "-overlay"),
29 | STYLE_CLASS_OVERLAY_HIDE = "".concat(STYLE_CLASS_OVERLAY, "-hide"),
30 | STYLE_CLASS_OVERLAY_FORCE = "".concat(STYLE_CLASS_OVERLAY, "-force"),
31 | STATE_CLOSED = 0,
32 | STATE_OPENING = 1,
33 | STATE_OPENED = 2,
34 | STATE_CLOSING = 3,
35 | STATE_INACTIVATING = 4,
36 | STATE_INACTIVATED = 5,
37 | STATE_ACTIVATING = 6,
38 | DURATION = 200,
39 | // COPY from PlainOverlay
40 | IS_EDGE = '-ms-scroll-limit' in document.documentElement.style && '-ms-ime-align' in document.documentElement.style && !window.navigator.msPointerEnabled,
41 | IS_TRIDENT = !IS_EDGE && !!document.uniqueID,
42 | // Future Edge might support `document.uniqueID`.
43 | isObject = function () {
44 | var toString = {}.toString,
45 | fnToString = {}.hasOwnProperty.toString,
46 | objFnString = fnToString.call(Object);
47 | return function (obj) {
48 | var proto, constr;
49 | return obj && toString.call(obj) === '[object Object]' && (!(proto = Object.getPrototypeOf(obj)) || (constr = proto.hasOwnProperty('constructor') && proto.constructor) && typeof constr === 'function' && fnToString.call(constr) === objFnString);
50 | };
51 | }(),
52 |
53 | /**
54 | * An object that has properties of instance.
55 | * @typedef {Object} props
56 | * @property {Element} elmContent - Content element.
57 | * @property {Element} elmOverlay - Overlay element. (Not PlainOverlay)
58 | * @property {PlainOverlay} plainOverlay - PlainOverlay instance.
59 | * @property {PlainDraggable} plainDraggable - PlainDraggable instance.
60 | * @property {number} state - Current state.
61 | * @property {Object} options - Options.
62 | * @property {props} parentProps - props that is effected with current props.
63 | * @property {{plainOverlay: boolean, option: boolean}} effectFinished - The effect finished.
64 | */
65 |
66 | /** @type {Object.<_id: number, props>} */
67 | insProps = {},
68 |
69 | /**
70 | * A `props` list, it have a `state` other than `STATE_CLOSED`.
71 | * A `props` is pushed to the end of this array, `shownProps[shownProps.length - 1]` can be active.
72 | * @type {Array.}
73 | */
74 | shownProps = [];
75 |
76 | var closeByEscKey = true,
77 | closeByOverlay = true,
78 | insId = 0,
79 | openCloseEffectProps; // A `props` that is running the "open/close" effect now.
80 |
81 | function forceReflow(target) {
82 | // Trident and Blink bug (reflow like `offsetWidth` can't update)
83 | setTimeout(function () {
84 | var parent = target.parentNode,
85 | next = target.nextSibling; // It has to be removed first for Blink.
86 |
87 | parent.insertBefore(parent.removeChild(target), next);
88 | }, 0);
89 | }
90 | /**
91 | * @param {Element} element - A target element.
92 | * @returns {boolean} `true` if connected element.
93 | */
94 |
95 |
96 | function isElement(element) {
97 | return !!(element && element.nodeType === Node.ELEMENT_NODE && // element instanceof HTMLElement &&
98 | typeof element.getBoundingClientRect === 'function' && !(element.compareDocumentPosition(document) & Node.DOCUMENT_POSITION_DISCONNECTED));
99 | }
100 |
101 | function finishOpening(props) {
102 | openCloseEffectProps = null;
103 | props.state = STATE_OPENED;
104 |
105 | if (props.parentProps) {
106 | props.parentProps.state = STATE_INACTIVATED;
107 | }
108 |
109 | if (props.options.onOpen) {
110 | props.options.onOpen.call(props.ins);
111 | }
112 | }
113 |
114 | function finishClosing(props) {
115 | shownProps.pop();
116 | openCloseEffectProps = null;
117 | props.state = STATE_CLOSED;
118 |
119 | if (props.parentProps) {
120 | props.parentProps.state = STATE_OPENED;
121 | props.parentProps = null;
122 | }
123 |
124 | if (props.options.onClose) {
125 | props.options.onClose.call(props.ins);
126 | }
127 | }
128 | /**
129 | * @param {props} props - `props` of instance.
130 | * @param {string} effectKey - `plainOverlay' or 'option`
131 | * @returns {void}
132 | */
133 |
134 |
135 | function finishOpenEffect(props, effectKey) {
136 | if (props.state !== STATE_OPENING) {
137 | return;
138 | }
139 |
140 | props.effectFinished[effectKey] = true;
141 |
142 | if (props.effectFinished.plainOverlay && (!props.options.openEffect || props.effectFinished.option)) {
143 | finishOpening(props);
144 | }
145 | }
146 | /**
147 | * @param {props} props - `props` of instance.
148 | * @param {string} effectKey - `plainOverlay' or 'option`
149 | * @returns {void}
150 | */
151 |
152 |
153 | function finishCloseEffect(props, effectKey) {
154 | if (props.state !== STATE_CLOSING) {
155 | return;
156 | }
157 |
158 | props.effectFinished[effectKey] = true;
159 |
160 | if (props.effectFinished.plainOverlay && (!props.options.closeEffect || props.effectFinished.option)) {
161 | finishClosing(props);
162 | }
163 | }
164 | /**
165 | * Process after preparing data and adjusting style.
166 | * @param {props} props - `props` of instance.
167 | * @param {boolean} [force] - Skip effect.
168 | * @returns {void}
169 | */
170 |
171 |
172 | function execOpening(props, force) {
173 | if (props.parentProps) {
174 | // inactivate parentProps
175 |
176 | /*
177 | Cases:
178 | - STATE_OPENED or STATE_ACTIVATING, regardless of force
179 | - STATE_INACTIVATING and force
180 | */
181 | var parentProps = props.parentProps,
182 | elmOverlay = parentProps.elmOverlay;
183 |
184 | if (parentProps.state === STATE_OPENED) {
185 | elmOverlay.style[CSSPrefix.getName('transitionDuration')] = props.options.duration === DURATION ? '' : "".concat(props.options.duration, "ms");
186 | }
187 |
188 | var elmOverlayClassList = mClassList(elmOverlay);
189 | elmOverlayClassList.toggle(STYLE_CLASS_OVERLAY_FORCE, !!force);
190 | elmOverlayClassList.add(STYLE_CLASS_OVERLAY_HIDE); // Update `state` regardless of force, for switchDraggable.
191 |
192 | parentProps.state = STATE_INACTIVATING;
193 | parentProps.plainOverlay.blockingDisabled = true;
194 | }
195 |
196 | props.state = STATE_OPENING;
197 | props.plainOverlay.blockingDisabled = false;
198 | props.effectFinished.plainOverlay = props.effectFinished.option = false;
199 | props.plainOverlay.show(force);
200 |
201 | if (props.options.openEffect) {
202 | if (force) {
203 | props.options.openEffect.call(props.ins);
204 | finishOpenEffect(props, 'option');
205 | } else {
206 | props.options.openEffect.call(props.ins, props.openEffectDone);
207 | }
208 | }
209 | }
210 | /**
211 | * Process after preparing data and adjusting style.
212 | * @param {props} props - `props` of instance.
213 | * @param {boolean} [force] - Skip effect.
214 | * @param {boolean} [sync] - `force` with sync-mode. (Skip restoring active element)
215 | * @returns {void}
216 | */
217 |
218 |
219 | function execClosing(props, force, sync) {
220 | if (props.parentProps) {
221 | // activate parentProps
222 |
223 | /*
224 | Cases:
225 | - STATE_INACTIVATED or STATE_INACTIVATING, regardless of `force`
226 | - STATE_ACTIVATING and `force`
227 | */
228 | var parentProps = props.parentProps,
229 | elmOverlay = parentProps.elmOverlay;
230 |
231 | if (parentProps.state === STATE_INACTIVATED) {
232 | elmOverlay.style[CSSPrefix.getName('transitionDuration')] = props.options.duration === DURATION ? '' : "".concat(props.options.duration, "ms");
233 | }
234 |
235 | var elmOverlayClassList = mClassList(elmOverlay);
236 | elmOverlayClassList.toggle(STYLE_CLASS_OVERLAY_FORCE, !!force);
237 | elmOverlayClassList.remove(STYLE_CLASS_OVERLAY_HIDE); // same condition as props
238 |
239 | parentProps.state = STATE_ACTIVATING;
240 | parentProps.plainOverlay.blockingDisabled = false;
241 | }
242 |
243 | props.state = STATE_CLOSING;
244 | props.effectFinished.plainOverlay = props.effectFinished.option = false;
245 | props.plainOverlay.hide(force, sync);
246 |
247 | if (props.options.closeEffect) {
248 | if (force) {
249 | props.options.closeEffect.call(props.ins);
250 | finishCloseEffect(props, 'option');
251 | } else {
252 | props.options.closeEffect.call(props.ins, props.closeEffectDone);
253 | }
254 | }
255 | }
256 | /**
257 | * Finish the "open/close" effect immediately with sync-mode.
258 | * @param {props} props - `props` of instance.
259 | * @returns {void}
260 | */
261 |
262 |
263 | function fixOpenClose(props) {
264 | if (props.state === STATE_OPENING) {
265 | execOpening(props, true);
266 | } else if (props.state === STATE_CLOSING) {
267 | execClosing(props, true, true);
268 | }
269 | }
270 | /**
271 | * @param {props} props - `props` of instance.
272 | * @param {boolean} [force] - Skip effect.
273 | * @returns {void}
274 | */
275 |
276 |
277 | function _open(props, force) {
278 | if (props.state !== STATE_CLOSED && props.state !== STATE_CLOSING && props.state !== STATE_OPENING || props.state === STATE_OPENING && !force || props.state !== STATE_OPENING && props.options.onBeforeOpen && props.options.onBeforeOpen.call(props.ins) === false) {
279 | return false;
280 | }
281 | /*
282 | Cases:
283 | - STATE_CLOSED or STATE_CLOSING, regardless of `force`
284 | - STATE_OPENING and `force`
285 | */
286 |
287 |
288 | if (props.state === STATE_CLOSED) {
289 | if (openCloseEffectProps) {
290 | fixOpenClose(openCloseEffectProps);
291 | }
292 |
293 | openCloseEffectProps = props;
294 |
295 | if (shownProps.length) {
296 | props.parentProps = shownProps[shownProps.length - 1];
297 | }
298 |
299 | shownProps.push(props);
300 | mClassList(props.elmOverlay).add(STYLE_CLASS_OVERLAY_FORCE).remove(STYLE_CLASS_OVERLAY_HIDE);
301 | }
302 |
303 | execOpening(props, force);
304 | return true;
305 | }
306 | /**
307 | * @param {props} props - `props` of instance.
308 | * @param {boolean} [force] - Skip effect.
309 | * @returns {void}
310 | */
311 |
312 |
313 | function _close(props, force) {
314 | if (props.state === STATE_CLOSED || props.state === STATE_CLOSING && !force || props.state !== STATE_CLOSING && props.options.onBeforeClose && props.options.onBeforeClose.call(props.ins) === false) {
315 | return false;
316 | }
317 | /*
318 | Cases:
319 | - Other than STATE_CLOSED and STATE_CLOSING, regardless of `force`
320 | - STATE_CLOSING and `force`
321 | */
322 |
323 |
324 | if (openCloseEffectProps && openCloseEffectProps !== props) {
325 | fixOpenClose(openCloseEffectProps);
326 | openCloseEffectProps = null;
327 | }
328 | /*
329 | Cases:
330 | - STATE_OPENED, STATE_OPENING or STATE_INACTIVATED, regardless of `force`
331 | - STATE_CLOSING and `force`
332 | */
333 |
334 |
335 | if (props.state === STATE_INACTIVATED) {
336 | // -> STATE_OPENED
337 | var topProps;
338 |
339 | while ((topProps = shownProps[shownProps.length - 1]) !== props) {
340 | execClosing(topProps, true, true);
341 | }
342 | }
343 | /*
344 | Cases:
345 | - STATE_OPENED or STATE_OPENING, regardless of `force`
346 | - STATE_CLOSING and `force`
347 | */
348 |
349 |
350 | if (props.state === STATE_OPENED) {
351 | openCloseEffectProps = props;
352 | }
353 |
354 | execClosing(props, force);
355 | return true;
356 | }
357 | /**
358 | * @param {props} props - `props` of instance.
359 | * @param {Object} newOptions - New options.
360 | * @returns {void}
361 | */
362 |
363 |
364 | function _setOptions(props, newOptions) {
365 | var options = props.options,
366 | plainOverlay = props.plainOverlay; // closeButton
367 |
368 | if (newOptions.hasOwnProperty('closeButton') && (newOptions.closeButton = isElement(newOptions.closeButton) ? newOptions.closeButton : newOptions.closeButton == null ? void 0 : false) !== false && newOptions.closeButton !== options.closeButton) {
369 | if (options.closeButton) {
370 | // Remove
371 | options.closeButton.removeEventListener('click', props.handleClose, false);
372 | }
373 |
374 | options.closeButton = newOptions.closeButton;
375 |
376 | if (options.closeButton) {
377 | // Add
378 | options.closeButton.addEventListener('click', props.handleClose, false);
379 | }
380 | } // duration
381 | // Check by PlainOverlay
382 |
383 |
384 | plainOverlay.duration = newOptions.duration;
385 | options.duration = plainOverlay.duration; // overlayBlur
386 | // Check by PlainOverlay
387 |
388 | plainOverlay.blur = newOptions.overlayBlur;
389 | options.overlayBlur = plainOverlay.blur; // effect functions and event listeners
390 |
391 | ['openEffect', 'closeEffect', 'onOpen', 'onClose', 'onBeforeOpen', 'onBeforeClose'].forEach(function (option) {
392 | if (typeof newOptions[option] === 'function') {
393 | options[option] = newOptions[option];
394 | } else if (newOptions.hasOwnProperty(option) && newOptions[option] == null) {
395 | options[option] = void 0;
396 | }
397 | });
398 | }
399 |
400 | var PlainModal = /*#__PURE__*/function () {
401 | /**
402 | * Create a `PlainModal` instance.
403 | * @param {Element} content - An element that is shown as the content of the modal window.
404 | * @param {Object} [options] - Options.
405 | */
406 | function PlainModal(content, options) {
407 | _classCallCheck(this, PlainModal);
408 |
409 | var props = {
410 | ins: this,
411 | options: {
412 | // Initial options (not default)
413 | closeButton: void 0,
414 | duration: DURATION,
415 | overlayBlur: false
416 | },
417 | state: STATE_CLOSED,
418 | effectFinished: {
419 | plainOverlay: false,
420 | option: false
421 | }
422 | };
423 | Object.defineProperty(this, '_id', {
424 | value: ++insId
425 | });
426 | props._id = this._id;
427 | insProps[this._id] = props;
428 |
429 | if (!content.nodeType || content.nodeType !== Node.ELEMENT_NODE || content.ownerDocument.defaultView !== window) {
430 | throw new Error('This `content` is not accepted.');
431 | }
432 |
433 | props.elmContent = content;
434 |
435 | if (!options) {
436 | options = {};
437 | } else if (!isObject(options)) {
438 | throw new Error('Invalid options.');
439 | } // Setup window
440 |
441 |
442 | if (!document.getElementById(STYLE_ELEMENT_ID)) {
443 | var head = document.getElementsByTagName('head')[0] || document.documentElement,
444 | sheet = head.insertBefore(document.createElement('style'), head.firstChild);
445 | sheet.type = 'text/css';
446 | sheet.id = STYLE_ELEMENT_ID;
447 | sheet.textContent = CSS_TEXT;
448 |
449 | if (IS_TRIDENT || IS_EDGE) {
450 | forceReflow(sheet);
451 | } // Trident bug
452 | // for closeByEscKey
453 |
454 |
455 | window.addEventListener('keydown', function (event) {
456 | var key, topProps;
457 |
458 | if (closeByEscKey && ((key = event.key.toLowerCase()) === 'escape' || key === 'esc') && (topProps = shownProps.length && shownProps[shownProps.length - 1]) && _close(topProps)) {
459 | event.preventDefault();
460 | event.stopImmediatePropagation(); // preventDefault stops other listeners, maybe.
461 |
462 | event.stopPropagation();
463 | }
464 | }, true);
465 | }
466 |
467 | mClassList(content).add(STYLE_CLASS_CONTENT); // Overlay
468 |
469 | props.plainOverlay = new PlainOverlay({
470 | face: content,
471 | onShow: function onShow() {
472 | finishOpenEffect(props, 'plainOverlay');
473 | },
474 | onHide: function onHide() {
475 | finishCloseEffect(props, 'plainOverlay');
476 | }
477 | }); // The `content` is now contained into PlainOverlay, and update `display`.
478 |
479 | if (window.getComputedStyle(content, '').display === 'none') {
480 | content.style.display = 'block';
481 | } // Trident can not get parent of SVG by parentElement.
482 |
483 |
484 | var elmPlainOverlayBody = content.parentNode; // elmOverlayBody of PlainOverlay
485 |
486 | mClassList(elmPlainOverlayBody.parentNode).add(STYLE_CLASS); // elmOverlay of PlainOverlay
487 | // elmOverlay (own overlay)
488 |
489 | var elmOverlay = props.elmOverlay = elmPlainOverlayBody.appendChild(document.createElement('div'));
490 | elmOverlay.className = STYLE_CLASS_OVERLAY; // for closeByOverlay
491 |
492 | elmOverlay.addEventListener('click', function (event) {
493 | if (event.target === elmOverlay && closeByOverlay) {
494 | _close(props);
495 | }
496 | }, true); // Prepare removable event listeners for each instance.
497 |
498 | props.handleClose = function () {
499 | _close(props);
500 | }; // Callback functions for additional effects, prepare these to allow to be used as listener.
501 |
502 |
503 | props.openEffectDone = function () {
504 | finishOpenEffect(props, 'option');
505 | };
506 |
507 | props.closeEffectDone = function () {
508 | finishCloseEffect(props, 'option');
509 | };
510 |
511 | props.effectDone = function () {
512 | if (props.state === STATE_OPENING) {
513 | finishOpenEffect(props, 'option');
514 | } else if (props.state === STATE_CLOSING) {
515 | finishCloseEffect(props, 'option');
516 | }
517 | };
518 |
519 | _setOptions(props, options);
520 | }
521 | /**
522 | * @param {Object} options - New options.
523 | * @returns {PlainModal} Current instance itself.
524 | */
525 |
526 |
527 | _createClass(PlainModal, [{
528 | key: "setOptions",
529 | value: function setOptions(options) {
530 | if (isObject(options)) {
531 | _setOptions(insProps[this._id], options);
532 | }
533 |
534 | return this;
535 | }
536 | /**
537 | * Open the modal window.
538 | * @param {boolean} [force] - Show it immediately without effect.
539 | * @param {Object} [options] - New options.
540 | * @returns {PlainModal} Current instance itself.
541 | */
542 |
543 | }, {
544 | key: "open",
545 | value: function open(force, options) {
546 | if (arguments.length < 2 && typeof force !== 'boolean') {
547 | options = force;
548 | force = false;
549 | }
550 |
551 | this.setOptions(options);
552 |
553 | _open(insProps[this._id], force);
554 |
555 | return this;
556 | }
557 | /**
558 | * Close the modal window.
559 | * @param {boolean} [force] - Close it immediately without effect.
560 | * @returns {PlainModal} Current instance itself.
561 | */
562 |
563 | }, {
564 | key: "close",
565 | value: function close(force) {
566 | _close(insProps[this._id], force);
567 |
568 | return this;
569 | }
570 | }, {
571 | key: "state",
572 | get: function get() {
573 | return insProps[this._id].state;
574 | }
575 | }, {
576 | key: "closeButton",
577 | get: function get() {
578 | return insProps[this._id].options.closeButton;
579 | },
580 | set: function set(value) {
581 | _setOptions(insProps[this._id], {
582 | closeButton: value
583 | });
584 | }
585 | }, {
586 | key: "duration",
587 | get: function get() {
588 | return insProps[this._id].options.duration;
589 | },
590 | set: function set(value) {
591 | _setOptions(insProps[this._id], {
592 | duration: value
593 | });
594 | }
595 | }, {
596 | key: "overlayBlur",
597 | get: function get() {
598 | return insProps[this._id].options.overlayBlur;
599 | },
600 | set: function set(value) {
601 | _setOptions(insProps[this._id], {
602 | overlayBlur: value
603 | });
604 | }
605 | }, {
606 | key: "openEffect",
607 | get: function get() {
608 | return insProps[this._id].options.openEffect;
609 | },
610 | set: function set(value) {
611 | _setOptions(insProps[this._id], {
612 | openEffect: value
613 | });
614 | }
615 | }, {
616 | key: "closeEffect",
617 | get: function get() {
618 | return insProps[this._id].options.closeEffect;
619 | },
620 | set: function set(value) {
621 | _setOptions(insProps[this._id], {
622 | closeEffect: value
623 | });
624 | }
625 | }, {
626 | key: "effectDone",
627 | get: function get() {
628 | return insProps[this._id].effectDone;
629 | }
630 | }, {
631 | key: "onOpen",
632 | get: function get() {
633 | return insProps[this._id].options.onOpen;
634 | },
635 | set: function set(value) {
636 | _setOptions(insProps[this._id], {
637 | onOpen: value
638 | });
639 | }
640 | }, {
641 | key: "onClose",
642 | get: function get() {
643 | return insProps[this._id].options.onClose;
644 | },
645 | set: function set(value) {
646 | _setOptions(insProps[this._id], {
647 | onClose: value
648 | });
649 | }
650 | }, {
651 | key: "onBeforeOpen",
652 | get: function get() {
653 | return insProps[this._id].options.onBeforeOpen;
654 | },
655 | set: function set(value) {
656 | _setOptions(insProps[this._id], {
657 | onBeforeOpen: value
658 | });
659 | }
660 | }, {
661 | key: "onBeforeClose",
662 | get: function get() {
663 | return insProps[this._id].options.onBeforeClose;
664 | },
665 | set: function set(value) {
666 | _setOptions(insProps[this._id], {
667 | onBeforeClose: value
668 | });
669 | }
670 | }], [{
671 | key: "closeByEscKey",
672 | get: function get() {
673 | return closeByEscKey;
674 | },
675 | set: function set(value) {
676 | if (typeof value === 'boolean') {
677 | closeByEscKey = value;
678 | }
679 | }
680 | }, {
681 | key: "closeByOverlay",
682 | get: function get() {
683 | return closeByOverlay;
684 | },
685 | set: function set(value) {
686 | if (typeof value === 'boolean') {
687 | closeByOverlay = value;
688 | }
689 | }
690 | }, {
691 | key: "STATE_CLOSED",
692 | get: function get() {
693 | return STATE_CLOSED;
694 | }
695 | }, {
696 | key: "STATE_OPENING",
697 | get: function get() {
698 | return STATE_OPENING;
699 | }
700 | }, {
701 | key: "STATE_OPENED",
702 | get: function get() {
703 | return STATE_OPENED;
704 | }
705 | }, {
706 | key: "STATE_CLOSING",
707 | get: function get() {
708 | return STATE_CLOSING;
709 | }
710 | }, {
711 | key: "STATE_INACTIVATING",
712 | get: function get() {
713 | return STATE_INACTIVATING;
714 | }
715 | }, {
716 | key: "STATE_INACTIVATED",
717 | get: function get() {
718 | return STATE_INACTIVATED;
719 | }
720 | }, {
721 | key: "STATE_ACTIVATING",
722 | get: function get() {
723 | return STATE_ACTIVATING;
724 | }
725 | }]);
726 |
727 | return PlainModal;
728 | }();
729 |
730 | PlainModal.limit = true;
731 | export default PlainModal;
--------------------------------------------------------------------------------
/test/spec/closeByEscKey.js:
--------------------------------------------------------------------------------
1 | describe('closeByEscKey', function() {
2 | 'use strict';
3 |
4 | var window, document, utils, PlainModal, traceLog, shownProps, pageDone,
5 | modal1, modal2, modal3, allModals;
6 |
7 | function escKeyDown() {
8 | var event;
9 | try {
10 | event = new window.KeyboardEvent('keydown', {key: 'Escape'});
11 | } catch (error) {
12 | event = document.createEvent('KeyboardEvent');
13 | if (event.initKeyboardEvent) {
14 | event.initKeyboardEvent('keydown', true, true, document.defaultView,
15 | // (... charArg, keyArg ...) or (... keyArg, locationArg ...) (IE)
16 | 'Escape', 'Escape', 0, '', false);
17 | } else if (event.initKeyEvent) {
18 | event.initKeyEvent('keydown', true, true, document.defaultView,
19 | false, false, false, false, 27, 0);
20 | } else {
21 | throw new Error('Can\'t init event');
22 | }
23 | }
24 | window.dispatchEvent(event);
25 | }
26 |
27 | beforeAll(function(beforeDone) {
28 | loadPage('spec/common.html', function(pageWindow, pageDocument, pageBody, done) {
29 | window = pageWindow;
30 | document = pageDocument;
31 | utils = window.utils;
32 | PlainModal = window.PlainModal;
33 | traceLog = PlainModal.traceLog;
34 | shownProps = PlainModal.shownProps;
35 |
36 | modal1 = new PlainModal(document.getElementById('elm1'), {duration: 50});
37 | modal2 = new PlainModal(document.getElementById('elm2'), {duration: 50});
38 | modal3 = new PlainModal(document.getElementById('elm3'), {duration: 50});
39 | allModals = [modal1, modal2, modal3];
40 |
41 | window.addEventListener('keydown', function(event) {
42 | var key;
43 | if ((key = event.key.toLowerCase()) === 'escape' || key === 'esc') {
44 | window.cntEscKey++;
45 | }
46 | }, true);
47 |
48 | pageDone = done;
49 | beforeDone();
50 | });
51 | });
52 |
53 | afterAll(function() {
54 | pageDone();
55 | });
56 |
57 | it('Check Edition (to be LIMIT: ' + !!self.top.LIMIT + ')', function() {
58 | expect(!!window.PlainModal.limit).toBe(!!self.top.LIMIT);
59 | });
60 |
61 | it('Normal flow - keydown -> close()', function(done) {
62 | modal1.onOpen = modal1.onClose = modal1.onBeforeOpen = modal1.onBeforeClose =
63 | modal2.onOpen = modal2.onClose = modal2.onBeforeOpen = modal2.onBeforeClose =
64 | modal3.onOpen = modal3.onClose = modal3.onBeforeOpen = modal3.onBeforeClose = null;
65 |
66 | var timer1;
67 | utils.makeState(allModals,
68 | [PlainModal.STATE_OPENED, PlainModal.STATE_CLOSED],
69 | function() {
70 | modal1.close(true);
71 | modal2.close(true);
72 | modal3.close(true);
73 | timer1 = setTimeout(function() {
74 | modal1.open(true);
75 | }, 10);
76 | return true;
77 | },
78 | function() {
79 | clearTimeout(timer1);
80 |
81 | expect(modal1.state).toBe(PlainModal.STATE_OPENED);
82 | expect(shownProps.map(function(props) { return props.ins; }))
83 | .toEqual([modal1]);
84 |
85 | modal1.onClose = function() {
86 | setTimeout(function() {
87 | expect(modal1.state).toBe(PlainModal.STATE_CLOSED);
88 | expect(shownProps).toEqual([]);
89 | expect(window.cntEscKey).toBe(0); // window get no event
90 |
91 | expect(traceLog).toEqual([
92 | ' ', 'CLOSE', '_id:' + modal1._id,
93 |
94 | // START: close
95 | '', '_id:' + modal1._id, 'state:STATE_OPENED',
96 | 'openCloseEffectProps:NONE',
97 |
98 | '', '_id:' + modal1._id, 'state:STATE_OPENED',
99 | 'force:false', 'sync:false',
100 | 'state:STATE_CLOSING',
101 |
102 | '', '_id:' + modal1._id, 'state:STATE_CLOSING',
103 | 'plainDraggable:NONE',
104 | ' ',
105 |
106 | // PlainOverlay.hide()
107 | '_id:' + modal1._id, ' ',
108 |
109 | '_id:' + modal1._id, ' ',
110 | // DONE: close
111 |
112 | '', '_id:' + modal1._id, 'state:STATE_CLOSING',
113 | 'effectKey:plainOverlay',
114 | 'effectFinished.plainOverlay:true',
115 | 'effectFinished.option:false', 'closeEffect:NO',
116 |
117 | '', '_id:' + modal1._id, 'state:STATE_CLOSING',
118 | 'shownProps:NONE',
119 | 'state:STATE_CLOSED',
120 | ' ',
121 |
122 | '_id:' + modal1._id, ' '
123 | ]);
124 |
125 | done();
126 | }, 0);
127 | };
128 |
129 | traceLog.length = 0;
130 | window.cntEscKey = 0;
131 | PlainModal.closeByEscKey = true;
132 | escKeyDown();
133 | }
134 | );
135 | });
136 |
137 | it('PlainModal.closeByEscKey = false, keydown -> ignored', function(done) {
138 | modal1.onOpen = modal1.onClose = modal1.onBeforeOpen = modal1.onBeforeClose =
139 | modal2.onOpen = modal2.onClose = modal2.onBeforeOpen = modal2.onBeforeClose =
140 | modal3.onOpen = modal3.onClose = modal3.onBeforeOpen = modal3.onBeforeClose = null;
141 |
142 | var timer1;
143 | utils.makeState(allModals,
144 | [PlainModal.STATE_OPENED, PlainModal.STATE_CLOSED],
145 | function() {
146 | modal1.close(true);
147 | modal2.close(true);
148 | modal3.close(true);
149 | timer1 = setTimeout(function() {
150 | modal1.open(true);
151 | }, 10);
152 | return true;
153 | },
154 | function() {
155 | clearTimeout(timer1);
156 |
157 | expect(modal1.state).toBe(PlainModal.STATE_OPENED);
158 | expect(shownProps.map(function(props) { return props.ins; }))
159 | .toEqual([modal1]);
160 |
161 | setTimeout(function() {
162 | expect(modal1.state).toBe(PlainModal.STATE_OPENED);
163 | expect(shownProps.map(function(props) { return props.ins; }))
164 | .toEqual([modal1]);
165 | expect(window.cntEscKey).toBe(1); // window get the event
166 |
167 | expect(traceLog).toEqual([]);
168 |
169 | done();
170 | }, 100);
171 |
172 | traceLog.length = 0;
173 | window.cntEscKey = 0;
174 | PlainModal.closeByEscKey = false;
175 | escKeyDown();
176 | }
177 | );
178 | });
179 |
180 | it('STATE_CLOSED, keydown -> ignored', function(done) {
181 | modal1.onOpen = modal1.onClose = modal1.onBeforeOpen = modal1.onBeforeClose =
182 | modal2.onOpen = modal2.onClose = modal2.onBeforeOpen = modal2.onBeforeClose =
183 | modal3.onOpen = modal3.onClose = modal3.onBeforeOpen = modal3.onBeforeClose = null;
184 |
185 | utils.makeState(allModals,
186 | PlainModal.STATE_CLOSED,
187 | function() {
188 | modal1.close(true);
189 | modal2.close(true);
190 | modal3.close(true);
191 | return true;
192 | },
193 | function() {
194 |
195 | expect(modal1.state).toBe(PlainModal.STATE_CLOSED);
196 | expect(shownProps).toEqual([]);
197 |
198 | setTimeout(function() {
199 | expect(modal1.state).toBe(PlainModal.STATE_CLOSED);
200 | expect(shownProps).toEqual([]);
201 | expect(window.cntEscKey).toBe(1); // window get the event
202 |
203 | expect(traceLog).toEqual([]);
204 |
205 | done();
206 | }, 100);
207 |
208 | traceLog.length = 0;
209 | window.cntEscKey = 0;
210 | PlainModal.closeByEscKey = true;
211 | escKeyDown();
212 | }
213 | );
214 | });
215 |
216 | it('STATE_OPENING, keydown -> close()', function(done) {
217 | modal1.onOpen = modal1.onClose = modal1.onBeforeOpen = modal1.onBeforeClose =
218 | modal2.onOpen = modal2.onClose = modal2.onBeforeOpen = modal2.onBeforeClose =
219 | modal3.onOpen = modal3.onClose = modal3.onBeforeOpen = modal3.onBeforeClose = null;
220 |
221 | var timer1;
222 | utils.makeState(allModals,
223 | [PlainModal.STATE_OPENING, PlainModal.STATE_CLOSED],
224 | function() {
225 | modal1.close(true);
226 | modal2.close(true);
227 | modal3.close(true);
228 | timer1 = setTimeout(function() {
229 | modal1.open();
230 | }, 10);
231 | return true;
232 | },
233 | function() {
234 | clearTimeout(timer1);
235 |
236 | expect(modal1.state).toBe(PlainModal.STATE_OPENING);
237 | expect(shownProps.map(function(props) { return props.ins; }))
238 | .toEqual([modal1]);
239 |
240 | modal1.onClose = function() {
241 | setTimeout(function() {
242 | expect(modal1.state).toBe(PlainModal.STATE_CLOSED);
243 | expect(shownProps).toEqual([]);
244 | expect(window.cntEscKey).toBe(0); // window get no event
245 |
246 | expect(traceLog).toEqual([
247 | ' ', 'CLOSE', '_id:' + modal1._id,
248 |
249 | // START: close
250 | '', '_id:' + modal1._id, 'state:STATE_OPENING',
251 | 'openCloseEffectProps:' + modal1._id,
252 |
253 | '', '_id:' + modal1._id, 'state:STATE_OPENING',
254 | 'force:false', 'sync:false',
255 | 'state:STATE_CLOSING',
256 |
257 | '', '_id:' + modal1._id, 'state:STATE_CLOSING',
258 | 'plainDraggable:NONE',
259 | ' ',
260 |
261 | // PlainOverlay.hide()
262 | '_id:' + modal1._id, ' ',
263 |
264 | '_id:' + modal1._id, ' ',
265 | // DONE: close
266 |
267 | '', '_id:' + modal1._id, 'state:STATE_CLOSING',
268 | 'effectKey:plainOverlay',
269 | 'effectFinished.plainOverlay:true',
270 | 'effectFinished.option:false', 'closeEffect:NO',
271 |
272 | '', '_id:' + modal1._id, 'state:STATE_CLOSING',
273 | 'shownProps:NONE',
274 | 'state:STATE_CLOSED',
275 | ' ',
276 |
277 | '_id:' + modal1._id, ' '
278 | ]);
279 |
280 | done();
281 | }, 0);
282 | };
283 |
284 | traceLog.length = 0;
285 | window.cntEscKey = 0;
286 | PlainModal.closeByEscKey = true;
287 | escKeyDown();
288 | }
289 | );
290 | });
291 |
292 | it('STATE_CLOSING, keydown -> close() -> CANCEL', function(done) {
293 | modal1.onOpen = modal1.onClose = modal1.onBeforeOpen = modal1.onBeforeClose =
294 | modal2.onOpen = modal2.onClose = modal2.onBeforeOpen = modal2.onBeforeClose =
295 | modal3.onOpen = modal3.onClose = modal3.onBeforeOpen = modal3.onBeforeClose = null;
296 |
297 | var timer1;
298 | utils.makeState(allModals,
299 | [PlainModal.STATE_CLOSING, PlainModal.STATE_CLOSED],
300 | function() {
301 | modal1.close(true);
302 | modal2.close(true);
303 | modal3.close(true);
304 | timer1 = setTimeout(function() {
305 | modal1.open(true);
306 | timer1 = setTimeout(function() {
307 | modal1.close();
308 | }, 10);
309 | }, 10);
310 | return true;
311 | },
312 | function() {
313 | clearTimeout(timer1);
314 |
315 | expect(modal1.state).toBe(PlainModal.STATE_CLOSING);
316 | expect(shownProps.map(function(props) { return props.ins; }))
317 | .toEqual([modal1]);
318 |
319 | setTimeout(function() {
320 | expect(modal1.state).toBe(PlainModal.STATE_CLOSING);
321 | expect(shownProps.map(function(props) { return props.ins; }))
322 | .toEqual([modal1]);
323 | expect(window.cntEscKey).toBe(1); // window get the event
324 |
325 | expect(traceLog).toEqual([
326 | ' ', 'CLOSE', '_id:' + modal1._id,
327 |
328 | '', '_id:' + modal1._id, 'state:STATE_CLOSING', 'CANCEL', ' '
329 | ]);
330 |
331 | done();
332 | }, 0);
333 |
334 | traceLog.length = 0;
335 | window.cntEscKey = 0;
336 | PlainModal.closeByEscKey = true;
337 | escKeyDown();
338 | }
339 | );
340 | });
341 |
342 | it('STATE_OPENED * 3, keydown * 2 -> close() * 2', function(done) {
343 | modal1.onOpen = modal1.onClose = modal1.onBeforeOpen = modal1.onBeforeClose =
344 | modal2.onOpen = modal2.onClose = modal2.onBeforeOpen = modal2.onBeforeClose =
345 | modal3.onOpen = modal3.onClose = modal3.onBeforeOpen = modal3.onBeforeClose = null;
346 |
347 | var timer1, timer2, timer3;
348 | utils.makeState(allModals,
349 | [PlainModal.STATE_INACTIVATED, PlainModal.STATE_INACTIVATED, PlainModal.STATE_OPENED],
350 | function() {
351 | modal1.close(true);
352 | modal2.close(true);
353 | modal3.close(true);
354 | timer1 = setTimeout(function() { modal1.open(true); }, 10);
355 | timer2 = setTimeout(function() { modal2.open(true); }, 10);
356 | timer3 = setTimeout(function() { modal3.open(true); }, 10);
357 | return true;
358 | },
359 | function() {
360 | clearTimeout(timer1);
361 | clearTimeout(timer2);
362 | clearTimeout(timer3);
363 |
364 | expect(modal1.state).toBe(PlainModal.STATE_INACTIVATED);
365 | expect(modal2.state).toBe(PlainModal.STATE_INACTIVATED);
366 | expect(modal3.state).toBe(PlainModal.STATE_OPENED);
367 | expect(shownProps.map(function(props) { return props.ins; }))
368 | .toEqual([modal1, modal2, modal3]);
369 |
370 | modal2.onClose = function() {
371 | setTimeout(function() {
372 | expect(modal1.state).toBe(PlainModal.STATE_OPENED);
373 | expect(modal2.state).toBe(PlainModal.STATE_CLOSED);
374 | expect(modal3.state).toBe(PlainModal.STATE_CLOSED);
375 | expect(shownProps.map(function(props) { return props.ins; }))
376 | .toEqual([modal1]);
377 | expect(window.cntEscKey).toBe(0); // window get no event
378 |
379 | expect(traceLog).toEqual([
380 | ' ', 'CLOSE', '_id:' + modal3._id,
381 |
382 | // 3 START: close
383 | '', '_id:' + modal3._id, 'state:STATE_OPENED',
384 | 'openCloseEffectProps:NONE',
385 |
386 | '', '_id:' + modal3._id, 'state:STATE_OPENED',
387 | 'force:false', 'sync:false',
388 |
389 | 'parentProps._id:' + modal2._id, 'parentProps.state:STATE_INACTIVATED',
390 | 'elmOverlay.duration:50ms',
391 | 'elmOverlay.CLASS_FORCE:false', 'elmOverlay.CLASS_HIDE:false',
392 | 'parentProps.state:STATE_ACTIVATING',
393 | 'state:STATE_CLOSING',
394 |
395 | '', '_id:' + modal3._id, 'state:STATE_CLOSING',
396 | 'plainDraggable:NONE',
397 | ' ',
398 |
399 | // PlainOverlay.hide()
400 | '_id:' + modal3._id, ' ',
401 |
402 | '_id:' + modal3._id, ' ',
403 | // DONE: close
404 |
405 | '', '_id:' + modal3._id, 'state:STATE_CLOSING',
406 | 'effectKey:plainOverlay',
407 | 'effectFinished.plainOverlay:true',
408 | 'effectFinished.option:false', 'closeEffect:NO',
409 |
410 | '', '_id:' + modal3._id, 'state:STATE_CLOSING',
411 | 'shownProps:' + modal1._id + ',' + modal2._id,
412 | 'state:STATE_CLOSED',
413 |
414 | 'parentProps._id:' + modal2._id, 'parentProps.state:STATE_ACTIVATING',
415 | 'parentProps.state:STATE_OPENED',
416 |
417 | '', '_id:' + modal2._id, 'state:STATE_OPENED',
418 | 'plainDraggable:NONE',
419 | ' ',
420 |
421 | 'parentProps(UNLINK):' + modal2._id,
422 |
423 | ' ',
424 |
425 | '_id:' + modal3._id, ' ',
426 |
427 | ' ', 'CLOSE', '_id:' + modal2._id,
428 |
429 | // 2 START: close
430 | '', '_id:' + modal2._id, 'state:STATE_OPENED',
431 | 'openCloseEffectProps:NONE',
432 |
433 | '', '_id:' + modal2._id, 'state:STATE_OPENED',
434 | 'force:false', 'sync:false',
435 |
436 | 'parentProps._id:' + modal1._id, 'parentProps.state:STATE_INACTIVATED',
437 | 'elmOverlay.duration:50ms',
438 | 'elmOverlay.CLASS_FORCE:false', 'elmOverlay.CLASS_HIDE:false',
439 | 'parentProps.state:STATE_ACTIVATING',
440 | 'state:STATE_CLOSING',
441 |
442 | '', '_id:' + modal2._id, 'state:STATE_CLOSING',
443 | 'plainDraggable:NONE',
444 | ' ',
445 |
446 | // PlainOverlay.hide()
447 | '_id:' + modal2._id, ' ',
448 |
449 | '_id:' + modal2._id, ' ',
450 | // DONE: close
451 |
452 | '', '_id:' + modal2._id, 'state:STATE_CLOSING',
453 | 'effectKey:plainOverlay',
454 | 'effectFinished.plainOverlay:true',
455 | 'effectFinished.option:false', 'closeEffect:NO',
456 |
457 | '', '_id:' + modal2._id, 'state:STATE_CLOSING',
458 | 'shownProps:' + modal1._id,
459 | 'state:STATE_CLOSED',
460 |
461 | 'parentProps._id:' + modal1._id, 'parentProps.state:STATE_ACTIVATING',
462 | 'parentProps.state:STATE_OPENED',
463 |
464 | '', '_id:' + modal1._id, 'state:STATE_OPENED',
465 | 'plainDraggable:NONE',
466 | ' ',
467 |
468 | 'parentProps(UNLINK):' + modal1._id,
469 |
470 | ' ',
471 |
472 | '_id:' + modal2._id, ' '
473 | ]);
474 |
475 | done();
476 | }, 0);
477 | };
478 |
479 | traceLog.length = 0;
480 | window.cntEscKey = 0;
481 | PlainModal.closeByEscKey = true;
482 | escKeyDown();
483 | setTimeout(function() {
484 | escKeyDown();
485 | }, 100);
486 | }
487 | );
488 | });
489 |
490 | it('STATE_OPENED * 2, keydown * 4 -> close() * 2', function(done) {
491 | modal1.onOpen = modal1.onClose = modal1.onBeforeOpen = modal1.onBeforeClose =
492 | modal2.onOpen = modal2.onClose = modal2.onBeforeOpen = modal2.onBeforeClose =
493 | modal3.onOpen = modal3.onClose = modal3.onBeforeOpen = modal3.onBeforeClose = null;
494 |
495 | var timer1, timer2;
496 | utils.makeState(allModals,
497 | [PlainModal.STATE_OPENED, PlainModal.STATE_CLOSED, PlainModal.STATE_INACTIVATED],
498 | function() {
499 | modal1.close(true);
500 | modal2.close(true);
501 | modal3.close(true);
502 | // 3, 1
503 | timer1 = setTimeout(function() { modal3.open(true); }, 10);
504 | timer2 = setTimeout(function() { modal1.open(true); }, 10);
505 | return true;
506 | },
507 | function() {
508 | clearTimeout(timer1);
509 | clearTimeout(timer2);
510 |
511 | expect(modal3.state).toBe(PlainModal.STATE_INACTIVATED);
512 | expect(modal1.state).toBe(PlainModal.STATE_OPENED);
513 | expect(modal2.state).toBe(PlainModal.STATE_CLOSED);
514 | expect(shownProps.map(function(props) { return props.ins; }))
515 | .toEqual([modal3, modal1]);
516 |
517 | traceLog.length = 0;
518 | window.cntEscKey = 0;
519 | PlainModal.closeByEscKey = true;
520 |
521 | utils.intervalExec([
522 | // ====================================
523 | function() {
524 | escKeyDown(); // 1
525 | },
526 | // ====================================
527 | 100, function() {
528 | expect(modal3.state).toBe(PlainModal.STATE_OPENED);
529 | expect(modal1.state).toBe(PlainModal.STATE_CLOSED);
530 | expect(modal2.state).toBe(PlainModal.STATE_CLOSED);
531 | expect(shownProps.map(function(props) { return props.ins; }))
532 | .toEqual([modal3]);
533 | expect(window.cntEscKey).toBe(0); // window get no event
534 |
535 | escKeyDown(); // 3
536 | },
537 | // ====================================
538 | 100, function() {
539 | expect(modal3.state).toBe(PlainModal.STATE_CLOSED);
540 | expect(modal1.state).toBe(PlainModal.STATE_CLOSED);
541 | expect(modal2.state).toBe(PlainModal.STATE_CLOSED);
542 | expect(shownProps).toEqual([]);
543 | expect(window.cntEscKey).toBe(0); // window get no event
544 |
545 | escKeyDown(); // No modal
546 | },
547 | // ====================================
548 | 50, function() {
549 | expect(modal3.state).toBe(PlainModal.STATE_CLOSED);
550 | expect(modal1.state).toBe(PlainModal.STATE_CLOSED);
551 | expect(modal2.state).toBe(PlainModal.STATE_CLOSED);
552 | expect(shownProps).toEqual([]);
553 | expect(window.cntEscKey).toBe(1); // window get the event
554 |
555 | escKeyDown(); // No modal
556 | },
557 | // ====================================
558 | 50, function() {
559 | expect(modal3.state).toBe(PlainModal.STATE_CLOSED);
560 | expect(modal1.state).toBe(PlainModal.STATE_CLOSED);
561 | expect(modal2.state).toBe(PlainModal.STATE_CLOSED);
562 | expect(shownProps).toEqual([]);
563 | expect(window.cntEscKey).toBe(2); // window get the event
564 |
565 | expect(traceLog).toEqual([
566 | ' ', 'CLOSE', '_id:' + modal1._id,
567 |
568 | // 1 START: close
569 | '', '_id:' + modal1._id, 'state:STATE_OPENED',
570 | 'openCloseEffectProps:NONE',
571 |
572 | '', '_id:' + modal1._id, 'state:STATE_OPENED',
573 | 'force:false', 'sync:false',
574 |
575 | 'parentProps._id:' + modal3._id, 'parentProps.state:STATE_INACTIVATED',
576 | 'elmOverlay.duration:50ms',
577 | 'elmOverlay.CLASS_FORCE:false', 'elmOverlay.CLASS_HIDE:false',
578 | 'parentProps.state:STATE_ACTIVATING',
579 | 'state:STATE_CLOSING',
580 |
581 | '', '_id:' + modal1._id, 'state:STATE_CLOSING',
582 | 'plainDraggable:NONE',
583 | ' ',
584 |
585 | // PlainOverlay.hide()
586 | '_id:' + modal1._id, ' ',
587 |
588 | '_id:' + modal1._id, ' ',
589 | // DONE: close
590 |
591 | '', '_id:' + modal1._id, 'state:STATE_CLOSING',
592 | 'effectKey:plainOverlay',
593 | 'effectFinished.plainOverlay:true',
594 | 'effectFinished.option:false', 'closeEffect:NO',
595 |
596 | '', '_id:' + modal1._id, 'state:STATE_CLOSING',
597 | 'shownProps:' + modal3._id,
598 | 'state:STATE_CLOSED',
599 |
600 | 'parentProps._id:' + modal3._id, 'parentProps.state:STATE_ACTIVATING',
601 | 'parentProps.state:STATE_OPENED',
602 |
603 | '', '_id:' + modal3._id, 'state:STATE_OPENED',
604 | 'plainDraggable:NONE',
605 | ' ',
606 |
607 | 'parentProps(UNLINK):' + modal3._id,
608 |
609 | ' ',
610 |
611 | '_id:' + modal1._id, ' ',
612 |
613 | ' ', 'CLOSE', '_id:' + modal3._id,
614 |
615 | // 3 START: close
616 | '', '_id:' + modal3._id, 'state:STATE_OPENED',
617 | 'openCloseEffectProps:NONE',
618 |
619 | '', '_id:' + modal3._id, 'state:STATE_OPENED',
620 | 'force:false', 'sync:false',
621 | 'state:STATE_CLOSING',
622 |
623 | '', '_id:' + modal3._id, 'state:STATE_CLOSING',
624 | 'plainDraggable:NONE',
625 | ' ',
626 |
627 | // PlainOverlay.hide()
628 | '_id:' + modal3._id, ' ',
629 |
630 | '_id:' + modal3._id, ' ',
631 | // DONE: close
632 |
633 | '', '_id:' + modal3._id, 'state:STATE_CLOSING',
634 | 'effectKey:plainOverlay',
635 | 'effectFinished.plainOverlay:true',
636 | 'effectFinished.option:false', 'closeEffect:NO',
637 |
638 | '', '_id:' + modal3._id, 'state:STATE_CLOSING',
639 | 'shownProps:NONE',
640 | 'state:STATE_CLOSED',
641 | ' ',
642 |
643 | '_id:' + modal3._id, ' '
644 | ]);
645 | },
646 | // ====================================
647 | 0, done
648 | ]);
649 | }
650 | );
651 | });
652 |
653 | });
654 |
--------------------------------------------------------------------------------
/plain-modal.esm.js:
--------------------------------------------------------------------------------
1 | /* ================================================
2 | DON'T MANUALLY EDIT THIS FILE
3 | ================================================ */
4 |
5 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
6 |
7 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
8 |
9 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
10 |
11 | /*
12 | * PlainModal
13 | * https://anseki.github.io/plain-modal/
14 | *
15 | * Copyright (c) 2021 anseki
16 | * Licensed under the MIT license.
17 | */
18 | import CSSPrefix from 'cssprefix';
19 | import mClassList from 'm-class-list';
20 | import PlainOverlay from 'plain-overlay';
21 | /* Static ESM */ /* import CSS_TEXT from './default.scss' */ var CSS_TEXT = ".plainmodal .plainmodal-overlay{-webkit-tap-highlight-color:rgba(0,0,0,0);transform:translateZ(0);box-shadow:0 0 1px rgba(0,0,0,0)}.plainmodal.plainoverlay{background-color:transparent;cursor:auto}.plainmodal .plainmodal-content{z-index:9000}.plainmodal .plainmodal-overlay{width:100%;height:100%;position:absolute;left:0;top:0;background-color:rgba(136,136,136,.6);transition-property:opacity;transition-duration:200ms;transition-timing-function:linear;opacity:1}.plainmodal .plainmodal-overlay.plainmodal-overlay-hide{opacity:0}.plainmodal .plainmodal-overlay.plainmodal-overlay-force{transition-property:none}"; // [DRAG]
22 |
23 | import PlainDraggable from 'plain-draggable'; // [/DRAG]
24 |
25 | mClassList.ignoreNative = true;
26 |
27 | var APP_ID = 'plainmodal',
28 | STYLE_ELEMENT_ID = "".concat(APP_ID, "-style"),
29 | STYLE_CLASS = APP_ID,
30 | STYLE_CLASS_CONTENT = "".concat(APP_ID, "-content"),
31 | STYLE_CLASS_OVERLAY = "".concat(APP_ID, "-overlay"),
32 | STYLE_CLASS_OVERLAY_HIDE = "".concat(STYLE_CLASS_OVERLAY, "-hide"),
33 | STYLE_CLASS_OVERLAY_FORCE = "".concat(STYLE_CLASS_OVERLAY, "-force"),
34 | STATE_CLOSED = 0,
35 | STATE_OPENING = 1,
36 | STATE_OPENED = 2,
37 | STATE_CLOSING = 3,
38 | STATE_INACTIVATING = 4,
39 | STATE_INACTIVATED = 5,
40 | STATE_ACTIVATING = 6,
41 | DURATION = 200,
42 | // COPY from PlainOverlay
43 | IS_EDGE = '-ms-scroll-limit' in document.documentElement.style && '-ms-ime-align' in document.documentElement.style && !window.navigator.msPointerEnabled,
44 | IS_TRIDENT = !IS_EDGE && !!document.uniqueID,
45 | // Future Edge might support `document.uniqueID`.
46 | isObject = function () {
47 | var toString = {}.toString,
48 | fnToString = {}.hasOwnProperty.toString,
49 | objFnString = fnToString.call(Object);
50 | return function (obj) {
51 | var proto, constr;
52 | return obj && toString.call(obj) === '[object Object]' && (!(proto = Object.getPrototypeOf(obj)) || (constr = proto.hasOwnProperty('constructor') && proto.constructor) && typeof constr === 'function' && fnToString.call(constr) === objFnString);
53 | };
54 | }(),
55 |
56 | /**
57 | * An object that has properties of instance.
58 | * @typedef {Object} props
59 | * @property {Element} elmContent - Content element.
60 | * @property {Element} elmOverlay - Overlay element. (Not PlainOverlay)
61 | * @property {PlainOverlay} plainOverlay - PlainOverlay instance.
62 | * @property {PlainDraggable} plainDraggable - PlainDraggable instance.
63 | * @property {number} state - Current state.
64 | * @property {Object} options - Options.
65 | * @property {props} parentProps - props that is effected with current props.
66 | * @property {{plainOverlay: boolean, option: boolean}} effectFinished - The effect finished.
67 | */
68 |
69 | /** @type {Object.<_id: number, props>} */
70 | insProps = {},
71 |
72 | /**
73 | * A `props` list, it have a `state` other than `STATE_CLOSED`.
74 | * A `props` is pushed to the end of this array, `shownProps[shownProps.length - 1]` can be active.
75 | * @type {Array.}
76 | */
77 | shownProps = [];
78 |
79 | var closeByEscKey = true,
80 | closeByOverlay = true,
81 | insId = 0,
82 | openCloseEffectProps; // A `props` that is running the "open/close" effect now.
83 |
84 | function forceReflow(target) {
85 | // Trident and Blink bug (reflow like `offsetWidth` can't update)
86 | setTimeout(function () {
87 | var parent = target.parentNode,
88 | next = target.nextSibling; // It has to be removed first for Blink.
89 |
90 | parent.insertBefore(parent.removeChild(target), next);
91 | }, 0);
92 | }
93 | /**
94 | * @param {Element} element - A target element.
95 | * @returns {boolean} `true` if connected element.
96 | */
97 |
98 |
99 | function isElement(element) {
100 | return !!(element && element.nodeType === Node.ELEMENT_NODE && // element instanceof HTMLElement &&
101 | typeof element.getBoundingClientRect === 'function' && !(element.compareDocumentPosition(document) & Node.DOCUMENT_POSITION_DISCONNECTED));
102 | } // [DRAG]
103 |
104 |
105 | function switchDraggable(props) {
106 | if (props.plainDraggable) {
107 | var disabled = !(props.options.dragHandle && props.state === STATE_OPENED);
108 | props.plainDraggable.disabled = disabled;
109 |
110 | if (!disabled) {
111 | props.plainDraggable.position();
112 | }
113 | }
114 | } // [/DRAG]
115 |
116 |
117 | function finishOpening(props) {
118 | openCloseEffectProps = null;
119 | props.state = STATE_OPENED;
120 | switchDraggable(props); // [DRAG/]
121 |
122 | if (props.parentProps) {
123 | props.parentProps.state = STATE_INACTIVATED;
124 | }
125 |
126 | if (props.options.onOpen) {
127 | props.options.onOpen.call(props.ins);
128 | }
129 | }
130 |
131 | function finishClosing(props) {
132 | shownProps.pop();
133 | openCloseEffectProps = null;
134 | props.state = STATE_CLOSED;
135 |
136 | if (props.parentProps) {
137 | props.parentProps.state = STATE_OPENED;
138 | switchDraggable(props.parentProps); // [DRAG/]
139 |
140 | props.parentProps = null;
141 | }
142 |
143 | if (props.options.onClose) {
144 | props.options.onClose.call(props.ins);
145 | }
146 | }
147 | /**
148 | * @param {props} props - `props` of instance.
149 | * @param {string} effectKey - `plainOverlay' or 'option`
150 | * @returns {void}
151 | */
152 |
153 |
154 | function finishOpenEffect(props, effectKey) {
155 | if (props.state !== STATE_OPENING) {
156 | return;
157 | }
158 |
159 | props.effectFinished[effectKey] = true;
160 |
161 | if (props.effectFinished.plainOverlay && (!props.options.openEffect || props.effectFinished.option)) {
162 | finishOpening(props);
163 | }
164 | }
165 | /**
166 | * @param {props} props - `props` of instance.
167 | * @param {string} effectKey - `plainOverlay' or 'option`
168 | * @returns {void}
169 | */
170 |
171 |
172 | function finishCloseEffect(props, effectKey) {
173 | if (props.state !== STATE_CLOSING) {
174 | return;
175 | }
176 |
177 | props.effectFinished[effectKey] = true;
178 |
179 | if (props.effectFinished.plainOverlay && (!props.options.closeEffect || props.effectFinished.option)) {
180 | finishClosing(props);
181 | }
182 | }
183 | /**
184 | * Process after preparing data and adjusting style.
185 | * @param {props} props - `props` of instance.
186 | * @param {boolean} [force] - Skip effect.
187 | * @returns {void}
188 | */
189 |
190 |
191 | function execOpening(props, force) {
192 | if (props.parentProps) {
193 | // inactivate parentProps
194 |
195 | /*
196 | Cases:
197 | - STATE_OPENED or STATE_ACTIVATING, regardless of force
198 | - STATE_INACTIVATING and force
199 | */
200 | var parentProps = props.parentProps,
201 | elmOverlay = parentProps.elmOverlay;
202 |
203 | if (parentProps.state === STATE_OPENED) {
204 | elmOverlay.style[CSSPrefix.getName('transitionDuration')] = props.options.duration === DURATION ? '' : "".concat(props.options.duration, "ms");
205 | }
206 |
207 | var elmOverlayClassList = mClassList(elmOverlay);
208 | elmOverlayClassList.toggle(STYLE_CLASS_OVERLAY_FORCE, !!force);
209 | elmOverlayClassList.add(STYLE_CLASS_OVERLAY_HIDE); // Update `state` regardless of force, for switchDraggable.
210 |
211 | parentProps.state = STATE_INACTIVATING;
212 | parentProps.plainOverlay.blockingDisabled = true;
213 | switchDraggable(parentProps); // [DRAG/]
214 | }
215 |
216 | props.state = STATE_OPENING;
217 | props.plainOverlay.blockingDisabled = false;
218 | props.effectFinished.plainOverlay = props.effectFinished.option = false;
219 | props.plainOverlay.show(force);
220 |
221 | if (props.options.openEffect) {
222 | if (force) {
223 | props.options.openEffect.call(props.ins);
224 | finishOpenEffect(props, 'option');
225 | } else {
226 | props.options.openEffect.call(props.ins, props.openEffectDone);
227 | }
228 | }
229 | }
230 | /**
231 | * Process after preparing data and adjusting style.
232 | * @param {props} props - `props` of instance.
233 | * @param {boolean} [force] - Skip effect.
234 | * @param {boolean} [sync] - `force` with sync-mode. (Skip restoring active element)
235 | * @returns {void}
236 | */
237 |
238 |
239 | function execClosing(props, force, sync) {
240 | if (props.parentProps) {
241 | // activate parentProps
242 |
243 | /*
244 | Cases:
245 | - STATE_INACTIVATED or STATE_INACTIVATING, regardless of `force`
246 | - STATE_ACTIVATING and `force`
247 | */
248 | var parentProps = props.parentProps,
249 | elmOverlay = parentProps.elmOverlay;
250 |
251 | if (parentProps.state === STATE_INACTIVATED) {
252 | elmOverlay.style[CSSPrefix.getName('transitionDuration')] = props.options.duration === DURATION ? '' : "".concat(props.options.duration, "ms");
253 | }
254 |
255 | var elmOverlayClassList = mClassList(elmOverlay);
256 | elmOverlayClassList.toggle(STYLE_CLASS_OVERLAY_FORCE, !!force);
257 | elmOverlayClassList.remove(STYLE_CLASS_OVERLAY_HIDE); // same condition as props
258 |
259 | parentProps.state = STATE_ACTIVATING;
260 | parentProps.plainOverlay.blockingDisabled = false;
261 | }
262 |
263 | props.state = STATE_CLOSING;
264 | switchDraggable(props); // [DRAG/]
265 |
266 | props.effectFinished.plainOverlay = props.effectFinished.option = false;
267 | props.plainOverlay.hide(force, sync);
268 |
269 | if (props.options.closeEffect) {
270 | if (force) {
271 | props.options.closeEffect.call(props.ins);
272 | finishCloseEffect(props, 'option');
273 | } else {
274 | props.options.closeEffect.call(props.ins, props.closeEffectDone);
275 | }
276 | }
277 | }
278 | /**
279 | * Finish the "open/close" effect immediately with sync-mode.
280 | * @param {props} props - `props` of instance.
281 | * @returns {void}
282 | */
283 |
284 |
285 | function fixOpenClose(props) {
286 | if (props.state === STATE_OPENING) {
287 | execOpening(props, true);
288 | } else if (props.state === STATE_CLOSING) {
289 | execClosing(props, true, true);
290 | }
291 | }
292 | /**
293 | * @param {props} props - `props` of instance.
294 | * @param {boolean} [force] - Skip effect.
295 | * @returns {void}
296 | */
297 |
298 |
299 | function _open(props, force) {
300 | if (props.state !== STATE_CLOSED && props.state !== STATE_CLOSING && props.state !== STATE_OPENING || props.state === STATE_OPENING && !force || props.state !== STATE_OPENING && props.options.onBeforeOpen && props.options.onBeforeOpen.call(props.ins) === false) {
301 | return false;
302 | }
303 | /*
304 | Cases:
305 | - STATE_CLOSED or STATE_CLOSING, regardless of `force`
306 | - STATE_OPENING and `force`
307 | */
308 |
309 |
310 | if (props.state === STATE_CLOSED) {
311 | if (openCloseEffectProps) {
312 | fixOpenClose(openCloseEffectProps);
313 | }
314 |
315 | openCloseEffectProps = props;
316 |
317 | if (shownProps.length) {
318 | props.parentProps = shownProps[shownProps.length - 1];
319 | }
320 |
321 | shownProps.push(props);
322 | mClassList(props.elmOverlay).add(STYLE_CLASS_OVERLAY_FORCE).remove(STYLE_CLASS_OVERLAY_HIDE);
323 | }
324 |
325 | execOpening(props, force);
326 | return true;
327 | }
328 | /**
329 | * @param {props} props - `props` of instance.
330 | * @param {boolean} [force] - Skip effect.
331 | * @returns {void}
332 | */
333 |
334 |
335 | function _close(props, force) {
336 | if (props.state === STATE_CLOSED || props.state === STATE_CLOSING && !force || props.state !== STATE_CLOSING && props.options.onBeforeClose && props.options.onBeforeClose.call(props.ins) === false) {
337 | return false;
338 | }
339 | /*
340 | Cases:
341 | - Other than STATE_CLOSED and STATE_CLOSING, regardless of `force`
342 | - STATE_CLOSING and `force`
343 | */
344 |
345 |
346 | if (openCloseEffectProps && openCloseEffectProps !== props) {
347 | fixOpenClose(openCloseEffectProps);
348 | openCloseEffectProps = null;
349 | }
350 | /*
351 | Cases:
352 | - STATE_OPENED, STATE_OPENING or STATE_INACTIVATED, regardless of `force`
353 | - STATE_CLOSING and `force`
354 | */
355 |
356 |
357 | if (props.state === STATE_INACTIVATED) {
358 | // -> STATE_OPENED
359 | var topProps;
360 |
361 | while ((topProps = shownProps[shownProps.length - 1]) !== props) {
362 | execClosing(topProps, true, true);
363 | }
364 | }
365 | /*
366 | Cases:
367 | - STATE_OPENED or STATE_OPENING, regardless of `force`
368 | - STATE_CLOSING and `force`
369 | */
370 |
371 |
372 | if (props.state === STATE_OPENED) {
373 | openCloseEffectProps = props;
374 | }
375 |
376 | execClosing(props, force);
377 | return true;
378 | }
379 | /**
380 | * @param {props} props - `props` of instance.
381 | * @param {Object} newOptions - New options.
382 | * @returns {void}
383 | */
384 |
385 |
386 | function _setOptions(props, newOptions) {
387 | var options = props.options,
388 | plainOverlay = props.plainOverlay; // closeButton
389 |
390 | if (newOptions.hasOwnProperty('closeButton') && (newOptions.closeButton = isElement(newOptions.closeButton) ? newOptions.closeButton : newOptions.closeButton == null ? void 0 : false) !== false && newOptions.closeButton !== options.closeButton) {
391 | if (options.closeButton) {
392 | // Remove
393 | options.closeButton.removeEventListener('click', props.handleClose, false);
394 | }
395 |
396 | options.closeButton = newOptions.closeButton;
397 |
398 | if (options.closeButton) {
399 | // Add
400 | options.closeButton.addEventListener('click', props.handleClose, false);
401 | }
402 | } // duration
403 | // Check by PlainOverlay
404 |
405 |
406 | plainOverlay.duration = newOptions.duration;
407 | options.duration = plainOverlay.duration; // overlayBlur
408 | // Check by PlainOverlay
409 |
410 | plainOverlay.blur = newOptions.overlayBlur;
411 | options.overlayBlur = plainOverlay.blur; // [DRAG]
412 | // dragHandle
413 |
414 | if (newOptions.hasOwnProperty('dragHandle') && (newOptions.dragHandle = isElement(newOptions.dragHandle) ? newOptions.dragHandle : newOptions.dragHandle == null ? void 0 : false) !== false && newOptions.dragHandle !== options.dragHandle) {
415 | options.dragHandle = newOptions.dragHandle;
416 |
417 | if (options.dragHandle) {
418 | if (!props.plainDraggable) {
419 | props.plainDraggable = new PlainDraggable(props.elmContent);
420 | }
421 |
422 | props.plainDraggable.handle = options.dragHandle;
423 | }
424 |
425 | switchDraggable(props);
426 | } // [/DRAG]
427 | // effect functions and event listeners
428 |
429 |
430 | ['openEffect', 'closeEffect', 'onOpen', 'onClose', 'onBeforeOpen', 'onBeforeClose'].forEach(function (option) {
431 | if (typeof newOptions[option] === 'function') {
432 | options[option] = newOptions[option];
433 | } else if (newOptions.hasOwnProperty(option) && newOptions[option] == null) {
434 | options[option] = void 0;
435 | }
436 | });
437 | }
438 |
439 | var PlainModal = /*#__PURE__*/function () {
440 | /**
441 | * Create a `PlainModal` instance.
442 | * @param {Element} content - An element that is shown as the content of the modal window.
443 | * @param {Object} [options] - Options.
444 | */
445 | function PlainModal(content, options) {
446 | _classCallCheck(this, PlainModal);
447 |
448 | var props = {
449 | ins: this,
450 | options: {
451 | // Initial options (not default)
452 | closeButton: void 0,
453 | duration: DURATION,
454 | dragHandle: void 0,
455 | // [DRAG/]
456 | overlayBlur: false
457 | },
458 | state: STATE_CLOSED,
459 | effectFinished: {
460 | plainOverlay: false,
461 | option: false
462 | }
463 | };
464 | Object.defineProperty(this, '_id', {
465 | value: ++insId
466 | });
467 | props._id = this._id;
468 | insProps[this._id] = props;
469 |
470 | if (!content.nodeType || content.nodeType !== Node.ELEMENT_NODE || content.ownerDocument.defaultView !== window) {
471 | throw new Error('This `content` is not accepted.');
472 | }
473 |
474 | props.elmContent = content;
475 |
476 | if (!options) {
477 | options = {};
478 | } else if (!isObject(options)) {
479 | throw new Error('Invalid options.');
480 | } // Setup window
481 |
482 |
483 | if (!document.getElementById(STYLE_ELEMENT_ID)) {
484 | var head = document.getElementsByTagName('head')[0] || document.documentElement,
485 | sheet = head.insertBefore(document.createElement('style'), head.firstChild);
486 | sheet.type = 'text/css';
487 | sheet.id = STYLE_ELEMENT_ID;
488 | sheet.textContent = CSS_TEXT;
489 |
490 | if (IS_TRIDENT || IS_EDGE) {
491 | forceReflow(sheet);
492 | } // Trident bug
493 | // for closeByEscKey
494 |
495 |
496 | window.addEventListener('keydown', function (event) {
497 | var key, topProps;
498 |
499 | if (closeByEscKey && ((key = event.key.toLowerCase()) === 'escape' || key === 'esc') && (topProps = shownProps.length && shownProps[shownProps.length - 1]) && _close(topProps)) {
500 | event.preventDefault();
501 | event.stopImmediatePropagation(); // preventDefault stops other listeners, maybe.
502 |
503 | event.stopPropagation();
504 | }
505 | }, true);
506 | }
507 |
508 | mClassList(content).add(STYLE_CLASS_CONTENT); // Overlay
509 |
510 | props.plainOverlay = new PlainOverlay({
511 | face: content,
512 | onShow: function onShow() {
513 | finishOpenEffect(props, 'plainOverlay');
514 | },
515 | onHide: function onHide() {
516 | finishCloseEffect(props, 'plainOverlay');
517 | }
518 | }); // The `content` is now contained into PlainOverlay, and update `display`.
519 |
520 | if (window.getComputedStyle(content, '').display === 'none') {
521 | content.style.display = 'block';
522 | } // Trident can not get parent of SVG by parentElement.
523 |
524 |
525 | var elmPlainOverlayBody = content.parentNode; // elmOverlayBody of PlainOverlay
526 |
527 | mClassList(elmPlainOverlayBody.parentNode).add(STYLE_CLASS); // elmOverlay of PlainOverlay
528 | // elmOverlay (own overlay)
529 |
530 | var elmOverlay = props.elmOverlay = elmPlainOverlayBody.appendChild(document.createElement('div'));
531 | elmOverlay.className = STYLE_CLASS_OVERLAY; // for closeByOverlay
532 |
533 | elmOverlay.addEventListener('click', function (event) {
534 | if (event.target === elmOverlay && closeByOverlay) {
535 | _close(props);
536 | }
537 | }, true); // Prepare removable event listeners for each instance.
538 |
539 | props.handleClose = function () {
540 | _close(props);
541 | }; // Callback functions for additional effects, prepare these to allow to be used as listener.
542 |
543 |
544 | props.openEffectDone = function () {
545 | finishOpenEffect(props, 'option');
546 | };
547 |
548 | props.closeEffectDone = function () {
549 | finishCloseEffect(props, 'option');
550 | };
551 |
552 | props.effectDone = function () {
553 | if (props.state === STATE_OPENING) {
554 | finishOpenEffect(props, 'option');
555 | } else if (props.state === STATE_CLOSING) {
556 | finishCloseEffect(props, 'option');
557 | }
558 | };
559 |
560 | _setOptions(props, options);
561 | }
562 | /**
563 | * @param {Object} options - New options.
564 | * @returns {PlainModal} Current instance itself.
565 | */
566 |
567 |
568 | _createClass(PlainModal, [{
569 | key: "setOptions",
570 | value: function setOptions(options) {
571 | if (isObject(options)) {
572 | _setOptions(insProps[this._id], options);
573 | }
574 |
575 | return this;
576 | }
577 | /**
578 | * Open the modal window.
579 | * @param {boolean} [force] - Show it immediately without effect.
580 | * @param {Object} [options] - New options.
581 | * @returns {PlainModal} Current instance itself.
582 | */
583 |
584 | }, {
585 | key: "open",
586 | value: function open(force, options) {
587 | if (arguments.length < 2 && typeof force !== 'boolean') {
588 | options = force;
589 | force = false;
590 | }
591 |
592 | this.setOptions(options);
593 |
594 | _open(insProps[this._id], force);
595 |
596 | return this;
597 | }
598 | /**
599 | * Close the modal window.
600 | * @param {boolean} [force] - Close it immediately without effect.
601 | * @returns {PlainModal} Current instance itself.
602 | */
603 |
604 | }, {
605 | key: "close",
606 | value: function close(force) {
607 | _close(insProps[this._id], force);
608 |
609 | return this;
610 | }
611 | }, {
612 | key: "state",
613 | get: function get() {
614 | return insProps[this._id].state;
615 | }
616 | }, {
617 | key: "closeButton",
618 | get: function get() {
619 | return insProps[this._id].options.closeButton;
620 | },
621 | set: function set(value) {
622 | _setOptions(insProps[this._id], {
623 | closeButton: value
624 | });
625 | }
626 | }, {
627 | key: "duration",
628 | get: function get() {
629 | return insProps[this._id].options.duration;
630 | },
631 | set: function set(value) {
632 | _setOptions(insProps[this._id], {
633 | duration: value
634 | });
635 | }
636 | }, {
637 | key: "overlayBlur",
638 | get: function get() {
639 | return insProps[this._id].options.overlayBlur;
640 | },
641 | set: function set(value) {
642 | _setOptions(insProps[this._id], {
643 | overlayBlur: value
644 | });
645 | } // [DRAG]
646 |
647 | }, {
648 | key: "dragHandle",
649 | get: function get() {
650 | return insProps[this._id].options.dragHandle;
651 | },
652 | set: function set(value) {
653 | _setOptions(insProps[this._id], {
654 | dragHandle: value
655 | });
656 | } // [/DRAG]
657 |
658 | }, {
659 | key: "openEffect",
660 | get: function get() {
661 | return insProps[this._id].options.openEffect;
662 | },
663 | set: function set(value) {
664 | _setOptions(insProps[this._id], {
665 | openEffect: value
666 | });
667 | }
668 | }, {
669 | key: "closeEffect",
670 | get: function get() {
671 | return insProps[this._id].options.closeEffect;
672 | },
673 | set: function set(value) {
674 | _setOptions(insProps[this._id], {
675 | closeEffect: value
676 | });
677 | }
678 | }, {
679 | key: "effectDone",
680 | get: function get() {
681 | return insProps[this._id].effectDone;
682 | }
683 | }, {
684 | key: "onOpen",
685 | get: function get() {
686 | return insProps[this._id].options.onOpen;
687 | },
688 | set: function set(value) {
689 | _setOptions(insProps[this._id], {
690 | onOpen: value
691 | });
692 | }
693 | }, {
694 | key: "onClose",
695 | get: function get() {
696 | return insProps[this._id].options.onClose;
697 | },
698 | set: function set(value) {
699 | _setOptions(insProps[this._id], {
700 | onClose: value
701 | });
702 | }
703 | }, {
704 | key: "onBeforeOpen",
705 | get: function get() {
706 | return insProps[this._id].options.onBeforeOpen;
707 | },
708 | set: function set(value) {
709 | _setOptions(insProps[this._id], {
710 | onBeforeOpen: value
711 | });
712 | }
713 | }, {
714 | key: "onBeforeClose",
715 | get: function get() {
716 | return insProps[this._id].options.onBeforeClose;
717 | },
718 | set: function set(value) {
719 | _setOptions(insProps[this._id], {
720 | onBeforeClose: value
721 | });
722 | }
723 | }], [{
724 | key: "closeByEscKey",
725 | get: function get() {
726 | return closeByEscKey;
727 | },
728 | set: function set(value) {
729 | if (typeof value === 'boolean') {
730 | closeByEscKey = value;
731 | }
732 | }
733 | }, {
734 | key: "closeByOverlay",
735 | get: function get() {
736 | return closeByOverlay;
737 | },
738 | set: function set(value) {
739 | if (typeof value === 'boolean') {
740 | closeByOverlay = value;
741 | }
742 | }
743 | }, {
744 | key: "STATE_CLOSED",
745 | get: function get() {
746 | return STATE_CLOSED;
747 | }
748 | }, {
749 | key: "STATE_OPENING",
750 | get: function get() {
751 | return STATE_OPENING;
752 | }
753 | }, {
754 | key: "STATE_OPENED",
755 | get: function get() {
756 | return STATE_OPENED;
757 | }
758 | }, {
759 | key: "STATE_CLOSING",
760 | get: function get() {
761 | return STATE_CLOSING;
762 | }
763 | }, {
764 | key: "STATE_INACTIVATING",
765 | get: function get() {
766 | return STATE_INACTIVATING;
767 | }
768 | }, {
769 | key: "STATE_INACTIVATED",
770 | get: function get() {
771 | return STATE_INACTIVATED;
772 | }
773 | }, {
774 | key: "STATE_ACTIVATING",
775 | get: function get() {
776 | return STATE_ACTIVATING;
777 | }
778 | }]);
779 |
780 | return PlainModal;
781 | }();
782 | /* [DRAG/]
783 | PlainModal.limit = true;
784 | [DRAG/] */
785 |
786 |
787 | export default PlainModal;
--------------------------------------------------------------------------------