Files
band-management/resources/vuexy-admin-v10.11.1/html-version/Bootstrap5/vuexy-html-admin-template/starter-kit/js/template-customizer.js

1331 lines
45 KiB
JavaScript

import customizerStyle from './_template-customizer/_template-customizer.scss'
import customizerMarkup from './_template-customizer/_template-customizer.html'
const CONTROLS = [
'color',
'theme',
'skins',
'semiDark',
'contentLayout',
'headerType',
'layoutCollapsed',
'layoutNavbarOptions',
'rtl',
'layoutFooterFixed',
'showDropdownOnHover'
]
const THEMES = ['light', 'dark', 'system']
let layoutNavbarVar
const cl = document.documentElement.classList
if (cl.contains('layout-navbar-fixed')) layoutNavbarVar = 'sticky'
else if (cl.contains('layout-navbar-hidden')) layoutNavbarVar = 'hidden'
else layoutNavbarVar = 'static'
const DISPLAY_CUSTOMIZER = true
const DEFAULT_THEME = document.documentElement.getAttribute('data-bs-theme') || 'light'
const DEFAULT_SKIN = document.getElementsByTagName('HTML')[0].getAttribute('data-skin') || 0
const DEFAULT_CONTENT_LAYOUT = cl.contains('layout-wide') ? 'wide' : 'compact'
let headerType
if (cl.contains('layout-menu-offcanvas')) {
headerType = 'static-offcanvas'
} else if (cl.contains('layout-menu-fixed')) {
headerType = 'fixed'
} else if (cl.contains('layout-menu-fixed-offcanvas')) {
headerType = 'fixed-offcanvas'
} else {
headerType = 'static'
}
const DEFAULT_HEADER_TYPE = headerType
const DEFAULT_MENU_COLLAPSED = !!cl.contains('layout-menu-collapsed')
const DEFAULT_NAVBAR_FIXED = layoutNavbarVar
const DEFAULT_TEXT_DIR = document.documentElement.getAttribute('dir') === 'rtl'
const DEFAULT_FOOTER_FIXED = !!cl.contains('layout-footer-fixed')
const DEFAULT_SHOW_DROPDOWN_ON_HOVER = true
let primaryColorFlag
const rootStyles = getComputedStyle(document.documentElement)
class TemplateCustomizer {
constructor({
displayCustomizer,
lang,
defaultPrimaryColor,
defaultSkin,
defaultTheme,
defaultSemiDark,
defaultContentLayout,
defaultHeaderType,
defaultMenuCollapsed,
defaultNavbarType,
defaultTextDir,
defaultFooterFixed,
defaultShowDropdownOnHover,
controls,
themes,
availableColors,
availableSkins,
availableThemes,
availableContentLayouts,
availableHeaderTypes,
availableMenuCollapsed,
availableNavbarOptions,
availableDirections,
onSettingsChange
}) {
if (this._ssr) return
if (!window.Helpers) throw new Error('window.Helpers required.')
this.settings = {}
this.settings.displayCustomizer = typeof displayCustomizer !== 'undefined' ? displayCustomizer : DISPLAY_CUSTOMIZER
this.settings.lang = lang || 'en'
if (defaultPrimaryColor) {
this.settings.defaultPrimaryColor = defaultPrimaryColor
primaryColorFlag = true
} else {
this.settings.defaultPrimaryColor = rootStyles.getPropertyValue('--bs-primary').trim()
primaryColorFlag = false
}
this.settings.defaultTheme = defaultTheme || DEFAULT_THEME
this.settings.defaultSemiDark = typeof defaultSemiDark !== 'undefined' ? defaultSemiDark : false
this.settings.defaultContentLayout =
typeof defaultContentLayout !== 'undefined' ? defaultContentLayout : DEFAULT_CONTENT_LAYOUT
this.settings.defaultHeaderType = defaultHeaderType || DEFAULT_HEADER_TYPE
this.settings.defaultMenuCollapsed =
typeof defaultMenuCollapsed !== 'undefined' ? defaultMenuCollapsed : DEFAULT_MENU_COLLAPSED
this.settings.defaultNavbarType =
typeof defaultNavbarType !== 'undefined' ? defaultNavbarType : DEFAULT_NAVBAR_FIXED
this.settings.defaultTextDir = defaultTextDir === 'rtl' ? true : false || DEFAULT_TEXT_DIR
this.settings.defaultFooterFixed =
typeof defaultFooterFixed !== 'undefined' ? defaultFooterFixed : DEFAULT_FOOTER_FIXED
this.settings.defaultShowDropdownOnHover =
typeof defaultShowDropdownOnHover !== 'undefined' ? defaultShowDropdownOnHover : DEFAULT_SHOW_DROPDOWN_ON_HOVER
this.settings.controls = controls || CONTROLS
this.settings.availableColors = availableColors || TemplateCustomizer.COLORS
this.settings.availableSkins = availableSkins || TemplateCustomizer.SKINS
this.settings.availableThemes = availableThemes || TemplateCustomizer.THEMES
this.settings.availableContentLayouts = availableContentLayouts || TemplateCustomizer.CONTENT
this.settings.availableHeaderTypes = availableHeaderTypes || TemplateCustomizer.HEADER_TYPES
this.settings.availableMenuCollapsed = availableMenuCollapsed || TemplateCustomizer.LAYOUTS
this.settings.availableNavbarOptions = availableNavbarOptions || TemplateCustomizer.NAVBAR_OPTIONS
this.settings.availableDirections = availableDirections || TemplateCustomizer.DIRECTIONS
this.settings.defaultSkin = this._getDefaultSkin(typeof defaultSkin !== 'undefined' ? defaultSkin : DEFAULT_SKIN)
this.settings.themes = themes || THEMES
if (this.settings.themes.length < 2) {
const i = this.settings.controls.indexOf('theme')
if (i !== -1) {
this.settings.controls = this.settings.controls.slice(0, i).concat(this.settings.controls.slice(i + 1))
}
}
this.settings.onSettingsChange = typeof onSettingsChange === 'function' ? onSettingsChange : () => {}
this._loadSettings()
this._listeners = []
this._controls = {}
this._initDirection()
this.setContentLayout(this.settings.contentLayout, false)
this.setHeaderType(this.settings.headerType, false)
this.setLayoutNavbarOption(this.settings.layoutNavbarOptions, false)
this.setLayoutFooterFixed(this.settings.layoutFooterFixed, false)
this.setDropdownOnHover(this.settings.showDropdownOnHover, false)
this._setup()
}
setColor(color, defaultChange = false) {
// Use Helpers method
window.Helpers.setColor(color, defaultChange)
}
setTheme(theme) {
this._setSetting('Theme', theme)
}
setSkin(skinName, updateStorage = true, cb = null) {
if (!this._hasControls('skins')) return
const skin = this._getSkinByName(skinName)
if (!skin) return
this.settings.skin = skin
if (updateStorage) this._setSetting('Skin', skinName)
if (updateStorage) this.settings.onSettingsChange.call(this, this.settings)
}
setLayoutNavbarOption(navbarType, updateStorage = true) {
if (!this._hasControls('layoutNavbarOptions')) return
this.settings.layoutNavbarOptions = navbarType
if (updateStorage) this._setSetting('FixedNavbarOption', navbarType)
window.Helpers.setNavbar(navbarType)
if (updateStorage) this.settings.onSettingsChange.call(this, this.settings)
}
setContentLayout(contentLayout, updateStorage = true) {
if (!this._hasControls('contentLayout')) return
this.settings.contentLayout = contentLayout
if (updateStorage) this._setSetting('contentLayout', contentLayout)
window.Helpers.setContentLayout(contentLayout)
if (updateStorage) this.settings.onSettingsChange.call(this, this.settings)
}
setHeaderType(pos, updateStorage = true) {
if (!this._hasControls('headerType')) return
if (!['static', 'static-offcanvas', 'fixed', 'fixed-offcanvas'].includes(pos)) return
this.settings.headerType = pos
if (updateStorage) this._setSetting('HeaderType', pos)
window.Helpers.setPosition(
pos === 'fixed' || pos === 'fixed-offcanvas',
pos === 'static-offcanvas' || pos === 'fixed-offcanvas'
)
if (updateStorage) this.settings.onSettingsChange.call(this, this.settings)
// Perfect Scrollbar change on Layout change
let menuScroll = window.Helpers.menuPsScroll
const PerfectScrollbarLib = window.PerfectScrollbar
if (this.settings.headerType === 'fixed' || this.settings.headerType === 'fixed-offcanvas') {
// Set perfect scrollbar wheelPropagation false for fixed layout
if (PerfectScrollbarLib && menuScroll) {
window.Helpers.menuPsScroll.destroy()
menuScroll = new PerfectScrollbarLib(document.querySelector('.menu-inner'), {
suppressScrollX: true,
wheelPropagation: false
})
window.Helpers.menuPsScroll = menuScroll
}
} else if (menuScroll) {
// Destroy perfect scrollbar for static layout
window.Helpers.menuPsScroll.destroy()
}
}
setLayoutFooterFixed(fixed, updateStorage = true) {
// if (!this._hasControls('layoutFooterFixed')) return
this.settings.layoutFooterFixed = fixed
if (updateStorage) this._setSetting('FixedFooter', fixed)
window.Helpers.setFooterFixed(fixed)
if (updateStorage) this.settings.onSettingsChange.call(this, this.settings)
}
setDropdownOnHover(open, updateStorage = true) {
if (!this._hasControls('showDropdownOnHover')) return
this.settings.showDropdownOnHover = open
if (updateStorage) this._setSetting('ShowDropdownOnHover', open)
if (window.Helpers.mainMenu) {
window.Helpers.mainMenu.destroy()
config.showDropdownOnHover = open
const { Menu } = window
window.Helpers.mainMenu = new Menu(document.getElementById('layout-menu'), {
orientation: 'horizontal',
closeChildren: true,
showDropdownOnHover: config.showDropdownOnHover
})
}
if (updateStorage) this.settings.onSettingsChange.call(this, this.settings)
}
setRtl(rtl) {
if (!this._hasControls('rtl')) return
this._setSetting('Rtl', String(rtl))
}
setLang(lang, updateStorage = true, force = false) {
if (lang === this.settings.lang && !force) return
if (!TemplateCustomizer.LANGUAGES[lang]) throw new Error(`Language "${lang}" not found!`)
const t = TemplateCustomizer.LANGUAGES[lang]
;[
'panel_header',
'panel_sub_header',
'theming_header',
'color_label',
'theme_label',
'style_switch_light',
'style_switch_dark',
'layout_header',
'layout_label',
'layout_header_label',
'content_label',
'layout_static',
'layout_offcanvas',
'layout_fixed',
'layout_fixed_offcanvas',
'layout_dd_open_label',
'layout_navbar_label',
'layout_footer_label',
'misc_header',
'skin_label',
'semiDark_label',
'direction_label'
].forEach(key => {
const el = this.container.querySelector(`.template-customizer-t-${key}`)
// eslint-disable-next-line no-unused-expressions
el && (el.textContent = t[key])
})
this.settings.lang = lang
if (updateStorage) this._setSetting('Lang', lang)
if (updateStorage) this.settings.onSettingsChange.call(this, this.settings)
}
// Update theme settings control
update() {
if (this._ssr) return
const hasNavbar = !!document.querySelector('.layout-navbar')
const hasMenu = !!document.querySelector('.layout-menu')
const hasHorizontalMenu = !!document.querySelector('.layout-menu-horizontal.menu, .layout-menu-horizontal .menu')
const hasFooter = !!document.querySelector('.content-footer')
if (this._controls.showDropdownOnHover) {
if (hasMenu) {
this._controls.showDropdownOnHover.setAttribute('disabled', 'disabled')
this._controls.showDropdownOnHover.classList.add('disabled')
} else {
this._controls.showDropdownOnHover.removeAttribute('disabled')
this._controls.showDropdownOnHover.classList.remove('disabled')
}
}
if (this._controls.layoutNavbarOptions) {
if (!hasNavbar) {
this._controls.layoutNavbarOptions.setAttribute('disabled', 'disabled')
this._controls.layoutNavbarOptionsW.classList.add('disabled')
} else {
this._controls.layoutNavbarOptions.removeAttribute('disabled')
this._controls.layoutNavbarOptionsW.classList.remove('disabled')
}
// Horizontal menu fixed layout - disabled fixed navbar switch
if (hasHorizontalMenu && hasNavbar && this.settings.headerType === 'fixed') {
this._controls.layoutNavbarOptions.setAttribute('disabled', 'disabled')
this._controls.layoutNavbarOptionsW.classList.add('disabled')
}
}
if (this._controls.layoutFooterFixed) {
if (!hasFooter) {
this._controls.layoutFooterFixed.setAttribute('disabled', 'disabled')
this._controls.layoutFooterFixedW.classList.add('disabled')
} else {
this._controls.layoutFooterFixed.removeAttribute('disabled')
this._controls.layoutFooterFixedW.classList.remove('disabled')
}
}
if (this._controls.headerType) {
// Disable menu layouts options if menu (vertical or horizontal) is not there
if (hasMenu || hasHorizontalMenu) {
// (Updated condition)
this._controls.headerType.removeAttribute('disabled')
} else {
this._controls.headerType.setAttribute('disabled', 'disabled')
}
}
}
// Clear local storage
clearLocalStorage() {
if (this._ssr) return
const layoutName = this._getLayoutName()
const keysToRemove = [
'Color',
'Theme',
'Skin',
'SemiDark',
'LayoutCollapsed',
'FixedNavbarOption',
'HeaderType',
'contentLayout',
'Rtl',
'Lang'
]
keysToRemove.forEach(key => {
const localStorageKey = `templateCustomizer-${layoutName}--${key}`
localStorage.removeItem(localStorageKey)
})
this._showResetBtnNotification(false)
}
// Clear local storage
destroy() {
if (this._ssr) return
this._cleanup()
this.settings = null
this.container.parentNode.removeChild(this.container)
this.container = null
}
_loadSettings() {
// Get settings
const rtlOption = this._getSetting('Rtl')
const color = this._getSetting('Color')
const theme = this._getSetting('Theme')
const skin = this._getSetting('Skin')
const semiDark = this._getSetting('SemiDark') // Default value will be set from main.js
const contentLayout = this._getSetting('contentLayout')
const collapsedMenu = this._getSetting('LayoutCollapsed') // Value will be set from main.js
const dropdownOnHover = this._getSetting('ShowDropdownOnHover') // Value will be set from main.js
const navbarOption = this._getSetting('FixedNavbarOption')
const fixedFooter = this._getSetting('FixedFooter')
const layoutType = this._getSetting('HeaderType')
// Reset Button
if (
rtlOption ||
theme ||
skin ||
contentLayout ||
collapsedMenu ||
navbarOption ||
layoutType ||
color ||
semiDark
) {
this._showResetBtnNotification(true)
} else {
this._showResetBtnNotification(false)
}
// Header Type
this.settings.headerType = ['static', 'static-offcanvas', 'fixed', 'fixed-offcanvas'].includes(layoutType)
? layoutType
: this.settings.defaultHeaderType
// ! Set settings by following priority: Local Storage, Theme Config, HTML Classes
this.settings.rtl = rtlOption !== '' ? rtlOption === 'true' : this.settings.defaultTextDir
// Color
if (color) {
primaryColorFlag = true
}
this.settings.color = color || this.settings.defaultPrimaryColor
this.setColor(this.settings.color, primaryColorFlag)
// Style
this.settings.themesOpt = this.settings.themes.includes(theme) ? theme : this.settings.defaultTheme
// Get the systemTheme value using JS
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
// appliedTheme will be used to set the theme based on the settings, we keep this separate as we can't set 'system' or 'system' in data-bs-theme
let appliedTheme
if (this.settings.themes.includes(theme)) {
appliedTheme = theme === 'system' ? systemTheme : theme
} else if (this.settings.defaultTheme === 'system') {
appliedTheme = systemTheme
} else {
appliedTheme = this.settings.defaultTheme
}
this.settings.theme = this.settings.defaultTheme
document.documentElement.setAttribute('data-bs-theme', appliedTheme)
// Semi Dark
this.settings.semiDark = semiDark ? semiDark === 'true' : this.settings.defaultSemiDark
//! FIX: Added data-semidark-menu attribute to avoid semi dark menu flicker effect on page load
if (this.settings.semiDark) document.documentElement.setAttribute('data-semidark-menu', this.settings.semiDark)
// Content Layout
this.settings.contentLayout = contentLayout || this.settings.defaultContentLayout
// Layout Collapsed
this.settings.layoutCollapsed = collapsedMenu ? collapsedMenu === 'true' : this.settings.defaultMenuCollapsed
// Add layout-menu-collapsed class to the body if the menu is collapsed
if (this.settings.layoutCollapsed) document.documentElement.classList.add('layout-menu-collapsed')
// Dropdown on Hover
this.settings.showDropdownOnHover = dropdownOnHover
? dropdownOnHover === 'true'
: this.settings.defaultShowDropdownOnHover
// Navbar Option
this.settings.layoutNavbarOptions = ['static', 'sticky', 'hidden'].includes(navbarOption)
? navbarOption
: this.settings.defaultNavbarType
// Footer Fixed
this.settings.layoutFooterFixed = fixedFooter ? fixedFooter === 'true' : this.settings.defaultFooterFixed
this.settings.skin = this._getSkinByName(this._getSetting('Skin'), true)
// Filter options depending on available controls
if (!this._hasControls('rtl')) this.settings.rtl = document.documentElement.getAttribute('dir') === 'rtl'
if (!this._hasControls('theme')) this.settings.theme = window.Helpers.isDarkStyle() ? 'dark' : 'light'
if (!this._hasControls('contentLayout')) this.settings.contentLayout = null
if (!this._hasControls('headerType')) this.settings.headerType = null
if (!this._hasControls('layoutCollapsed')) this.settings.layoutCollapsed = null
if (!this._hasControls('layoutNavbarOptions')) this.settings.layoutNavbarOptions = null
if (!this._hasControls('skins')) this.settings.skin = null
if (!this._hasControls('semiDark')) this.settings.semiDark = null
}
// Setup theme settings controls and events
_setup(_container = document) {
// Function to create customizer elements
const createOptionElement = (nameVal, title, inputName, image, isIcon = false) => {
const divElement = document.createElement('div')
divElement.classList.add('col-4', 'px-2')
// Determine the correct classes based on whether it's an icon or image
const optionClass = isIcon
? 'custom-option custom-option-icon'
: 'custom-option custom-option-image custom-option-image-radio'
// Create the inner HTML structure
divElement.innerHTML = `
<div class="form-check ${optionClass} mb-0">
<label class="form-check-label custom-option-content p-0" for="${inputName}${nameVal}">
<span class="custom-option-body mb-0 scaleX-n1-rtl"></span>
</label>
<input
name="${inputName}"
class="form-check-input d-none"
type="radio"
value="${nameVal}"
id="${inputName}${nameVal}" />
</div>
<label class="form-check-label small text-nowrap text-body" for="${inputName}${nameVal}">${title}</label>
`
if (isIcon) {
// If it's an icon, insert the icon HTML directly
divElement.querySelector('.custom-option-body').innerHTML = image
} else {
// Otherwise, assume it's an SVG file name and fetch its content
fetch(`${assetsPath}img/customizer/${image}`)
.then(response => response.text())
.then(svgContent => {
// Insert the SVG content into the HTML
divElement.querySelector('.custom-option-body').innerHTML = svgContent
})
.catch(error => console.error('Error loading SVG:', error))
}
return divElement
}
this._cleanup()
this.container = this._getElementFromString(customizerMarkup)
// Customizer visibility
//
this.container.setAttribute('style', `visibility: ${this.settings.displayCustomizer ? 'visible' : 'hidden'}`)
// Open btn
const openBtn = this.container.querySelector('.template-customizer-open-btn')
const openBtnCb = () => {
this.container.classList.add('template-customizer-open')
this.update()
if (this._updateInterval) clearInterval(this._updateInterval)
this._updateInterval = setInterval(() => {
this.update()
}, 500)
}
openBtn.addEventListener('click', openBtnCb)
this._listeners.push([openBtn, 'click', openBtnCb])
// Reset btn
const resetBtn = this.container.querySelector('.template-customizer-reset-btn')
const resetBtnCb = () => {
this.clearLocalStorage()
window.location.reload()
}
resetBtn.addEventListener('click', resetBtnCb)
this._listeners.push([resetBtn, 'click', resetBtnCb])
// Close btn
const closeBtn = this.container.querySelector('.template-customizer-close-btn')
const closeBtnCb = () => {
this.container.classList.remove('template-customizer-open')
if (this._updateInterval) {
clearInterval(this._updateInterval)
this._updateInterval = null
}
}
closeBtn.addEventListener('click', closeBtnCb)
this._listeners.push([closeBtn, 'click', closeBtnCb])
// Color
const colorW = this.container.querySelector('.template-customizer-color')
const colorOpt = colorW.querySelector('.template-customizer-colors-options')
if (!this._hasControls('color')) {
colorW.parentNode.removeChild(colorW)
} else {
const inputName = 'colorRadioIcon'
this.settings.availableColors.forEach(color => {
const colorEl = `<div class="form-check custom-option custom-option-icon mb-0">
<label class="form-check-label custom-option-content p-0" for="${inputName}${color.name}">
<span class="custom-option-body mb-0 scaleX-n1-rtl" style="background-color: ${color.color};"></span>
</label>
<input
name="${inputName}"
class="form-check-input d-none"
type="radio"
value="${color.color}"
data-color="${color.color}"
id="${inputName}${color.name}" />
</div>`
// convert colorEl string to HTML element
const colorEle = this._getElementFromString(colorEl)
colorOpt.appendChild(colorEle)
})
colorOpt.appendChild(
this._getElementFromString(
'<div class="form-check custom-option custom-option-icon mb-0"><label class="form-check-label custom-option-content" for="colorRadioIcon"><span class="custom-option-body customizer-nano-picker mb-50"><i class="ti tabler-color-picker icon-base"></i></span></label><input name="colorRadioIcon" class="form-check-input picker d-none" type="radio" value="picker" id="colorRadioIcon" /> </div>'
)
)
const colorSelector = colorOpt.querySelector(`input[value="${this.settings.color}"]`)
if (colorSelector) {
colorSelector.setAttribute('checked', 'checked')
colorOpt.querySelector('input[value="picker"]').removeAttribute('checked')
} else {
colorOpt.querySelector('input[value="picker"]').setAttribute('checked', 'checked')
}
const colorCb = e => {
if (e.target.value === 'picker') {
document.querySelector('.custom-option-content .pcr-button').click()
} else {
this._setSetting('Color', e.target.dataset.color)
this.setColor(
e.target.dataset.color,
() => {
this._loadingState(false)
},
true
)
}
}
colorOpt.addEventListener('change', colorCb)
this._listeners.push([colorOpt, 'change', colorCb])
}
// Theme
const themeW = this.container.querySelector('.template-customizer-theme')
const themeOpt = themeW.querySelector('.template-customizer-themes-options')
if (!this._hasControls('theme')) {
themeW.parentNode.removeChild(themeW)
} else {
this.settings.availableThemes.forEach(theme => {
const themeEl = createOptionElement(theme.name, theme.title, 'customRadioIcon', theme.image, true)
themeOpt.appendChild(themeEl)
})
if (themeOpt.querySelector(`input[value="${this.settings.themesOpt}"]`)) {
themeOpt.querySelector(`input[value="${this.settings.themesOpt}"]`).setAttribute('checked', 'checked')
}
// themeCb
const themeCb = e => {
document.documentElement.setAttribute('data-bs-theme', e.target.value)
if (this._hasControls('semiDark')) {
const semiDarkL = this.container.querySelector('.template-customizer-semiDark')
if (e.target.value === 'dark') {
semiDarkL.classList.add('d-none')
} else {
semiDarkL.classList.remove('d-none')
}
}
// Update below commented code for data-bs-theme-value attribute value matches with e.target.value
window.Helpers.syncThemeToggles(e.target.value)
this.setTheme(e.target.value, true, () => {
this._loadingState(false)
})
}
themeOpt.addEventListener('change', themeCb)
this._listeners.push([themeOpt, 'change', themeCb])
}
// Skin
const skinsW = this.container.querySelector('.template-customizer-skins')
const skinsWInner = skinsW.querySelector('.template-customizer-skins-options')
if (!this._hasControls('skins')) {
skinsW.parentNode.removeChild(skinsW)
} else {
this.settings.availableSkins.forEach(skin => {
const skinEl = createOptionElement(skin.name, skin.title, 'skinRadios', skin.image)
skinsWInner.appendChild(skinEl)
})
skinsWInner.querySelector(`input[value="${this.settings.skin.name}"]`).setAttribute('checked', 'checked')
document.documentElement.setAttribute('data-skin', this.settings.skin.name)
const skinCb = e => {
document.documentElement.setAttribute('data-skin', e.target.value)
this.setSkin(e.target.value, true, () => {
this._loadingState(false, true)
})
}
skinsWInner.addEventListener('change', skinCb)
this._listeners.push([skinsWInner, 'change', skinCb])
}
// SemiDark
// update menu's data-bs-theme attribute to dark & light on switch changes on & off respectively
const semiDarkSwitch = this.container.querySelector('.template-customizer-semi-dark-switch')
const semiDarkSection = this.container.querySelector('.template-customizer-semiDark')
if (document.documentElement.getAttribute('data-bs-theme') === 'dark') {
semiDarkSection.classList.add('d-none')
}
if (!this._hasControls('semiDark')) {
semiDarkSection.remove()
} else if (this._hasControls('semiDark') && this._getSetting('Theme') === 'dark') {
semiDarkSwitch.classList.add('d-none')
} else {
if (this.settings.semiDark) {
semiDarkSwitch.setAttribute('checked', 'checked')
}
const semiDarkSwitchCb = e => {
const isDark = e.target.checked
const theme = isDark ? 'dark' : 'light'
if (theme === 'dark') {
document.getElementById('layout-menu').setAttribute('data-bs-theme', theme)
//! FIX: Added data-semidark-menu attribute to avoid semi dark menu flicker effect on page load
document.documentElement.setAttribute('data-semidark-menu', 'true')
} else {
document.getElementById('layout-menu').removeAttribute('data-bs-theme')
document.documentElement.removeAttribute('data-semidark-menu')
}
this._setSetting('SemiDark', isDark)
}
semiDarkSwitch.addEventListener('change', semiDarkSwitchCb)
this._listeners.push([semiDarkSwitch, 'change', semiDarkSwitchCb])
}
const themingW = this.container.querySelector('.template-customizer-theming')
if (
!this._hasControls('color') &&
!this._hasControls('theme') &&
!this._hasControls('skins') &&
!this._hasControls('semiDark')
) {
themingW.parentNode.removeChild(themingW)
}
// Layout wrapper
const layoutW = this.container.querySelector('.template-customizer-layout')
if (!this._hasControls('contentLayout headerType layoutCollapsed layoutNavbarOptions rtl', true)) {
layoutW.parentNode.removeChild(layoutW)
} else {
// Layouts Collapsed: Expanded, Collapsed
const layoutCollapsedW = this.container.querySelector('.template-customizer-layouts')
if (!this._hasControls('layoutCollapsed')) {
layoutCollapsedW.parentNode.removeChild(layoutCollapsedW)
} else {
setTimeout(() => {
if (document.querySelector('.layout-menu-horizontal')) {
layoutCollapsedW.parentNode.removeChild(layoutCollapsedW)
}
}, 100)
const layoutCollapsedOpt = layoutCollapsedW.querySelector('.template-customizer-layouts-options')
this.settings.availableMenuCollapsed.forEach(layoutOpt => {
const layoutsEl = createOptionElement(layoutOpt.name, layoutOpt.title, 'layoutsRadios', layoutOpt.image)
layoutCollapsedOpt.appendChild(layoutsEl)
})
layoutCollapsedOpt
.querySelector(`input[value="${this.settings.layoutCollapsed ? 'collapsed' : 'expanded'}"]`)
.setAttribute('checked', 'checked')
const layoutCb = e => {
window.Helpers.setCollapsed(e.target.value === 'collapsed', true)
this._setSetting('LayoutCollapsed', e.target.value === 'collapsed')
}
layoutCollapsedOpt.addEventListener('change', layoutCb)
this._listeners.push([layoutCollapsedOpt, 'change', layoutCb])
}
// CONTENT
const contentWrapper = this.container.querySelector('.template-customizer-content')
// ? Hide RTL control in following 2 case
if (!this._hasControls('contentLayout')) {
contentWrapper.parentNode.removeChild(contentWrapper)
} else {
const contentOpt = contentWrapper.querySelector('.template-customizer-content-options')
this.settings.availableContentLayouts.forEach(content => {
const contentEl = createOptionElement(content.name, content.title, 'contentRadioIcon', content.image)
contentOpt.appendChild(contentEl)
})
contentOpt.querySelector(`input[value="${this.settings.contentLayout}"]`).setAttribute('checked', 'checked')
const contentCb = e => {
this._loading = true
this._loadingState(true, true)
this.setContentLayout(e.target.value, true, () => {
this._loading = false
this._loadingState(false, true)
})
}
contentOpt.addEventListener('change', contentCb)
this._listeners.push([contentOpt, 'change', contentCb])
}
// Header Layout Type
const headerTypeW = this.container.querySelector('.template-customizer-headerOptions')
const templateName = document.documentElement.getAttribute('data-template').split('-')
if (!this._hasControls('headerType')) {
headerTypeW.parentNode.removeChild(headerTypeW)
} else {
const headerOpt = headerTypeW.querySelector('.template-customizer-header-options')
setTimeout(() => {
if (templateName.includes('vertical')) {
headerTypeW.parentNode.removeChild(headerTypeW)
}
}, 100)
this.settings.availableHeaderTypes.forEach(header => {
const headerEl = createOptionElement(header.name, header.title, 'headerRadioIcon', header.image)
headerOpt.appendChild(headerEl)
})
headerOpt.querySelector(`input[value="${this.settings.headerType}"]`).setAttribute('checked', 'checked')
const headerTypeCb = e => {
this.setHeaderType(e.target.value)
}
headerOpt.addEventListener('change', headerTypeCb)
this._listeners.push([headerOpt, 'change', headerTypeCb])
}
// Layout Navbar Options
const navbarOption = this.container.querySelector('.template-customizer-layoutNavbarOptions')
if (!this._hasControls('layoutNavbarOptions')) {
navbarOption.parentNode.removeChild(navbarOption)
} else {
setTimeout(() => {
if (templateName.includes('horizontal')) {
navbarOption.parentNode.removeChild(navbarOption)
}
}, 100)
const navbarTypeOpt = navbarOption.querySelector('.template-customizer-navbar-options')
this.settings.availableNavbarOptions.forEach(navbarOpt => {
const navbarEl = createOptionElement(navbarOpt.name, navbarOpt.title, 'navbarOptionRadios', navbarOpt.image)
navbarTypeOpt.appendChild(navbarEl)
})
// check navbar option from settings
navbarTypeOpt
.querySelector(`input[value="${this.settings.layoutNavbarOptions}"]`)
.setAttribute('checked', 'checked')
const navbarCb = e => {
this._loading = true
this._loadingState(true, true)
this.setLayoutNavbarOption(e.target.value, true, () => {
this._loading = false
this._loadingState(false, true)
})
}
navbarTypeOpt.addEventListener('change', navbarCb)
this._listeners.push([navbarTypeOpt, 'change', navbarCb])
}
// RTL
const directionW = this.container.querySelector('.template-customizer-directions')
// ? Hide RTL control in following 2 case
if (!this._hasControls('rtl')) {
directionW.parentNode.removeChild(directionW)
} else {
const directionOpt = directionW.querySelector('.template-customizer-directions-options')
this.settings.availableDirections.forEach(dir => {
const dirEl = createOptionElement(dir.name, dir.title, 'directionRadioIcon', dir.image)
directionOpt.appendChild(dirEl)
})
directionOpt
.querySelector(`input[value="${this.settings.rtl ? 'rtl' : 'ltr'}"]`)
.setAttribute('checked', 'checked')
const rtlCb = e => {
this._setSetting('Lang', this.settings.lang)
// For demo purpose, we will use EN as LTR and AR as RTL Language
this._setSetting('Lang', this.settings.lang === 'ar' ? 'en' : 'ar')
this.settings.rtl = e.target.value === 'rtl'
// Cache the language setting
const currentLang = this._getSetting('Lang')
const languageDropdown = document.querySelector('.dropdown-language .dropdown-menu')
if (languageDropdown) {
const dropdownItem = languageDropdown.querySelector(`[data-language="${currentLang}"]`)
dropdownItem.click()
}
// Use querySelector for cleaner and more flexible selection
this._initDirection()
this.setRtl(e.target.value === 'rtl', true, () => {
this._loadingState(false)
})
}
directionOpt.addEventListener('change', rtlCb)
this._listeners.push([directionOpt, 'change', rtlCb])
}
}
setTimeout(() => {
const layoutCustom = this.container.querySelector('.template-customizer-layout')
const layoutTheme = this.container.querySelector('.template-customizer-theming')
const checkSemiDarkWrapper = document.documentElement.getAttribute('data-bs-theme')
let checkSemiDark = false
if (checkSemiDarkWrapper === 'light' && document.querySelector('.layout-menu')) {
if (document.querySelector('.layout-menu').getAttribute('data-bs-theme') === 'dark') {
checkSemiDark = true
}
if (checkSemiDark === true) {
const semiDarkSwitch = layoutTheme.querySelector('.template-customizer-semi-dark-switch')
semiDarkSwitch.setAttribute('checked', 'checked')
}
}
if (document.querySelector('.menu-vertical')) {
if (!this._hasControls('rtl contentLayout layoutCollapsed layoutNavbarOptions', true)) {
if (layoutCustom) {
layoutCustom.parentNode.removeChild(layoutCustom)
}
}
} else if (document.querySelector('.menu-horizontal')) {
if (!this._hasControls('rtl contentLayout headerType', true)) {
if (layoutCustom) {
layoutCustom.parentNode.removeChild(layoutCustom)
}
}
}
}, 100)
// Set language
this.setLang(this.settings.lang, false, true)
// Append container
if (_container === document) {
if (_container.body) {
_container.body.appendChild(this.container)
} else {
window.addEventListener('DOMContentLoaded', () => _container.body.appendChild(this.container))
}
} else {
_container.appendChild(this.container)
}
}
_initDirection() {
if (this._hasControls('rtl')) document.documentElement.setAttribute('dir', this.settings.rtl ? 'rtl' : 'ltr')
}
_loadingState(enable, skins) {
this.container.classList[enable ? 'add' : 'remove'](`template-customizer-loading${skins ? '-theme' : ''}`)
}
_getElementFromString(str) {
const wrapper = document.createElement('div')
wrapper.innerHTML = str
return wrapper.firstChild
}
// Set settings in LocalStorage with layout & key
_setSetting(key, val) {
const layoutName = this._getLayoutName()
try {
localStorage.setItem(`templateCustomizer-${layoutName}--${key}`, String(val))
this._showResetBtnNotification()
} catch (e) {
// Catch Error
}
}
// Set settings in LocalStorage with layout & key
_getSetting(key) {
let result = null
const layoutName = this._getLayoutName()
try {
result = localStorage.getItem(`templateCustomizer-${layoutName}--${key}`)
} catch (e) {
// Catch error
}
return String(result || '')
}
_showResetBtnNotification(show = true) {
setTimeout(() => {
const resetBtnAttr = this.container.querySelector('.template-customizer-reset-btn .badge')
if (show) {
resetBtnAttr.classList.remove('d-none')
} else {
resetBtnAttr.classList.add('d-none')
}
}, 200)
}
// Get layout name to set unique
_getLayoutName() {
return document.getElementsByTagName('HTML')[0].getAttribute('data-template')
}
_removeListeners() {
for (let i = 0, l = this._listeners.length; i < l; i++) {
this._listeners[i][0].removeEventListener(this._listeners[i][1], this._listeners[i][2])
}
}
_cleanup() {
this._removeListeners()
this._listeners = []
this._controls = {}
if (this._updateInterval) {
clearInterval(this._updateInterval)
this._updateInterval = null
}
}
get _ssr() {
return typeof window === 'undefined'
}
// Check controls availability
_hasControls(controls, oneOf = false) {
return controls.split(' ').reduce((result, control) => {
if (this.settings.controls.indexOf(control) !== -1) {
if (oneOf || result !== false) result = true
} else if (!oneOf || result !== true) result = false
return result
}, null)
}
// Get the default Skin
_getDefaultSkin(skinId) {
const skin = typeof skinId === 'string' ? this._getSkinByName(skinId, false) : this.settings.availableSkins[skinId]
if (!skin) {
throw new Error(`Skin ID "${skinId}" not found!`)
}
return skin
}
// Get theme by skinId/skinName
_getSkinByName(skinName, returnDefault = false) {
const skins = this.settings.availableSkins
for (let i = 0, l = skins.length; i < l; i++) {
if (skins[i].name === skinName) return skins[i]
}
return returnDefault ? this.settings.defaultSkin : null
}
}
// Colors
TemplateCustomizer.COLORS = [
{
name: 'primary',
title: 'Primary',
color: rootStyles.getPropertyValue('--bs-primary').trim()
},
{
name: 'success',
title: 'Success',
color: '#0D9394'
},
{
name: 'warning',
title: 'Warning',
color: '#FFAB1D'
},
{
name: 'danger',
title: 'Danger',
color: '#EB3D63'
},
{
name: 'info',
title: 'Info',
color: '#2092EC'
}
]
// Themes
TemplateCustomizer.THEMES = [
{
name: 'light',
title: 'Light',
image: '<i class="ti tabler-sun icon-base mb-0"></i>'
},
{
name: 'dark',
title: 'Dark',
image: '<i class="ti tabler-moon icon-base mb-0"></i>'
},
{
name: 'system',
title: 'System',
image: '<i class="ti tabler-device-desktop-analytics icon-base mb-0"></i>'
}
]
// Skins
TemplateCustomizer.SKINS = [
{
name: 'default',
title: 'Default',
image: 'skin-default.svg'
},
{
name: 'bordered',
title: 'Bordered',
image: 'skin-border.svg'
}
]
// Layouts
TemplateCustomizer.LAYOUTS = [
{
name: 'expanded',
title: 'Expanded',
image: 'layouts-expanded.svg'
},
{
name: 'collapsed',
title: 'Collapsed',
image: 'layouts-collapsed.svg'
}
]
// Navbar Options
TemplateCustomizer.NAVBAR_OPTIONS = [
{
name: 'sticky',
title: 'Sticky',
image: 'navbar-sticky.svg'
},
{
name: 'static',
title: 'Static',
image: 'navbar-static.svg'
},
{
name: 'hidden',
title: 'Hidden',
image: 'navbar-hidden.svg'
}
]
// Header Types
TemplateCustomizer.HEADER_TYPES = [
{
name: 'fixed',
title: 'Fixed',
image: 'horizontal-fixed.svg'
},
{
name: 'static',
title: 'Static',
image: 'horizontal-static.svg'
}
]
// Content Types
TemplateCustomizer.CONTENT = [
{
name: 'compact',
title: 'Compact',
image: 'content-compact.svg'
},
{
name: 'wide',
title: 'Wide',
image: 'content-wide.svg'
}
]
// Directions
TemplateCustomizer.DIRECTIONS = [
{
name: 'ltr',
title: 'Left to Right (En)',
image: 'direction-ltr.svg'
},
{
name: 'rtl',
title: 'Right to Left (Ar)',
image: 'direction-rtl.svg'
}
]
// Theme setting language
TemplateCustomizer.LANGUAGES = {
en: {
panel_header: 'Template Customizer',
panel_sub_header: 'Customize and preview in real time',
theming_header: 'Theming',
color_label: 'Primary Color',
theme_label: 'Theme',
skin_label: 'Skins',
semiDark_label: 'Semi Dark',
layout_header: 'Layout',
layout_label: 'Menu (Navigation)',
layout_header_label: 'Header Types',
content_label: 'Content',
layout_navbar_label: 'Navbar Type',
direction_label: 'Direction'
},
fr: {
panel_header: 'Modèle De Personnalisation',
panel_sub_header: 'Personnalisez et prévisualisez en temps réel',
theming_header: 'Thématisation',
color_label: 'Couleur primaire',
theme_label: 'Thème',
skin_label: 'Peaux',
semiDark_label: 'Demi-foncé',
layout_header: 'Disposition',
layout_label: 'Menu (Navigation)',
layout_header_label: "Types d'en-tête",
content_label: 'Contenu',
layout_navbar_label: 'Type de barre de navigation',
direction_label: 'Direction'
},
ar: {
panel_header: 'أداة تخصيص القالب',
panel_sub_header: 'تخصيص ومعاينة في الوقت الحقيقي',
theming_header: 'السمات',
color_label: 'اللون الأساسي',
theme_label: 'سمة',
skin_label: 'جلود',
semiDark_label: 'شبه داكن',
layout_header: 'تَخطِيط',
layout_label: 'القائمة (الملاحة)',
layout_header_label: 'أنواع الرأس',
content_label: 'محتوى',
layout_navbar_label: 'نوع شريط التنقل',
direction_label: 'اتجاه'
},
de: {
panel_header: 'Vorlagen-Anpasser',
panel_sub_header: 'Anpassen und Vorschau in Echtzeit',
theming_header: 'Themen',
color_label: 'Grundfarbe',
theme_label: 'Thema',
skin_label: 'Skins',
semiDark_label: 'Halbdunkel',
layout_header: 'Layout',
layout_label: 'Menü (Navigation)',
layout_header_label: 'Header-Typen',
content_label: 'Inhalt',
layout_navbar_label: 'Art der Navigationsleiste',
direction_label: 'Richtung'
}
}
window.TemplateCustomizer = TemplateCustomizer
export { TemplateCustomizer }
/**
* Initialize color picker functionality for template customization
* Caches DOM elements and handles color picker setup
*/
const initializeColorPicker = () => {
// Cache DOM elements
const elements = {
pickerWrapper: document.querySelector('.template-customizer-colors-options input[value="picker"]'),
pickerEl: document.querySelector('.customizer-nano-picker'),
pcrButton: document.querySelector('.custom-option-content .pcr-button')
}
// Early return if required elements missing
if (!elements.pickerWrapper || !elements.pickerEl) {
console.warn('Required color picker elements not found')
return
}
// Seth the current color based on the checked state of the picker wrapper
const currentColor =
elements.pickerWrapper.getAttribute('checked') === 'checked'
? window.templateCustomizer._getSetting('Color')
? window.templateCustomizer._getSetting('Color')
: window.templateCustomizer.settings.defaultPrimaryColor
: '#FF4961'
// Configure and initialize Pickr color picker
const picker = new Pickr({
el: elements.pickerEl,
theme: 'nano',
default: currentColor,
defaultRepresentation: 'HEX',
comparison: false,
components: {
hue: true,
preview: true,
interaction: {
input: true
}
}
})
// Add color picker icon
picker._root.button.classList.add('ti', 'tabler-color-picker')
// Handle color changes
picker.on('change', color => {
const hexColor = color.toHEXA().toString()
const rgbaColor = color.toRGBA().toString()
// Update picker button color if exists
elements.pcrButton?.style.setProperty('--pcr-color', rgbaColor)
// Update selected option state
elements.pickerWrapper.checked = true
window.Helpers.updateCustomOptionCheck(elements.pickerWrapper)
// Update theme color settings
window.templateCustomizer._setSetting('Color', hexColor)
window.templateCustomizer.setColor(hexColor, true)
})
}
window.onload = () => {
initializeColorPicker()
const pcrButton = document.querySelector('.custom-option-content .pcr-button')
pcrButton?.style.setProperty('--pcr-color', window.templateCustomizer.settings.defaultPrimaryColor)
}