PageSourceSearch

https://semie.cooking/assets/js/lib/tingle.js

js semie.cooking collected 2026-10-08 19:43:16 UTC 12,455 bytes, 437 lines download raw bytes

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

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.