Brak opisu
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

helpers.js 41KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368
  1. // Constants
  2. const TRANS_EVENTS = ['transitionend', 'webkitTransitionEnd', 'oTransitionEnd']
  3. const TRANS_PROPERTIES = ['transition', 'MozTransition', 'webkitTransition', 'WebkitTransition', 'OTransition']
  4. // Inline styles used for full navbar layout & sticky layout
  5. const INLINE_STYLES = `
  6. .layout-menu-fixed .layout-navbar-full .layout-menu,
  7. .layout-menu-fixed-offcanvas .layout-navbar-full .layout-menu {
  8. top: {navbarHeight}px !important;
  9. }
  10. .layout-page {
  11. padding-top: {navbarHeight}px !important;
  12. }
  13. .content-wrapper {
  14. padding-bottom: {footerHeight}px !important;
  15. }`
  16. // Guard
  17. function requiredParam(name) {
  18. throw new Error(`Parameter required${name ? `: \`${name}\`` : ''}`)
  19. }
  20. const Helpers = {
  21. // Root Element
  22. ROOT_EL: typeof window !== 'undefined' ? document.documentElement : null,
  23. prefix: getComputedStyle(document.documentElement).getPropertyValue('--prefix').trim(),
  24. // Large screens breakpoint
  25. LAYOUT_BREAKPOINT: 1200,
  26. // Resize delay in milliseconds
  27. RESIZE_DELAY: 200,
  28. menuPsScroll: null,
  29. mainMenu: null,
  30. // Internal variables
  31. _curStyle: null,
  32. _styleEl: null,
  33. _resizeTimeout: null,
  34. _resizeCallback: null,
  35. _transitionCallback: null,
  36. _transitionCallbackTimeout: null,
  37. _listeners: [],
  38. _initialized: false,
  39. _autoUpdate: false,
  40. // _lastWindowHeight: 0,
  41. // *******************************************************************************
  42. // * Utilities
  43. // ---
  44. // Set cookie with expiration
  45. _setCookie(name, value, daysToExpire = 365, path = '/', domain = '') {
  46. const cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`
  47. let expires = ''
  48. if (daysToExpire) {
  49. const expirationDate = new Date()
  50. expirationDate.setTime(expirationDate.getTime() + daysToExpire * 24 * 60 * 60 * 1000)
  51. expires = `; expires=${expirationDate.toUTCString()}`
  52. }
  53. const pathString = `; path=${path}`
  54. const domainString = domain ? `; domain=${domain}` : ''
  55. document.cookie = `${cookie}${expires}${pathString}${domainString}`
  56. },
  57. // Get cookie by name
  58. _getCookie(name) {
  59. const cookies = document.cookie.split('; ')
  60. for (let i = 0; i < cookies.length; i++) {
  61. const [cookieName, cookieValue] = cookies[i].split('=')
  62. if (decodeURIComponent(cookieName) === name) {
  63. return decodeURIComponent(cookieValue)
  64. }
  65. }
  66. return null
  67. },
  68. // ---
  69. // Scroll To Active Menu Item
  70. _scrollToActive(animate = false, duration = 500) {
  71. const layoutMenu = this.getLayoutMenu()
  72. if (!layoutMenu) return
  73. let activeEl = layoutMenu.querySelector('li.menu-item.active:not(.open)')
  74. if (activeEl) {
  75. // t = current time
  76. // b = start value
  77. // c = change in value
  78. // d = duration
  79. const easeInOutQuad = (t, b, c, d) => {
  80. t /= d / 2
  81. if (t < 1) return (c / 2) * t * t + b
  82. t -= 1
  83. return (-c / 2) * (t * (t - 2) - 1) + b
  84. }
  85. const element = this.getLayoutMenu().querySelector('.menu-inner')
  86. if (typeof activeEl === 'string') {
  87. activeEl = document.querySelector(activeEl)
  88. }
  89. if (typeof activeEl !== 'number') {
  90. activeEl = activeEl.getBoundingClientRect().top + element.scrollTop
  91. }
  92. // If active element's top position is less than 2/3 (66%) of menu height than do not scroll
  93. if (activeEl < parseInt((element.clientHeight * 2) / 3, 10)) return
  94. const start = element.scrollTop
  95. const change = activeEl - start - parseInt(element.clientHeight / 2, 10)
  96. const startDate = +new Date()
  97. if (animate === true) {
  98. const animateScroll = () => {
  99. const currentDate = +new Date()
  100. const currentTime = currentDate - startDate
  101. const val = easeInOutQuad(currentTime, start, change, duration)
  102. element.scrollTop = val
  103. if (currentTime < duration) {
  104. requestAnimationFrame(animateScroll)
  105. } else {
  106. element.scrollTop = change
  107. }
  108. }
  109. animateScroll()
  110. } else {
  111. element.scrollTop = change
  112. }
  113. }
  114. },
  115. // ---
  116. // Swipe In Gesture
  117. _swipeIn(targetEl, callback) {
  118. const { Hammer } = window
  119. if (typeof Hammer !== 'undefined' && typeof targetEl === 'string') {
  120. // Swipe menu gesture
  121. const swipeInElement = document.querySelector(targetEl)
  122. if (swipeInElement) {
  123. const hammerInstance = new Hammer(swipeInElement)
  124. hammerInstance.on('panright', callback)
  125. }
  126. }
  127. },
  128. // ---
  129. // Swipe Out Gesture
  130. _swipeOut(targetEl, callback) {
  131. const { Hammer } = window
  132. if (typeof Hammer !== 'undefined' && typeof targetEl === 'string') {
  133. setTimeout(() => {
  134. // Swipe menu gesture
  135. const swipeOutElement = document.querySelector(targetEl)
  136. if (swipeOutElement) {
  137. const hammerInstance = new Hammer(swipeOutElement)
  138. hammerInstance.get('pan').set({ direction: Hammer.DIRECTION_ALL, threshold: 250 })
  139. hammerInstance.on('panleft', callback)
  140. }
  141. }, 500)
  142. }
  143. },
  144. // ---
  145. // Add classes
  146. _addClass(cls, el = this.ROOT_EL) {
  147. if (el && el.length !== undefined) {
  148. // Add classes to multiple elements
  149. el.forEach(e => {
  150. if (e) {
  151. cls.split(' ').forEach(c => e.classList.add(c))
  152. }
  153. })
  154. } else if (el) {
  155. // Add classes to single element
  156. cls.split(' ').forEach(c => el.classList.add(c))
  157. }
  158. },
  159. // ---
  160. // Remove classes
  161. _removeClass(cls, el = this.ROOT_EL) {
  162. if (el && el.length !== undefined) {
  163. // Remove classes to multiple elements
  164. el.forEach(e => {
  165. if (e) {
  166. cls.split(' ').forEach(c => e.classList.remove(c))
  167. }
  168. })
  169. } else if (el) {
  170. // Remove classes to single element
  171. cls.split(' ').forEach(c => el.classList.remove(c))
  172. }
  173. },
  174. // Toggle classes
  175. _toggleClass(el = this.ROOT_EL, cls1, cls2) {
  176. if (el.classList.contains(cls1)) {
  177. el.classList.replace(cls1, cls2)
  178. } else {
  179. el.classList.replace(cls2, cls1)
  180. }
  181. },
  182. // ---
  183. // Has class
  184. _hasClass(cls, el = this.ROOT_EL) {
  185. let result = false
  186. cls.split(' ').forEach(c => {
  187. if (el.classList.contains(c)) result = true
  188. })
  189. return result
  190. },
  191. _findParent(el, cls) {
  192. if ((el && el.tagName.toUpperCase() === 'BODY') || el.tagName.toUpperCase() === 'HTML') return null
  193. el = el.parentNode
  194. while (el && el.tagName.toUpperCase() !== 'BODY' && !el.classList.contains(cls)) {
  195. el = el.parentNode
  196. }
  197. el = el && el.tagName.toUpperCase() !== 'BODY' ? el : null
  198. return el
  199. },
  200. // ---
  201. // Trigger window event
  202. _triggerWindowEvent(name) {
  203. if (typeof window === 'undefined') return
  204. if (document.createEvent) {
  205. let event
  206. if (typeof Event === 'function') {
  207. event = new Event(name)
  208. } else {
  209. event = document.createEvent('Event')
  210. event.initEvent(name, false, true)
  211. }
  212. window.dispatchEvent(event)
  213. } else {
  214. window.fireEvent(`on${name}`, document.createEventObject())
  215. }
  216. },
  217. // ---
  218. // Trigger event
  219. _triggerEvent(name) {
  220. this._triggerWindowEvent(`layout${name}`)
  221. this._listeners.filter(listener => listener.event === name).forEach(listener => listener.callback.call(null))
  222. },
  223. // ---
  224. // Update style
  225. _updateInlineStyle(navbarHeight = 0, footerHeight = 0) {
  226. if (!this._styleEl) {
  227. this._styleEl = document.createElement('style')
  228. this._styleEl.type = 'text/css'
  229. document.head.appendChild(this._styleEl)
  230. }
  231. const newStyle = INLINE_STYLES.replace(/\{navbarHeight\}/gi, navbarHeight).replace(
  232. /\{footerHeight\}/gi,
  233. footerHeight
  234. )
  235. if (this._curStyle !== newStyle) {
  236. this._curStyle = newStyle
  237. this._styleEl.textContent = newStyle
  238. }
  239. },
  240. // ---
  241. // Remove style
  242. _removeInlineStyle() {
  243. if (this._styleEl) document.head.removeChild(this._styleEl)
  244. this._styleEl = null
  245. this._curStyle = null
  246. },
  247. // ---
  248. // Redraw layout menu (Safari bugfix)
  249. _redrawLayoutMenu() {
  250. const layoutMenu = this.getLayoutMenu()
  251. if (layoutMenu && layoutMenu.querySelector('.menu')) {
  252. const inner = layoutMenu.querySelector('.menu-inner')
  253. const { scrollTop } = inner
  254. const pageScrollTop = document.documentElement.scrollTop
  255. layoutMenu.style.display = 'none'
  256. // layoutMenu.offsetHeight
  257. layoutMenu.style.display = ''
  258. inner.scrollTop = scrollTop
  259. document.documentElement.scrollTop = pageScrollTop
  260. return true
  261. }
  262. return false
  263. },
  264. // ---
  265. // Check for transition support
  266. _supportsTransitionEnd() {
  267. if (window.QUnit) return false
  268. const el = document.body || document.documentElement
  269. if (!el) return false
  270. let result = false
  271. TRANS_PROPERTIES.forEach(evnt => {
  272. if (typeof el.style[evnt] !== 'undefined') result = true
  273. })
  274. return result
  275. },
  276. // ---
  277. // Calculate current navbar height
  278. _getNavbarHeight() {
  279. const layoutNavbar = this.getLayoutNavbar()
  280. if (!layoutNavbar) return 0
  281. if (!this.isSmallScreen()) return layoutNavbar.getBoundingClientRect().height
  282. // Needs some logic to get navbar height on small screens
  283. const clonedEl = layoutNavbar.cloneNode(true)
  284. clonedEl.id = null
  285. clonedEl.style.visibility = 'hidden'
  286. clonedEl.style.position = 'absolute'
  287. Array.prototype.slice.call(clonedEl.querySelectorAll('.collapse.show')).forEach(el => this._removeClass('show', el))
  288. layoutNavbar.parentNode.insertBefore(clonedEl, layoutNavbar)
  289. const navbarHeight = clonedEl.getBoundingClientRect().height
  290. clonedEl.parentNode.removeChild(clonedEl)
  291. return navbarHeight
  292. },
  293. // ---
  294. // Get current footer height
  295. _getFooterHeight() {
  296. const layoutFooter = this.getLayoutFooter()
  297. if (!layoutFooter) return 0
  298. return layoutFooter.getBoundingClientRect().height
  299. },
  300. // ---
  301. // Get animation duration of element
  302. _getAnimationDuration(el) {
  303. const duration = window.getComputedStyle(el).transitionDuration
  304. return parseFloat(duration) * (duration.indexOf('ms') !== -1 ? 1 : 1000)
  305. },
  306. // ---
  307. // Set menu hover state
  308. _setMenuHoverState(hovered) {
  309. this[hovered ? '_addClass' : '_removeClass']('layout-menu-hover')
  310. },
  311. // ---
  312. // Toggle collapsed
  313. _setCollapsed(collapsed) {
  314. if (this.isSmallScreen()) {
  315. if (collapsed) {
  316. this._removeClass('layout-menu-expanded')
  317. } else {
  318. setTimeout(
  319. () => {
  320. this._addClass('layout-menu-expanded')
  321. },
  322. this._redrawLayoutMenu() ? 5 : 0
  323. )
  324. }
  325. } else {
  326. this[collapsed ? '_addClass' : '_removeClass']('layout-menu-collapsed')
  327. }
  328. },
  329. // ---
  330. // Add layout sidenav toggle animationEnd event
  331. _bindLayoutAnimationEndEvent(modifier, cb) {
  332. const menu = this.getMenu()
  333. const duration = menu ? this._getAnimationDuration(menu) + 50 : 0
  334. if (!duration) {
  335. modifier.call(this)
  336. cb.call(this)
  337. return
  338. }
  339. this._transitionCallback = e => {
  340. if (e.target !== menu) return
  341. this._unbindLayoutAnimationEndEvent()
  342. cb.call(this)
  343. }
  344. TRANS_EVENTS.forEach(e => {
  345. menu.addEventListener(e, this._transitionCallback, false)
  346. })
  347. modifier.call(this)
  348. this._transitionCallbackTimeout = setTimeout(() => {
  349. this._transitionCallback.call(this, { target: menu })
  350. }, duration)
  351. },
  352. // ---
  353. // Remove layout sidenav toggle animationEnd event
  354. _unbindLayoutAnimationEndEvent() {
  355. const menu = this.getMenu()
  356. if (this._transitionCallbackTimeout) {
  357. clearTimeout(this._transitionCallbackTimeout)
  358. this._transitionCallbackTimeout = null
  359. }
  360. if (menu && this._transitionCallback) {
  361. TRANS_EVENTS.forEach(e => {
  362. menu.removeEventListener(e, this._transitionCallback, false)
  363. })
  364. }
  365. if (this._transitionCallback) {
  366. this._transitionCallback = null
  367. }
  368. },
  369. // ---
  370. // Bind delayed window resize event
  371. _bindWindowResizeEvent() {
  372. this._unbindWindowResizeEvent()
  373. const cb = () => {
  374. if (this._resizeTimeout) {
  375. clearTimeout(this._resizeTimeout)
  376. this._resizeTimeout = null
  377. }
  378. this._triggerEvent('resize')
  379. }
  380. this._resizeCallback = () => {
  381. if (this._resizeTimeout) clearTimeout(this._resizeTimeout)
  382. this._resizeTimeout = setTimeout(cb, this.RESIZE_DELAY)
  383. }
  384. window.addEventListener('resize', this._resizeCallback, false)
  385. },
  386. // ---
  387. // Unbind delayed window resize event
  388. _unbindWindowResizeEvent() {
  389. if (this._resizeTimeout) {
  390. clearTimeout(this._resizeTimeout)
  391. this._resizeTimeout = null
  392. }
  393. if (this._resizeCallback) {
  394. window.removeEventListener('resize', this._resizeCallback, false)
  395. this._resizeCallback = null
  396. }
  397. },
  398. _bindMenuMouseEvents() {
  399. if (this._menuMouseEnter && this._menuMouseLeave && this._windowTouchStart) return
  400. const layoutMenu = this.getLayoutMenu()
  401. if (!layoutMenu) return this._unbindMenuMouseEvents()
  402. if (!this._menuMouseEnter) {
  403. this._menuMouseEnter = () => {
  404. if (
  405. this.isSmallScreen() ||
  406. !this._hasClass('layout-menu-collapsed') ||
  407. this.isOffcanvas() ||
  408. this._hasClass('layout-transitioning')
  409. ) {
  410. return this._setMenuHoverState(false)
  411. }
  412. return this._setMenuHoverState(true)
  413. }
  414. layoutMenu.addEventListener('mouseenter', this._menuMouseEnter, false)
  415. layoutMenu.addEventListener('touchstart', this._menuMouseEnter, false)
  416. }
  417. if (!this._menuMouseLeave) {
  418. this._menuMouseLeave = () => {
  419. this._setMenuHoverState(false)
  420. }
  421. layoutMenu.addEventListener('mouseleave', this._menuMouseLeave, false)
  422. }
  423. if (!this._windowTouchStart) {
  424. this._windowTouchStart = e => {
  425. if (!e || !e.target || !this._findParent(e.target, '.layout-menu')) {
  426. this._setMenuHoverState(false)
  427. }
  428. }
  429. window.addEventListener('touchstart', this._windowTouchStart, true)
  430. }
  431. },
  432. _unbindMenuMouseEvents() {
  433. if (!this._menuMouseEnter && !this._menuMouseLeave && !this._windowTouchStart) return
  434. const layoutMenu = this.getLayoutMenu()
  435. if (this._menuMouseEnter) {
  436. if (layoutMenu) {
  437. layoutMenu.removeEventListener('mouseenter', this._menuMouseEnter, false)
  438. layoutMenu.removeEventListener('touchstart', this._menuMouseEnter, false)
  439. }
  440. this._menuMouseEnter = null
  441. }
  442. if (this._menuMouseLeave) {
  443. if (layoutMenu) {
  444. layoutMenu.removeEventListener('mouseleave', this._menuMouseLeave, false)
  445. }
  446. this._menuMouseLeave = null
  447. }
  448. if (this._windowTouchStart) {
  449. if (layoutMenu) {
  450. window.addEventListener('touchstart', this._windowTouchStart, true)
  451. }
  452. this._windowTouchStart = null
  453. }
  454. this._setMenuHoverState(false)
  455. },
  456. // *******************************************************************************
  457. // * Methods
  458. scrollToActive(animate = false) {
  459. this._scrollToActive(animate)
  460. },
  461. swipeIn(el, callback) {
  462. this._swipeIn(el, callback)
  463. },
  464. swipeOut(el, callback) {
  465. this._swipeOut(el, callback)
  466. },
  467. // ---
  468. // Collapse / expand layout
  469. setCollapsed(collapsed = requiredParam('collapsed'), animate = true) {
  470. const layoutMenu = this.getLayoutMenu()
  471. if (!layoutMenu) return
  472. this._unbindLayoutAnimationEndEvent()
  473. if (animate && this._supportsTransitionEnd()) {
  474. this._addClass('layout-transitioning')
  475. if (collapsed) this._setMenuHoverState(false)
  476. this._bindLayoutAnimationEndEvent(
  477. () => {
  478. // Collapse / Expand
  479. this._setCollapsed(collapsed)
  480. },
  481. () => {
  482. this._removeClass('layout-transitioning')
  483. this._triggerWindowEvent('resize')
  484. this._triggerEvent('toggle')
  485. this._setMenuHoverState(false)
  486. }
  487. )
  488. } else {
  489. this._addClass('layout-no-transition')
  490. if (collapsed) this._setMenuHoverState(false)
  491. // Collapse / Expand
  492. this._setCollapsed(collapsed)
  493. setTimeout(() => {
  494. this._removeClass('layout-no-transition')
  495. this._triggerWindowEvent('resize')
  496. this._triggerEvent('toggle')
  497. this._setMenuHoverState(false)
  498. }, 1)
  499. }
  500. },
  501. // ---
  502. // Toggle layout
  503. toggleCollapsed(animate = true) {
  504. this.setCollapsed(!this.isCollapsed(), animate)
  505. },
  506. // ---
  507. // Set layout positioning
  508. setPosition(fixed = requiredParam('fixed'), offcanvas = requiredParam('offcanvas')) {
  509. this._removeClass('layout-menu-offcanvas layout-menu-fixed layout-menu-fixed-offcanvas')
  510. if (!fixed && offcanvas) {
  511. this._addClass('layout-menu-offcanvas')
  512. } else if (fixed && !offcanvas) {
  513. this._addClass('layout-menu-fixed')
  514. this._redrawLayoutMenu()
  515. } else if (fixed && offcanvas) {
  516. this._addClass('layout-menu-fixed-offcanvas')
  517. this._redrawLayoutMenu()
  518. }
  519. this.update()
  520. },
  521. // Update light/dark image based on current style
  522. switchImage(style) {
  523. // Handle 'system' style by checking user's preferred color scheme
  524. if (style === 'system') {
  525. style = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
  526. }
  527. // Get all images that need to be switched
  528. const switchImagesList = Array.from(document.querySelectorAll(`[data-app-${style}-img]`))
  529. // Loop through the images and update their `src` attribute
  530. switchImagesList.forEach(imageEl => {
  531. const setImage = imageEl.getAttribute(`data-app-${style}-img`)
  532. if (setImage) {
  533. const imagePath = `${assetsPath}img/${setImage}` // Using window.assetsPath for relative path
  534. // Preload the image to prevent flickering
  535. const img = new Image()
  536. img.src = imagePath
  537. // Once preloaded, set the image and make it visible
  538. img.onload = () => {
  539. imageEl.src = img.src
  540. imageEl.style.visibility = 'visible' // Make the image visible
  541. }
  542. // Hide the image during the switch to prevent flickering
  543. imageEl.style.visibility = 'hidden'
  544. }
  545. })
  546. },
  547. // *******************************************************************************
  548. // * Getters
  549. getLayoutMenu() {
  550. return document.querySelector('.layout-menu')
  551. },
  552. getMenu() {
  553. const layoutMenu = this.getLayoutMenu()
  554. if (!layoutMenu) return null
  555. return !this._hasClass('menu', layoutMenu) ? layoutMenu.querySelector('.menu') : layoutMenu
  556. },
  557. getLayoutNavbar() {
  558. return document.querySelector('.layout-navbar')
  559. },
  560. getLayoutFooter() {
  561. return document.querySelector('.content-footer')
  562. },
  563. getLayoutContainer() {
  564. return document.querySelector('.layout-page')
  565. },
  566. // *******************************************************************************
  567. // * Setters
  568. setNavbarFixed(fixed = requiredParam('fixed')) {
  569. this[fixed ? '_addClass' : '_removeClass']('layout-navbar-fixed')
  570. this.update()
  571. },
  572. setNavbar(type) {
  573. if (type === 'sticky') {
  574. this._addClass('layout-navbar-fixed')
  575. this._removeClass('layout-navbar-hidden')
  576. } else if (type === 'hidden') {
  577. this._addClass('layout-navbar-hidden')
  578. this._removeClass('layout-navbar-fixed')
  579. } else {
  580. this._removeClass('layout-navbar-hidden')
  581. this._removeClass('layout-navbar-fixed')
  582. }
  583. this.update()
  584. },
  585. setFooterFixed(fixed = requiredParam('fixed')) {
  586. this[fixed ? '_addClass' : '_removeClass']('layout-footer-fixed')
  587. this.update()
  588. },
  589. // Add to Helpers object in helpers.js
  590. setColor(color, defaultColor) {
  591. if (!color) return
  592. // Get current primary color from styles
  593. const currentStyle = getComputedStyle(document.documentElement).getPropertyValue('--bs-primary').trim()
  594. // Only proceed if the color is different from current or default color is true
  595. if (color === currentStyle && !defaultColor) return
  596. const r = parseInt(color.slice(1, 3), 16)
  597. const g = parseInt(color.slice(3, 5), 16)
  598. const b = parseInt(color.slice(5, 7), 16)
  599. let styleSheet = document.getElementById('custom-css')
  600. if (!styleSheet) {
  601. styleSheet = document.createElement('style')
  602. styleSheet.id = 'custom-css'
  603. document.head.appendChild(styleSheet)
  604. }
  605. function calculateRatio(percentage) {
  606. const numericValue = parseFloat(percentage)
  607. const result = (100 - numericValue) / 100
  608. return result
  609. }
  610. const yiq = (r * 299 + g * 587 + b * 114) / 1000
  611. const ratio = getComputedStyle(document.documentElement).getPropertyValue('--bs-min-contrast-ratio').trim() * 100
  612. const subtleRatio = getComputedStyle(document.documentElement)
  613. .getPropertyValue('--bs-bg-label-tint-amount')
  614. .trim('%')
  615. const borderSubtleRatio = getComputedStyle(document.documentElement)
  616. .getPropertyValue('--bs-border-subtle-amount')
  617. .trim()
  618. // Calculate contrast color based on YIQ and ratio
  619. const contrastColor = yiq >= ratio ? '#000' : '#fff' // window.config.colors.black : window.config.colors.white
  620. // Set CSS custom properties
  621. styleSheet.innerHTML = `:root, [data-bs-theme=light], [data-bs-theme=dark] {
  622. --bs-primary: ${color};
  623. --bs-primary-rgb: ${r}, ${g}, ${b};
  624. --bs-primary-bg-subtle: color-mix(in sRGB, ${window.config.colors.cardColor} ${subtleRatio}, ${color});
  625. --bs-primary-border-subtle: rgba(${r}, ${g}, ${b}, ${calculateRatio(borderSubtleRatio)});
  626. --bs-primary-contrast: ${contrastColor}
  627. }`
  628. // Store in cookie for persistence (handled by template-customizer now)
  629. const layoutTemplate = document.documentElement.getAttribute('data-template') || ''
  630. const isAdmin = !layoutTemplate.includes('front')
  631. const cookieName = isAdmin ? 'admin-primaryColor' : 'front-primaryColor'
  632. // Only set cookie here if not called from template-customizer
  633. if (typeof this._setCookie === 'function' && defaultColor) {
  634. this._setCookie(cookieName, color, 365)
  635. }
  636. },
  637. applySkin(skin) {
  638. if (!skin) return
  639. let skinName = skin
  640. // If template customizer is available, process skin name/id
  641. if (window.TemplateCustomizer && window.TemplateCustomizer.SKINS) {
  642. const availableSkins = window.TemplateCustomizer.SKINS
  643. // If skin is numeric, find the corresponding skin object
  644. if (!isNaN(parseInt(skin))) {
  645. const skinId = parseInt(skin)
  646. const skinObj = availableSkins.find(s => s.id === skinId)
  647. if (skinObj) {
  648. skinName = skinObj.name
  649. }
  650. }
  651. // If not numeric, check if it's a valid skin name
  652. else if (typeof skin === 'string') {
  653. const skinObj = availableSkins.find(s => s.name === skin)
  654. if (!skinObj) {
  655. // If not a valid skin name, try to use default
  656. skinName = 'default'
  657. }
  658. }
  659. }
  660. // Set data attribute for easier targeting in CSS - this is all we need now
  661. document.documentElement.setAttribute('data-skin', skinName || 'default')
  662. // Only set cookie for admin layouts
  663. const layoutName = document.documentElement.getAttribute('data-template') || ''
  664. const isAdmin = !layoutName.includes('front')
  665. if (isAdmin) {
  666. // Set cookie for 1 year
  667. if (typeof this._setCookie === 'function') {
  668. this._setCookie('customize_skin', skinName, 365)
  669. } else {
  670. // Fallback cookie setting
  671. const expirationDate = new Date()
  672. expirationDate.setTime(expirationDate.getTime() + 365 * 24 * 60 * 60 * 1000)
  673. document.cookie = `customize_skin=${skinName}; expires=${expirationDate.toUTCString()}; path=/`
  674. }
  675. }
  676. },
  677. setContentLayout(contentLayout = requiredParam('contentLayout')) {
  678. setTimeout(() => {
  679. const contentArea = document.querySelector('.content-wrapper > div') // For content area
  680. let navbarArea
  681. if (document.querySelector('.layout-wrapper.layout-navbar-full')) {
  682. navbarArea = document.querySelector('.layout-navbar-full .layout-navbar > div') // For horizontal navbar area
  683. } else {
  684. navbarArea = document.querySelector('.layout-content-navbar .layout-navbar') // For vertical navbar area
  685. }
  686. const footerArea = document.querySelector('.content-footer > div') // For footer area
  687. const containerFluid = [].slice.call(document.querySelectorAll('.container-fluid')) // To get container-fluid
  688. const containerXxl = [].slice.call(document.querySelectorAll('.container-xxl')) // To get container-xxl
  689. let horizontalMenu = false // For horizontal menu
  690. let horizontalMenuArea // For horizontal menu area
  691. // Condition to check if layout is horizontal menu
  692. if (document.querySelector('.content-wrapper > .menu-horizontal > div')) {
  693. horizontalMenu = true
  694. horizontalMenuArea = document.querySelector('.content-wrapper > .menu-horizontal > div')
  695. }
  696. // If compact mode layout
  697. if (contentLayout === 'compact') {
  698. // Remove container fluid class from content area, navbar area and footer area
  699. if (containerFluid.some(el => [contentArea, navbarArea, footerArea].includes(el))) {
  700. this._removeClass('container-fluid', [contentArea, navbarArea, footerArea])
  701. this._addClass('container-xxl', [contentArea, navbarArea, footerArea])
  702. }
  703. // For horizontal menu only
  704. if (horizontalMenu) {
  705. this._removeClass('container-fluid', horizontalMenuArea)
  706. this._addClass('container-xxl', horizontalMenuArea)
  707. }
  708. } else {
  709. // If wide mode layout
  710. // Remove container xxl class from content area, navbar area and footer area
  711. if (containerXxl.some(el => [contentArea, navbarArea, footerArea].includes(el))) {
  712. this._removeClass('container-xxl', [contentArea, navbarArea, footerArea])
  713. this._addClass('container-fluid', [contentArea, navbarArea, footerArea])
  714. }
  715. // For horizontal menu only
  716. if (horizontalMenu) {
  717. this._removeClass('container-xxl', horizontalMenuArea)
  718. this._addClass('container-fluid', horizontalMenuArea)
  719. }
  720. }
  721. }, 100)
  722. },
  723. // *******************************************************************************
  724. // * Update
  725. update() {
  726. if (
  727. (this.getLayoutNavbar() &&
  728. ((!this.isSmallScreen() && this.isLayoutNavbarFull() && this.isFixed()) || this.isNavbarFixed())) ||
  729. (this.getLayoutFooter() && this.isFooterFixed())
  730. ) {
  731. this._updateInlineStyle(this._getNavbarHeight(), this._getFooterHeight())
  732. }
  733. this._bindMenuMouseEvents()
  734. },
  735. setAutoUpdate(enable = requiredParam('enable')) {
  736. if (enable && !this._autoUpdate) {
  737. this.on('resize.Helpers:autoUpdate', () => this.update())
  738. this._autoUpdate = true
  739. } else if (!enable && this._autoUpdate) {
  740. this.off('resize.Helpers:autoUpdate')
  741. this._autoUpdate = false
  742. }
  743. },
  744. // Update custom option based on element
  745. updateCustomOptionCheck(el) {
  746. if (el.checked) {
  747. // If custom option element is radio, remove checked from the siblings (closest `.row`)
  748. if (el.type === 'radio') {
  749. const customRadioOptionList = [].slice.call(el.closest('.row').querySelectorAll('.custom-option'))
  750. customRadioOptionList.map(function (customRadioOptionEL) {
  751. customRadioOptionEL.closest('.custom-option').classList.remove('checked')
  752. })
  753. }
  754. el.closest('.custom-option').classList.add('checked')
  755. } else {
  756. el.closest('.custom-option').classList.remove('checked')
  757. }
  758. },
  759. // *******************************************************************************
  760. // * Tests
  761. isRtl() {
  762. return (
  763. document.querySelector('body').getAttribute('dir') === 'rtl' ||
  764. document.querySelector('html').getAttribute('dir') === 'rtl'
  765. )
  766. },
  767. isMobileDevice() {
  768. return typeof window.orientation !== 'undefined' || navigator.userAgent.indexOf('IEMobile') !== -1
  769. },
  770. isSmallScreen() {
  771. return (
  772. (window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth) < this.LAYOUT_BREAKPOINT
  773. )
  774. },
  775. isLayoutNavbarFull() {
  776. return !!document.querySelector('.layout-wrapper.layout-navbar-full')
  777. },
  778. isCollapsed() {
  779. if (this.isSmallScreen()) {
  780. return !this._hasClass('layout-menu-expanded')
  781. }
  782. return this._hasClass('layout-menu-collapsed')
  783. },
  784. isFixed() {
  785. return this._hasClass('layout-menu-fixed layout-menu-fixed-offcanvas')
  786. },
  787. isOffcanvas() {
  788. return this._hasClass('layout-menu-offcanvas layout-menu-fixed-offcanvas')
  789. },
  790. isNavbarFixed() {
  791. return (
  792. this._hasClass('layout-navbar-fixed') || (!this.isSmallScreen() && this.isFixed() && this.isLayoutNavbarFull())
  793. )
  794. },
  795. isFooterFixed() {
  796. return this._hasClass('layout-footer-fixed')
  797. },
  798. isLightStyle() {
  799. return document.documentElement.getAttribute('data-bs-theme') === 'light'
  800. },
  801. isDarkStyle() {
  802. return document.documentElement.getAttribute('data-bs-theme') === 'dark'
  803. },
  804. // *******************************************************************************
  805. // * Events
  806. on(event = requiredParam('event'), callback = requiredParam('callback')) {
  807. const [_event] = event.split('.')
  808. let [, ...namespace] = event.split('.')
  809. namespace = namespace.join('.') || null
  810. this._listeners.push({ event: _event, namespace, callback })
  811. },
  812. off(event = requiredParam('event')) {
  813. const [_event] = event.split('.')
  814. let [, ...namespace] = event.split('.')
  815. namespace = namespace.join('.') || null
  816. this._listeners
  817. .filter(listener => listener.event === _event && listener.namespace === namespace)
  818. .forEach(listener => this._listeners.splice(this._listeners.indexOf(listener), 1))
  819. },
  820. // *******************************************************************************
  821. // * Dark / Light / Auto Mode
  822. getStoredTheme: themeName => {
  823. if (window.templateCustomizer) {
  824. themeName = window.templateCustomizer._getSetting('Theme')
  825. } else {
  826. themeName = document.getElementsByTagName('HTML')[0].getAttribute('data-bs-theme')
  827. }
  828. return (
  829. themeName ||
  830. (window.templateCustomizer.settings.defaultTheme ? window.templateCustomizer.settings.defaultTheme : 'light')
  831. )
  832. },
  833. setStoredTheme: (templateName, theme) => {
  834. localStorage.setItem(`templateCustomizer-${templateName}--Theme`, theme)
  835. },
  836. getPreferredTheme: themeName => {
  837. const storedTheme = Helpers.getStoredTheme(themeName)
  838. if (storedTheme) {
  839. return storedTheme
  840. }
  841. return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
  842. },
  843. setTheme: theme => {
  844. if (theme === 'system') {
  845. document.documentElement.setAttribute(
  846. 'data-bs-theme',
  847. window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
  848. )
  849. } else {
  850. document.documentElement.setAttribute('data-bs-theme', theme)
  851. }
  852. // Set cookies for server-side theme detection
  853. const isAdmin = !window.location.pathname.includes('/front-pages/')
  854. const modeCookieName = isAdmin ? 'admin-mode' : 'front-mode'
  855. const colorPrefCookieName = isAdmin ? 'admin-colorPref' : 'front-colorPref'
  856. // Set cookie function
  857. const setCookie = (name, value, days = 365) => {
  858. const d = new Date()
  859. d.setTime(d.getTime() + days * 24 * 60 * 60 * 1000)
  860. const expires = 'expires=' + d.toUTCString()
  861. document.cookie = name + '=' + value + ';' + expires + ';path=/'
  862. }
  863. // Set the theme cookie
  864. setCookie(modeCookieName, theme)
  865. // If theme is system, also set the preferred color based on system preference
  866. if (theme === 'system') {
  867. const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
  868. const colorPref = prefersDark ? 'dark' : 'light'
  869. setCookie(colorPrefCookieName, colorPref)
  870. }
  871. // if (window.templateCustomizer && theme !== 'light') {
  872. // window.templateCustomizer._showResetBtnNotification(true)
  873. // }
  874. },
  875. showActiveTheme: (theme, focus = false) => {
  876. const themeSwitcher = document.querySelector('#nav-theme')
  877. if (!themeSwitcher) {
  878. return
  879. }
  880. const themeSwitcherText = document.querySelector('#nav-theme-text')
  881. const activeThemeIcon = document.querySelector('.theme-icon-active')
  882. const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`)
  883. const svgOfActiveBtn = btnToActive.querySelector('i').getAttribute('data-icon')
  884. document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
  885. element.classList.remove('active')
  886. element.setAttribute('aria-pressed', 'false')
  887. })
  888. btnToActive.classList.add('active')
  889. btnToActive.setAttribute('aria-pressed', 'true')
  890. const classList = Array.from(activeThemeIcon.classList)
  891. const filteredClassList = classList.filter(className => !className.startsWith('bx-'))
  892. activeThemeIcon.setAttribute('class', `bx-${svgOfActiveBtn} ${filteredClassList.join(' ')}`)
  893. const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`
  894. themeSwitcher.setAttribute('aria-label', themeSwitcherLabel)
  895. if (focus) {
  896. themeSwitcher.focus()
  897. }
  898. },
  899. syncThemeToggles: e => {
  900. document.querySelectorAll('[data-bs-theme-value]').forEach(toggle => {
  901. if (toggle.getAttribute('data-bs-theme-value') === e) {
  902. toggle.click()
  903. }
  904. })
  905. },
  906. syncCustomOptions: e => {
  907. const currEl = document.querySelector(`.template-customizer-themes-options input[value="${e}"]`)
  908. if (currEl) {
  909. currEl.checked = true
  910. window.Helpers.updateCustomOptionCheck(currEl)
  911. }
  912. },
  913. // *******************************************************************************
  914. // * LTR / RTL
  915. syncCustomOptionsRtl: e => {
  916. const currRtlEl = document.querySelector(`.template-customizer-directions-options input[value="${e}"]`)
  917. if (currRtlEl) {
  918. currRtlEl.checked = true
  919. window.Helpers.updateCustomOptionCheck(currRtlEl)
  920. }
  921. },
  922. // *******************************************************************************
  923. // * Life cycle
  924. init() {
  925. if (this._initialized) return
  926. this._initialized = true
  927. // Initialize `style` element
  928. this._updateInlineStyle(0)
  929. // Bind window resize event
  930. this._bindWindowResizeEvent()
  931. // Bind init event
  932. this.off('init._Helpers')
  933. this.on('init._Helpers', () => {
  934. this.off('resize._Helpers:redrawMenu')
  935. this.on('resize._Helpers:redrawMenu', () => {
  936. // eslint-disable-next-line no-unused-expressions
  937. this.isSmallScreen() && !this.isCollapsed() && this._redrawLayoutMenu()
  938. })
  939. // Force repaint in IE 10
  940. if (typeof document.documentMode === 'number' && document.documentMode < 11) {
  941. this.off('resize._Helpers:ie10RepaintBody')
  942. this.on('resize._Helpers:ie10RepaintBody', () => {
  943. if (this.isFixed()) return
  944. const { scrollTop } = document.documentElement
  945. document.body.style.display = 'none'
  946. document.body.style.display = 'block'
  947. document.documentElement.scrollTop = scrollTop
  948. })
  949. }
  950. })
  951. this._triggerEvent('init')
  952. },
  953. destroy() {
  954. if (!this._initialized) return
  955. this._initialized = false
  956. this._removeClass('layout-transitioning')
  957. this._removeInlineStyle()
  958. this._unbindLayoutAnimationEndEvent()
  959. this._unbindWindowResizeEvent()
  960. this._unbindMenuMouseEvents()
  961. this.setAutoUpdate(false)
  962. this.off('init._Helpers')
  963. // Remove all listeners except `init`
  964. this._listeners
  965. .filter(listener => listener.event !== 'init')
  966. .forEach(listener => this._listeners.splice(this._listeners.indexOf(listener), 1))
  967. },
  968. // ---
  969. // Init Password Toggle
  970. initPasswordToggle() {
  971. const toggler = document.querySelectorAll('.form-password-toggle i')
  972. if (typeof toggler !== 'undefined' && toggler !== null) {
  973. toggler.forEach(el => {
  974. el.addEventListener('click', e => {
  975. e.preventDefault()
  976. const formPasswordToggle = el.closest('.form-password-toggle')
  977. const formPasswordToggleIcon = formPasswordToggle.querySelector('i')
  978. const formPasswordToggleInput = formPasswordToggle.querySelector('input')
  979. if (formPasswordToggleInput.getAttribute('type') === 'text') {
  980. formPasswordToggleInput.setAttribute('type', 'password')
  981. formPasswordToggleIcon.classList.replace('bx-show', 'bx-hide')
  982. } else if (formPasswordToggleInput.getAttribute('type') === 'password') {
  983. formPasswordToggleInput.setAttribute('type', 'text')
  984. formPasswordToggleIcon.classList.replace('bx-hide', 'bx-show')
  985. }
  986. })
  987. })
  988. }
  989. },
  990. //--
  991. // Init custom option check
  992. initCustomOptionCheck() {
  993. const _this = this
  994. const custopOptionList = [].slice.call(document.querySelectorAll('.custom-option .form-check-input'))
  995. custopOptionList.map(function (customOptionEL) {
  996. // Update custom options check on page load
  997. _this.updateCustomOptionCheck(customOptionEL)
  998. // Update custom options check on click
  999. customOptionEL.addEventListener('click', e => {
  1000. _this.updateCustomOptionCheck(customOptionEL)
  1001. })
  1002. })
  1003. },
  1004. // ---
  1005. // Init Speech To Text
  1006. initSpeechToText() {
  1007. const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition
  1008. const speechToText = document.querySelectorAll('.speech-to-text')
  1009. if (SpeechRecognition !== undefined && SpeechRecognition !== null) {
  1010. if (typeof speechToText !== 'undefined' && speechToText !== null) {
  1011. const recognition = new SpeechRecognition()
  1012. const toggler = document.querySelectorAll('.speech-to-text i')
  1013. toggler.forEach(el => {
  1014. let listening = false
  1015. el.addEventListener('click', () => {
  1016. el.closest('.input-group').querySelector('.form-control').focus()
  1017. recognition.onspeechstart = () => {
  1018. listening = true
  1019. }
  1020. if (listening === false) {
  1021. recognition.start()
  1022. }
  1023. recognition.onerror = () => {
  1024. listening = false
  1025. }
  1026. recognition.onresult = event => {
  1027. el.closest('.input-group').querySelector('.form-control').value = event.results[0][0].transcript
  1028. }
  1029. recognition.onspeechend = () => {
  1030. listening = false
  1031. recognition.stop()
  1032. }
  1033. })
  1034. })
  1035. }
  1036. }
  1037. },
  1038. // ---
  1039. // Init Navbar Dropdown (i.e notification) PerfectScrollbar
  1040. initNavbarDropdownScrollbar() {
  1041. const scrollbarContainer = document.querySelectorAll('.navbar-dropdown .scrollable-container')
  1042. const { PerfectScrollbar } = window
  1043. if (PerfectScrollbar !== undefined) {
  1044. if (typeof scrollbarContainer !== 'undefined' && scrollbarContainer !== null) {
  1045. scrollbarContainer.forEach(el => {
  1046. // eslint-disable-next-line no-new
  1047. new PerfectScrollbar(el, {
  1048. wheelPropagation: false,
  1049. suppressScrollX: true
  1050. })
  1051. })
  1052. }
  1053. }
  1054. },
  1055. // Ajax Call Promise
  1056. ajaxCall(url) {
  1057. return new Promise((resolve, reject) => {
  1058. const req = new XMLHttpRequest()
  1059. req.open('GET', url)
  1060. req.onload = () => (req.status === 200 ? resolve(req.response) : reject(Error(req.statusText)))
  1061. req.onerror = e => reject(Error(`Network Error: ${e}`))
  1062. req.send()
  1063. })
  1064. },
  1065. // ---
  1066. // SidebarToggle (Used in Apps)
  1067. initSidebarToggle() {
  1068. const sidebarToggler = document.querySelectorAll('[data-bs-toggle="sidebar"]')
  1069. sidebarToggler.forEach(el => {
  1070. el.addEventListener('click', () => {
  1071. const target = el.getAttribute('data-target')
  1072. const overlay = el.getAttribute('data-overlay')
  1073. const appOverlay = document.querySelectorAll('.app-overlay')
  1074. const targetEl = document.querySelectorAll(target)
  1075. targetEl.forEach(tel => {
  1076. tel.classList.toggle('show')
  1077. if (
  1078. typeof overlay !== 'undefined' &&
  1079. overlay !== null &&
  1080. overlay !== false &&
  1081. typeof appOverlay !== 'undefined'
  1082. ) {
  1083. if (tel.classList.contains('show')) {
  1084. appOverlay[0].classList.add('show')
  1085. } else {
  1086. appOverlay[0].classList.remove('show')
  1087. }
  1088. appOverlay[0].addEventListener('click', e => {
  1089. e.currentTarget.classList.remove('show')
  1090. tel.classList.remove('show')
  1091. })
  1092. }
  1093. })
  1094. })
  1095. })
  1096. },
  1097. // get css variables for theme colors
  1098. getCssVar(color, isChartJs = false) {
  1099. if (isChartJs === true) {
  1100. return getComputedStyle(document.documentElement).getPropertyValue(`--${window.Helpers.prefix}${color}`).trim()
  1101. }
  1102. return `var(--${window.Helpers.prefix}${color})`
  1103. },
  1104. // get maxlength count and display it for input and textarea
  1105. maxLengthCount(inputElement, infoElement, maxLength) {
  1106. const currentLength = inputElement.value.length
  1107. const remaining = maxLength - currentLength
  1108. infoElement.className = 'maxLength label-success'
  1109. if (remaining >= 0) {
  1110. infoElement.textContent = `You typed ${currentLength} out of ${maxLength} characters.`
  1111. }
  1112. if (remaining <= 0) {
  1113. infoElement.textContent = `You typed ${currentLength} out of ${maxLength} characters.`
  1114. infoElement.classList.remove('label-success')
  1115. infoElement.classList.add('label-danger')
  1116. }
  1117. }
  1118. }
  1119. // *******************************************************************************
  1120. // * Initialization
  1121. if (typeof window !== 'undefined') {
  1122. Helpers.init()
  1123. if (Helpers.isMobileDevice() && window.chrome) {
  1124. document.documentElement.classList.add('layout-menu-100vh')
  1125. }
  1126. // Update layout after page load
  1127. if (document.readyState === 'complete') Helpers.update()
  1128. else
  1129. document.addEventListener('DOMContentLoaded', function onContentLoaded() {
  1130. Helpers.update()
  1131. document.removeEventListener('DOMContentLoaded', onContentLoaded)
  1132. })
  1133. }
  1134. // ---
  1135. window.Helpers = Helpers
  1136. export { Helpers }