@xuda.io/xuda-framework-plugin-tailwind
Version:
Xuda Tailwind UI Framework plugin
791 lines (710 loc) • 28.4 kB
JavaScript
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">​</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',
},
};
}
};