├── .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 | 5 | 6 | 13 | 14 | 15 | 16 |
17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plain-modal", 3 | "version": "1.0.34", 4 | "description": "The simple library for customizable modal window.", 5 | "keywords": [ 6 | "modal", 7 | "popup", 8 | "dialog", 9 | "draggable", 10 | "drag", 11 | "ui", 12 | "simple", 13 | "customizable", 14 | "window" 15 | ], 16 | "main": "plain-modal.min.js", 17 | "homepage": "https://anseki.github.io/plain-modal/", 18 | "repository": { 19 | "type": "git", 20 | "url": "git://github.com/anseki/plain-modal.git" 21 | }, 22 | "moduleType": [], 23 | "ignore": [ 24 | "**/.*", 25 | "node_modules", 26 | "bower_components", 27 | "test", 28 | "tests" 29 | ], 30 | "license": "MIT", 31 | "authors": [ 32 | "anseki " 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 | 37 |
38 |
39 | elm2 40 | 41 |
42 |
43 | elm3 44 | 45 |
46 | 47 | 48 | 49 | 50 | 51 | 66 | 67 |

Test

68 | 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 | ``); 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 | 49 | useEsc 50 | 51 | 116 | 117 | 121 | 122 |

Test

123 | 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 | [![npm](https://img.shields.io/npm/v/plain-modal.svg)](https://www.npmjs.com/package/plain-modal) [![GitHub issues](https://img.shields.io/github/issues/anseki/plain-modal.svg)](https://github.com/anseki/plain-modal/issues) [![dependencies](https://img.shields.io/badge/dependencies-No%20dependency-brightgreen.svg)](package.json) [![license](https://img.shields.io/badge/license-MIT-blue.svg)](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 | 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 | 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; --------------------------------------------------------------------------------