SmartestHome/admin-canvas/render/canvas-sdk/window-chrome.js

122 lines
4.1 KiB
JavaScript

/*
* AdminWindow — floating panel chrome for the admin canvas.
*
* A near-verbatim copy of digest-engine/render/digest-canvas-sdk/window-chrome.js
* (docs/project-plan.md Phase 12), duplicated rather than shared per Phase 13's
* decision to keep the two canvases fully decoupled. Only the `digest-` class/name
* prefix has been renamed to `admin-` throughout; the behaviour is identical.
*
* Windows look like windows (title bar, rounded corners, shadow, glow accent) but
* are not actually draggable. The canvas is read on a wall-mounted kiosk; there is
* no pointer doing window management, and real drag-and-drop would only add state
* that nothing persists.
*
* x/y/w/h are optional. Given, the window is absolutely positioned (percentages of
* the canvas) — that is the escape hatch for a deliberately composed layout.
* Omitted, the window participates in the canvas's normal flow/grid layout, which
* is what render.js does by default so the layout survives any window count.
*/
(function (global) {
'use strict';
var counter = 0;
function escapeHtml(text) {
return String(text)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
// Intentionally tiny: bold, italic, inline code and paragraph breaks — pulling in
// a markdown parser for three inline forms would violate this project's
// no-unnecessary-deps rule.
function renderInline(text) {
return escapeHtml(text)
.replace(/`([^`]+)`/g, '<code>$1</code>')
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
.replace(/(^|[\s(])\*([^*]+)\*/g, '$1<em>$2</em>');
}
function renderContent(content) {
var body = document.createElement('div');
body.className = 'admin-window-body';
if (Array.isArray(content)) {
var list = document.createElement('ul');
list.className = 'admin-window-list';
content.forEach(function (item) {
var li = document.createElement('li');
li.innerHTML = renderInline(item);
list.appendChild(li);
});
body.appendChild(list);
return body;
}
// Duck-typed rather than `instanceof Node`: render.js hands stat/chart windows
// their mount point through here, and an identity check against a global that
// may not exist in every embedding context is a needless way to lose it.
if (content && typeof content.nodeType === 'number') {
body.appendChild(content);
return body;
}
String(content === undefined || content === null ? '' : content)
.split(/\n{2,}/)
.forEach(function (block) {
if (!block.trim()) { return; }
var p = document.createElement('p');
p.innerHTML = renderInline(block).replace(/\n/g, '<br>');
body.appendChild(p);
});
return body;
}
var AdminWindow = {
open: function (options) {
options = options || {};
var container = options.container || document.body;
var el = document.createElement('section');
el.className = 'admin-window';
el.id = options.id || ('admin-window-' + (++counter));
if (options.variant) {
el.classList.add('admin-window-' + options.variant);
}
var bar = document.createElement('header');
bar.className = 'admin-window-titlebar';
var dots = document.createElement('span');
dots.className = 'admin-window-dots';
dots.setAttribute('aria-hidden', 'true');
bar.appendChild(dots);
var title = document.createElement('h2');
title.className = 'admin-window-title';
title.textContent = options.title || '';
bar.appendChild(title);
el.appendChild(bar);
el.appendChild(renderContent(options.content));
if (typeof options.x === 'number' && typeof options.y === 'number') {
el.classList.add('admin-window-positioned');
el.style.left = options.x + '%';
el.style.top = options.y + '%';
}
if (typeof options.w === 'number') { el.style.width = options.w + '%'; }
if (typeof options.h === 'number') { el.style.height = options.h + '%'; }
container.appendChild(el);
return el;
}
};
global.AdminWindow = AdminWindow;
})(window);