Нет описания
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

template-customizer.js 62KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826
  1. import './_template-customizer/_template-customizer.scss'
  2. import customizerMarkup from './_template-customizer/_template-customizer.html?raw'
  3. const CONTROLS = [
  4. 'color',
  5. 'theme',
  6. 'skins',
  7. 'semiDark',
  8. 'contentLayout',
  9. 'headerType',
  10. 'layoutCollapsed',
  11. 'layoutNavbarOptions',
  12. 'rtl',
  13. 'layoutFooterFixed',
  14. 'showDropdownOnHover'
  15. ]
  16. const THEMES = ['light', 'dark', 'system']
  17. let layoutNavbarVar
  18. const cl = document.documentElement.classList
  19. if (cl.contains('layout-navbar-fixed')) layoutNavbarVar = 'sticky'
  20. else if (cl.contains('layout-navbar-hidden')) layoutNavbarVar = 'hidden'
  21. else layoutNavbarVar = 'static'
  22. const DISPLAY_CUSTOMIZER = true
  23. const DEFAULT_THEME = document.documentElement.getAttribute('data-bs-theme') || 'light'
  24. const DEFAULT_SKIN = document.getElementsByTagName('HTML')[0].getAttribute('data-skin') || 0
  25. const DEFAULT_CONTENT_LAYOUT = cl.contains('layout-wide') ? 'wide' : 'compact'
  26. let headerType
  27. if (cl.contains('layout-menu-offcanvas')) {
  28. headerType = 'static-offcanvas'
  29. } else if (cl.contains('layout-menu-fixed')) {
  30. headerType = 'fixed'
  31. } else if (cl.contains('layout-menu-fixed-offcanvas')) {
  32. headerType = 'fixed-offcanvas'
  33. } else {
  34. headerType = 'static'
  35. }
  36. const DEFAULT_HEADER_TYPE = headerType
  37. const DEFAULT_MENU_COLLAPSED = !!cl.contains('layout-menu-collapsed')
  38. const DEFAULT_NAVBAR_FIXED = layoutNavbarVar
  39. const DEFAULT_TEXT_DIR = document.documentElement.getAttribute('dir') === 'rtl'
  40. const DEFAULT_FOOTER_FIXED = !!cl.contains('layout-footer-fixed')
  41. const DEFAULT_SHOW_DROPDOWN_ON_HOVER = true
  42. let primaryColorFlag
  43. const rootStyles = getComputedStyle(document.documentElement)
  44. class TemplateCustomizer {
  45. constructor({
  46. displayCustomizer,
  47. lang,
  48. defaultPrimaryColor,
  49. defaultSkin,
  50. defaultTheme,
  51. defaultSemiDark,
  52. defaultContentLayout,
  53. defaultHeaderType,
  54. defaultMenuCollapsed,
  55. defaultNavbarType,
  56. defaultTextDir,
  57. defaultFooterFixed,
  58. defaultShowDropdownOnHover,
  59. controls,
  60. themes,
  61. availableColors,
  62. availableSkins,
  63. availableThemes,
  64. availableContentLayouts,
  65. availableHeaderTypes,
  66. availableMenuCollapsed,
  67. availableNavbarOptions,
  68. availableDirections,
  69. onSettingsChange
  70. }) {
  71. if (this._ssr) return
  72. if (!window.Helpers) throw new Error('window.Helpers required.')
  73. this.settings = {}
  74. this.settings.displayCustomizer = typeof displayCustomizer !== 'undefined' ? displayCustomizer : DISPLAY_CUSTOMIZER
  75. this.settings.lang = lang || 'en'
  76. if (defaultPrimaryColor) {
  77. this.settings.defaultPrimaryColor = defaultPrimaryColor
  78. primaryColorFlag = true
  79. } else {
  80. this.settings.defaultPrimaryColor = rootStyles.getPropertyValue('--bs-primary').trim()
  81. primaryColorFlag = false
  82. }
  83. this.settings.defaultTheme = defaultTheme || DEFAULT_THEME
  84. this.settings.defaultSemiDark = typeof defaultSemiDark !== 'undefined' ? defaultSemiDark : false
  85. this.settings.defaultContentLayout =
  86. typeof defaultContentLayout !== 'undefined' ? defaultContentLayout : DEFAULT_CONTENT_LAYOUT
  87. this.settings.defaultHeaderType = defaultHeaderType || DEFAULT_HEADER_TYPE
  88. this.settings.defaultMenuCollapsed =
  89. typeof defaultMenuCollapsed !== 'undefined' ? defaultMenuCollapsed : DEFAULT_MENU_COLLAPSED
  90. this.settings.defaultNavbarType =
  91. typeof defaultNavbarType !== 'undefined' ? defaultNavbarType : DEFAULT_NAVBAR_FIXED
  92. this.settings.defaultTextDir = defaultTextDir === 'rtl' ? true : false || DEFAULT_TEXT_DIR
  93. this.settings.defaultFooterFixed =
  94. typeof defaultFooterFixed !== 'undefined' ? defaultFooterFixed : DEFAULT_FOOTER_FIXED
  95. this.settings.defaultShowDropdownOnHover =
  96. typeof defaultShowDropdownOnHover !== 'undefined' ? defaultShowDropdownOnHover : DEFAULT_SHOW_DROPDOWN_ON_HOVER
  97. this.settings.controls = controls || CONTROLS
  98. this.settings.availableColors = availableColors || TemplateCustomizer.COLORS
  99. this.settings.availableSkins = availableSkins || TemplateCustomizer.SKINS
  100. this.settings.availableThemes = availableThemes || TemplateCustomizer.THEMES
  101. this.settings.availableContentLayouts = availableContentLayouts || TemplateCustomizer.CONTENT
  102. this.settings.availableHeaderTypes = availableHeaderTypes || TemplateCustomizer.HEADER_TYPES
  103. this.settings.availableMenuCollapsed = availableMenuCollapsed || TemplateCustomizer.LAYOUTS
  104. this.settings.availableNavbarOptions = availableNavbarOptions || TemplateCustomizer.NAVBAR_OPTIONS
  105. this.settings.availableDirections = availableDirections || TemplateCustomizer.DIRECTIONS
  106. this.settings.defaultSkin = this._getDefaultSkin(typeof defaultSkin !== 'undefined' ? defaultSkin : DEFAULT_SKIN)
  107. this.settings.themes = themes || THEMES
  108. if (this.settings.themes.length < 2) {
  109. const i = this.settings.controls.indexOf('theme')
  110. if (i !== -1) {
  111. this.settings.controls = this.settings.controls.slice(0, i).concat(this.settings.controls.slice(i + 1))
  112. }
  113. }
  114. this.settings.onSettingsChange = typeof onSettingsChange === 'function' ? onSettingsChange : () => {}
  115. this._loadSettings()
  116. this._listeners = []
  117. this._controls = {}
  118. this._initDirection()
  119. this.setContentLayout(this.settings.contentLayout, false)
  120. this.setHeaderType(this.settings.headerType, false)
  121. this.setLayoutNavbarOption(this.settings.layoutNavbarOptions, false)
  122. this.setLayoutFooterFixed(this.settings.layoutFooterFixed, false)
  123. this.setDropdownOnHover(this.settings.showDropdownOnHover, false)
  124. this._setup()
  125. }
  126. setColor(color, defaultChange = false) {
  127. // Check if color is actually different from current setting
  128. const currentColor = this._getSetting('PrimaryColor')
  129. const defaultPrimaryColor = TemplateCustomizer.COLORS[0].color
  130. // Only apply changes if color is different or being explicitly set (defaultChange)
  131. if (color && (color !== currentColor || defaultChange)) {
  132. // Use Helpers method
  133. window.Helpers.setColor(color, defaultChange)
  134. // Save color in cookie
  135. const isAdmin = this._isAdminLayout()
  136. const cookieName = isAdmin ? 'admin-primaryColor' : 'front-primaryColor'
  137. // If color is the default, remove the cookie to use CSS default
  138. if (color === defaultPrimaryColor && !defaultChange) {
  139. this._deleteCookie(cookieName)
  140. } else {
  141. this._setCookie(cookieName, color, 365)
  142. }
  143. // Save to settings
  144. this._setSetting('PrimaryColor', color)
  145. }
  146. // Update color picker UI
  147. const colorOptions = document.querySelectorAll(
  148. '.template-customizer-color .template-customizer-colors-options input[type="radio"]'
  149. )
  150. let foundMatchingOption = false
  151. // First check if this is one of the predefined color options
  152. colorOptions.forEach(option => {
  153. const isMatch = option.value === color
  154. option.checked = isMatch
  155. if (isMatch) {
  156. foundMatchingOption = true
  157. if (typeof window.Helpers.updateCustomOptionCheck === 'function') {
  158. window.Helpers.updateCustomOptionCheck(option)
  159. }
  160. }
  161. })
  162. // If no predefined color matches, select the picker option
  163. if (!foundMatchingOption) {
  164. const pickerOption = document.querySelector('.template-customizer-colors-options input[value="picker"]')
  165. if (pickerOption) {
  166. pickerOption.checked = true
  167. if (typeof window.Helpers.updateCustomOptionCheck === 'function') {
  168. window.Helpers.updateCustomOptionCheck(pickerOption)
  169. }
  170. // Update picker button color
  171. const pcrButton = document.querySelector('.custom-option-content .pcr-button')
  172. if (pcrButton) {
  173. pcrButton.style.setProperty('--pcr-color', color)
  174. }
  175. }
  176. }
  177. // Trigger settings change callback
  178. }
  179. setTheme(theme) {
  180. const layoutName = this._getLayoutName()
  181. const isAdmin = !layoutName.includes('front')
  182. this._setSetting('Theme', theme)
  183. const modeCookieName = isAdmin ? 'admin-mode' : 'front-mode'
  184. const colorPrefCookieName = isAdmin ? 'admin-colorPref' : 'front-colorPref'
  185. if (theme !== '' && this._checkCookie(modeCookieName)) {
  186. if (theme === 'system') {
  187. this._setCookie(modeCookieName, 'system', 365)
  188. if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
  189. this._setCookie(colorPrefCookieName, 'dark', 365)
  190. } else {
  191. this._setCookie(colorPrefCookieName, 'light', 365)
  192. }
  193. } else {
  194. if (theme === 'dark') {
  195. this._setCookie(modeCookieName, 'dark', 365)
  196. this._setCookie(colorPrefCookieName, 'dark', 365)
  197. } else {
  198. this._setCookie(modeCookieName, 'light', 365)
  199. this._setCookie(colorPrefCookieName, 'light', 365)
  200. }
  201. }
  202. } else {
  203. this._setCookie(modeCookieName, theme || 'light', 365)
  204. }
  205. }
  206. setSkin(skinName, updateStorage = true, cb = null) {
  207. if (!this._hasControls('skins')) return
  208. let skin
  209. // If skin is a number or numeric string, find skin by id
  210. if (!isNaN(parseInt(skinName))) {
  211. const skinId = parseInt(skinName)
  212. skin = this.settings.availableSkins.find(s => s.id === skinId)
  213. // If found by ID, use that skin
  214. if (!skin) return
  215. skinName = skin.name // Convert ID to name for further processing
  216. } else {
  217. // Otherwise find skin by name
  218. skin = this.settings.availableSkins.find(s => s.name === skinName)
  219. if (!skin) return
  220. }
  221. this.settings.skin = skin
  222. // Update skin radio inputs in the customizer UI
  223. const skinRadios = document.querySelectorAll('.template-customizer-skin-options input[type="radio"]')
  224. if (skinRadios.length) {
  225. skinRadios.forEach(radio => {
  226. if (radio.value === skin.name) {
  227. radio.checked = true
  228. // If using custom options, update those too
  229. const customOptionEl = radio.closest('.custom-option')
  230. if (customOptionEl && window.Helpers && typeof window.Helpers.updateCustomOptionCheck === 'function') {
  231. window.Helpers.updateCustomOptionCheck(radio)
  232. }
  233. } else {
  234. radio.checked = false
  235. }
  236. })
  237. }
  238. // Set data attribute
  239. document.documentElement.setAttribute('data-skin', skin.name)
  240. // Only set cookie for admin layouts
  241. if (updateStorage) {
  242. // Set localStorage
  243. this._setSetting('Skin', skinName)
  244. // Only set cookie in admin layout
  245. if (this._isAdminLayout()) {
  246. // Use standardized cookie name
  247. this._setCookie('customize_skin', skin.name, 365)
  248. }
  249. this.settings.onSettingsChange.call(this, this.settings)
  250. }
  251. if (cb) {
  252. cb()
  253. }
  254. }
  255. setLayoutNavbarOption(navbarType, updateStorage = true) {
  256. if (!this._hasControls('layoutNavbarOptions')) return
  257. this.settings.layoutNavbarOptions = navbarType
  258. if (updateStorage) this._setSetting('FixedNavbarOption', navbarType)
  259. window.Helpers.setNavbar(navbarType)
  260. if (updateStorage) this.settings.onSettingsChange.call(this, this.settings)
  261. }
  262. setContentLayout(contentLayout, updateStorage = true) {
  263. if (!this._hasControls('contentLayout')) return
  264. this.settings.contentLayout = contentLayout
  265. if (updateStorage) this._setSetting('contentLayout', contentLayout)
  266. window.Helpers.setContentLayout(contentLayout)
  267. if (updateStorage) this.settings.onSettingsChange.call(this, this.settings)
  268. }
  269. setHeaderType(pos, updateStorage = true) {
  270. if (!this._hasControls('headerType')) return
  271. if (!['static', 'static-offcanvas', 'fixed', 'fixed-offcanvas'].includes(pos)) return
  272. this.settings.headerType = pos
  273. if (updateStorage) this._setSetting('HeaderType', pos)
  274. window.Helpers.setPosition(
  275. pos === 'fixed' || pos === 'fixed-offcanvas',
  276. pos === 'static-offcanvas' || pos === 'fixed-offcanvas'
  277. )
  278. if (updateStorage) this.settings.onSettingsChange.call(this, this.settings)
  279. // Perfect Scrollbar change on Layout change
  280. let menuScroll = window.Helpers.menuPsScroll
  281. const PerfectScrollbarLib = window.PerfectScrollbar
  282. if (this.settings.headerType === 'fixed' || this.settings.headerType === 'fixed-offcanvas') {
  283. // Set perfect scrollbar wheelPropagation false for fixed layout
  284. if (PerfectScrollbarLib && menuScroll) {
  285. window.Helpers.menuPsScroll.destroy()
  286. menuScroll = new PerfectScrollbarLib(document.querySelector('.menu-inner'), {
  287. suppressScrollX: true,
  288. wheelPropagation: false
  289. })
  290. window.Helpers.menuPsScroll = menuScroll
  291. }
  292. } else if (menuScroll) {
  293. // Destroy perfect scrollbar for static layout
  294. window.Helpers.menuPsScroll.destroy()
  295. }
  296. }
  297. setLayoutFooterFixed(fixed, updateStorage = true) {
  298. // if (!this._hasControls('layoutFooterFixed')) return
  299. this.settings.layoutFooterFixed = fixed
  300. if (updateStorage) this._setSetting('FixedFooter', fixed)
  301. window.Helpers.setFooterFixed(fixed)
  302. if (updateStorage) this.settings.onSettingsChange.call(this, this.settings)
  303. }
  304. setDropdownOnHover(open, updateStorage = true) {
  305. if (!this._hasControls('showDropdownOnHover')) return
  306. // Convert to boolean if string
  307. const isOpen = typeof open === 'string' ? open === 'true' : Boolean(open)
  308. this.settings.showDropdownOnHover = isOpen
  309. // Only proceed if we're in a horizontal layout
  310. if (document.querySelector('.layout-menu-horizontal.menu, .layout-menu-horizontal .menu')) {
  311. if (window.Helpers.mainMenu) {
  312. window.Helpers.mainMenu.destroy()
  313. const { Menu } = window
  314. window.Helpers.mainMenu = new Menu(document.getElementById('layout-menu'), {
  315. orientation: 'horizontal',
  316. closeChildren: true,
  317. showDropdownOnHover: isOpen
  318. })
  319. }
  320. // Set localStorage
  321. if (updateStorage) {
  322. this._setSetting('ShowDropdownOnHover', isOpen)
  323. // Set cookie with standardized name
  324. this._setCookie('showDropdownOnHover', isOpen.toString(), 365)
  325. }
  326. if (updateStorage) this.settings.onSettingsChange.call(this, this.settings)
  327. }
  328. }
  329. setRtl(rtl) {
  330. if (!this._hasControls('rtl')) return
  331. this._setSetting('Rtl', String(rtl))
  332. this._setCookie('direction', rtl ? 'true' : 'false', 365)
  333. }
  334. setLang(lang, updateStorage = true, force = false) {
  335. if (lang === this.settings.lang && !force) return
  336. if (!TemplateCustomizer.LANGUAGES[lang]) throw new Error(`Language "${lang}" not found!`)
  337. const t = TemplateCustomizer.LANGUAGES[lang]
  338. ;[
  339. 'panel_header',
  340. 'panel_sub_header',
  341. 'theming_header',
  342. 'color_label',
  343. 'theme_label',
  344. 'style_switch_light',
  345. 'style_switch_dark',
  346. 'layout_header',
  347. 'layout_label',
  348. 'layout_header_label',
  349. 'content_label',
  350. 'layout_static',
  351. 'layout_offcanvas',
  352. 'layout_fixed',
  353. 'layout_fixed_offcanvas',
  354. 'layout_dd_open_label',
  355. 'layout_navbar_label',
  356. 'layout_footer_label',
  357. 'misc_header',
  358. 'skin_label',
  359. 'semiDark_label',
  360. 'direction_label'
  361. ].forEach(key => {
  362. const el = this.container.querySelector(`.template-customizer-t-${key}`)
  363. // eslint-disable-next-line no-unused-expressions
  364. el && (el.textContent = t[key])
  365. })
  366. this.settings.lang = lang
  367. if (updateStorage) this._setSetting('Lang', lang)
  368. if (updateStorage) this.settings.onSettingsChange.call(this, this.settings)
  369. }
  370. // Update theme settings control
  371. update() {
  372. if (this._ssr) return
  373. const hasNavbar = !!document.querySelector('.layout-navbar')
  374. const hasMenu = !!document.querySelector('.layout-menu')
  375. const hasHorizontalMenu = !!document.querySelector('.layout-menu-horizontal.menu, .layout-menu-horizontal .menu')
  376. const hasFooter = !!document.querySelector('.content-footer')
  377. if (this._controls.showDropdownOnHover) {
  378. if (hasMenu) {
  379. this._controls.showDropdownOnHover.setAttribute('disabled', 'disabled')
  380. this._controls.showDropdownOnHover.classList.add('disabled')
  381. } else {
  382. this._controls.showDropdownOnHover.removeAttribute('disabled')
  383. this._controls.showDropdownOnHover.classList.remove('disabled')
  384. }
  385. }
  386. if (this._controls.layoutNavbarOptions) {
  387. if (!hasNavbar) {
  388. this._controls.layoutNavbarOptions.setAttribute('disabled', 'disabled')
  389. this._controls.layoutNavbarOptionsW.classList.add('disabled')
  390. } else {
  391. this._controls.layoutNavbarOptions.removeAttribute('disabled')
  392. this._controls.layoutNavbarOptionsW.classList.remove('disabled')
  393. }
  394. // Horizontal menu fixed layout - disabled fixed navbar switch
  395. if (hasHorizontalMenu && hasNavbar && this.settings.headerType === 'fixed') {
  396. this._controls.layoutNavbarOptions.setAttribute('disabled', 'disabled')
  397. this._controls.layoutNavbarOptionsW.classList.add('disabled')
  398. }
  399. }
  400. if (this._controls.layoutFooterFixed) {
  401. if (!hasFooter) {
  402. this._controls.layoutFooterFixed.setAttribute('disabled', 'disabled')
  403. this._controls.layoutFooterFixedW.classList.add('disabled')
  404. } else {
  405. this._controls.layoutFooterFixed.removeAttribute('disabled')
  406. this._controls.layoutFooterFixedW.classList.remove('disabled')
  407. }
  408. }
  409. if (this._controls.headerType) {
  410. // Disable menu layouts options if menu (vertical or horizontal) is not there
  411. if (hasMenu || hasHorizontalMenu) {
  412. // (Updated condition)
  413. this._controls.headerType.removeAttribute('disabled')
  414. } else {
  415. this._controls.headerType.setAttribute('disabled', 'disabled')
  416. }
  417. }
  418. }
  419. // Clear local storage
  420. clearLocalStorage() {
  421. if (this._ssr) return
  422. // Clear all local storage settings
  423. const layoutName = this._getLayoutName()
  424. const keysToRemove = [
  425. 'Color',
  426. 'Theme',
  427. 'Skin',
  428. 'SemiDark',
  429. 'LayoutCollapsed',
  430. 'FixedNavbarOption',
  431. 'HeaderType',
  432. 'contentLayout',
  433. 'Rtl',
  434. 'Lang'
  435. ]
  436. keysToRemove.forEach(key => {
  437. const localStorageKey = `templateCustomizer-${layoutName}--${key}`
  438. localStorage.removeItem(localStorageKey)
  439. })
  440. // Clear all cookies
  441. const cookiesToClear = [
  442. // Standardized cookie names
  443. 'customize_skin',
  444. 'customize_semi_dark',
  445. 'direction',
  446. 'LayoutCollapsed',
  447. 'contentLayout',
  448. 'navbarType',
  449. 'headerType',
  450. // Legacy cookie names for backward compatibility
  451. 'admin-mode',
  452. 'front-mode',
  453. 'admin-colorPref',
  454. 'front-colorPref',
  455. 'admin-primaryColor',
  456. 'front-primaryColor'
  457. ]
  458. cookiesToClear.forEach(cookie => {
  459. this._deleteCookie(cookie)
  460. })
  461. this._showResetBtnNotification(false)
  462. }
  463. // Clear local storage
  464. destroy() {
  465. if (this._ssr) return
  466. this._cleanup()
  467. this.settings = null
  468. this.container.parentNode.removeChild(this.container)
  469. this.container = null
  470. }
  471. _loadSettings() {
  472. // Get settings
  473. const rtlOption = this._getSetting('Rtl')
  474. const color = this._getSetting('Color')
  475. const theme = this._getSetting('Theme')
  476. const skin = this._getSetting('Skin')
  477. const semiDark = this._getSetting('SemiDark') // Default value will be set from main.js
  478. const contentLayout = this._getSetting('contentLayout')
  479. const collapsedMenu = this._getSetting('LayoutCollapsed') // Value will be set from main.js
  480. const dropdownOnHover = this._getSetting('ShowDropdownOnHover') // Value will be set from main.js
  481. const navbarOption = this._getSetting('FixedNavbarOption')
  482. const fixedFooter = this._getSetting('FixedFooter')
  483. const layoutType = this._getSetting('HeaderType')
  484. const layoutName = this._getLayoutName()
  485. const isAdmin = !layoutName.includes('front')
  486. const modeCookieName = isAdmin ? 'admin-mode' : 'front-mode'
  487. const colorPrefCookieName = isAdmin ? 'admin-colorPref' : 'front-colorPref'
  488. // Get dropdown hover state from cookie or fall back to localStorage then default
  489. const dropdownHoverCookie = this._getCookie('showDropdownOnHover')
  490. const dropdownHoverValue =
  491. dropdownHoverCookie !== null
  492. ? dropdownHoverCookie === 'true'
  493. : dropdownOnHover
  494. ? dropdownOnHover === 'true'
  495. : this.settings.defaultShowDropdownOnHover
  496. // Reset Button
  497. if (
  498. rtlOption ||
  499. theme ||
  500. skin ||
  501. contentLayout ||
  502. collapsedMenu ||
  503. navbarOption ||
  504. layoutType ||
  505. color ||
  506. semiDark
  507. ) {
  508. this._showResetBtnNotification(true)
  509. } else {
  510. this._showResetBtnNotification(false)
  511. }
  512. // Header Type
  513. this.settings.headerType = ['static', 'static-offcanvas', 'fixed', 'fixed-offcanvas'].includes(layoutType)
  514. ? layoutType
  515. : this.settings.defaultHeaderType
  516. // ! Set settings by following priority: Local Storage, Theme Config, HTML Classes
  517. this.settings.rtl = rtlOption !== '' ? rtlOption === 'true' : this.settings.defaultTextDir
  518. // Color
  519. if (color) {
  520. primaryColorFlag = true
  521. }
  522. this.settings.color = color || this.settings.defaultPrimaryColor
  523. this.setColor(this.settings.color, primaryColorFlag)
  524. // Style
  525. this.settings.themesOpt = this.settings.themes.includes(theme) ? theme : this.settings.defaultTheme
  526. // Get the systemTheme value using JS
  527. const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
  528. // 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
  529. let appliedTheme
  530. if (this.settings.themes.includes(theme)) {
  531. appliedTheme = theme === 'system' ? systemTheme : theme
  532. } else if (this.settings.defaultTheme === 'system') {
  533. appliedTheme = systemTheme
  534. } else {
  535. appliedTheme = this.settings.defaultTheme
  536. }
  537. this.settings.theme = this.settings.defaultTheme
  538. document.documentElement.setAttribute('data-bs-theme', appliedTheme)
  539. if (this._getCookie(modeCookieName) === 'system') {
  540. if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
  541. this._setCookie(colorPrefCookieName, 'dark', 365)
  542. this.settings.theme = 'dark'
  543. } else {
  544. this._setCookie(colorPrefCookieName, 'light', 365)
  545. this.settings.theme = 'light'
  546. }
  547. } else {
  548. if (this.settings.themesOpt === 'system') {
  549. if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
  550. this.settings.theme = 'dark'
  551. } else {
  552. this.settings.theme = 'light'
  553. }
  554. } else {
  555. this.settings.theme = this.settings.themes.includes(theme) ? theme : this.settings.themesOpt
  556. }
  557. }
  558. // Retrieve skin from multiple sources in order of priority:
  559. // 1. localStorage via _getSetting('Skin')
  560. // 2. Cookie value
  561. // 3. Default skin from constructor
  562. const storedSkin = this._getSetting('Skin')
  563. // Only process special skin/semiDark settings for admin layouts
  564. if (this._isAdminLayout()) {
  565. // Use standardized cookie name
  566. const skinCookieName = 'customize_skin'
  567. const skinFromCookie = this._getCookie(skinCookieName)
  568. const skinFromAttribute = document.documentElement.getAttribute('data-skin')
  569. // Use first available source in priority order
  570. const skinToUse = storedSkin || skinFromCookie || skinFromAttribute || this.settings.defaultSkin.name
  571. this.settings.skin = this._getSkinByName(skinToUse, true)
  572. // Ensure HTML element has the correct data-skin attribute
  573. if (this.settings.skin && this.settings.skin.name) {
  574. document.documentElement.setAttribute('data-skin', this.settings.skin.name)
  575. }
  576. // Semi Dark - only for admin layouts
  577. // First check localStorage, then cookie, then fallback to default
  578. const storedSemiDark = this._getSetting('SemiDark')
  579. const semiDarkCookieName = 'customize_semi_dark'
  580. const semiDarkFromCookie = this._getCookie(semiDarkCookieName)
  581. // Use first available source in priority order and convert to proper boolean
  582. const semiDarkValue = storedSemiDark || semiDarkFromCookie || this.settings.defaultSemiDark.toString()
  583. // Convert string value to boolean
  584. this.settings.semiDark = semiDarkValue === 'true'
  585. // Apply semi-dark setting to DOM
  586. if (this.settings.semiDark) {
  587. document.documentElement.setAttribute('data-semidark-menu', true)
  588. const menuElement = document.getElementById('layout-menu')
  589. if (menuElement) {
  590. menuElement.setAttribute('data-bs-theme', 'dark')
  591. }
  592. }
  593. } else {
  594. // For front-end layouts, use default skin and turn off semiDark
  595. this.settings.skin = this._getSkinByName('default', true)
  596. document.documentElement.setAttribute('data-skin', 'default')
  597. this.settings.semiDark = false
  598. document.documentElement.removeAttribute('data-semidark-menu')
  599. }
  600. // Content Layout
  601. this.settings.contentLayout = contentLayout || this.settings.defaultContentLayout
  602. // Layout Collapsed
  603. this.settings.layoutCollapsed = collapsedMenu ? collapsedMenu === 'true' : this.settings.defaultMenuCollapsed
  604. // Add layout-menu-collapsed class to the body if the menu is collapsed
  605. if (this.settings.layoutCollapsed) document.documentElement.classList.add('layout-menu-collapsed')
  606. // Dropdown on Hover - only enable for horizontal layout
  607. if (document.querySelector('.layout-menu-horizontal.menu, .layout-menu-horizontal .menu')) {
  608. this.settings.showDropdownOnHover = dropdownHoverValue
  609. // Initialize menu with current setting
  610. if (window.Helpers.mainMenu) {
  611. window.Helpers.mainMenu.destroy()
  612. const { Menu } = window
  613. window.Helpers.mainMenu = new Menu(document.getElementById('layout-menu'), {
  614. orientation: 'horizontal',
  615. closeChildren: true,
  616. showDropdownOnHover: dropdownHoverValue
  617. })
  618. }
  619. } else {
  620. this.settings.showDropdownOnHover = false
  621. }
  622. // Navbar Option
  623. this.settings.layoutNavbarOptions = ['static', 'sticky', 'hidden'].includes(navbarOption)
  624. ? navbarOption
  625. : this.settings.defaultNavbarType
  626. // Footer Fixed
  627. this.settings.layoutFooterFixed = fixedFooter ? fixedFooter === 'true' : this.settings.defaultFooterFixed
  628. // Filter options depending on available controls
  629. if (!this._hasControls('rtl')) this.settings.rtl = document.documentElement.getAttribute('dir') === 'rtl'
  630. if (!this._hasControls('theme')) this.settings.theme = window.Helpers.isDarkStyle() ? 'dark' : 'light'
  631. if (!this._hasControls('contentLayout')) this.settings.contentLayout = null
  632. if (!this._hasControls('headerType')) this.settings.headerType = null
  633. if (!this._hasControls('layoutCollapsed')) this.settings.layoutCollapsed = null
  634. if (!this._hasControls('layoutNavbarOptions')) this.settings.layoutNavbarOptions = null
  635. if (!this._hasControls('skins')) this.settings.skin = null
  636. if (!this._hasControls('semiDark')) this.settings.semiDark = null
  637. }
  638. // Setup theme settings controls and events
  639. _setup(_container = document) {
  640. // Function to create customizer elements
  641. const createOptionElement = (nameVal, title, inputName, image, isIcon = false) => {
  642. const divElement = document.createElement('div')
  643. divElement.classList.add('col-4', 'px-2')
  644. // Determine the correct classes based on whether it's an icon or image
  645. const optionClass = isIcon
  646. ? 'custom-option custom-option-icon'
  647. : 'custom-option custom-option-image custom-option-image-radio'
  648. // Create the inner HTML structure
  649. divElement.innerHTML = `
  650. <div class="form-check ${optionClass} mb-0">
  651. <label class="form-check-label custom-option-content p-0" for="${inputName}${nameVal}">
  652. <span class="custom-option-body mb-0 scaleX-n1-rtl"></span>
  653. </label>
  654. <input
  655. name="${inputName}"
  656. class="form-check-input d-none"
  657. type="radio"
  658. value="${nameVal}"
  659. id="${inputName}${nameVal}" />
  660. </div>
  661. <label class="form-check-label small text-nowrap text-body" for="${inputName}${nameVal}">${title}</label>
  662. `
  663. if (isIcon) {
  664. // If it's an icon, insert the icon HTML directly
  665. divElement.querySelector('.custom-option-body').innerHTML = image
  666. } else {
  667. // Otherwise, assume it's an SVG file name and fetch its content
  668. fetch(`${assetsPath}img/customizer/${image}`)
  669. .then(response => response.text())
  670. .then(svgContent => {
  671. // Insert the SVG content into the HTML
  672. divElement.querySelector('.custom-option-body').innerHTML = svgContent
  673. })
  674. .catch(error => console.error('Error loading SVG:', error))
  675. }
  676. return divElement
  677. }
  678. this._cleanup()
  679. this.container = this._getElementFromString(customizerMarkup)
  680. // Customizer visibility
  681. //
  682. this.container.setAttribute('style', `visibility: ${this.settings.displayCustomizer ? 'visible' : 'hidden'}`)
  683. // Open btn
  684. const openBtn = this.container.querySelector('.template-customizer-open-btn')
  685. const openBtnCb = () => {
  686. this.container.classList.add('template-customizer-open')
  687. this.update()
  688. if (this._updateInterval) clearInterval(this._updateInterval)
  689. this._updateInterval = setInterval(() => {
  690. this.update()
  691. }, 500)
  692. }
  693. openBtn.addEventListener('click', openBtnCb)
  694. this._listeners.push([openBtn, 'click', openBtnCb])
  695. // Reset btn
  696. const resetBtn = this.container.querySelector('.template-customizer-reset-btn')
  697. const resetBtnCb = () => {
  698. // Just clear all storage and reload the page
  699. this.clearLocalStorage()
  700. window.location.reload()
  701. }
  702. resetBtn.addEventListener('click', resetBtnCb)
  703. this._listeners.push([resetBtn, 'click', resetBtnCb])
  704. // Close btn
  705. const closeBtn = this.container.querySelector('.template-customizer-close-btn')
  706. const closeBtnCb = () => {
  707. this.container.classList.remove('template-customizer-open')
  708. if (this._updateInterval) {
  709. clearInterval(this._updateInterval)
  710. this._updateInterval = null
  711. }
  712. }
  713. closeBtn.addEventListener('click', closeBtnCb)
  714. this._listeners.push([closeBtn, 'click', closeBtnCb])
  715. // Color
  716. const colorW = this.container.querySelector('.template-customizer-color')
  717. const colorOpt = colorW.querySelector('.template-customizer-colors-options')
  718. if (!this._hasControls('color')) {
  719. colorW.parentNode.removeChild(colorW)
  720. } else {
  721. const inputName = 'colorRadioIcon'
  722. this.settings.availableColors.forEach(color => {
  723. const colorEl = `<div class="form-check custom-option custom-option-icon mb-0">
  724. <label class="form-check-label custom-option-content p-0" for="${inputName}${color.name}">
  725. <span class="custom-option-body mb-0 scaleX-n1-rtl" style="background-color: ${color.color};"></span>
  726. </label>
  727. <input
  728. name="${inputName}"
  729. class="form-check-input d-none"
  730. type="radio"
  731. value="${color.color}"
  732. data-color="${color.color}"
  733. id="${inputName}${color.name}" />
  734. </div>`
  735. // convert colorEl string to HTML element
  736. const colorEle = this._getElementFromString(colorEl)
  737. colorOpt.appendChild(colorEle)
  738. })
  739. colorOpt.appendChild(
  740. this._getElementFromString(
  741. '<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="bx bxs-color-fill icon-base"></i></span></label><input name="colorRadioIcon" class="form-check-input picker d-none" type="radio" value="picker" id="colorRadioIcon" /> </div>'
  742. )
  743. )
  744. const colorSelector = colorOpt.querySelector(`input[value="${this.settings.color}"]`)
  745. if (colorSelector) {
  746. colorSelector.setAttribute('checked', 'checked')
  747. colorOpt.querySelector('input[value="picker"]').removeAttribute('checked')
  748. } else {
  749. colorOpt.querySelector('input[value="picker"]').setAttribute('checked', 'checked')
  750. }
  751. const colorCb = e => {
  752. if (e.target.value === 'picker') {
  753. document.querySelector('.custom-option-content .pcr-button').click()
  754. } else {
  755. this._setSetting('Color', e.target.dataset.color)
  756. this.setColor(
  757. e.target.dataset.color,
  758. () => {
  759. this._loadingState(false)
  760. },
  761. true
  762. )
  763. }
  764. }
  765. colorOpt.addEventListener('change', colorCb)
  766. this._listeners.push([colorOpt, 'change', colorCb])
  767. }
  768. // Theme
  769. const themeW = this.container.querySelector('.template-customizer-theme')
  770. const themeOpt = themeW.querySelector('.template-customizer-themes-options')
  771. if (!this._hasControls('theme')) {
  772. themeW.parentNode.removeChild(themeW)
  773. } else {
  774. this.settings.availableThemes.forEach(theme => {
  775. const themeEl = createOptionElement(theme.name, theme.title, 'customRadioIcon', theme.image, true)
  776. themeOpt.appendChild(themeEl)
  777. })
  778. if (themeOpt.querySelector(`input[value="${this.settings.themesOpt}"]`)) {
  779. themeOpt.querySelector(`input[value="${this.settings.themesOpt}"]`).setAttribute('checked', 'checked')
  780. }
  781. // themeCb
  782. const themeCb = e => {
  783. document.documentElement.setAttribute('data-bs-theme', e.target.value)
  784. if (this._hasControls('semiDark')) {
  785. const semiDarkL = this.container.querySelector('.template-customizer-semiDark')
  786. if (e.target.value === 'dark') {
  787. semiDarkL.classList.add('d-none')
  788. } else {
  789. semiDarkL.classList.remove('d-none')
  790. }
  791. }
  792. // Update below commented code for data-bs-theme-value attribute value matches with e.target.value
  793. window.Helpers.syncThemeToggles(e.target.value)
  794. this.setTheme(e.target.value, true, () => {
  795. this._loadingState(false)
  796. })
  797. }
  798. themeOpt.addEventListener('change', themeCb)
  799. this._listeners.push([themeOpt, 'change', themeCb])
  800. }
  801. // Skin
  802. const skinsW = this.container.querySelector('.template-customizer-skins')
  803. const skinsWInner = skinsW.querySelector('.template-customizer-skins-options')
  804. if (!this._hasControls('skins')) {
  805. skinsW.parentNode.removeChild(skinsW)
  806. } else {
  807. this.settings.availableSkins.forEach(skin => {
  808. const skinEl = createOptionElement(skin.name, skin.title, 'skinRadios', skin.image)
  809. skinsWInner.appendChild(skinEl)
  810. })
  811. // Get the current skin name, either from settings or data attribute
  812. const currentSkinName = this.settings.skin.name || document.documentElement.getAttribute('data-skin') || 'default'
  813. // Find the radio button with matching value and mark it checked
  814. const skinInput = skinsWInner.querySelector(`input[value="${currentSkinName}"]`)
  815. if (skinInput) {
  816. skinInput.setAttribute('checked', 'checked')
  817. // If using custom options, update those too
  818. const customOptionEl = skinInput.closest('.custom-option')
  819. if (customOptionEl && window.Helpers && typeof window.Helpers.updateCustomOptionCheck === 'function') {
  820. window.Helpers.updateCustomOptionCheck(skinInput)
  821. }
  822. }
  823. document.documentElement.setAttribute('data-skin', currentSkinName)
  824. const skinCb = e => {
  825. document.documentElement.setAttribute('data-skin', e.target.value)
  826. this.setSkin(e.target.value, true, () => {
  827. this._loadingState(false, true)
  828. })
  829. }
  830. skinsWInner.addEventListener('change', skinCb)
  831. this._listeners.push([skinsWInner, 'change', skinCb])
  832. }
  833. // SemiDark
  834. // update menu's data-bs-theme attribute to dark & light on switch changes on & off respectively
  835. const semiDarkSwitch = this.container.querySelector('.template-customizer-semi-dark-switch')
  836. const semiDarkSection = this.container.querySelector('.template-customizer-semiDark')
  837. if (document.documentElement.getAttribute('data-bs-theme') === 'dark') {
  838. semiDarkSection.classList.add('d-none')
  839. }
  840. if (!this._hasControls('semiDark')) {
  841. semiDarkSection.remove()
  842. } else if (this._hasControls('semiDark') && this._getSetting('Theme') === 'dark') {
  843. semiDarkSwitch.classList.add('d-none')
  844. } else {
  845. // Check the switch if semi-dark is enabled
  846. if (this.settings.semiDark) {
  847. semiDarkSwitch.checked = true
  848. } else {
  849. semiDarkSwitch.checked = false
  850. }
  851. const semiDarkSwitchCb = e => {
  852. const isDark = e.target.checked
  853. const theme = isDark ? 'dark' : 'light'
  854. // Check if we're in admin layout - only apply in admin
  855. if (!this._isAdminLayout()) {
  856. // Skip for front-end layouts
  857. return
  858. }
  859. if (theme === 'dark') {
  860. document.getElementById('layout-menu').setAttribute('data-bs-theme', theme)
  861. document.documentElement.setAttribute('data-semidark-menu', true)
  862. } else {
  863. document.getElementById('layout-menu').removeAttribute('data-bs-theme')
  864. document.documentElement.removeAttribute('data-semidark-menu')
  865. }
  866. // Save to localStorage
  867. this._setSetting('SemiDark', isDark)
  868. // Save to cookie for server-side access - use standardized cookie name
  869. this._setCookie('customize_semi_dark', isDark.toString(), 365)
  870. }
  871. semiDarkSwitch.addEventListener('change', semiDarkSwitchCb)
  872. this._listeners.push([semiDarkSwitch, 'change', semiDarkSwitchCb])
  873. }
  874. const themingW = this.container.querySelector('.template-customizer-theming')
  875. if (
  876. !this._hasControls('color') &&
  877. !this._hasControls('theme') &&
  878. !this._hasControls('skins') &&
  879. !this._hasControls('semiDark')
  880. ) {
  881. themingW.parentNode.removeChild(themingW)
  882. }
  883. // Layout wrapper
  884. const layoutW = this.container.querySelector('.template-customizer-layout')
  885. if (!this._hasControls('contentLayout headerType layoutCollapsed layoutNavbarOptions rtl', true)) {
  886. layoutW.parentNode.removeChild(layoutW)
  887. } else {
  888. // Layouts Collapsed: Expanded, Collapsed
  889. const layoutCollapsedW = this.container.querySelector('.template-customizer-layouts')
  890. if (!this._hasControls('layoutCollapsed')) {
  891. layoutCollapsedW.parentNode.removeChild(layoutCollapsedW)
  892. } else {
  893. setTimeout(() => {
  894. if (document.querySelector('.layout-menu-horizontal')) {
  895. layoutCollapsedW.parentNode.removeChild(layoutCollapsedW)
  896. }
  897. }, 100)
  898. const layoutCollapsedOpt = layoutCollapsedW.querySelector('.template-customizer-layouts-options')
  899. this.settings.availableMenuCollapsed.forEach(layoutOpt => {
  900. const layoutsEl = createOptionElement(layoutOpt.name, layoutOpt.title, 'layoutsRadios', layoutOpt.image)
  901. layoutCollapsedOpt.appendChild(layoutsEl)
  902. })
  903. layoutCollapsedOpt
  904. .querySelector(`input[value="${this.settings.layoutCollapsed ? 'collapsed' : 'expanded'}"]`)
  905. .setAttribute('checked', 'checked')
  906. const layoutCb = e => {
  907. window.Helpers.setCollapsed(e.target.value === 'collapsed', true)
  908. this._setSetting('LayoutCollapsed', e.target.value === 'collapsed')
  909. this._setCookie('LayoutCollapsed', e.target.value === 'collapsed', 365)
  910. }
  911. layoutCollapsedOpt.addEventListener('change', layoutCb)
  912. this._listeners.push([layoutCollapsedOpt, 'change', layoutCb])
  913. }
  914. // CONTENT
  915. const contentWrapper = this.container.querySelector('.template-customizer-content')
  916. // ? Hide RTL control in following 2 case
  917. if (!this._hasControls('contentLayout')) {
  918. contentWrapper.parentNode.removeChild(contentWrapper)
  919. } else {
  920. const contentOpt = contentWrapper.querySelector('.template-customizer-content-options')
  921. this.settings.availableContentLayouts.forEach(content => {
  922. const contentEl = createOptionElement(content.name, content.title, 'contentRadioIcon', content.image)
  923. contentOpt.appendChild(contentEl)
  924. })
  925. contentOpt.querySelector(`input[value="${this.settings.contentLayout}"]`).setAttribute('checked', 'checked')
  926. const contentCb = e => {
  927. this._loading = true
  928. this._loadingState(true, true)
  929. this.setContentLayout(e.target.value, true, () => {
  930. this._loading = false
  931. this._loadingState(false, true)
  932. })
  933. this._setCookie('contentLayout', e.target.value, 365)
  934. }
  935. contentOpt.addEventListener('change', contentCb)
  936. this._listeners.push([contentOpt, 'change', contentCb])
  937. }
  938. // Header Layout Type
  939. const headerTypeW = this.container.querySelector('.template-customizer-headerOptions')
  940. const templateName = document.documentElement.getAttribute('data-template').split('-')
  941. if (!this._hasControls('headerType')) {
  942. headerTypeW.parentNode.removeChild(headerTypeW)
  943. } else {
  944. const headerOpt = headerTypeW.querySelector('.template-customizer-header-options')
  945. setTimeout(() => {
  946. if (templateName.includes('vertical')) {
  947. headerTypeW.parentNode.removeChild(headerTypeW)
  948. }
  949. }, 100)
  950. this.settings.availableHeaderTypes.forEach(header => {
  951. const headerEl = createOptionElement(header.name, header.title, 'headerRadioIcon', header.image)
  952. headerOpt.appendChild(headerEl)
  953. })
  954. headerOpt.querySelector(`input[value="${this.settings.headerType}"]`).setAttribute('checked', 'checked')
  955. const headerTypeCb = e => {
  956. this.setHeaderType(e.target.value)
  957. this._setCookie('headerType', e.target.value, 365)
  958. }
  959. headerOpt.addEventListener('change', headerTypeCb)
  960. this._listeners.push([headerOpt, 'change', headerTypeCb])
  961. }
  962. // Layout Navbar Options
  963. const navbarOption = this.container.querySelector('.template-customizer-layoutNavbarOptions')
  964. if (!this._hasControls('layoutNavbarOptions')) {
  965. navbarOption.parentNode.removeChild(navbarOption)
  966. } else {
  967. setTimeout(() => {
  968. if (templateName.includes('horizontal')) {
  969. navbarOption.parentNode.removeChild(navbarOption)
  970. }
  971. }, 100)
  972. const navbarTypeOpt = navbarOption.querySelector('.template-customizer-navbar-options')
  973. this.settings.availableNavbarOptions.forEach(navbarOpt => {
  974. const navbarEl = createOptionElement(navbarOpt.name, navbarOpt.title, 'navbarOptionRadios', navbarOpt.image)
  975. navbarTypeOpt.appendChild(navbarEl)
  976. })
  977. // check navbar option from settings
  978. navbarTypeOpt
  979. .querySelector(`input[value="${this.settings.layoutNavbarOptions}"]`)
  980. .setAttribute('checked', 'checked')
  981. const navbarCb = e => {
  982. this._loading = true
  983. this._loadingState(true, true)
  984. this.setLayoutNavbarOption(e.target.value, true, () => {
  985. this._loading = false
  986. this._loadingState(false, true)
  987. })
  988. this._setCookie('navbarType', e.target.value, 365)
  989. }
  990. navbarTypeOpt.addEventListener('change', navbarCb)
  991. this._listeners.push([navbarTypeOpt, 'change', navbarCb])
  992. }
  993. // RTL
  994. const directionW = this.container.querySelector('.template-customizer-directions')
  995. // ? Hide RTL control in following 2 case
  996. if (!this._hasControls('rtl')) {
  997. directionW.parentNode.removeChild(directionW)
  998. } else {
  999. const directionOpt = directionW.querySelector('.template-customizer-directions-options')
  1000. this.settings.availableDirections.forEach(dir => {
  1001. const dirEl = createOptionElement(dir.name, dir.title, 'directionRadioIcon', dir.image)
  1002. directionOpt.appendChild(dirEl)
  1003. })
  1004. directionOpt
  1005. .querySelector(`input[value="${this.settings.rtl ? 'rtl' : 'ltr'}"]`)
  1006. .setAttribute('checked', 'checked')
  1007. const rtlCb = e => {
  1008. // For demo purpose, we will use EN as LTR and AR as RTL Language
  1009. this._setSetting('Lang', this.settings.lang === 'ar' ? 'en' : 'ar')
  1010. this.settings.rtl = e.target.value === 'rtl'
  1011. // Cache the language setting
  1012. const currentLang = this._getSetting('Lang')
  1013. const languageDropdown = document.querySelector('.dropdown-language .dropdown-menu')
  1014. if (languageDropdown) {
  1015. const dropdownItem = languageDropdown.querySelector(`[data-language="${currentLang}"]`)
  1016. dropdownItem.click()
  1017. }
  1018. // Use querySelector for cleaner and more flexible selection
  1019. this._initDirection()
  1020. this.setRtl(e.target.value === 'rtl', true, () => {
  1021. this._loadingState(false)
  1022. })
  1023. let layoutTemplate = document.documentElement.getAttribute('data-template') || ''
  1024. let isAdmin = !layoutTemplate.includes('front')
  1025. if (isAdmin) {
  1026. if (e.target.value === 'rtl') {
  1027. window.location.href = baseUrl + 'lang/ar'
  1028. } else {
  1029. window.location.href = baseUrl + 'lang/en'
  1030. }
  1031. } else {
  1032. // For front-end layouts, just reload the page
  1033. window.location.reload()
  1034. }
  1035. }
  1036. directionOpt.addEventListener('change', rtlCb)
  1037. this._listeners.push([directionOpt, 'change', rtlCb])
  1038. }
  1039. }
  1040. setTimeout(() => {
  1041. const layoutCustom = this.container.querySelector('.template-customizer-layout')
  1042. const layoutTheme = this.container.querySelector('.template-customizer-theming')
  1043. const checkSemiDarkWrapper = document.documentElement.getAttribute('data-bs-theme')
  1044. let checkSemiDark = false
  1045. if (checkSemiDarkWrapper === 'light' && document.querySelector('.layout-menu')) {
  1046. if (document.querySelector('.layout-menu').getAttribute('data-bs-theme') === 'dark') {
  1047. checkSemiDark = true
  1048. }
  1049. if (checkSemiDark === true) {
  1050. const semiDarkSwitch = layoutTheme.querySelector('.template-customizer-semi-dark-switch')
  1051. semiDarkSwitch.setAttribute('checked', 'checked')
  1052. }
  1053. }
  1054. if (document.querySelector('.menu-vertical')) {
  1055. if (!this._hasControls('rtl contentLayout layoutCollapsed layoutNavbarOptions', true)) {
  1056. if (layoutCustom) {
  1057. layoutCustom.parentNode.removeChild(layoutCustom)
  1058. }
  1059. }
  1060. } else if (document.querySelector('.menu-horizontal')) {
  1061. if (!this._hasControls('rtl contentLayout headerType', true)) {
  1062. if (layoutCustom) {
  1063. layoutCustom.parentNode.removeChild(layoutCustom)
  1064. }
  1065. }
  1066. }
  1067. }, 100)
  1068. // Set language
  1069. this.setLang(this.settings.lang, false, true)
  1070. // Append container
  1071. if (_container === document) {
  1072. if (_container.body) {
  1073. _container.body.appendChild(this.container)
  1074. } else {
  1075. window.addEventListener('DOMContentLoaded', () => _container.body.appendChild(this.container))
  1076. }
  1077. } else {
  1078. _container.appendChild(this.container)
  1079. }
  1080. }
  1081. _initDirection() {
  1082. if (this._hasControls('rtl')) {
  1083. document.documentElement.setAttribute(
  1084. 'dir',
  1085. this._checkCookie('direction')
  1086. ? this._getCookie('direction') === 'true'
  1087. ? 'rtl'
  1088. : 'ltr'
  1089. : this.settings.rtl
  1090. ? 'rtl'
  1091. : 'ltr'
  1092. )
  1093. }
  1094. }
  1095. _loadingState(enable, skins) {
  1096. this.container.classList[enable ? 'add' : 'remove'](`template-customizer-loading${skins ? '-theme' : ''}`)
  1097. }
  1098. _getElementFromString(str) {
  1099. const wrapper = document.createElement('div')
  1100. wrapper.innerHTML = str
  1101. return wrapper.firstChild
  1102. }
  1103. // Set settings in LocalStorage with layout & key
  1104. _setSetting(key, val) {
  1105. const layoutName = this._getLayoutName()
  1106. try {
  1107. localStorage.setItem(`templateCustomizer-${layoutName}--${key}`, String(val))
  1108. this._showResetBtnNotification()
  1109. } catch (e) {
  1110. // Catch Error
  1111. }
  1112. }
  1113. // Set settings in LocalStorage with layout & key
  1114. _getSetting(key) {
  1115. let result = null
  1116. const layoutName = this._getLayoutName()
  1117. try {
  1118. result = localStorage.getItem(`templateCustomizer-${layoutName}--${key}`)
  1119. } catch (e) {
  1120. // Catch error
  1121. }
  1122. return String(result || '')
  1123. }
  1124. _showResetBtnNotification(show = true) {
  1125. setTimeout(() => {
  1126. const resetBtnAttr = this.container.querySelector('.template-customizer-reset-btn .badge')
  1127. if (show) {
  1128. resetBtnAttr.classList.remove('d-none')
  1129. } else {
  1130. resetBtnAttr.classList.add('d-none')
  1131. }
  1132. }, 200)
  1133. }
  1134. // Get layout name to set unique
  1135. _getLayoutName() {
  1136. return document.getElementsByTagName('HTML')[0].getAttribute('data-template')
  1137. }
  1138. _removeListeners() {
  1139. for (let i = 0, l = this._listeners.length; i < l; i++) {
  1140. this._listeners[i][0].removeEventListener(this._listeners[i][1], this._listeners[i][2])
  1141. }
  1142. }
  1143. _cleanup() {
  1144. this._removeListeners()
  1145. this._listeners = []
  1146. this._controls = {}
  1147. if (this._updateInterval) {
  1148. clearInterval(this._updateInterval)
  1149. this._updateInterval = null
  1150. }
  1151. }
  1152. get _ssr() {
  1153. return typeof window === 'undefined'
  1154. }
  1155. // Check controls availability
  1156. _hasControls(controls, oneOf = false) {
  1157. return controls.split(' ').reduce((result, control) => {
  1158. if (this.settings.controls.indexOf(control) !== -1) {
  1159. if (oneOf || result !== false) result = true
  1160. } else if (!oneOf || result !== true) result = false
  1161. return result
  1162. }, null)
  1163. }
  1164. // Get the default Skin
  1165. _getDefaultSkin(skinId) {
  1166. const skins = this.settings.availableSkins
  1167. let skin = null
  1168. // If skinId is a number or a string that can be parsed as a number
  1169. if (!isNaN(parseInt(skinId))) {
  1170. const id = parseInt(skinId)
  1171. skin = skins.find(s => s.id === id)
  1172. }
  1173. // If skinId is a string (skin name)
  1174. else if (typeof skinId === 'string') {
  1175. skin = skins.find(s => s.name === skinId)
  1176. }
  1177. // If no skin found by id or name, use first available skin
  1178. if (!skin && skins.length > 0) {
  1179. skin = skins[0]
  1180. }
  1181. if (!skin) {
  1182. throw new Error(`Skin "${skinId}" not found!`)
  1183. }
  1184. return skin
  1185. }
  1186. // Get theme by skinId/skinName
  1187. _getSkinByName(skinName, returnDefault = false) {
  1188. const skins = this.settings.availableSkins
  1189. // If skinName is a valid skin name string, return it directly
  1190. for (let i = 0, l = skins.length; i < l; i++) {
  1191. if (skins[i].name === skinName) return skins[i]
  1192. }
  1193. // If skinName is a number or numeric string, try finding by id
  1194. if (!isNaN(parseInt(skinName))) {
  1195. const id = parseInt(skinName)
  1196. const skinById = skins.find(s => s.id === id)
  1197. if (skinById) return skinById
  1198. }
  1199. // If no match found, return default skin if requested
  1200. if (returnDefault) {
  1201. // First try to find skin with name 'default'
  1202. const defaultSkin = skins.find(s => s.name === 'default')
  1203. if (defaultSkin) return defaultSkin
  1204. // If no skin named 'default', return the first skin
  1205. if (skins.length > 0) return skins[0]
  1206. // If no skins at all, return null
  1207. return null
  1208. }
  1209. return null
  1210. }
  1211. _setCookie(name, value, daysToExpire, path = '/', domain = '') {
  1212. const cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`
  1213. let expires = ''
  1214. if (daysToExpire) {
  1215. const expirationDate = new Date()
  1216. expirationDate.setTime(expirationDate.getTime() + daysToExpire * 24 * 60 * 60 * 1000)
  1217. expires = `; expires=${expirationDate.toUTCString()}`
  1218. }
  1219. const pathString = `; path=${path}`
  1220. const domainString = domain ? `; domain=${domain}` : ''
  1221. document.cookie = `${cookie}${expires}${pathString}${domainString}`
  1222. }
  1223. _getCookie(name) {
  1224. const cookies = document.cookie.split('; ')
  1225. for (let i = 0; i < cookies.length; i++) {
  1226. const [cookieName, cookieValue] = cookies[i].split('=')
  1227. if (decodeURIComponent(cookieName) === name) {
  1228. return decodeURIComponent(cookieValue)
  1229. }
  1230. }
  1231. return null
  1232. }
  1233. _checkCookie(name) {
  1234. return this._getCookie(name) !== null
  1235. }
  1236. _deleteCookie(name) {
  1237. document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'
  1238. }
  1239. _isAdminLayout() {
  1240. const layoutName = this._getLayoutName()
  1241. return !layoutName.includes('front')
  1242. }
  1243. }
  1244. // Colors
  1245. TemplateCustomizer.COLORS = [
  1246. {
  1247. name: 'primary',
  1248. title: 'Primary',
  1249. color: rootStyles.getPropertyValue('--bs-primary').trim()
  1250. },
  1251. {
  1252. name: 'success',
  1253. title: 'Success',
  1254. color: '#0D9394'
  1255. },
  1256. {
  1257. name: 'warning',
  1258. title: 'Warning',
  1259. color: '#FFAB1D'
  1260. },
  1261. {
  1262. name: 'danger',
  1263. title: 'Danger',
  1264. color: '#EB3D63'
  1265. },
  1266. {
  1267. name: 'info',
  1268. title: 'Info',
  1269. color: '#2092EC'
  1270. }
  1271. ]
  1272. // Themes
  1273. TemplateCustomizer.THEMES = [
  1274. {
  1275. name: 'light',
  1276. title: 'Light',
  1277. image: '<i class="bx bx-sun icon-base mb-0"></i>'
  1278. },
  1279. {
  1280. name: 'dark',
  1281. title: 'Dark',
  1282. image: '<i class="bx bx-moon icon-base mb-0"></i>'
  1283. },
  1284. {
  1285. name: 'system',
  1286. title: 'System',
  1287. image: '<i class="bx bx-desktop icon-base mb-0"></i>'
  1288. }
  1289. ]
  1290. // Skins
  1291. TemplateCustomizer.SKINS = [
  1292. {
  1293. id: 0,
  1294. name: 'default',
  1295. title: 'Default',
  1296. image: 'skin-default.svg'
  1297. },
  1298. {
  1299. id: 1,
  1300. name: 'bordered',
  1301. title: 'Bordered',
  1302. image: 'skin-border.svg'
  1303. }
  1304. // To add a new skin, simply add a new entry here:
  1305. // Example:
  1306. // {
  1307. // id: 2,
  1308. // name: 'raspberry',
  1309. // title: 'Raspberry',
  1310. // image: 'skin-border.svg'
  1311. // }
  1312. ]
  1313. // Layouts
  1314. TemplateCustomizer.LAYOUTS = [
  1315. {
  1316. name: 'expanded',
  1317. title: 'Expanded',
  1318. image: 'layouts-expanded.svg'
  1319. },
  1320. {
  1321. name: 'collapsed',
  1322. title: 'Collapsed',
  1323. image: 'layouts-collapsed.svg'
  1324. }
  1325. ]
  1326. // Navbar Options
  1327. TemplateCustomizer.NAVBAR_OPTIONS = [
  1328. {
  1329. name: 'sticky',
  1330. title: 'Sticky',
  1331. image: 'navbar-sticky.svg'
  1332. },
  1333. {
  1334. name: 'static',
  1335. title: 'Static',
  1336. image: 'navbar-static.svg'
  1337. },
  1338. {
  1339. name: 'hidden',
  1340. title: 'Hidden',
  1341. image: 'navbar-hidden.svg'
  1342. }
  1343. ]
  1344. // Header Types
  1345. TemplateCustomizer.HEADER_TYPES = [
  1346. {
  1347. name: 'fixed',
  1348. title: 'Fixed',
  1349. image: 'horizontal-fixed.svg'
  1350. },
  1351. {
  1352. name: 'static',
  1353. title: 'Static',
  1354. image: 'horizontal-static.svg'
  1355. }
  1356. ]
  1357. // Content Types
  1358. TemplateCustomizer.CONTENT = [
  1359. {
  1360. name: 'compact',
  1361. title: 'Compact',
  1362. image: 'content-compact.svg'
  1363. },
  1364. {
  1365. name: 'wide',
  1366. title: 'Wide',
  1367. image: 'content-wide.svg'
  1368. }
  1369. ]
  1370. // Directions
  1371. TemplateCustomizer.DIRECTIONS = [
  1372. {
  1373. name: 'ltr',
  1374. title: 'Left to Right (En)',
  1375. image: 'direction-ltr.svg'
  1376. },
  1377. {
  1378. name: 'rtl',
  1379. title: 'Right to Left (Ar)',
  1380. image: 'direction-rtl.svg'
  1381. }
  1382. ]
  1383. // Theme setting language
  1384. TemplateCustomizer.LANGUAGES = {
  1385. en: {
  1386. panel_header: 'Template Customizer',
  1387. panel_sub_header: 'Customize and preview in real time',
  1388. theming_header: 'Theming',
  1389. color_label: 'Primary Color',
  1390. theme_label: 'Theme',
  1391. skin_label: 'Skins',
  1392. semiDark_label: 'Semi Dark',
  1393. layout_header: 'Layout',
  1394. layout_label: 'Menu (Navigation)',
  1395. layout_header_label: 'Header Types',
  1396. content_label: 'Content',
  1397. layout_navbar_label: 'Navbar Type',
  1398. direction_label: 'Direction'
  1399. },
  1400. fr: {
  1401. panel_header: 'Modèle De Personnalisation',
  1402. panel_sub_header: 'Personnalisez et prévisualisez en temps réel',
  1403. theming_header: 'Thématisation',
  1404. color_label: 'Couleur primaire',
  1405. theme_label: 'Thème',
  1406. skin_label: 'Peaux',
  1407. semiDark_label: 'Demi-foncé',
  1408. layout_header: 'Disposition',
  1409. layout_label: 'Menu (Navigation)',
  1410. layout_header_label: "Types d'en-tête",
  1411. content_label: 'Contenu',
  1412. layout_navbar_label: 'Type de barre de navigation',
  1413. direction_label: 'Direction'
  1414. },
  1415. ar: {
  1416. panel_header: 'أداة تخصيص القالب',
  1417. panel_sub_header: 'تخصيص ومعاينة في الوقت الحقيقي',
  1418. theming_header: 'السمات',
  1419. color_label: 'اللون الأساسي',
  1420. theme_label: 'سمة',
  1421. skin_label: 'جلود',
  1422. semiDark_label: 'شبه داكن',
  1423. layout_header: 'تَخطِيط',
  1424. layout_label: 'القائمة (الملاحة)',
  1425. layout_header_label: 'أنواع الرأس',
  1426. content_label: 'محتوى',
  1427. layout_navbar_label: 'نوع شريط التنقل',
  1428. direction_label: 'اتجاه'
  1429. },
  1430. de: {
  1431. panel_header: 'Vorlagen-Anpasser',
  1432. panel_sub_header: 'Anpassen und Vorschau in Echtzeit',
  1433. theming_header: 'Themen',
  1434. color_label: 'Grundfarbe',
  1435. theme_label: 'Thema',
  1436. skin_label: 'Skins',
  1437. semiDark_label: 'Halbdunkel',
  1438. layout_header: 'Layout',
  1439. layout_label: 'Menü (Navigation)',
  1440. layout_header_label: 'Header-Typen',
  1441. content_label: 'Inhalt',
  1442. layout_navbar_label: 'Art der Navigationsleiste',
  1443. direction_label: 'Richtung'
  1444. },
  1445. it: {
  1446. panel_header: 'UX personalizzatore',
  1447. panel_sub_header: 'Personalizza e visualizza in tempo reale',
  1448. theming_header: 'personalizzazione',
  1449. color_label: 'colore primario',
  1450. theme_label: 'tema',
  1451. skin_label: 'skin',
  1452. semiDark_label: 'semi-scuro',
  1453. layout_header: 'layout',
  1454. layout_label: 'menu (navigazione)',
  1455. layout_header_label: 'tipi di intestazione',
  1456. content_label: 'contenuto',
  1457. layout_navbar_label: 'tipo di barra di navigazione',
  1458. direction_label: 'direzione'
  1459. }
  1460. }
  1461. window.TemplateCustomizer = TemplateCustomizer
  1462. export { TemplateCustomizer }
  1463. /**
  1464. * Initialize color picker functionality for template customization
  1465. * Caches DOM elements and handles color picker setup
  1466. */
  1467. const initializeColorPicker = () => {
  1468. // Cache DOM elements
  1469. const elements = {
  1470. pickerWrapper: document.querySelector('.template-customizer-colors-options input[value="picker"]'),
  1471. pickerEl: document.querySelector('.customizer-nano-picker'),
  1472. pcrButton: document.querySelector('.custom-option-content .pcr-button')
  1473. }
  1474. // Early return if required elements missing
  1475. if (!elements.pickerWrapper || !elements.pickerEl) {
  1476. console.warn('Required color picker elements not found')
  1477. return
  1478. }
  1479. // Get the saved color from cookie
  1480. const layoutTemplate = document.documentElement.getAttribute('data-template') || ''
  1481. const isAdmin = !layoutTemplate.includes('front')
  1482. const cookieName = isAdmin ? 'admin-primaryColor' : 'front-primaryColor'
  1483. function getCookie(name) {
  1484. const cookies = document.cookie.split('; ')
  1485. for (let i = 0; i < cookies.length; i++) {
  1486. const [cookieName, cookieValue] = cookies[i].split('=')
  1487. if (decodeURIComponent(cookieName) === name) {
  1488. return decodeURIComponent(cookieValue)
  1489. }
  1490. }
  1491. return null
  1492. }
  1493. // First check if picker is checked
  1494. const isPrickerChecked =
  1495. elements.pickerWrapper.checked || elements.pickerWrapper.getAttribute('checked') === 'checked'
  1496. // Get saved color from cookie
  1497. const savedColor = getCookie(cookieName)
  1498. // If picker is checked, use custom color
  1499. // Otherwise use default color or fallback
  1500. const currentColor = isPrickerChecked
  1501. ? savedColor || window.templateCustomizer._getSetting('Color') || '#FF4961'
  1502. : window.templateCustomizer.settings.defaultPrimaryColor
  1503. // Configure and initialize Pickr color picker
  1504. const picker = new Pickr({
  1505. el: elements.pickerEl,
  1506. theme: 'nano',
  1507. default: currentColor,
  1508. defaultRepresentation: 'HEX',
  1509. comparison: false,
  1510. components: {
  1511. hue: true,
  1512. preview: true,
  1513. interaction: {
  1514. input: true
  1515. }
  1516. }
  1517. })
  1518. // Add color picker icon
  1519. picker._root.button.classList.add('bx', 'bxs-color-fill')
  1520. // Handle color changes
  1521. picker.on('change', color => {
  1522. const hexColor = color.toHEXA().toString()
  1523. const rgbaColor = color.toRGBA().toString()
  1524. // Update picker button color if exists
  1525. elements.pcrButton?.style.setProperty('--pcr-color', rgbaColor)
  1526. // Update selected option state
  1527. elements.pickerWrapper.checked = true
  1528. window.Helpers.updateCustomOptionCheck(elements.pickerWrapper)
  1529. // Update theme color settings
  1530. window.templateCustomizer._setSetting('Color', hexColor)
  1531. window.templateCustomizer.setColor(hexColor, true)
  1532. })
  1533. }
  1534. window.onload = () => {
  1535. initializeColorPicker()
  1536. // Get the saved color from the cookie or use default
  1537. const layoutTemplate = document.documentElement.getAttribute('data-template') || ''
  1538. const isAdmin = !layoutTemplate.includes('front')
  1539. const cookieName = isAdmin ? 'admin-primaryColor' : 'front-primaryColor'
  1540. // Get saved color from cookie
  1541. function getCookie(name) {
  1542. const cookies = document.cookie.split('; ')
  1543. for (let i = 0; i < cookies.length; i++) {
  1544. const [cookieName, cookieValue] = cookies[i].split('=')
  1545. if (decodeURIComponent(cookieName) === name) {
  1546. return decodeURIComponent(cookieValue)
  1547. }
  1548. }
  1549. return null
  1550. }
  1551. const savedColor = getCookie(cookieName) || window.templateCustomizer.settings.defaultPrimaryColor
  1552. const defaultPrimaryColor = TemplateCustomizer.COLORS[0].color
  1553. // Set the color for the picker button
  1554. const pcrButton = document.querySelector('.custom-option-content .pcr-button')
  1555. if (pcrButton) {
  1556. pcrButton.style.setProperty('--pcr-color', savedColor)
  1557. }
  1558. // Find and select the matching color option
  1559. const colorOptions = document.querySelectorAll(
  1560. '.template-customizer-color .template-customizer-colors-options input[type="radio"]'
  1561. )
  1562. let matchingOption = false
  1563. // First check if color matches any predefined color
  1564. colorOptions.forEach(option => {
  1565. if (option.value === savedColor) {
  1566. option.checked = true
  1567. matchingOption = true
  1568. if (typeof window.Helpers.updateCustomOptionCheck === 'function') {
  1569. window.Helpers.updateCustomOptionCheck(option)
  1570. }
  1571. }
  1572. })
  1573. // If no matching option and the saved color isn't the default, select picker
  1574. if (!matchingOption && colorOptions.length > 0 && savedColor !== defaultPrimaryColor) {
  1575. const pickerOption = document.querySelector('.template-customizer-colors-options input[value="picker"]')
  1576. if (pickerOption) {
  1577. pickerOption.checked = true
  1578. if (typeof window.Helpers.updateCustomOptionCheck === 'function') {
  1579. window.Helpers.updateCustomOptionCheck(pickerOption)
  1580. }
  1581. }
  1582. }
  1583. // If no match and color is the default, select the first option
  1584. else if (!matchingOption && savedColor === defaultPrimaryColor) {
  1585. const firstOption = colorOptions[0]
  1586. if (firstOption) {
  1587. firstOption.checked = true
  1588. if (typeof window.Helpers.updateCustomOptionCheck === 'function') {
  1589. window.Helpers.updateCustomOptionCheck(firstOption)
  1590. }
  1591. }
  1592. }
  1593. }