1/** 2 * tingle.js - A simple modal plugin written in pure JavaScript 3 * @version v0.16.0 4 * @link https://github.com/robinparisi/tingle#readme 5 * @license MIT 6 */ 7 8/* global define, module */ 9(function (root, factory) { 10 if (typeof define === "function" && define.amd) { 11 define(factory); 12 } else if (typeof exports === "object") { 13 module.exports = factory(); 14 } else { 15 root.tingle = factory(); 16 } 17})(this, function () { 18 /* ----------------------------------------------------------- */ 19 /* == modal */ 20 /* ----------------------------------------------------------- */ 21 22 var isBusy = false; 23 24 function Modal(options) { 25 var defaults = { 26 onClose: null, 27 onOpen: null, 28 beforeOpen: null, 29 beforeClose: null, 30 stickyFooter: false, 31 footer: false, 32 cssClass: [], 33 closeLabel: "Close", 34 closeMethods: ["overlay", "button", "escape"], 35 }; 36 37 // extends config 38 this.opts = extend({}, defaults, options); 39 40 // init modal 41 this.init(); 42 } 43 44 Modal.prototype.init = function () { 45 if (this.modal) { 46 return; 47 } 48 49 _build.call(this); 50 _bindEvents.call(this); 51 52 // insert modal in dom 53 document.body.appendChild(this.modal, document.body.firstChild); 54 55 if (this.opts.footer) { 56 this.addFooter(); 57 } 58 59 return this; 60 }; 61 62 Modal.prototype._busy = function (state) { 63 isBusy = state; 64 }; 65 66 Modal.prototype._isBusy = function () { 67 return isBusy; 68 }; 69 70 Modal.prototype.destroy = function () { 71 if (this.modal === null) { 72 return; 73 } 74 75 // restore scrolling 76 if (this.isOpen()) { 77 this.close(true); 78 } 79 80 // unbind all events 81 _unbindEvents.call(this); 82 83 // remove modal from dom 84 this.modal.parentNode.removeChild(this.modal); 85 86 this.modal = null; 87 }; 88 89 Modal.prototype.isOpen = function () { 90 return !!this.modal.classList.contains("tingle-modal--visible"); 91 }; 92 93 Modal.prototype.open = function () { 94 if (this._isBusy()) return; 95 this._busy(true); 96 97 var self = this; 98 99 // before open callback 100 if (typeof self.opts.beforeOpen === "function") { 101 self.opts.beforeOpen(); 102 } 103 104 if (this.modal.style.removeProperty) { 105 this.modal.style.removeProperty("display"); 106 } else { 107 this.modal.style.removeAttribute("display"); 108 } 109 110 // prevent text selection when opening multiple times 111 document.getSelection().removeAllRanges(); 112 113 // prevent double scroll 114 this._scrollPosition = window.pageYOffset; 115 document.body.classList.add("tingle-enabled"); 116 document.body.style.top = -this._scrollPosition + "px"; 117 // sticky footer 118 this.setStickyFooter(this.opts.stickyFooter); 119 120 // show modal 121 this.modal.classList.add("tingle-modal--visible"); 122 123 // onOpen callback 124 if (typeof self.opts.onOpen === "function") { 125 self.opts.onOpen.call(self); 126 } 127 128 self._busy(false); 129 130 // check if modal is bigger than screen height 131 this.checkOverflow(); 132 133 return this; 134 }; 135 136 Modal.prototype.close = function (force) { 137 if (this._isBusy()) return; 138 this._busy(true); 139 force = force || false; 140 141 // before close 142 if (typeof this.opts.beforeClose === "function") { 143 var close = this.opts.beforeClose.call(this); 144 if (!close) { 145 this._busy(false); 146 return; 147 } 148 } 149 150 document.body.classList.remove("tingle-enabled"); 151 document.body.style.top = null; 152 window.scrollTo({ 153 top: this._scrollPosition, 154 behavior: "instant", 155 }); 156 this.modal.classList.add("remove-loading"); 157 158 // using similar setup as onOpen 159 var self = this; 160 161 if (this.isOpen()) { 162 this.close(true); 163 } 164 165 // unbind all events 166 _unbindEvents.call(this); 167 168 // remove modal from dom 169 setTimeout(function () { 170 self.modal.parentNode.removeChild(self.modal); 171 self.modal = null; 172 }, 600); 173 174 // release modal 175 self._busy(false); 176 }; 177 178 Modal.prototype.setContent = function (content) { 179 // check type of content : String or Node 180 if (typeof content === "string") { 181 this.modalBoxContent.innerHTML = content; 182 } else { 183 this.modalBoxContent.innerHTML = ""; 184 this.modalBoxContent.appendChild(content); 185 } 186 187 if (this.isOpen()) { 188 // check if modal is bigger than screen height 189 this.checkOverflow(); 190 } 191 192 return this; 193 }; 194 195 Modal.prototype.getContent = function () { 196 return this.modalBoxContent; 197 }; 198 199 Modal.prototype.addFooter = function () { 200 // add footer to modal 201 _buildFooter.call(this); 202 203 return this; 204 }; 205 206 Modal.prototype.setFooterContent = function (content) { 207 // set footer content 208 this.modalBoxFooter.innerHTML = content; 209 210 return this; 211 }; 212 213 Modal.prototype.getFooterContent = function () { 214 return this.modalBoxFooter; 215 }; 216 217 Modal.prototype.setStickyFooter = function (isSticky) { 218 // if the modal is smaller than the viewport height, we don't need sticky 219 if (!this.isOverflow()) { 220 isSticky = false; 221 } 222 223 if (isSticky) { 224 if (this.modalBox.contains(this.modalBoxFooter)) { 225 this.modalBox.removeChild(this.modalBoxFooter); 226 this.modal.appendChild(this.modalBoxFooter); 227 this.modalBoxFooter.classList.add("tingle-modal-box__footer--sticky"); 228 _recalculateFooterPosition.call(this); 229 } 230 this.modalBoxContent.style["padding-bottom"] = 231 this.modalBoxFooter.clientHeight + 20 + "px"; 232 } else if (this.modalBoxFooter) { 233 if (!this.modalBox.contains(this.modalBoxFooter)) { 234 this.modal.removeChild(this.modalBoxFooter); 235 this.modalBox.appendChild(this.modalBoxFooter); 236 this.modalBoxFooter.style.width = "auto"; 237 this.modalBoxFooter.style.left = ""; 238 this.modalBoxContent.style["padding-bottom"] = ""; 239 this.modalBoxFooter.classList.remove( 240 "tingle-modal-box__footer--sticky" 241 ); 242 } 243 } 244 245 return this; 246 }; 247 248 Modal.prototype.addFooterBtn = function (label, cssClass, callback) { 249 var btn = document.createElement("button"); 250 251 // set label 252 btn.innerHTML = label; 253 254 // bind callback 255 btn.addEventListener("click", callback); 256 257 if (typeof cssClass === "string" && cssClass.length) { 258 // add classes to btn 259 cssClass.split(" ").forEach(function (item) { 260 btn.classList.add(item); 261 }); 262 } 263 264 this.modalBoxFooter.appendChild(btn); 265 266 return btn; 267 }; 268 269 Modal.prototype.resize = function () { 270 // eslint-disable-next-line no-console 271 console.warn("Resize is deprecated and will be removed in version 1.0"); 272 }; 273 274 Modal.prototype.isOverflow = function () {
275 var viewportHeight = window.innerHeight; 276 var modalHeight = this.modalBox.clientHeight; 277 278 return modalHeight >= viewportHeight; 279 }; 280 281 Modal.prototype.checkOverflow = function () { 282 // only if the modal is currently shown 283 if (this.modal.classList.contains("tingle-modal--visible")) { 284 if (this.isOverflow()) { 285 this.modal.classList.add("tingle-modal--overflow"); 286 } else { 287 this.modal.classList.remove("tingle-modal--overflow"); 288 } 289 290 if (!this.isOverflow() && this.opts.stickyFooter) { 291 this.setStickyFooter(false); 292 } else if (this.isOverflow() && this.opts.stickyFooter) { 293 _recalculateFooterPosition.call(this); 294 this.setStickyFooter(true); 295 } 296 } 297 }; 298 299 /* ----------------------------------------------------------- */ 300 /* == private methods */ 301 /* ----------------------------------------------------------- */ 302 303 function closeIcon() { 304 return '<svg width="13" height="13" viewBox="0 0 13 13" fill="none" class="transition"><use href="#icon-close"></use></svg>'; 305 } 306 307 function _scrollbarWidth() { 308 var scrollDiv = document.createElement("div"); 309 scrollDiv.style.width = "100px"; 310 scrollDiv.style.height = "100px"; 311 scrollDiv.style.overflow = "scroll"; 312 scrollDiv.style.position = "absolute"; 313 scrollDiv.style.top = "-9999px"; 314 document.body.appendChild(scrollDiv); 315 var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth; 316 document.body.removeChild(scrollDiv); 317 return scrollbarWidth; 318 } 319 320 function _recalculateFooterPosition() { 321 if (!this.modalBoxFooter) { 322 return; 323 } 324 this.modalBoxFooter.style.width = this.modalBox.clientWidth + "px"; 325 this.modalBoxFooter.style.left = this.modalBox.offsetLeft + "px"; 326 } 327 328 function _build() { 329 // wrapper 330 this.modal = document.createElement("div"); 331 this.modal.classList.add("tingle-modal"); 332 333 // remove cusor if no overlay close method 334 if ( 335 this.opts.closeMethods.length === 0 || 336 this.opts.closeMethods.indexOf("overlay") === -1 337 ) { 338 this.modal.classList.add("tingle-modal--noOverlayClose"); 339 } 340 341 this.modal.style.display = "none"; 342 343 // custom class 344 this.opts.cssClass.forEach(function (item) { 345 if (typeof item === "string") { 346 this.modal.classList.add(item); 347 } 348 }, this); 349 350 // close btn 351 if (this.opts.closeMethods.indexOf("button") !== -1) { 352 this.modalCloseBtn = document.createElement("button"); 353 this.modalCloseBtn.type = "button"; 354 this.modalCloseBtn.classList.add( 355 "tingle-modal__close", 356 "btn-reset", 357 "hover-svg-zoom", 358 "pointer", 359 "absolute", 360 "right-10", 361 "top-10", 362 "z-1", 363 "w-44", 364 "h-44", 365 "inline-flex", 366 "content-center" 367 ); 368 369 this.modalCloseBtnIcon = document.createElement("span"); 370 this.modalCloseBtnIcon.classList.add("tingle-modal__closeIcon"); 371 this.modalCloseBtnIcon.innerHTML = closeIcon(); 372 373 this.modalCloseBtnLabel = document.createElement("span"); 374 this.modalCloseBtnLabel.classList.add("tingle-modal__closeLabel"); 375 this.modalCloseBtnLabel.innerHTML = this.opts.closeLabel; 376 377 this.modalCloseBtn.appendChild(this.modalCloseBtnIcon); 378 this.modalCloseBtn.appendChild(this.modalCloseBtnLabel); 379 } 380 381 // modal 382 this.modalBox = document.createElement("div"); 383 this.modalBox.classList.add("tingle-modal-box"); 384 385 // modal box content 386 this.modalBoxContent = document.createElement("div"); 387 this.modalBoxContent.classList.add("tingle-modal-box__content"); 388 389 if (this.opts.closeMethods.indexOf("button") !== -1) { 390 this.modalBox.appendChild(this.modalCloseBtn); 391 } 392 393 this.modalBox.appendChild(this.modalBoxContent); 394 395 this.modal.appendChild(this.modalBox); 396 } 397 398 function _buildFooter() { 399 this.modalBoxFooter = document.createElement("div"); 400 this.modalBoxFooter.classList.add("tingle-modal-box__footer"); 401 this.modalBox.appendChild(this.modalBoxFooter); 402 } 403 404 function _bindEvents() { 405 this._events = { 406 clickCloseBtn: this.close.bind(this), 407 clickOverlay: _handleClickOutside.bind(this), 408 resize: this.checkOverflow.bind(this), 409 keyboardNav: _handleKeyboardNav.bind(this), 410 }; 411 412 if (this.opts.closeMethods.indexOf("button") !== -1) { 413 this.modalCloseBtn.addEventListener("click", this._events.clickCloseBtn); 414 } 415 416 this.modal.addEventListener("mousedown", this._events.clickOverlay); 417 window.addEventListener("resize", this._events.resize); 418 document.addEventListener("keydown", this._events.keyboardNav); 419 } 420 421 function _handleKeyboardNav(event) { 422 // escape key 423 if ( 424 this.opts.closeMethods.indexOf("escape") !== -1 && 425 event.which === 27 && 426 this.isOpen() 427 ) { 428 this.close(); 429 } 430 } 431 432 function _handleClickOutside(event) { 433 // on macOS, click on scrollbar (hidden mode) will trigger close event so we need to bypass this behavior b
433y detecting scrollbar mode 434 var scrollbarWidth = this.modal.offsetWidth - this.modal.clientWidth; 435 var clickedOnScrollbar = event.clientX >= this.modal.offsetWidth - 15; // 15px is macOS scrollbar default width 436 var isScrollable = this.modal.scrollHeight !== this.modal.offsetHeight; 437 if ( 438 navigator.platform === "MacIntel" && 439 scrollbarWidth === 0 && 440 clickedOnScrollbar && 441 isScrollable 442 ) { 443 return; 444 } 445 446 // if click is outside the modal 447 if ( 448 this.opts.closeMethods.indexOf("overlay") !== -1 && 449 !_findAncestor(event.target, "tingle-modal") && 450 event.clientX < this.modal.clientWidth 451 ) { 452 this.close(); 453 } 454 } 455 456 function _findAncestor(el, cls) { 457 while ((el = el.parentElement) && !el.classList.contains(cls)); 458 return el; 459 } 460 461 function _unbindEvents() { 462 if (this.opts.closeMethods.indexOf("button") !== -1) { 463 this.modalCloseBtn.removeEventListener( 464 "click", 465 this._events.clickCloseBtn 466 ); 467 } 468 this.modal.removeEventListener("mousedown", this._events.clickOverlay); 469 window.removeEventListener("resize", this._events.resize); 470 document.removeEventListener("keydown", this._events.keyboardNav); 471 } 472 473 /* ----------------------------------------------------------- */ 474 /* == helpers */ 475 /* ----------------------------------------------------------- */ 476 477 function extend() { 478 for (var i = 1; i < arguments.length; i++) { 479 for (var key in arguments[i]) { 480 if (arguments[i].hasOwnProperty(key)) { 481 arguments[0][key] = arguments[i][key]; 482 } 483 } 484 } 485 return arguments[0]; 486 } 487 488 /* ----------------------------------------------------------- */ 489 /* == return */ 490 /* ----------------------------------------------------------- */ 491 492 return { 493 modal: Modal, 494 }; 495});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.