UNPKG

@xuda.io/xuda-framework-plugin-tailwind

Version:

Xuda Tailwind UI Framework plugin

791 lines (710 loc) 28.4 kB
const _colors = { primary: ' border-gray-300 bg-white bg-red-600 text-gray-700 hover:bg-gray-50 focus:ring-indigo-500 ', secondary: ' border-gray-300 bg-white bg-red-600 text-gray-700 hover:bg-gray-50 focus:ring-indigo-500 ', tertiary: ' border-gray-300 bg-white bg-red-600 text-gray-700 hover:bg-gray-50 focus:ring-indigo-500 ', success: ' border-gray-300 bg-white bg-red-600 text-gray-700 hover:bg-gray-50 focus:ring-indigo-500 ', warning: ' border-gray-300 bg-white bg-red-600 text-gray-700 hover:bg-gray-50 focus:ring-indigo-500 ', danger: ' border-gray-300 bg-white bg-red-600 text-gray-700 hover:bg-gray-50 focus:ring-indigo-500 ', light: ' border-gray-300 bg-white bg-red-600 text-gray-700 hover:bg-gray-50 focus:ring-indigo-500 ', medium: ' border-gray-300 bg-white bg-red-600 text-gray-700 hover:bg-gray-50 focus:ring-indigo-500 ', dark: ' border-gray-300 bg-white bg-red-600 text-gray-700 hover:bg-gray-50 focus:ring-indigo-500 ', }; const _fields = { init: { type: 'string', label: 'Theme Configuration', value: ` window.tailwind = window.tailwind || {}; tailwind.config = { darkMode: 'class', theme: { screens: { sm: '480px', md: '768px', lg: '976px', xl: '1440px', }, extend: { colors: { 'xuda-blue': '#1fb6ff', 'xuda-purple': '#7e5bef', 'xuda-pink': '#ff49db', 'xuda-orange': '#ff7849', 'xuda-green': '#13ce66', 'xuda-yellow': '#ffc82c', 'xuda-gray-dark': '#273444', 'xuda-gray': '#8492a6', 'xuda-gray-light': '#d3dce6', }, fontFamily: { sans: ['Graphik', 'sans-serif'], serif: ['Merriweather', 'serif'], }, spacing: { '128': '32rem', '144': '36rem', }, borderRadius: { '4xl': '2rem', } } } } `, helper: '<a target="_blank" href="https://ionicframework.com/docs/theming/color-generator"> Color Generator </a>', render: 'code', render_params: { lang: 'js', }, handler: function () {}, }, }; function _htmlToElement(html) { const template = document.createElement('template'); template.innerHTML = html.trim(); return template.content.firstChild; } export function tabs(doc) { return { theme: { svg_icon: '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-palette" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"/> <path d="M12 21a9 9 0 1 1 0 -18a9 8 0 0 1 9 8a4.5 4 0 0 1 -4.5 4h-2.5a2 2 0 0 0 -1 3.75a1.3 1.3 0 0 1 -1 2.25" /> <circle cx="7.5" cy="10.5" r=".5" fill="currentColor" /> <circle cx="12" cy="7.5" r=".5" fill="currentColor" /> <circle cx="16.5" cy="10.5" r=".5" fill="currentColor" /> </svg>', label: 'Theme', type: 'single', fields: { init: _fields.init, }, }, }; } export const core = class { init(setup) { const theme_setup = setup?.theme?.init || _fields.init.value; if (!theme_setup) return null; var id = 'xuda-framework-plugin-tailwind-script-' + Date.now(); const script = document.createElement('script'); script.setAttribute('id', id); const tailwind_init_script = ` document.currentScript?.setAttribute('data-xuda-executed', 'true'); window.tailwind = window.tailwind || {}; ${theme_setup} window.__xudaTailwindNormalizeTheme = function () { if (!window.tailwind) return; // Create config once if missing -- downstream scripts (theme setup + // the app's header) assume tailwind.config exists. But never re-read // an existing config Proxy and write it back: that re-wrapping on every // call is what froze the page. if (!window.tailwind.config) window.tailwind.config = {}; const config = window.tailwind.config; // Use if(!x) guards rather than x = x or {}: reading then writing // back a property of the Play CDN's reactive config Proxy re-wraps that // property in a fresh Proxy on every call. if (!config.darkMode) config.darkMode = 'class'; if (!config.theme) config.theme = {}; if (!config.theme.extend) config.theme.extend = {}; if (config.theme.colors) { config.theme.extend.colors = { ...config.theme.colors, ...(config.theme.extend.colors || {}), }; delete config.theme.colors; } const normalizeAlphaColor = function (value) { if (Array.isArray(value)) { return value.map(normalizeAlphaColor); } if (value && typeof value === 'object') { return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, normalizeAlphaColor(entry)])); } if (typeof value !== 'string') { return value; } return value.replace(/calc\\(\\s*100%\\s*\\*\\s*<alpha-value>\\s*\\)/g, '100%').replace(/<alpha-value>/g, '1'); }; config.theme.extend.colors = normalizeAlphaColor(config.theme.extend.colors || {}); // Do NOT reassign window.tailwind.config here. The mutations above // already persist through the live config Proxy; writing the Proxy back // re-wraps it in a new Proxy layer every call, and compounded over the // 0/10/50/250ms + per-refresh schedule that makes the CDN's recursive // config-Proxy traversal explode -> "Page Unresponsive". }; window.__xudaTailwindNormalizeTheme(); setTimeout(window.__xudaTailwindNormalizeTheme, 10); setTimeout(window.__xudaTailwindNormalizeTheme, 50); setTimeout(window.__xudaTailwindNormalizeTheme, 250); // The app header registers tailwind.config.plugins asynchronously (its own // setInterval), often after the scheduled refreshes have run. Poll for the // plugins and trigger a capture the moment they appear (or change), so the // custom components (.btn-primary etc.) get serialized to CSS. (function () { if (window.__xudaTailwindPluginWatcher) return; window.__xudaTailwindPluginWatcher = true; var lastLen = -1, ticks = 0, reExecuted = false; var iv = setInterval(function () { var cfg = window.tailwind && window.tailwind.config; // The app's components use theme('colors.primary'); ensure it resolves // (to the app's --primary CSS var) or Tailwind reports an "invalid theme // value" and skips generating the component. if (cfg && cfg.theme && cfg.theme.extend && cfg.theme.extend.colors && !cfg.theme.extend.colors.primary) { cfg.theme.extend.colors.primary = 'var(--primary)'; } var len = (cfg && Array.isArray(cfg.plugins)) ? cfg.plugins.length : 0; // The app header's inline module can be injected inert (never executes); // if its plugins never register, re-execute it once so addComponents runs. if (!reExecuted && len === 0 && ticks > 6) { reExecuted = true; try { var us = Array.prototype.find.call(document.querySelectorAll('script'), function (s) { return (s.textContent || '').indexOf('tailwindcssMotion') !== -1; }); if (us) { var ns = document.createElement('script'); ns.type = 'module'; ns.textContent = us.textContent; document.head.appendChild(ns); } } catch (e) {} } if (len !== lastLen) { lastLen = len; if (len > 0 && typeof window.__xudaScheduleTailwindRefresh === 'function') { window.__xudaScheduleTailwindRefresh('plugin-watcher', 0); } } if (++ticks > 100) clearInterval(iv); }, 100); })(); `; script.textContent = tailwind_init_script; document.head.append(script); if (script.getAttribute('data-xuda-executed') !== 'true') { try { Function(tailwind_init_script)(); script.setAttribute('data-xuda-executed', 'fallback'); } catch (error) { console.error(error); } } return id; } discard(id) { const element = document.getElementById(id); element.remove(); } refresh(container, _ds) { return container; } rootTagName() { return 'div'; } customUiClasses() { return []; } renderEngineProperties() { return { restrict_tags: true, engine: function () {}, tags: { col: { attributes: { type: { label: 'Modal Position', type: 'string', render: 'select', options: [ { label: 'Stretch', value: 'stretch' }, { label: 'Start', value: 'start' }, { label: 'Center', value: 'center' }, { label: 'End', value: 'end' }, ], }, }, render() { return `<div > <button/> </div>`; }, }, }, tags2: { img: ['src', 'alt'] }, classes: [], common_tags: [], }; } }; export const bind = class { setter(elm, value) { const node = elm?.nodeType ? elm : elm?.[0]; if (!node) return; const tagName = node.tagName; if (tagName === 'SELECT') { setTimeout(() => { node.value = _.toString(value); }, 250); } else { const type = node.getAttribute('type'); switch (type) { case 'radio': node.checked = (node.getAttribute('value') === value); break; case 'checkbox': node.checked = !!value; break; case 'time': if (value.length === 5) { // force adding the :00 seconds value = value + ':00'; node.value = value; } break; default: node.value = value; } } } getter(elm) { const node = elm?.nodeType ? elm : elm?.[0]; if (!node) return; const type = node.getAttribute('type'); switch (type) { case 'radio': if (node.getAttribute('checked') || node.checked) { node.setAttribute('checked', true); return node.getAttribute('value'); } break; case 'checkbox': if (node.getAttribute('checked') || node.checked) { node.setAttribute('checked', true); return node.getAttribute('value'); } break; default: return node.value; } } listener(elm, callback) { const node = elm?.nodeType ? elm : elm?.[0]; if (!node) return; const tagName = node.tagName?.toLowerCase?.(); const type = (node.getAttribute('type') || node.type || '').toLowerCase(); const useLiveTextListener = tagName === 'textarea' || (tagName === 'input' && !['button', 'checkbox', 'file', 'hidden', 'image', 'radio', 'reset', 'submit'].includes(type)); const eventNames = useLiveTextListener ? ['input', 'keyup'] : ['change']; const listenerKey = '__xuda_tailwind_bind_listeners'; const listenerStateKey = `${listenerKey}_state`; if (node[listenerKey]) { for (let index = 0; index < node[listenerKey].length; index++) { node.removeEventListener(node[listenerKey][index].eventName, node[listenerKey][index].listener); } } if (node[listenerStateKey]?.debounceTimer) { clearTimeout(node[listenerStateKey].debounceTimer); } const state = { debounceTimer: null, lastValue: node.value, pendingEvent: null, pendingValue: node.value, }; node[listenerStateKey] = state; const listener = (e) => { if (!useLiveTextListener) { callback(e); return; } if (node.value === state.pendingValue) { return; } state.pendingEvent = e; state.pendingValue = node.value; clearTimeout(state.debounceTimer); state.debounceTimer = setTimeout(() => { state.debounceTimer = null; state.pendingValue = node.value; if (node.value === state.lastValue) { return; } state.lastValue = node.value; callback(state.pendingEvent); }, 200); }; node[listenerKey] = eventNames.map((eventName) => { node.addEventListener(eventName, listener); return { eventName, listener }; }); } }; export const toast = class { async create(type, message, title) { let svg = ''; switch (type) { case 'error': svg = `<svg class="w-6 h-6 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>`; console.info(message); break; case 'success': svg = `<svg class="w-6 h-6 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>`; break; case 'info': svg = `<svg class="w-6 h-6 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>`; break; case 'warning': svg = `<svg class="w-6 h-6 text-yellow-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>`; console.info(message); break; default: } this.$html = _htmlToElement(` <div class="w-full flex flex-col items-center space-y-4 sm:items-end"> <div class="max-w-sm w-full bg-white shadow-lg rounded-lg pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden"> <div class="p-4"> <div class="flex items-start"> <div class="flex-shrink-0"> ${svg} </div> <div class="ms-3 w-0 flex-1 pt-0.5"> <p class="text-sm font-medium text-gray-900"></p> ${title} <p class="text-sm text-gray-500"> ${message} </p> </div> <div class="ml-4 flex-shrink-0 flex"> <button class="bg-white rounded-md inline-flex text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> <span class="sr-only">Close</span> <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path> </svg> </button> </div> </div> </div> </div> </div> `); const container = document.getElementById('tailwind_toast_controller'); if (container) { container.appendChild(this.$html); } const closeBtn = this.$html.querySelector('button'); if (closeBtn) { closeBtn.addEventListener('click', () => { this.$html.remove(); }); } setTimeout(() => { this.$html.style.transition = 'opacity 0.6s'; this.$html.style.opacity = '0'; setTimeout(() => { this.$html.remove(); }, 600); }, 5000); } init(text) {} close() { this.$html.remove(); } }; export const loader = class { async create() { const loadingController = function () { var id = undefined; return { create: function () { id = Date.now(); }, present: function () { const html = `<div id="tailwind_loader_${id}" class="absolute z-[999] inset-0 px-4 sm:inset-0 sm:flex sm:items-center sm:justify-center spinny"> <div class="absolute inset-0 transition-opacity"> <div class="absolute inset-0 rounded-md bg-gray-900 opacity-30"></div> </div> <div class="transform transition-all sm:max-w-2xl w-full h-full flex items-center" role="dialog" aria-modal="true" aria-labelledby="modal-headline"> <div class="spinner spinner-add ease-linear my-auto mx-auto"> ${text ? text : 'Please wait..'} </div> </div> </div>`; const existing = document.getElementById('tailwind_loader_' + id); if (existing) existing.remove(); const el = _htmlToElement(html); document.body.prepend(el); return el; }, dismiss: function () { const el = document.getElementById('tailwind_loader_' + id); if (el) el.remove(); }, }; }; const loader = loadingController.create(); loader.present(); return loader; } init(text) {} close(id) { const el = document.getElementById('tailwind_loader_' + id); if (el) el.remove(); } }; export const popover = class { async open(e) { this.SESSION_ID = e.SESSION_ID; const popoverController = function () { return { present: function () { const existing = document.getElementById('xu_popover_modal_' + e.SESSION_ID); if (existing) existing.remove(); const html = ` <div id="xu_popover_modal_${e.SESSION_ID}" class="absolute z-10 inline-block w-64 text-sm font-light text-gray-500 transition-opacity duration-300 bg-white border border-gray-200 rounded-lg shadow-sm dark:text-gray-400 dark:bg-gray-800 dark:border-gray-600 " > <xu-popover-content-${e.SESSION_ID}> </div> `; document.body.insertAdjacentHTML('afterbegin', html); }, dismiss: function () { const el = document.getElementById('xu_popover_modal_' + e.SESSION_ID); if (el) el.remove(); }, }; }; let popover = new popoverController(); popover.present(); } async set(e) {} async close(e) { const el = document.getElementById('xu_popover_modal_' + this.SESSION_ID); if (el) el.remove(); } }; export const popup = class { async create(header, subheader, message, buttons) { var html_buttons = ''; buttons.forEach((val) => { html_buttons += ` <button id="${val.role || val}_button" type="button" class=" ${ _colors[val.cssClass] || _colors.secondary } mt-3 w-full inline-flex justify-center rounded-md border shadow-sm px-4 py-2 text-base font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"> ${val.text || val} </button> `; }); this.$html = _htmlToElement(` <div class="fixed z-10 inset-0 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true"> <div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"> <div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"></div> <span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span> <div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"> <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"> <div class="sm:flex sm:items-start"> <div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10"> <!-- Heroicon name: outline/exclamation --> <svg class="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> </svg> </div> <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"> <h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-title"> ${subheader} </h3> <div class="mt-2"> <p class="text-sm text-gray-500"> ${message} </p> </div> </div> </div> </div> <div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse"> ${html_buttons} </div> </div> </div> </div> `); document.body.appendChild(this.$html); buttons.forEach((val) => { const btn = document.getElementById(`${val.role || val}_button`); if (btn) { btn.addEventListener('click', () => { this.$html.remove(); if (val.handler) { val.handler(); } }); } }); } async update(text) {} async close(loader) { this.$html.remove(); } }; export const modal = class { async create(e) { let template_close_btn = ` <button type="button" class=""> <svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg> </button> `; let template_header = ` <div class="border-b py-3 px-4 flex items-center justify-between"> <h3 class="font-medium leading-6 text-gray-900 sm:text-lg" id="modal-title"> ${e.properties?.name} </h3> ${!e.properties?.disableModalClose ? template_close_btn : ''} </div>`; this.template = _htmlToElement(` <div class="relative z-10"> <div class="fixed inset-0 z-10 overflow-y-auto"> ${e.properties?.allowBackdropDismiss ? '<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity backDrop"></div>' : ''} <div class="flex min-h-full items-${e?.properties?.modalPosition ? e.properties?.modalPosition : 'end'} justify-center sm:items-center"> <div class="relative mx-4 my-2 w-full transform overflow-hidden rounded-lg border border-gray-50 shadow-xl transition-all sm:max-w-lg bg-white"> ${!e.properties?.hideHeader ? template_header : ''} <div class="template_body"> </div> </div> </div> </div> </div> `); this.close_callback = e.close_callback; this.modal_id = e.modal_id; this.modal = document.createElement(e.modal_id); document.body.appendChild(this.modal); this.modal.innerHTML = ''; this.modal.appendChild(this.template); if (!e.properties?.disableModalClose) { const closeBtn = this.modal.querySelector('button'); if (closeBtn) { closeBtn.addEventListener('click', () => { this.close(); }); } } if (e?.properties?.allowBackdropDismiss) { const backdrop = this.modal.querySelector('.backDrop'); if (backdrop) { backdrop.addEventListener('click', () => { this.close(); }); } } return this; } async init(e) { const xu_modal_controller = document.querySelector('xu-modal-controller'); const modalEl = document.querySelector(e.modal_id); if (!modalEl) return; const controllerData = func.runtime.ui.get_data(xu_modal_controller); const params = controllerData?.xuControllerParams?.[e.modal_id]; if (e.$container) { const containerId = func.runtime.ui.get_attr(e.$container, 'id'); if (containerId) modalEl.setAttribute('id', containerId); const containerData = func.runtime.ui.get_data(e.$container); if (containerData) { for (const [key, val] of Object.entries(containerData)) { func.runtime.ui.set_data(modalEl, key, val); } } } const templateBody = modalEl.querySelector('.template_body'); if (templateBody && params?.$dialogDiv) { const dialogNode = params.$dialogDiv?.nodeType ? params.$dialogDiv : params.$dialogDiv?.[0]; if (dialogNode) { templateBody.innerHTML = ''; templateBody.appendChild(dialogNode); } } } async present(modal) { await modal.present(); } async properties() { return { modalPosition: { label: 'Modal Position', type: 'string', render: 'select', options: [ { label: 'Stretch', value: 'stretch' }, { label: 'Start', value: 'start' }, { label: 'Center', value: 'center' }, { label: 'End', value: 'end' }, ], }, allowBackdropDismiss: { label: 'Allow Backdrop Dismiss', type: 'bool', render: 'checkbox', }, allowModalClose: { label: 'Allow Modal Close', type: 'bool', render: 'checkbox', }, hideHeader: { label: 'Hide Header', render: 'checkbox', type: 'bool', }, }; } async close() { this.modal.remove(); if (this.close_callback) this.close_callback(this.modal_id); } }; export const page = class { async create(e) { this.$page = document.createElement('div'); const $header = _htmlToElement(` <div class="flex py-3 px-2 border-b"> <h3 class="font-medium leading-6 text-gray-900 sm:text-lg" id="modal-title">${e.properties.name}</h3> </div> `); if (!e?.properties?.disablePageBack) { const $button = _htmlToElement(` <button type="button" class="mr-3"> <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clip-rule="evenodd"></path></svg> </button>`); $header.prepend($button); $button.addEventListener('click', async () => { this.page_back_callback(); }); } if (!e?.properties?.hideHeader) { if (!e?.properties?.disablePageBack) { this.$page.appendChild($header); } } } async init(e) { e.callback = (elm, page_back_callback) => { const targetElm = elm?.nodeType ? elm : elm?.[0]; if (targetElm) { // appendChild MOVES nodes, so when targetElm === this.$page (which can // happen when the page component is re-connected/re-rendered into itself, // e.g. via a tailwind refresh), `while (this.$page.firstChild)` rotates // the same children forever -> synchronous infinite loop ("Page // Unresponsive"). Guard the self-move (it's a no-op anyway). if (targetElm !== this.$page) { while (this.$page.firstChild) { targetElm.appendChild(this.$page.firstChild); } } else { console.warn('[xuda-tailwind] page mount target === $page; skipped self-move (was an infinite loop)'); } const divNode = e.div?.nodeType ? e.div : e.div?.[0]; if (divNode) targetElm.appendChild(divNode); } this.page_back_callback = page_back_callback; }; } async properties() { return { allowPageBack: { label: 'Allow Page Back', render: 'checkbox', type: 'bool', }, hideHeader: { label: 'Hide Header', render: 'checkbox', type: 'bool', }, }; } };