Files
IPB/static/js/global/root_component_themeEditor.js
T
2025-12-19 19:04:57 -08:00

101 lines
23 KiB
JavaScript

;(function(){"use strict";class IThemeEditor extends HTMLElement{constructor(){super();this.form=this.querySelector('form');this.inputs=this.form.elements;this.panels=this.querySelectorAll('editor-panel');this.currentPanel=document.getElementById('panel__start');this.panelHistory=[];this.settings={vars:{},text:{},layouts:{},images:{},headerPositions:{},dataAttributes:{}};this.iframe=document.getElementById('themeEditorIframe').contentWindow;this.iframeStyleTag='';this.iframeTempStyleTag='';this.styleTag=document.getElementById('themeEditorStyles');this.tempStyleTag=document.getElementById('themeEditorTempStyles');this.colorPickerText=this.querySelector('[data-color-picker-text]');this.colorPreview=this.querySelector('#panel__colorSelector [data-color-preview]');this.customCssDialog=this.querySelector("#dialog__customCSS");this.customCSSEditor=this.querySelector("#customCSS");this.customCssDialog.addEventListener('close',e=>this.updateCustomCSS(e));this.activeColor='';this.init();this.form.addEventListener('click',this);this.form.addEventListener('input',this);this.form.addEventListener('change',this);this.form.addEventListener('ips:codeboxAfterInit',e=>{if(e?.detail?.instance){this.cssEditorInstance=e.detail.instance;this.showOrHideCSSWarning();}
this.showOrHideRevertButton();});this.debounceTimeout=undefined;this.form.addEventListener('ips:codebox#update',e=>{if(e?.detail?.instance){this.cssEditorInstance=e.detail.instance;}
if(this.customCSSEditor instanceof Element&&e?.detail?.elem?.closest('#customCSS')===this.customCSSEditor){if(this.debounceTimeout!==undefined){clearTimeout(this.debounceTimeout);}
this.debounceTimeout=setTimeout(()=>{this.showOrHideCSSWarning();this.showOrHideRevertButton();},200);}})
this.form.addEventListener('change',e=>this.unsavedChanges=true);this.form.addEventListener('keydown',e=>this.preventReturnKeySubmission(e));window.addEventListener("beforeunload",e=>this.handleBeforeUnload(e));document.getElementById('themeEditorIframe').addEventListener('load',e=>this.iframeSync());document.addEventListener("ips:colorScheme",this.closeColorPicker);window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change',()=>{if(document.documentElement.getAttribute('data-ips-scheme-active')!='system')return;let active=(window.matchMedia('(prefers-color-scheme: dark)').matches)?'dark':'light';this.querySelector(`[data-workspace='color'] [value='${active}']`).click();});}
init(){ips.utils.color.loadIro().then(()=>{this.colorPicker=new ips.utils.iro.ColorPicker(this.querySelector('color-picker'),{width:250,color:"hsl(0 0% 0%)",layout:[{component:ips.utils.iro.ui.Box,options:{}},{component:ips.utils.iro.ui.Slider,options:{sliderType:'hue'}}]});this.colorPicker.on('input:change',color=>this.colorPickerChange(color));this.colorPicker.on('input:end',color=>this.colorPickerDone(color));});this.unsavedChanges=false;if(document.referrer&&!document.referrer.includes("/admin/")){document.getElementById('themeEditorIframe').src=document.referrer;}
this.setAttribute('data-loading',true);setTimeout(()=>{this.removeAttribute('data-loading');},3000);this.customSettingUIs();}
customSettingUIs(){let headerDrag=`
<div id="content__header__design">
<p class="i-padding-block_2 i-text-align_center i-color_soft i-flex_11">
<span class="i-display_hover">Click and drag to reorder header elements</span><span class="i-display_no-hover">Tap, hold and then drag to reorder header elements for the desktop header.</span>
</p>
<ul class="theme-editor__reorder" data-ips-reorder>
<li draggable="true" style="view-transition-name: el-1" data-pos="1"></li>
<li draggable="true" style="view-transition-name: el-2" data-pos="2"></li>
<li draggable="true" style="view-transition-name: el-3" data-pos="3"></li>
<li class="theme-editor__reorder-break"></li>
<li draggable="true" style="view-transition-name: el-4" data-pos="4"></li>
<li draggable="true" style="view-transition-name: el-5" data-pos="5"></li>
<li draggable="true" style="view-transition-name: el-6" data-pos="6"></li>
<li class="theme-editor__reorder-break"></li>
<li draggable="true" style="view-transition-name: el-7" data-pos="7"></li>
<li draggable="true" style="view-transition-name: el-8" data-pos="8"></li>
<li draggable="true" style="view-transition-name: el-9" data-pos="9"></li>
<li class="theme-editor__reorder-break"></li>
<li draggable="true" style="view-transition-name: el-10" data-pos="10"></li>
<li draggable="true" style="view-transition-name: el-11" data-pos="11"></li>
<li draggable="true" style="view-transition-name: el-12" data-pos="12"></li>
</ul>
</div>`;this.querySelector('[data-setting="set__i-position-search"]').insertAdjacentHTML('afterend',headerDrag);let positionInputs=this.form.querySelectorAll('[name^="set__i-position-"]');positionInputs.forEach(input=>{let label=this.querySelector(`[for="${input.id}"]`),draggableEl=this.querySelector(`[data-ips-reorder] [data-pos='${input.value}']`);draggableEl.setAttribute('data-label',label.textContent);draggableEl.setAttribute('data-name',input.id);});this.dragLabels=document.querySelectorAll('[data-ips-reorder] [draggable]');this.dragLabels.forEach(item=>{item.addEventListener('dragstart',ev=>this.handleDragStart(ev),false);item.addEventListener('dragenter',ev=>this.handleDragEnter(ev),false);item.addEventListener('dragover',ev=>this.handleDragOver(ev),false);item.addEventListener('dragleave',ev=>this.handleDragLeave(ev),false);item.addEventListener('drop',ev=>this.handleDrop(ev),false);item.addEventListener('dragend',ev=>this.handleDragEnd(ev),false);});}
changeWorkspace(e,target){let type=target.parentElement.getAttribute('data-workspace');if(target.matches(':only-child')){target.toggleAttribute('data-active');document.documentElement.setAttribute(`data-workspace-${type}`,target.hasAttribute('data-active'));}else{for(let el of target.parentElement.children){el.removeAttribute('data-active');}
target.setAttribute('data-active',true);document.documentElement.setAttribute(`data-workspace-${type}`,target.value);}}
panelNavigation(event,target){this.panels.forEach(p=>{p.setAttribute('aria-hidden','true');p.inert=true;});if(target.getAttribute('data-panel-nav')==='back'){this.currentPanel=document.getElementById(this.panelHistory.pop());this.currentPanel.setAttribute('aria-hidden','false');this.currentPanel.inert=false;this.currentPanel.removeAttribute('data-panel-prev');this.currentPanel.querySelector("button")?.focus();}else{if(target.hasAttribute('data-color-tool')){this.toggleColorPanel(target);}
let oldPanel=target.closest('editor-panel');this.currentPanel=document.getElementById(target.getAttribute('aria-controls'));this.currentPanel.setAttribute('aria-hidden','false');this.currentPanel.inert=false;this.currentPanel.querySelector("button, input, select, textarea")?.focus();this.currentPanel.querySelector(`[data-panel-nav="back"]`).innerText=oldPanel.querySelector('[data-panel-name]')?.innerText??'Back';this.panelHistory.push(oldPanel.id);oldPanel.setAttribute('data-panel-prev',true);}}
buildVariablesStyleTag(){this.styleContent='';this.iframeTempStyleTag.textContent='';for(const[key,value]of Object.entries(this.settings.vars)){if(['set__i-logo-text','set__i-logo-slogan'].includes(key))continue;if(this.settings.images[key]){if(this.settings.images[key].length){this.styleContent+=`--${key}: url("`+this.settings.images[key]+`");`;}}else{this.styleContent+=`--${key}: ${value};`}}
this.styleTag.textContent=`:root{ ${this.styleContent} }`;this.iframeStyleTag.textContent=`:root{ ${this.styleContent} }`;window.Debug?.log(this.iframeStyleTag.textContent);}
updateLiveText(setting,value){this.iframe.document.querySelectorAll(`[data-ips-theme-text='${setting}']`).forEach(le=>le.innerText=value);}
updateImagePreview(e){const preview=e.target.parentElement.querySelector('[data-file-preview]');preview.textContent='';delete this.settings.images[e.target.name];if(e.target.files.length>0){const src=URL.createObjectURL(e.target.files[0]);preview.innerHTML=`<img src="${src}" alt="">`;preview.parentElement.querySelector('[data-delete-logo]')?.remove();this.settings.images[e.target.name]=src;this.applyUploadedImageToIframe(e.target.name,src);}}
applyUploadedImageToIframe(name,value=null){let setting=name.replace("set__",""),elements=this.iframe.document.querySelectorAll(`[data-ips-theme-image='${setting}']`);elements.forEach(el=>{let picture=el.closest('picture');if(value){el.src=value;el.offsetHeight;el.hidden=false;if(picture)picture.hidden=false;el.removeAttribute('width');el.removeAttribute('height');}else{el.hidden=true;if(picture)picture.hidden=true;}});}
deleteLogoImage(event,target){const fileInput=target.parentElement.querySelector('[type=file]');fileInput.value='';delete this.settings.images[target.getAttribute('data-file-preview-delete')];fileInput.dispatchEvent(new Event("change",{bubbles:true}));if(!fileInput.parentElement.querySelector('[data-delete-logo]')){fileInput.insertAdjacentHTML('afterend',`<input type='hidden' name='delete__${fileInput.id}' value='1' data-delete-logo>`);}
this.applyUploadedImageToIframe(target.getAttribute('data-file-preview-delete'));}
updateLogoMaxHeightController(){}
updateDynamicMaxValueFor(receiver){}
closeColorPicker(event){if(document.querySelector("#panel__colorSelector[aria-hidden='false']")){document.querySelector('#panel__colorSelector [data-panel-nav="back"]').click();}}
preventReturnKeySubmission(e){let input=e.target;if(input.matches('i-theme-editor input')&&e.keyCode==13){e.preventDefault();if(input.min&&input.min>Number(input.value)){input.value=input.min;}else if(input.max&&input.max<Number(input.value)){input.value=input.max;}
input.dispatchEvent(new Event("change",{bubbles:true}));return false;}}
changingRangeInput(e,target){cancelAnimationFrame(this.rebuildStyleDebounce);this.rebuildStyleDebounce=requestAnimationFrame(()=>{this.iframeTempStyleTag.textContent=`:root{ --${e.target.id}: ${e.target.value}; }`;if(e.target.matches("#panel__oklch input")){this.tempStyleTag.textContent=`:root{ --${e.target.id}: ${e.target.value}; }`;}
let numInput=this.querySelector(`[data-range-output=${e.target.id}]`);if(numInput)numInput.value=e.target.value;});}
changingTextInput(e,target){this.updateLiveText(e.target.name,e.target.value);}
settingHasBeenChanged(e,target){let setting=target.name,value=target.value;this.settings.vars[setting]=value;if(e.target.matches('[type="range"]')){let numInput=this.querySelector(`[data-range-output=${e.target.id}]`);if(numInput)numInput.value=value;}else if(target.matches('[type="text"]')){this.settings.text[setting]=value;}
let attr='data-ips-theme-setting-'+setting.replace('set__i-','');if(this.iframe.document.documentElement.hasAttribute(attr)){this.settings.dataAttributes[attr]=value;this.applyDataAttributes(attr,value);}
if(target.matches('[type="file"]'))this.updateImagePreview(e);this.toggleRevertButton(target);let container=document.querySelector(`theme-setting[data-setting='${setting}']`);if(container.matches('[data-setting-refresh]'))this.refreshIframe(e);this.buildVariablesStyleTag();this.updateCustomCSS();}
syncInputsWithPseudo(e,target){let pseudoInput=e.target,input=document.getElementById(e.target.getAttribute('data-range-output'));if(pseudoInput.max)pseudoInput.value=Math.min(parseInt(pseudoInput.max),Math.max(parseInt(pseudoInput.min)||0,parseInt(pseudoInput.value)||0));if(e.target.type=='checkbox'){input.value=(e.target.checked)?1:0;}else{input.value=e.target.value;}
input.dispatchEvent(new Event("change",{bubbles:true}));}
async updateCustomCSS(){let content=document.querySelector('#customCSS').textContent;this.customCSSEditor.value=content;if(content.length){const{content:parsed}=await ips.fetch('?app=core&module=system&controller=themeeditor',{method:'post',data:{do:'customCss',content:content}});this.iframeCustomCSS.textContent=parsed;}else{this.iframeCustomCSS.textContent=content;}}
revertCustomCSS(event,target){if(confirm("This will revert your CSS back to its previously saved value. Any unsaved changes will be lost.")){const originalContent=this.querySelector('#customCSS-saved').innerText;document.querySelector("#customCSS").value=originalContent;this.cssEditorInstance?.setValue(originalContent);this.updateCustomCSS();target.hidden=true;}}
async showOrHideCSSWarning(){await ips.ui._codehighlighting.whenLoaded();const dialog=this.querySelector('#dialog__customCSS');const warning=dialog.querySelector('#customCSS-warning');if(!this.cssEditorInstance){Debug.warn('No editor instance found!');warning.removeAttribute('hidden');return;}
const preTag=document.createElement('pre');preTag.classList.add('language-ipscss');preTag.innerText=this.cssEditorInstance.getValue();const highlighted=await ips.ui.codehighlighting.highlight(this.cssEditorInstance.getValue(),'ipscss');preTag.innerHTML=highlighted.value;const hasTemplateTag=!!preTag.querySelector('.language-ipscss :is(.ipsShortcut, .ipsVarTag, .ipsVarTagFilter, .ipsBlockTag)');if(hasTemplateTag){warning.removeAttribute('hidden');}else{warning.setAttribute('hidden','');}
preTag.remove();}
showOrHideRevertButton(){const originalContent=this.querySelector('#customCSS-saved').innerHTML;const currentContent=this.querySelector('#customCSS').innerHTML;this.querySelectorAll('[data-revert-custom-css]').forEach(revertButton=>{if(currentContent===originalContent){revertButton.setAttribute('hidden','');}else{this.unsavedChanges=true;revertButton.removeAttribute('hidden');}});}
setColorPickerFromComputedStyle(){this.colorPreview.style.setProperty('--_background',this.activeColor.value);let color=window.getComputedStyle(this.colorPreview).getPropertyValue('background-color');if(color.startsWith("color(srgb")){let values=color.replace(/color\(srgb |\)/g,'').split(" ");this.colorPicker.color.set(`rgb(${values[0] * 255} ${values[1] * 255} ${values[2] * 255})`);}else{this.colorPicker.color.set(color);}}
toggleColorPanel(target){let panel=document.getElementById('panel__colorSelector');this.activeColor=this.inputs[target.getAttribute('data-controls')];this.querySelector('[data-active-name]').innerText=target.innerText;if(target.hasAttribute('data-swatch-categories')){panel.setAttribute('data-show-swatches',target.getAttribute('data-swatch-categories'));}else{panel.removeAttribute('data-show-swatches');}
let swatchTab=document.querySelector('[aria-controls="content__colorSelector__swatches_panel"]');if(swatchTab.getAttribute("aria-expanded")==="false"){swatchTab.click()}
if(target.getAttribute('data-color-tool')!=''){panel.setAttribute('data-show-tool',target.getAttribute('data-color-tool'));}
this.querySelector('[data-swatch][aria-current]')?.removeAttribute('aria-current');this.querySelector(`[data-swatch][value='${this.activeColor.value}']`)?.setAttribute('aria-current','true');this.setColorPickerFromComputedStyle();this.colorPickerText.value=`hsl(${this.colorPicker.color.hsl.h} ${this.colorPicker.color.hsl.s}% ${this.colorPicker.color.hsl.l}%)`;}
colorPickerChange(color){cancelAnimationFrame(this.rebuildStyleDebounce);this.rebuildStyleDebounce=requestAnimationFrame(()=>{let newValue=`hsl(${color.hsl.h} ${color.hsl.s}% ${color.hsl.l}%)`,styleContent=`--${this.activeColor.name}: ${newValue};`;this.iframeTempStyleTag.textContent=`:root{ ${styleContent} }`;this.colorPickerText.value=newValue;this.colorPreview.style.setProperty('--_background',newValue);});}
colorPickerContrastPolyfill(color){let relativeLightness=((color.red*299)+(color.green*587)+(color.blue*114))/1000/256*100;return Math.floor(relativeLightness);}
colorPickerDone(color){this.querySelector('[data-swatch][aria-current]')?.removeAttribute('aria-current');let newValue=`hsl(${color.hsl.h} ${color.hsl.s}% ${color.hsl.l}%)`;this.activeColor.value=newValue;this.settings.vars[this.activeColor.name]=this.activeColor.value;if(this.activeColor.matches('[name=light__i-primary], [name=light__i-secondary], [name=dark__i-primary], [name=dark__i-secondary]')){this.inputs[this.activeColor.name+'-relative-l'].value=this.colorPickerContrastPolyfill(color);this.settings.vars[this.activeColor.name+'-relative-l']=this.colorPickerContrastPolyfill(color);}
this.colorPickerText.value=newValue;this.colorPreview.style.setProperty('--_background',newValue);this.toggleRevertButton(this.activeColor);this.unsavedChanges=true;this.buildVariablesStyleTag();}
colorPickerTextBlur(e,target){if(!e.target.value.startsWith('#')&&!e.target.value.startsWith('hsl(')&&!e.target.value.startsWith('rgb(')){window.Debug?.log('Not a supported color. Hex, RGB and HSL are supported.');return;}
this.activeColor.value=e.target.value;this.settings.vars[this.activeColor.name]=this.activeColor.value;this.setColorPickerFromComputedStyle();this.colorPickerDone(this.colorPicker.color);}
swatchClick(event,target){this.querySelector('[data-swatch][aria-current]')?.removeAttribute('aria-current');target.setAttribute('aria-current','true');this.activeColor.value=target.value;this.settings.vars[this.activeColor.name]=this.activeColor.value;this.colorPreview.style.setProperty('--_background',target.value);this.buildVariablesStyleTag();this.toggleRevertButton(this.activeColor);this.setColorPickerFromComputedStyle();this.unsavedChanges=true;}
toggleRevertButton(settingInput){let revertButton=document.querySelector(`[data-revert="${settingInput.name}"]`),hasDefaultValue=settingInput.hasAttribute("data-default");if(revertButton&&hasDefaultValue){revertButton.hidden=(settingInput.value==settingInput.getAttribute("data-default"));}}
revertSetting(event,target){target.hidden=true;let controls=target.getAttribute('data-revert'),defaultValue=this.inputs[controls].getAttribute('data-default'),fieldType=target.parentElement.getAttribute('data-type');if(fieldType==='image'){delete this.settings.images[controls];if(defaultValue.length){const preview=target.parentElement.querySelector('[data-file-preview]');preview.textContent='';preview.innerHTML=`<img src="${defaultValue}" alt="">`;}
this.applyUploadedImageToIframe(controls,defaultValue);}else{this.inputs[controls].value=defaultValue;let checkbox=document.getElementById(`${controls}__checkbox`);if(checkbox){checkbox.checked=(defaultValue==="1")?true:false;}}
this.settings.vars[controls]=defaultValue;if(fieldType!=="image"){this.inputs[controls].dispatchEvent(new Event("change",{bubbles:true}));}
this.buildVariablesStyleTag();this.updateCustomCSS();}
refreshIframe(e){this.settings.vars[e.target.name]=e.target.value;ips.fetch('?app=core&module=system&controller=themeeditor',{method:'post',data:{do:'store',data:this.settings}});this.setAttribute('data-loading',true);setTimeout(()=>{this.removeAttribute('data-loading');},3000);document.getElementById('themeEditorIframe').contentDocument.location.reload(true);}
applyDataAttributes(attr,value){this.iframe.document.documentElement.setAttribute(attr,value);}
iframeSync(){this.iframeStyleTag=this.iframe.document.getElementById('themeEditorStyles');this.iframeTempStyleTag=this.iframe.document.getElementById('themeEditorTempStyles');this.iframeCustomCSS=this.iframe.document.getElementById('themeCustomCSS');this.buildVariablesStyleTag();for(const[setting,value]of Object.entries(this.settings.images)){this.applyUploadedImageToIframe(setting,value);}
this.updateCustomCSS();for(const[setting,value]of Object.entries(this.settings.text)){this.updateLiveText(setting,value);}
for(const[setting,value]of Object.entries(this.settings.headerPositions)){this.repositionHeaderElement(setting,value);}
this.repositionHeaderElementPolyfills();for(const[attr,value]of Object.entries(this.settings.dataAttributes)){this.applyDataAttributes(attr,value);}
this.removeAttribute('data-loading');}
handleEvent(event){const target=event.target.closest(`[data-on-${event.type}]`);if(!target)return;const method=target.getAttribute(`data-on-${event.type}`);if(this[`${method}`]){return this[`${method}`](event,target)}else{(window.Debug||console).warn(`The ${method} method, triggered by [data-on-${event.type}="${method}"] was not found`);}}
handleDragStart(e){e.currentTarget.setAttribute('data-dragging',true);this.firstDrag=e.currentTarget;e.dataTransfer.effectAllowed='move';e.dataTransfer.setData('text/html',e.currentTarget.outerHTML);}
handleDragOver(e){e?.preventDefault();e.dataTransfer.dropEffect='move';return false;}
handleDragEnter(e){e.currentTarget.setAttribute('data-dropzone',true);}
handleDragLeave(e){e.currentTarget.removeAttribute('data-dropzone');}
handleDrop(e){e?.stopPropagation();if(this.firstDrag!=e.currentTarget){this.secondDrag=e.currentTarget;if(document.startViewTransition){document.startViewTransition(()=>this.swapDraggables(this.firstDrag,this.secondDrag))}else{this.swapDraggables(this.firstDrag,this.secondDrag)}}}
handleDragEnd(e){e.currentTarget.removeAttribute('data-dragging');this.dragLabels.forEach(item=>item.removeAttribute('data-dropzone'));}
swapDraggables(origin,destination){let el={fromName:origin.getAttribute('data-name'),fromPos:origin.getAttribute('data-pos'),fromDrag:origin.previousSibling,toName:destination.getAttribute('data-name'),toPos:destination.getAttribute('data-pos'),toDrag:destination.previousSibling}
el.fromDrag.after(destination);el.toDrag.after(origin);origin.setAttribute('data-pos',el.toPos);destination.setAttribute('data-pos',el.fromPos);let input;if(input=this.form.querySelector(`[id='${el.fromName}']`)){input.value=el.toPos;this.settings.vars[input.name]=input.value;this.settings.headerPositions[input.name]=input.value;this.repositionHeaderElement(input.name,input.value);}
if(input=this.form.querySelector(`[id='${el.toName}']`)){input.value=el.fromPos;this.settings.vars[input.name]=input.value;this.settings.headerPositions[input.name]=input.value;this.repositionHeaderElement(input.name,input.value);}
this.repositionHeaderElementPolyfills();this.updateLogoMaxHeightController();this.buildVariablesStyleTag();this.unsavedChanges=true;}
repositionHeaderElement(settingName,settingValue){let name=settingName.replace(/(^set__i-position-)/gi,""),elementOne=this.iframe.document.querySelector(`[data-ips-header-content='${name}']`),destination=this.iframe.document.querySelector(`[data-ips-header-position='${settingValue}']`);if(!elementOne||!destination||destination.contains(elementOne))return;const parentOne=elementOne.closest(`[data-ips-header-position]`);destination.append(elementOne);if(destination.firstElementChild!=destination.lastElementChild){parentOne.append(destination.firstElementChild);}}
repositionHeaderElementPolyfills(){this.iframe.document.querySelectorAll('.ipsHeader__top, .ipsHeader__primary, .ipsHeader__secondary, .ipsHeaderExtra').forEach(h=>{h.hidden=!h.querySelector('[data-ips-header-content]');})}
saveChanges(event,target){this.unsavedChanges=false;}
closeEditor(e,target){ips.utils.cookie.set('themeEditorLocation',document.getElementById('themeEditorIframe').contentDocument.location);if(this.unsavedChanges){e.preventDefault();document.getElementById('toggleCloseConfirmationDialog').click();}}
handleBeforeUnload(e){if(this.unsavedChanges&&!(document.getElementById('closeConfirmationDialog').open)){e.preventDefault();e.returnValue=true;}}}
ips.ui.registerWebComponent("themeEditor",IThemeEditor);Debug.log(`Submitted the web component constructor, IThemeEditor, for ${"themeEditor"}`);})();;