A modern, dependency-free UI library for Tampermonkey scripts
Αυτός ο κώδικας δεν πρέπει να εγκατασταθεί άμεσα. Είναι μια βιβλιοθήκη για άλλους κώδικες που περιλαμβάνεται μέσω της οδηγίας meta // @require https://update.greasyfork.org/scripts/564901/1749919/CKUI.js
// ==UserScript==
// @name CKUI
// @namespace ckylin-script-lib-ckui
// @version 2.4.2
// @description A modern, dependency-free UI library for Tampermonkey scripts
// @match http://*
// @match https://*
// @grant unsafeWindow
// @author CKylinMC
// @license GPL-3.0-only
// ==/UserScript==
// 调试用
if (typeof unsafeWindow === 'undefined' || !unsafeWindow) {
window.unsafeWindow = window;
}
(function(unsafeWindow, document) {
'use strict';
if (unsafeWindow.ckui && unsafeWindow.ckui.__initialized) {
console.log('[CKUI] Already initialized, skipping...');
return;
}
const globalConfig = {
zIndexBase: 999990,
currentTheme: 'light'
};
if (!unsafeWindow.__ckui_mouseTrackingEnabled) {
unsafeWindow.__ckui_mouseTrackingEnabled = false;
unsafeWindow.__ckui_lastMouseX = null;
unsafeWindow.__ckui_lastMouseY = null;
}
class InstanceManager {
constructor() {
this.modals = new Map();
this.floatWindows = new Map();
this.forms = new Map();
}
register(type, id, instance) {
const map = this[type];
if (map) {
map.set(id, instance);
}
}
unregister(type, id) {
const map = this[type];
if (map) {
map.delete(id);
}
}
get(type, id) {
const map = this[type];
return map ? map.get(id) : null;
}
has(type, id) {
const map = this[type];
return map ? map.has(id) : false;
}
}
const instanceManager = new InstanceManager();
class Reactive {
constructor(value) {
this._value = value;
this._subscribers = [];
}
get value() {
return this._value;
}
set value(newValue) {
if (this._value !== newValue) {
this._value = newValue;
this._notify();
}
}
subscribe(callback) {
this._subscribers.push(callback);
return () => {
this._subscribers = this._subscribers.filter(cb => cb !== callback);
};
}
_notify() {
this._subscribers.forEach(callback => {
try {
callback(this._value);
} catch (e) {
console.error('[CKUI] Reactive callback error:', e);
}
});
}
}
const utils = {
uuid() {
return 'ckui-' + Date.now().toString(36) + '-' + Math.random().toString(36).substr(2, 9);
},
createElement(tag, props = {}, children = []) {
const el = document.createElement(tag);
Object.entries(props).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
}
if (key === 'style' && typeof value === 'object') {
Object.assign(el.style, value);
} else if (key === 'class' || key === 'className') {
el.className = value;
} else if (key.startsWith('on') && typeof value === 'function') {
el.addEventListener(key.substring(2).toLowerCase(), value);
} else if (key === 'dataset' && typeof value === 'object') {
Object.assign(el.dataset, value);
} else if (key === 'disabled' && value === false) {
return;
} else {
el.setAttribute(key, value);
}
});
children.forEach(child => {
if (typeof child === 'string') {
el.appendChild(document.createTextNode(child));
} else if (child instanceof Node) {
el.appendChild(child);
}
});
return el;
},
injectStyles(css, id = null) {
const styleId = id || `ckui-style-${Date.now()}`;
let style = document.getElementById(styleId);
if (!style) {
style = document.createElement('style');
style.id = styleId;
document.head.appendChild(style);
}
style.textContent = css;
return style;
},
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
},
createShadowHost(options = {}) {
const host = document.createElement('div');
host.className = 'ckui-shadow-host';
const zIndex = globalConfig.zIndexBase + 20;
host.style.cssText = `all: initial; position: fixed; top: 0; left: 0; width: 0; height: 0;`;
host.style.zIndex = zIndex;
const shadowRoot = host.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = coreStyles;
shadowRoot.appendChild(style);
const container = document.createElement('div');
// container.style.cssText = `position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 1;`;
container.style.cssText = `z-index: 1;`;
shadowRoot.appendChild(container);
document.body.appendChild(host);
return {
host,
shadowRoot,
container
};
}
};
const coreStyles = `
/* CKUI Core Styles - Isolated with high specificity */
.ckui-root {
/* all: initial; */
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 14px;
line-height: 1.5;
color: #333;
box-sizing: border-box;
/* CSS Variables for customization */
--ckui-primary: #0f172a;
--ckui-primary-hover: #1e293b;
--ckui-secondary: #f8fafc;
--ckui-secondary-hover: #f1f5f9;
--ckui-border: #e5e7eb;
--ckui-border-dark: #cbd5e1;
--ckui-text: #0f172a;
--ckui-text-secondary: #64748b;
--ckui-text-muted: #94a3b8;
--ckui-bg: white;
--ckui-bg-secondary: #f8fafc;
--ckui-success: #10b981;
--ckui-success-hover: #059669;
--ckui-danger: #ef4444;
--ckui-danger-hover: #dc2626;
--ckui-warning: #f59e0b;
--ckui-info: #3b82f6;
--ckui-btn-primary-text: white;
--ckui-btn-danger-text: white;
--ckui-btn-success-text: white;
--ckui-radius: 6px;
--ckui-radius-sm: 4px;
--ckui-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.05);
--ckui-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
--ckui-shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.1);
--ckui-spacing-xs: 4px;
--ckui-spacing-sm: 8px;
--ckui-spacing: 12px;
--ckui-spacing-md: 16px;
--ckui-spacing-lg: 20px;
--ckui-spacing-xl: 24px;
--ckui-animation-duration: 0.2s;
--ckui-animation-easing: ease-out;
}
.ckui-root *, .ckui-root *::before, .ckui-root *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* Overlay */
.ckui-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
animation: ckui-fade-in var(--ckui-animation-duration) var(--ckui-animation-easing);
}
/* Modal */
.ckui-modal {
position: relative;
background: var(--ckui-bg);
border-radius: var(--ckui-radius);
box-shadow: var(--ckui-shadow-lg);
border: 1px solid var(--ckui-border);
max-width: 90vw;
max-height: 90vh;
display: flex;
flex-direction: column;
animation: ckui-modal-in var(--ckui-animation-duration) var(--ckui-animation-easing);
}
.ckui-modal-header {
padding: var(--ckui-spacing-lg) var(--ckui-spacing-xl);
border-bottom: 1px solid var(--ckui-border);
display: flex;
align-items: center;
justify-content: space-between;
}
.ckui-modal-title {
font-size: 18px;
font-weight: 600;
color: var(--ckui-text);
margin: 0;
display: flex;
align-items: center;
gap: 0;
}
.ckui-modal-title.has-icon {
gap: 12px;
}
.ckui-modal-icon {
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.ckui-modal-icon.circle {
border-radius: 50%;
}
.ckui-modal-icon.square {
border-radius: 4px;
}
.ckui-modal-icon img {
width: 100%;
height: 100%;
object-fit: cover;
}
.ckui-modal-close {
background: none;
border: none;
font-size: 24px;
color: var(--ckui-text-secondary);
cursor: pointer;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--ckui-radius);
transition: all var(--ckui-animation-duration);
}
.ckui-modal-close:hover {
background: var(--ckui-secondary-hover);
color: var(--ckui-text);
}
.ckui-modal-body {
padding: var(--ckui-spacing-xl);
overflow-y: auto;
flex: 1;
}
.ckui-modal-footer {
padding: var(--ckui-spacing-md) var(--ckui-spacing-xl);
border-top: 1px solid var(--ckui-border);
display: flex;
justify-content: flex-end;
gap: var(--ckui-spacing);
}
/* Float Window */
.ckui-float-window {
position: fixed;
background: var(--ckui-bg);
border-radius: var(--ckui-radius);
box-shadow: var(--ckui-shadow-lg);
border: 1px solid var(--ckui-border);
display: flex;
flex-direction: column;
min-width: 200px;
max-width: 90vw;
max-height: 90vh;
}
.ckui-float-header {
padding: var(--ckui-spacing) var(--ckui-spacing-md);
background: var(--ckui-bg);
border-bottom: 1px solid var(--ckui-border);
color: var(--ckui-text);
cursor: move;
user-select: none;
border-radius: var(--ckui-radius) var(--ckui-radius) 0 0;
display: flex;
align-items: center;
justify-content: space-between;
}
.ckui-float-title {
font-size: 14px;
font-weight: 600;
margin: 0;
display: flex;
align-items: center;
gap: 0;
}
.ckui-float-title.has-icon {
gap: 8px;
}
.ckui-float-icon {
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.ckui-float-icon.circle {
border-radius: 50%;
}
.ckui-float-icon.square {
border-radius: 4px;
}
.ckui-float-icon img {
width: 100%;
height: 100%;
object-fit: cover;
}
.ckui-float-controls {
display: flex;
gap: 4px;
}
.ckui-float-btn {
background: transparent;
border: none;
color: var(--ckui-text-secondary);
width: 24px;
height: 24px;
border-radius: var(--ckui-radius-sm);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
transition: all var(--ckui-animation-duration);
}
.ckui-float-btn:hover {
background: var(--ckui-secondary-hover);
color: var(--ckui-text);
}
.ckui-float-body {
padding: var(--ckui-spacing-md);
overflow-y: auto;
flex: 1;
}
/* Notification */
.ckui-notification-container {
position: fixed;
top: var(--ckui-spacing-lg);
right: var(--ckui-spacing-lg);
display: flex;
flex-direction: column;
gap: var(--ckui-spacing);
pointer-events: none;
}
.ckui-notification {
background: var(--ckui-bg);
border-radius: var(--ckui-radius);
box-shadow: var(--ckui-shadow);
border: 1px solid var(--ckui-border);
padding: var(--ckui-spacing-md) var(--ckui-spacing-lg);
min-width: 300px;
max-width: 400px;
display: flex;
align-items: flex-start;
gap: var(--ckui-spacing);
pointer-events: auto;
animation: ckui-slide-in-right var(--ckui-animation-duration) var(--ckui-animation-easing);
}
.ckui-notification.success { border-left: 3px solid var(--ckui-success); }
.ckui-notification.error { border-left: 3px solid var(--ckui-danger); }
.ckui-notification.warning { border-left: 3px solid var(--ckui-warning); }
.ckui-notification.info { border-left: 3px solid var(--ckui-info); }
.ckui-notification-icon {
font-size: 20px;
flex-shrink: 0;
}
.ckui-notification-content {
flex: 1;
}
.ckui-notification-title {
font-weight: 600;
color: var(--ckui-text);
margin-bottom: var(--ckui-spacing-xs);
}
.ckui-notification-message {
color: var(--ckui-text-secondary);
font-size: 13px;
}
.ckui-notification-close {
background: none;
border: none;
color: #94a3b8;
cursor: pointer;
font-size: 16px;
padding: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
/* Button */
.ckui-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: var(--ckui-spacing-sm) var(--ckui-spacing-md);
height: 36px;
border: 1px solid var(--ckui-border);
border-radius: var(--ckui-radius);
background: var(--ckui-bg);
color: var(--ckui-text);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all var(--ckui-animation-duration);
outline: none;
user-select: none;
margin-right: 5px;
}
.ckui-btn:hover {
background: var(--ckui-secondary);
border-color: var(--ckui-border-dark);
}
.ckui-btn:active {
transform: scale(0.98);
}
.ckui-btn-primary {
background: var(--ckui-primary);
color: var(--ckui-btn-primary-text);
border-color: var(--ckui-primary);
}
.ckui-btn-primary:hover {
background: var(--ckui-primary-hover);
border-color: var(--ckui-primary-hover);
}
.ckui-btn-danger {
background: var(--ckui-danger);
color: var(--ckui-btn-danger-text);
border-color: var(--ckui-danger);
}
.ckui-btn-danger:hover {
background: var(--ckui-danger-hover);
border-color: var(--ckui-danger-hover);
}
.ckui-btn-success {
background: var(--ckui-success);
color: var(--ckui-btn-success-text);
border-color: var(--ckui-success);
}
.ckui-btn-success:hover {
background: var(--ckui-success-hover);
border-color: var(--ckui-success-hover);
}
/* Input */
.ckui-input {
display: inline-block;
width: 100%;
height: 36px;
padding: var(--ckui-spacing-sm) var(--ckui-spacing);
border: 1px solid var(--ckui-border);
border-radius: var(--ckui-radius);
font-size: 14px;
color: var(--ckui-text);
background: var(--ckui-bg);
transition: all var(--ckui-animation-duration);
outline: none;
}
.ckui-input:focus {
border-color: var(--ckui-info);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.ckui-input:disabled {
background: var(--ckui-secondary);
color: var(--ckui-text-muted);
cursor: not-allowed;
}
/* Textarea */
.ckui-textarea {
display: block;
width: 100%;
padding: var(--ckui-spacing-sm) var(--ckui-spacing);
border: 1px solid var(--ckui-border);
border-radius: var(--ckui-radius);
font-size: 14px;
color: var(--ckui-text);
background: var(--ckui-bg);
transition: all var(--ckui-animation-duration);
outline: none;
resize: vertical;
min-height: 80px;
font-family: inherit;
}
.ckui-textarea:focus {
border-color: var(--ckui-info);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
/* Select */
.ckui-select {
display: inline-block;
width: 100%;
height: 36px;
padding: var(--ckui-spacing-sm) var(--ckui-spacing);
border: 1px solid var(--ckui-border);
border-radius: var(--ckui-radius);
font-size: 14px;
color: var(--ckui-text);
background: var(--ckui-bg);
transition: all var(--ckui-animation-duration);
outline: none;
cursor: pointer;
}
.ckui-select:focus {
border-color: var(--ckui-info);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
/* Checkbox & Radio */
.ckui-checkbox, .ckui-radio {
display: inline-flex;
align-items: center;
cursor: pointer;
user-select: none;
}
.ckui-checkbox input, .ckui-radio input {
margin-right: 8px;
width: 16px;
height: 16px;
cursor: pointer;
}
/* Label */
.ckui-label {
display: block;
margin-bottom: var(--ckui-spacing-sm);
color: var(--ckui-text);
font-weight: 500;
font-size: 14px;
}
/* Form Group */
.ckui-form-group {
margin-bottom: var(--ckui-spacing);
}
.ckui-form-group.has-error .ckui-input,
.ckui-form-group.has-error .ckui-textarea,
.ckui-form-group.has-error .ckui-select {
border-color: var(--ckui-danger);
}
.ckui-form-error {
color: var(--ckui-danger);
font-size: 12px;
margin-top: 4px;
line-height: 1.4;
display: block;
}
/* Layout */
.ckui-row {
display: flex;
flex-wrap: wrap;
margin: 0 calc(-1 * var(--ckui-spacing-sm));
}
.ckui-col {
flex: 1;
padding: 0 var(--ckui-spacing-sm);
}
.ckui-space {
display: flex;
gap: var(--ckui-spacing);
align-items: center;
}
.ckui-divider {
height: 1px;
background: var(--ckui-border);
margin: var(--ckui-spacing-md) 0;
}
/* Card */
.ckui-card {
background: var(--ckui-bg);
border-radius: var(--ckui-radius);
border: 1px solid var(--ckui-border);
box-shadow: var(--ckui-shadow-sm);
padding: var(--ckui-spacing-md);
display: flex;
flex-direction: column;
}
.ckui-card-title {
font-size: 16px;
font-weight: 600;
color: var(--ckui-text);
margin-bottom: var(--ckui-spacing-sm);
line-height: 1.5;
}
.ckui-card > *:last-child {
margin-bottom: 0;
}
/* Loading */
.ckui-loading {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid var(--ckui-border);
border-top: 2px solid var(--ckui-primary);
border-radius: 50%;
animation: ckui-spin 0.8s linear infinite;
}
/* Animations */
@keyframes ckui-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes ckui-fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes ckui-modal-in {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes ckui-modal-out {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.95);
}
}
@keyframes ckui-slide-in-right {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes ckui-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Theme - Dark Mode */
.ckui-root.ckui-dark {
--ckui-primary: #3b82f6;
--ckui-primary-hover: #2563eb;
--ckui-secondary: #1e293b;
--ckui-secondary-hover: #334155;
--ckui-border: #1e293b;
--ckui-border-dark: #334155;
--ckui-text: #f1f5f9;
--ckui-text-secondary: #94a3b8;
--ckui-text-muted: #64748b;
--ckui-bg: #0f172a;
--ckui-bg-secondary: #1e293b;
--ckui-btn-primary-text: white;
--ckui-btn-danger-text: white;
--ckui-btn-success-text: white;
}
.ckui-root.ckui-dark .ckui-modal,
.ckui-root.ckui-dark .ckui-float-window,
.ckui-root.ckui-dark .ckui-notification,
.ckui-root.ckui-dark .ckui-card {
background: var(--ckui-bg);
border-color: var(--ckui-border);
color: var(--ckui-text);
}
.ckui-root.ckui-dark .ckui-modal-header,
.ckui-root.ckui-dark .ckui-modal-footer,
.ckui-root.ckui-dark .ckui-float-header {
background: var(--ckui-bg);
border-color: var(--ckui-border);
}
.ckui-root.ckui-dark .ckui-modal-body,
.ckui-root.ckui-dark .ckui-float-body {
background: var(--ckui-bg);
color: var(--ckui-text) !important;
}
.ckui-root.ckui-dark .ckui-modal-body *,
.ckui-root.ckui-dark .ckui-float-body * {
color: inherit;
}
.ckui-root.ckui-dark .ckui-modal-title,
.ckui-root.ckui-dark .ckui-float-title,
.ckui-root.ckui-dark .ckui-card-title,
.ckui-root.ckui-dark .ckui-label {
color: var(--ckui-text);
}
.ckui-root.ckui-dark .ckui-modal-close,
.ckui-root.ckui-dark .ckui-float-btn,
.ckui-root.ckui-dark .ckui-notification-close {
color: var(--ckui-text-secondary);
}
.ckui-root.ckui-dark .ckui-modal-close:hover,
.ckui-root.ckui-dark .ckui-float-btn:hover {
background: var(--ckui-secondary-hover);
color: var(--ckui-text);
}
.ckui-root.ckui-dark .ckui-input,
.ckui-root.ckui-dark .ckui-textarea,
.ckui-root.ckui-dark .ckui-select {
background: var(--ckui-bg-secondary);
border-color: var(--ckui-border-dark);
color: var(--ckui-text);
}
.ckui-root.ckui-dark .ckui-btn {
background: var(--ckui-secondary) !important;
border-color: var(--ckui-border-dark) !important;
color: var(--ckui-text) !important;
}
.ckui-root.ckui-dark .ckui-btn:hover {
background: var(--ckui-secondary-hover) !important;
border-color: var(--ckui-border-dark) !important;
}
.ckui-root.ckui-dark .ckui-divider {
background: var(--ckui-border);
}
.ckui-root.ckui-dark .ckui-notification-title {
color: var(--ckui-text);
}
.ckui-root.ckui-dark .ckui-notification-message {
color: var(--ckui-text-secondary);
}
/* Dark mode notification color enhancements */
.ckui-root.ckui-dark .ckui-notification.success {
border-left-color: #22c55e;
background-color: var(--ckui-bg);
background-image: linear-gradient(to right, rgba(34, 197, 94, 0.2) 0%, transparent 30%);
}
.ckui-root.ckui-dark .ckui-notification.error {
border-left-color: #f87171;
background-color: var(--ckui-bg);
background-image: linear-gradient(to right, rgba(248, 113, 113, 0.2) 0%, transparent 30%);
}
.ckui-root.ckui-dark .ckui-notification.warning {
border-left-color: #fbbf24;
background-color: var(--ckui-bg);
background-image: linear-gradient(to right, rgba(251, 191, 36, 0.2) 0%, transparent 30%);
}
.ckui-root.ckui-dark .ckui-notification.info {
border-left-color: #60a5fa;
background-color: var(--ckui-bg);
background-image: linear-gradient(to right, rgba(96, 165, 250, 0.2) 0%, transparent 30%);
}
/* 深色模式下primary按钮直接使用变量,无需重定义 */
.ckui-root.ckui-dark .ckui-btn-primary {
background: var(--ckui-primary) !important;
color: var(--ckui-btn-primary-text) !important;
border-color: var(--ckui-primary) !important;
}
.ckui-root.ckui-dark .ckui-btn-primary:hover {
background: var(--ckui-primary-hover) !important;
border-color: var(--ckui-primary-hover) !important;
}
.ckui-root.ckui-dark .ckui-btn-danger {
--ckui-danger: #dc2626;
--ckui-danger-hover: #b91c1c;
background: var(--ckui-danger);
color: var(--ckui-btn-danger-text);
border-color: var(--ckui-danger);
}
.ckui-root.ckui-dark .ckui-btn-danger:hover {
background: var(--ckui-danger-hover);
border-color: var(--ckui-danger-hover);
}
.ckui-root.ckui-dark .ckui-btn-success {
--ckui-success: #059669;
--ckui-success-hover: #047857;
background: var(--ckui-success);
color: var(--ckui-btn-success-text);
border-color: var(--ckui-success);
}
.ckui-root.ckui-dark .ckui-btn-success:hover {
background: var(--ckui-success-hover);
border-color: var(--ckui-success-hover);
}
.ckui-root.ckui-dark .ckui-overlay {
background: rgba(0, 0, 0, 0.8);
}
.ckui-root.ckui-dark .ckui-input:focus,
.ckui-root.ckui-dark .ckui-textarea:focus,
.ckui-root.ckui-dark .ckui-select:focus {
border-color: #60a5fa;
box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.2);
}
/* Tags Input */
.ckui-tags-container {
display: flex;
flex-wrap: wrap;
gap: var(--ckui-spacing-sm);
padding: var(--ckui-spacing-sm);
border: 1px solid var(--ckui-border);
border-radius: var(--ckui-radius);
background: var(--ckui-bg);
min-height: 40px;
align-items: center;
transition: all var(--ckui-animation-duration);
}
.ckui-tags-container:focus-within {
border-color: var(--ckui-info);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.ckui-tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
background: var(--ckui-secondary);
border: 1px solid var(--ckui-border);
border-radius: var(--ckui-radius-sm);
font-size: 13px;
color: var(--ckui-text);
max-width: 200px;
}
.ckui-tag-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ckui-tag-remove {
background: none;
border: none;
color: var(--ckui-text-secondary);
cursor: pointer;
padding: 0;
width: 14px;
height: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
line-height: 1;
border-radius: 50%;
transition: all var(--ckui-animation-duration);
}
.ckui-tag-remove:hover {
background: var(--ckui-border);
color: var(--ckui-text);
}
.ckui-tags-input {
border: none;
outline: none;
background: transparent;
flex: 1;
min-width: 100px;
font-size: 14px;
color: var(--ckui-text);
padding: 4px;
}
.ckui-tags-input::placeholder {
color: var(--ckui-text-muted);
}
/* Tags Error */
.ckui-tags-container.error {
border-color: var(--ckui-danger);
animation: ckui-shake 0.4s ease-in-out;
}
.ckui-tags-error {
color: var(--ckui-danger);
font-size: 12px;
margin-top: 4px;
line-height: 1.4;
}
.ckui-tags-error.warning {
color: var(--ckui-warning);
}
@keyframes ckui-shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-8px); }
75% { transform: translateX(8px); }
}
/* Select Tags Dropdown */
.ckui-select-tags-wrapper {
position: relative;
}
.ckui-select-tags-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
margin-top: 4px;
background: var(--ckui-bg);
border: 1px solid var(--ckui-border);
border-radius: var(--ckui-radius);
box-shadow: var(--ckui-shadow);
max-height: 200px;
overflow-y: auto;
z-index: 1000;
display: none;
}
.ckui-select-tags-dropdown.active {
display: block;
}
.ckui-select-tags-option {
padding: var(--ckui-spacing-sm) var(--ckui-spacing);
cursor: pointer;
transition: all var(--ckui-animation-duration);
font-size: 14px;
color: var(--ckui-text);
}
.ckui-select-tags-option:hover,
.ckui-select-tags-option.selected {
background: var(--ckui-secondary);
}
.ckui-select-tags-option.highlighted {
background: var(--ckui-secondary-hover);
}
.ckui-select-tags-option.disabled {
color: var(--ckui-text-muted);
cursor: not-allowed;
opacity: 0.5;
}
/* Dark mode for tags */
.ckui-root.ckui-dark .ckui-tags-container {
background: var(--ckui-bg-secondary);
border-color: var(--ckui-border-dark);
}
.ckui-root.ckui-dark .ckui-tags-container:focus-within {
border-color: #60a5fa;
box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.2);
}
.ckui-root.ckui-dark .ckui-tag {
background: var(--ckui-secondary);
border-color: var(--ckui-border-dark);
}
.ckui-root.ckui-dark .ckui-tags-input {
color: var(--ckui-text);
}
.ckui-root.ckui-dark .ckui-select-tags-dropdown {
background: var(--ckui-bg);
border-color: var(--ckui-border-dark);
}
.ckui-root.ckui-dark .ckui-select-tags-option:hover,
.ckui-root.ckui-dark .ckui-select-tags-option.selected {
background: var(--ckui-secondary);
}
.ckui-root.ckui-dark .ckui-select-tags-option.highlighted {
background: var(--ckui-secondary-hover);
}
/* Hidden Area */
.ckui-hidden-area {
display: none;
}
.ckui-hidden-area.visible {
display: block;
}
/* Detail Component */
.ckui-detail {
border: 1px solid var(--ckui-border);
border-radius: var(--ckui-radius);
background: var(--ckui-bg);
overflow: hidden;
margin-bottom: var(--ckui-spacing);
}
.ckui-detail-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--ckui-spacing) var(--ckui-spacing-md);
cursor: pointer;
user-select: none;
transition: background var(--ckui-animation-duration);
background: var(--ckui-bg);
}
.ckui-detail-header:hover {
background: var(--ckui-secondary);
}
.ckui-detail-title {
font-weight: 600;
color: var(--ckui-text);
font-size: 14px;
margin: 0;
}
.ckui-detail-icon {
transition: transform var(--ckui-animation-duration);
color: var(--ckui-text-secondary);
font-size: 16px;
line-height: 1;
}
.ckui-detail.open .ckui-detail-icon {
transform: rotate(180deg);
}
.ckui-detail-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
}
.ckui-detail.open .ckui-detail-content {
max-height: 2000px;
transition: max-height 0.5s ease-in;
}
.ckui-detail-body {
padding: var(--ckui-spacing-md);
border-top: 1px solid var(--ckui-border);
}
/* Dark mode for detail */
.ckui-root.ckui-dark .ckui-detail {
background: var(--ckui-bg);
border-color: var(--ckui-border-dark);
}
.ckui-root.ckui-dark .ckui-detail-header {
background: var(--ckui-bg);
}
.ckui-root.ckui-dark .ckui-detail-header:hover {
background: var(--ckui-secondary);
}
.ckui-root.ckui-dark .ckui-detail-body {
border-top-color: var(--ckui-border-dark);
}
`;
class NotificationManager {
constructor() {
this.container = null;
this.shadowHost = null;
this.useShadow = false;
this.init();
}
init(useShadow = false) {
if (!this.container) {
this.useShadow = useShadow;
const className = globalConfig.currentTheme === 'dark'
? 'ckui-root ckui-dark ckui-notification-container'
: 'ckui-root ckui-notification-container';
if (useShadow) {
const shadow = utils.createShadowHost();
this.shadowHost = shadow.host;
shadow.container.style.pointerEvents = 'none';
this.container = utils.createElement('div', {
class: className,
style: { zIndex: globalConfig.zIndexBase + 10 }
});
shadow.container.appendChild(this.container);
} else {
this.container = utils.createElement('div', {
class: className,
style: { zIndex: globalConfig.zIndexBase + 10 }
});
document.body.appendChild(this.container);
}
}
}
show(options = {}) {
const {
title = '',
message = '',
type = 'info',
duration = 3000,
closable = true,
shadow = false
} = options;
if (shadow && !this.useShadow) {
this.destroy();
this.init(true);
} else if (!shadow && this.useShadow) {
this.destroy();
this.init(false);
} else if (!this.container) {
this.init(shadow);
}
const icons = {
success: '✓',
error: '✕',
warning: '⚠',
info: 'ℹ'
};
const notification = utils.createElement('div', {
class: `ckui-notification ${type}`
}, [
utils.createElement('div', {
class: 'ckui-notification-icon'
}, [icons[type] || icons.info]),
utils.createElement('div', {
class: 'ckui-notification-content'
}, [
title ? utils.createElement('div', {
class: 'ckui-notification-title'
}, [title]) : null,
message ? utils.createElement('div', {
class: 'ckui-notification-message'
}, [message]) : null
].filter(Boolean)),
closable ? utils.createElement('button', {
class: 'ckui-notification-close',
onclick: () => this.close(notification)
}, ['×']) : null
].filter(Boolean));
this.container.appendChild(notification);
if (duration > 0) {
setTimeout(() => this.close(notification), duration);
}
return notification;
}
close(notification) {
if (notification && notification.parentNode) {
notification.style.animation = 'ckui-fade-out 0.2s ease-out forwards';
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 200);
}
}
success(message, title = '成功') {
return this.show({ type: 'success', title, message });
}
error(message, title = '错误') {
return this.show({ type: 'error', title, message });
}
warning(message, title = '警告') {
return this.show({ type: 'warning', title, message });
}
info(message, title = '提示') {
return this.show({ type: 'info', title, message });
}
destroy() {
if (this.container && this.container.parentNode) {
this.container.parentNode.removeChild(this.container);
}
if (this.shadowHost && this.shadowHost.parentNode) {
this.shadowHost.parentNode.removeChild(this.shadowHost);
}
this.container = null;
this.shadowHost = null;
this.useShadow = false;
}
}
class Modal {
constructor(options = {}) {
this.id = options.id || null;
this.options = {
title: options.title || '提示',
content: options.content || '',
width: options.width || '500px',
showClose: options.showClose !== false,
footer: options.footer,
onOk: options.onOk,
onCancel: options.onCancel,
maskClosable: options.maskClosable !== false,
shadow: options.shadow || false,
allowHtml: options.allowHtml || false,
icon: options.icon || null,
iconShape: options.iconShape || 'square',
iconWidth: options.iconWidth || '24px',
...options
};
this.overlay = null;
this.modal = null;
this.resolvePromise = null;
this.rejectPromise = null;
this.isShowing = false;
this.shadowHost = null;
this.handleEscape = null;
if (this.id) {
instanceManager.register('modals', this.id, this);
}
}
show() {
if (this.isShowing) {
this.close(false);
}
this.isShowing = true;
return new Promise((resolve, reject) => {
this.resolvePromise = resolve;
this.rejectPromise = reject;
this.render();
});
}
refresh(options = {}) {
this.options = { ...this.options, ...options };
if (this.isShowing) {
this.close(false);
this.show();
}
return this;
}
render() {
if (this.options.shadow) {
const shadow = utils.createShadowHost();
this.shadowHost = shadow.host;
}
const className = globalConfig.currentTheme === 'dark'
? 'ckui-root ckui-dark ckui-overlay'
: 'ckui-root ckui-overlay';
this.overlay = utils.createElement('div', {
class: className,
style: { zIndex: globalConfig.zIndexBase + 8 },
onclick: (e) => {
if (this.options.maskClosable && e.target === this.overlay) {
this.cancel();
}
}
});
if (!this.options.shadow) {
const existingOverlays = document.querySelectorAll('.ckui-overlay');
if (existingOverlays.length > 0) {
const maxZ = Math.max(...Array.from(existingOverlays).map(el => parseInt(el.style.zIndex) || 0));
this.overlay.style.zIndex = maxZ + 1;
}
}
this.modal = utils.createElement('div', {
class: 'ckui-modal',
style: { width: this.options.width }
});
const titleChildren = [];
if (this.options.icon) {
const iconEl = utils.createElement('span', {
class: `ckui-modal-icon ${this.options.iconShape}`,
style: {
width: this.options.iconWidth,
height: this.options.iconWidth,
fontSize: this.options.iconWidth
}
});
if (this.options.icon.startsWith('http://') || this.options.icon.startsWith('https://') || this.options.icon.startsWith('//') || this.options.icon.startsWith('data:')) {
const img = utils.createElement('img', {
src: this.options.icon,
alt: 'icon'
});
iconEl.appendChild(img);
} else {
iconEl.textContent = this.options.icon;
}
titleChildren.push(iconEl);
}
const titleTextEl = utils.createElement('span');
if (this.options.allowHtml && typeof this.options.title === 'string') {
titleTextEl.innerHTML = this.options.title;
} else {
titleTextEl.textContent = this.options.title;
}
titleChildren.push(titleTextEl);
const titleEl = utils.createElement('h3', {
class: this.options.icon ? 'ckui-modal-title has-icon' : 'ckui-modal-title'
}, titleChildren);
const header = utils.createElement('div', {
class: 'ckui-modal-header'
}, [
titleEl,
this.options.showClose ? utils.createElement('button', {
class: 'ckui-modal-close',
onclick: () => this.cancel()
}, ['×']) : null
].filter(Boolean));
const body = utils.createElement('div', {
class: 'ckui-modal-body'
});
if (typeof this.options.content === 'string') {
if (this.options.allowHtml) {
body.innerHTML = this.options.content;
} else {
body.textContent = this.options.content;
}
} else if (this.options.content instanceof Node) {
body.appendChild(this.options.content);
}
let footer;
if (this.options.footer === null) {
footer = null;
} else if (this.options.footer === 'alert') {
footer = utils.createElement('div', {
class: 'ckui-modal-footer'
}, [
utils.createElement('button', {
class: 'ckui-btn ckui-btn-primary',
onclick: () => this.ok()
}, ['确定'])
]);
} else if (this.options.footer) {
footer = utils.createElement('div', {
class: 'ckui-modal-footer'
});
if (this.options.footer instanceof Node) {
footer.appendChild(this.options.footer);
} else {
footer.innerHTML = this.options.footer;
}
} else {
footer = utils.createElement('div', {
class: 'ckui-modal-footer'
}, [
utils.createElement('button', {
class: 'ckui-btn',
onclick: () => this.cancel()
}, ['取消']),
utils.createElement('button', {
class: 'ckui-btn ckui-btn-primary',
onclick: () => this.ok()
}, ['确定'])
]);
}
[header, body, footer].filter(Boolean).forEach(el => {
this.modal.appendChild(el);
});
this.overlay.appendChild(this.modal);
if (this.options.shadow && this.shadowHost) {
this.shadowHost.shadowRoot.querySelector('div').appendChild(this.overlay);
} else {
document.body.appendChild(this.overlay);
}
this.handleEscape = (e) => {
if (e.key === 'Escape') {
this.cancel();
}
};
document.addEventListener('keydown', this.handleEscape);
}
async ok() {
let shouldClose = true;
let returnValue = true;
if (this.options.onOk) {
const result = await this.options.onOk();
if (result === false) {
shouldClose = false;
} else {
returnValue = result;
}
}
if (shouldClose) {
this.close(returnValue);
}
}
cancel() {
if (this.options.onCancel) {
this.options.onCancel();
}
this.close(false);
}
close(result) {
if (this.overlay && this.overlay.parentNode) {
this.isShowing = false;
if (this.handleEscape) {
document.removeEventListener('keydown', this.handleEscape);
this.handleEscape = null;
}
this.overlay.style.animation = 'ckui-fade-out 0.2s ease-out forwards';
this.modal.style.animation = 'ckui-modal-out 0.2s ease-out forwards';
setTimeout(() => {
if (this.overlay && this.overlay.parentNode) {
this.overlay.parentNode.removeChild(this.overlay);
}
if (this.shadowHost && this.shadowHost.parentNode) {
this.shadowHost.parentNode.removeChild(this.shadowHost);
this.shadowHost = null;
}
if (result) {
this.resolvePromise && this.resolvePromise(result);
} else {
this.rejectPromise && this.rejectPromise();
}
}, 200);
}
}
destroy() {
this.close(false);
if (this.id) {
instanceManager.unregister('modals', this.id);
}
}
static confirm(options) {
const modal = new Modal(options);
return modal.show();
}
static alert(options) {
if (typeof options === 'string') {
options = { content: options };
}
const modal = new Modal({
maskClosable: false,
showClose: false,
...options,
footer: 'alert'
});
return modal.show().then(() => undefined).catch(() => undefined);
}
static prompt(options) {
if (typeof options === 'string') {
options = { title: options };
}
const inputValue = options.defaultValue || '';
const input = utils.createElement('input', {
class: 'ckui-root ckui-input',
type: 'text',
placeholder: options.placeholder || '',
value: inputValue
});
const modal = new Modal({
title: options.title || '请输入',
content: input,
width: options.width || '400px',
maskClosable: false,
...options,
onOk: () => {
return true;
}
});
return modal.show()
.then(() => input.value)
.catch(() => null);
}
static select(options) {
if (!options.options || !Array.isArray(options.options)) {
throw new Error('select() requires options array');
}
let selectedValue = options.defaultValue || null;
const container = utils.createElement('div', {
class: 'ckui-root',
style: { padding: '8px 0' }
});
const select = utils.createElement('select', {
class: 'ckui-select',
style: {
width: '100%',
fontSize: '15px',
height: '40px',
lineHeight: '1.5',
padding: '8px 12px'
}
});
options.options.forEach(opt => {
const option = utils.createElement('option', {
value: opt.value !== undefined ? opt.value : opt
});
option.textContent = opt.label || opt.toString();
option.style.padding = '8px 12px';
option.style.lineHeight = '1.8';
if (opt.value === options.defaultValue || opt === options.defaultValue) {
option.selected = true;
selectedValue = opt.value !== undefined ? opt.value : opt;
}
select.appendChild(option);
});
select.onchange = () => {
selectedValue = select.value;
};
container.appendChild(select);
const modal = new Modal({
title: options.title || '请选择',
content: container,
width: options.width || '400px',
maskClosable: false,
onOk: () => {
return selectedValue;
}
});
return modal.show();
}
static sortableList(options) {
if (!options.items || !Array.isArray(options.items)) {
throw new Error('sortableList() requires items array');
}
let items = [...options.items];
let draggedIndex = null;
let dragOverIndex = null;
let highlightIndices = new Set();
const container = utils.createElement('div', {
class: 'ckui-root',
style: { minHeight: '200px', maxHeight: '400px', overflowY: 'auto', padding: '8px' }
});
const updateHighlights = () => {
const itemElements = container.querySelectorAll('.ckui-item');
itemElements.forEach((el, index) => {
const isHighlighted = highlightIndices.has(index);
if (isHighlighted) {
el.style.borderColor = '#10b981';
el.style.background = 'rgba(16, 185, 129, 0.25)';
el.style.boxShadow = '0 0 0 3px rgba(16, 185, 129, 0.3)';
} else {
el.style.borderColor = 'var(--ckui-border, #ccc)';
el.style.background = 'var(--ckui-bg, #fff)';
el.style.boxShadow = '';
}
});
};
const blinkHighlight = (...indices) => {
highlightIndices.clear();
indices.forEach(i => highlightIndices.add(i));
requestAnimationFrame(() => {
updateHighlights();
setTimeout(() => {
highlightIndices.clear();
requestAnimationFrame(() => {
updateHighlights();
setTimeout(() => {
indices.forEach(i => highlightIndices.add(i));
requestAnimationFrame(() => {
updateHighlights();
setTimeout(() => {
highlightIndices.clear();
requestAnimationFrame(() => {
updateHighlights();
});
}, 400);
});
}, 400);
});
}, 400);
});
};
const renderList = () => {
container.innerHTML = '';
items.forEach((item, index) => {
const itemEl = document.createElement('div');
itemEl.className = 'ckui-item';
itemEl.draggable = true;
itemEl.dataset.index = index;
const isHighlighted = highlightIndices.has(index);
const isDragging = draggedIndex === index;
itemEl.className = isHighlighted ? 'ckui-item ckui-item-highlight' : 'ckui-item';
itemEl.style.cssText = `
padding: 12px 16px;
margin-bottom: 8px;
border: 2px ${isDragging ? 'dashed' : 'solid'} var(--ckui-border, #ccc);
border-radius: var(--ckui-radius, 4px);
background: var(--ckui-bg, #fff);
cursor: grab;
display: flex;
align-items: center;
justify-content: space-between;
user-select: none;
transition: all 0.3s ease-in-out;
opacity: ${isDragging ? '0.6' : '1'};
`;
if (isDragging) {
itemEl.style.borderColor = '#3b82f6';
itemEl.style.background = 'rgba(59, 130, 246, 0.1)';
itemEl.style.cursor = 'grabbing';
}
if (isHighlighted) {
itemEl.style.borderColor = '#10b981';
itemEl.style.borderStyle = 'solid';
itemEl.style.background = 'rgba(16, 185, 129, 0.25)';
itemEl.style.boxShadow = '0 0 0 3px rgba(16, 185, 129, 0.3)';
itemEl.style.opacity = '1';
}
const leftContent = document.createElement('div');
leftContent.style.cssText = 'display: flex; align-items: center; gap: 12px; pointer-events: none;';
leftContent.innerHTML = `
<span style="color: #999; font-size: 18px;">☰</span>
<span style="color: #333;">${item.label || item}</span>
`;
const controls = document.createElement('div');
controls.style.cssText = 'display: flex; gap: 4px; pointer-events: auto;';
const createBtn = (text, onClick) => {
const btn = document.createElement('button');
btn.textContent = text;
btn.style.cssText = `
padding: 4px 10px;
cursor: pointer;
background: var(--ckui-secondary, #f8fafc);
border: 1px solid var(--ckui-border, #e5e7eb);
border-radius: var(--ckui-radius-sm, 4px);
color: var(--ckui-text-secondary, #64748b);
font-size: 16px;
line-height: 1;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 28px;
height: 28px;
`;
btn.onclick = (e) => {
e.stopPropagation();
onClick();
};
btn.onmouseenter = () => {
btn.style.background = 'var(--ckui-primary, #0f172a)';
btn.style.color = 'white';
btn.style.borderColor = 'var(--ckui-primary, #0f172a)';
btn.style.transform = 'scale(1.1)';
};
btn.onmouseleave = () => {
btn.style.background = 'var(--ckui-secondary, #f8fafc)';
btn.style.color = 'var(--ckui-text-secondary, #64748b)';
btn.style.borderColor = 'var(--ckui-border, #e5e7eb)';
btn.style.transform = 'scale(1)';
};
btn.onmousedown = (e) => e.stopPropagation();
return btn;
};
if (index > 0) {
controls.appendChild(createBtn('↑', () => {
[items[index], items[index - 1]] = [items[index - 1], items[index]];
blinkHighlight(index - 1, index);
}));
}
if (index < items.length - 1) {
controls.appendChild(createBtn('↓', () => {
[items[index], items[index + 1]] = [items[index + 1], items[index]];
blinkHighlight(index, index + 1);
}));
}
itemEl.appendChild(leftContent);
itemEl.appendChild(controls);
itemEl.addEventListener('dragstart', (e) => {
draggedIndex = index;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', index);
});
itemEl.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
if (draggedIndex !== null && index !== draggedIndex) {
const movedItem = items[draggedIndex];
items.splice(draggedIndex, 1);
items.splice(index, 0, movedItem);
draggedIndex = index;
renderList();
}
});
itemEl.addEventListener('drop', (e) => {
e.preventDefault();
e.stopPropagation();
if (draggedIndex !== null) {
blinkHighlight(index);
}
});
itemEl.addEventListener('dragend', () => {
draggedIndex = null;
renderList();
});
container.appendChild(itemEl);
});
};
renderList();
const modal = new Modal({
title: options.title || '排序列表',
content: container,
width: options.width || '500px',
maskClosable: false,
onOk: () => {
return items;
}
});
return modal.show();
}
}
class FloatWindow {
constructor(options = {}) {
this.id = options.id || null;
this.options = {
title: options.title || '浮动窗口',
content: options.content || '',
width: options.width || '400px',
height: options.height || 'auto',
x: options.x || 100,
y: options.y || 100,
padding: options.padding || '16px',
draggable: options.draggable !== false,
minimizable: options.minimizable !== false,
closable: options.closable !== false,
onClose: options.onClose || null,
shadow: options.shadow || false,
allowHtml: options.allowHtml || false,
icon: options.icon || null,
iconShape: options.iconShape || 'square',
iconWidth: options.iconWidth || '20px',
...options
};
this.window = null;
this.isDragging = false;
this.isMinimized = false;
this.isShowing = false;
this.dragStartX = 0;
this.dragStartY = 0;
this.windowStartX = 0;
this.windowStartY = 0;
this.onCloseCallbacks = [];
this.onCloseOnceCallbacks = [];
this.shadowHost = null;
this.dragHandlers = null;
if (this.id) {
instanceManager.register('floatWindows', this.id, this);
}
}
show() {
if (this.isShowing && this.window && this.window.parentNode) {
this.window.style.display = '';
this.window.style.transition = 'opacity 0.2s ease-out, transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1)';
this.window.style.opacity = '1';
this.window.style.transform = 'scale(1)';
setTimeout(() => {
if (this.window) {
this.window.style.transition = '';
}
}, 200);
return this;
}
this.isShowing = true;
this.render();
return this;
}
refresh(options = {}) {
this.options = { ...this.options, ...options };
if (this.isShowing) {
const wasMinimized = this.isMinimized;
this.close();
this.show();
if (wasMinimized) {
this.toggleMinimize();
}
}
return this;
}
moveToMouse(offsetX = 0, offsetY = 0) {
return new Promise((resolve) => {
let mouseX, mouseY;
if (unsafeWindow.__ckui_mouseTrackingEnabled && unsafeWindow.__ckui_lastMouseX !== null) {
mouseX = unsafeWindow.__ckui_lastMouseX;
mouseY = unsafeWindow.__ckui_lastMouseY;
} else if (!unsafeWindow.__ckui_mouseTrackingEnabled) {
if (unsafeWindow.__ckui_lastMouseX === null) {
unsafeWindow.__ckui_lastMouseX = unsafeWindow.innerWidth / 2;
unsafeWindow.__ckui_lastMouseY = unsafeWindow.innerHeight / 2;
document.addEventListener('mousemove', (e) => {
unsafeWindow.__ckui_lastMouseX = e.clientX;
unsafeWindow.__ckui_lastMouseY = e.clientY;
}, { passive: true });
}
mouseX = unsafeWindow.__ckui_lastMouseX;
mouseY = unsafeWindow.__ckui_lastMouseY;
} else {
mouseX = unsafeWindow.innerWidth / 2;
mouseY = unsafeWindow.innerHeight / 2;
}
if (!this.window) {
this.render();
}
const rect = this.window.getBoundingClientRect();
const centerX = mouseX - (rect.width / 2) + offsetX;
const centerY = mouseY - (rect.height / 2) + offsetY;
const viewportWidth = unsafeWindow.innerWidth;
const viewportHeight = unsafeWindow.innerHeight;
let finalX = centerX;
let finalY = centerY;
if (finalX < 20) finalX = 20;
if (finalY < 20) finalY = 20;
if (finalX + rect.width > viewportWidth - 20) {
finalX = viewportWidth - rect.width - 20;
}
if (finalY + rect.height > viewportHeight - 20) {
finalY = viewportHeight - rect.height - 20;
}
this.window.style.left = finalX + 'px';
this.window.style.top = finalY + 'px';
requestAnimationFrame(() => {
this.window.style.transition = 'opacity 0.2s ease-out, transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1)';
this.window.style.opacity = '1';
this.window.style.transform = 'scale(1)';
setTimeout(() => {
if (this.window) {
this.window.style.transition = '';
}
resolve(this);
}, 200);
});
});
}
onClose(callback, once = false) {
if (once) {
this.onCloseOnceCallbacks.push(callback);
} else {
this.onCloseCallbacks.push(callback);
}
return this;
}
offClose(callback) {
this.onCloseCallbacks = this.onCloseCallbacks.filter(cb => cb !== callback);
this.onCloseOnceCallbacks = this.onCloseOnceCallbacks.filter(cb => cb !== callback);
return this;
}
render() {
const className = globalConfig.currentTheme === 'dark'
? 'ckui-root ckui-dark ckui-float-window'
: 'ckui-root ckui-float-window';
this.window = utils.createElement('div', {
class: className,
style: {
left: this.options.x + 'px',
top: this.options.y + 'px',
width: this.options.width,
height: this.options.height,
zIndex: globalConfig.zIndexBase + 7,
opacity: '0',
transform: 'scale(0.95)'
}
});
let parent = document.body;
if (this.options.shadow) {
const shadow = utils.createShadowHost();
this.shadowHost = shadow.host;
parent = shadow.container;
}
const titleChildren = [];
if (this.options.icon) {
const iconEl = utils.createElement('span', {
class: `ckui-float-icon ${this.options.iconShape}`,
style: {
width: this.options.iconWidth,
height: this.options.iconWidth,
fontSize: this.options.iconWidth
}
});
if (this.options.icon.startsWith('http://') || this.options.icon.startsWith('https://') || this.options.icon.startsWith('data:')) {
const img = utils.createElement('img', {
src: this.options.icon,
alt: 'icon'
});
iconEl.appendChild(img);
} else {
iconEl.textContent = this.options.icon;
}
titleChildren.push(iconEl);
}
const titleTextEl = utils.createElement('span');
if (this.options.allowHtml && typeof this.options.title === 'string') {
titleTextEl.innerHTML = this.options.title;
} else {
titleTextEl.textContent = this.options.title;
}
titleChildren.push(titleTextEl);
const titleEl = utils.createElement('h3', {
class: this.options.icon ? 'ckui-float-title has-icon' : 'ckui-float-title'
}, titleChildren);
const header = utils.createElement('div', {
class: 'ckui-float-header'
}, [
titleEl,
utils.createElement('div', {
class: 'ckui-float-controls'
}, [
this.options.minimizable ? utils.createElement('button', {
class: 'ckui-float-btn',
onclick: () => this.toggleMinimize()
}, ['−']) : null,
this.options.closable ? utils.createElement('button', {
class: 'ckui-float-btn',
onclick: () => this.close()
}, ['×']) : null
].filter(Boolean))
]);
this.body = utils.createElement('div', {
class: 'ckui-float-body',
style: { padding: this.options.padding }
});
if (typeof this.options.content === 'string') {
if (this.options.allowHtml) {
this.body.innerHTML = this.options.content;
} else {
this.body.textContent = this.options.content;
}
} else if (this.options.content instanceof Node) {
this.body.appendChild(this.options.content);
}
this.window.appendChild(header);
this.window.appendChild(this.body);
if (this.options.draggable) {
this.setupDragging(header);
}
parent.appendChild(this.window);
this.adjustPosition();
requestAnimationFrame(() => {
this.window.style.transition = 'opacity 0.2s ease-out, transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1)';
this.window.style.opacity = '1';
this.window.style.transform = 'scale(1)';
setTimeout(() => {
if (this.window) {
this.window.style.transition = '';
}
}, 200);
});
}
adjustPosition() {
if (!this.window) return;
const rect = this.window.getBoundingClientRect();
const viewportWidth = unsafeWindow.innerWidth;
const viewportHeight = unsafeWindow.innerHeight;
let x = parseInt(this.window.style.left);
let y = parseInt(this.window.style.top);
if (rect.right > viewportWidth) {
x = viewportWidth - rect.width - 20;
}
if (rect.bottom > viewportHeight) {
y = viewportHeight - rect.height - 20;
}
if (x < 20) {
x = 20;
}
if (y < 20) {
y = 20;
}
this.window.style.left = x + 'px';
this.window.style.top = y + 'px';
}
setupDragging(header) {
header.style.cursor = 'move';
const onMouseDown = (e) => {
this.isDragging = true;
this.dragStartX = e.clientX;
this.dragStartY = e.clientY;
const rect = this.window.getBoundingClientRect();
this.windowStartX = rect.left;
this.windowStartY = rect.top;
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
e.preventDefault();
};
const onMouseMove = (e) => {
if (!this.isDragging) return;
const deltaX = e.clientX - this.dragStartX;
const deltaY = e.clientY - this.dragStartY;
this.window.style.left = (this.windowStartX + deltaX) + 'px';
this.window.style.top = (this.windowStartY + deltaY) + 'px';
};
const onMouseUp = () => {
this.isDragging = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
header.addEventListener('mousedown', onMouseDown);
this.dragHandlers = {
header,
onMouseDown,
onMouseMove,
onMouseUp
};
}
toggleMinimize() {
this.isMinimized = !this.isMinimized;
this.body.style.display = this.isMinimized ? 'none' : 'block';
}
setContent(content) {
if (typeof content === 'string') {
if (this.options.allowHtml) {
this.body.innerHTML = content;
} else {
this.body.textContent = content;
}
} else if (content instanceof Node) {
this.body.innerHTML = '';
this.body.appendChild(content);
}
return this;
}
close() {
if (this.window && this.window.parentNode) {
this.isShowing = false;
if (this.dragHandlers) {
const { header, onMouseDown, onMouseMove, onMouseUp } = this.dragHandlers;
if (header) {
header.removeEventListener('mousedown', onMouseDown);
}
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
this.dragHandlers = null;
}
this.window.style.transition = 'opacity 0.2s ease-out, transform 0.2s cubic-bezier(0.6, -0.28, 0.74, 0.05)';
this.window.style.opacity = '0';
this.window.style.transform = 'scale(0.95)';
setTimeout(() => {
if (this.window && this.window.parentNode) {
this.window.parentNode.removeChild(this.window);
}
if (this.shadowHost && this.shadowHost.parentNode) {
this.shadowHost.parentNode.removeChild(this.shadowHost);
this.shadowHost = null;
}
if (this.options.onClose) {
try {
this.options.onClose();
} catch (e) {
console.error('[CKUI] onClose callback error:', e);
}
}
this.onCloseCallbacks.forEach(callback => {
try {
callback();
} catch (e) {
console.error('[CKUI] onClose callback error:', e);
}
});
this.onCloseOnceCallbacks.forEach(callback => {
try {
callback();
} catch (e) {
console.error('[CKUI] onClose callback error:', e);
}
});
this.onCloseOnceCallbacks = [];
}, 200);
}
return this;
}
destroy() {
this.close();
if (this.id) {
instanceManager.unregister('floatWindows', this.id);
}
this.onCloseCallbacks = [];
this.onCloseOnceCallbacks = [];
}
}
class FormBuilder {
constructor(config = {}) {
this.id = config.id || null;
this.config = config;
this.fields = [];
this.values = new Reactive({});
if (this.id) {
instanceManager.register('forms', this.id, this);
}
}
destroy() {
if (this.id) {
instanceManager.unregister('forms', this.id);
}
if (this.values && this.values._subscribers) {
this.values._subscribers = [];
}
this.fields = [];
}
addField(field) {
this.fields.push(field);
if (field.name && field.value !== undefined) {
this.values.value[field.name] = field.value;
}
return this;
}
input(options) {
return this.addField({
type: 'input',
inputType: 'text',
...options
});
}
textarea(options) {
return this.addField({
type: 'textarea',
...options
});
}
select(options) {
return this.addField({
type: 'select',
...options
});
}
checkbox(options) {
return this.addField({
type: 'checkbox',
...options
});
}
radio(options) {
return this.addField({
type: 'radio',
...options
});
}
tags(options) {
return this.addField({
type: 'tags',
value: options.value || [],
maxTags: options.maxTags || Infinity,
...options
});
}
selectTags(options) {
return this.addField({
type: 'selectTags',
value: options.value || [],
options: options.options || [],
allowCustom: options.allowCustom !== false,
maxTags: options.maxTags || Infinity,
...options
});
}
button(options) {
return this.addField({
type: 'button',
...options
});
}
hiddenarea(options) {
return this.addField({
type: 'hiddenarea',
...options
});
}
detail(options) {
return this.addField({
type: 'detail',
...options
});
}
space(options) {
if (typeof options === 'number') {
options = { size: options };
}
return this.addField({
type: 'space',
size: 16,
direction: 'vertical',
...options
});
}
html(options) {
if (typeof options === 'string') {
return this.addField({
type: 'html',
content: options
});
}
return this.addField({
type: 'html',
...options
});
}
element(options) {
if (options instanceof Node || typeof options === 'function') {
return this.addField({
type: 'element',
element: options
});
}
return this.addField({
type: 'element',
...options
});
}
elements(options) {
if (Array.isArray(options) || options instanceof NodeList) {
return this.addField({
type: 'elements',
elements: Array.from(options)
});
}
return this.addField({
type: 'elements',
...options
});
}
render() {
const form = utils.createElement('form', {
class: 'ckui-form'
});
this.fields.forEach(field => {
const group = this.renderField(field);
if (group) {
form.appendChild(group);
}
});
return form;
}
renderField(field) {
switch (field.type) {
case 'input':
return this.renderInput(field);
case 'textarea':
return this.renderTextarea(field);
case 'select':
return this.renderSelect(field);
case 'checkbox':
return this.renderCheckbox(field);
case 'radio':
return this.renderRadio(field);
case 'tags':
return this.renderTags(field);
case 'selectTags':
return this.renderSelectTags(field);
case 'button':
return this.renderButton(field);
case 'hiddenarea':
return this.renderHiddenArea(field);
case 'detail':
return this.renderDetail(field);
case 'html':
return this.renderHtml(field);
case 'element':
return this.renderElement(field);
case 'elements':
return this.renderElements(field);
case 'space':
return this.renderSpace(field);
default:
return null;
}
}
renderInput(field) {
const group = utils.createElement('div', { class: 'ckui-root ckui-form-group' });
if (field.label) {
group.appendChild(utils.createElement('label', {
class: 'ckui-root ckui-label'
}, [field.label]));
}
const input = utils.createElement('input', {
class: 'ckui-root ckui-input',
type: field.inputType || 'text',
placeholder: field.placeholder || '',
value: field.value || '',
name: field.name || ''
});
const errorEl = utils.createElement('span', {
class: 'ckui-form-error',
style: { display: 'none' }
});
const validate = () => {
if (field.validator) {
const result = field.validator(input.value, this.values.value);
if (result === true) {
group.classList.remove('has-error');
errorEl.style.display = 'none';
return true;
} else {
group.classList.add('has-error');
errorEl.textContent = result || field.errorMessage || '输入格式不正确';
errorEl.style.display = 'block';
return false;
}
}
return true;
};
input.addEventListener('input', (e) => {
const currentValues = { ...this.values.value };
currentValues[field.name] = e.target.value;
this.values.value = currentValues;
validate();
if (field.onChange) {
field.onChange(e.target.value, this.values.value);
}
});
input.addEventListener('blur', () => {
validate();
});
group.appendChild(input);
group.appendChild(errorEl);
return group;
}
renderTextarea(field) {
const group = utils.createElement('div', { class: 'ckui-root ckui-form-group' });
if (field.label) {
group.appendChild(utils.createElement('label', {
class: 'ckui-root ckui-label'
}, [field.label]));
}
const textarea = utils.createElement('textarea', {
class: 'ckui-root ckui-textarea',
placeholder: field.placeholder || '',
name: field.name || ''
});
textarea.value = field.value || '';
const errorEl = utils.createElement('span', {
class: 'ckui-form-error',
style: { display: 'none' }
});
const validate = () => {
if (field.validator) {
const result = field.validator(textarea.value, this.values.value);
if (result === true) {
group.classList.remove('has-error');
errorEl.style.display = 'none';
return true;
} else {
group.classList.add('has-error');
errorEl.textContent = result || field.errorMessage || '输入格式不正确';
errorEl.style.display = 'block';
return false;
}
}
return true;
};
textarea.addEventListener('input', (e) => {
const currentValues = { ...this.values.value };
currentValues[field.name] = e.target.value;
this.values.value = currentValues;
validate();
if (field.onChange) {
field.onChange(e.target.value, this.values.value);
}
});
textarea.addEventListener('blur', () => {
validate();
});
group.appendChild(textarea);
group.appendChild(errorEl);
return group;
}
renderSelect(field) {
const group = utils.createElement('div', { class: 'ckui-root ckui-form-group' });
if (field.label) {
group.appendChild(utils.createElement('label', {
class: 'ckui-root ckui-label'
}, [field.label]));
}
const select = utils.createElement('select', {
class: 'ckui-root ckui-select',
name: field.name || ''
});
(field.options || []).forEach(opt => {
const option = utils.createElement('option', {
value: opt.value
}, [opt.label || opt.value]);
if (opt.value === field.value) {
option.selected = true;
}
select.appendChild(option);
});
select.addEventListener('change', (e) => {
const currentValues = { ...this.values.value };
currentValues[field.name] = e.target.value;
this.values.value = currentValues;
if (field.onChange) {
field.onChange(e.target.value);
}
});
group.appendChild(select);
return group;
}
renderCheckbox(field) {
const group = utils.createElement('div', { class: 'ckui-root ckui-form-group' });
const label = utils.createElement('label', {
class: 'ckui-root ckui-checkbox'
});
const input = utils.createElement('input', {
type: 'checkbox',
name: field.name || ''
});
if (field.value) {
input.checked = true;
}
const errorEl = utils.createElement('span', {
class: 'ckui-form-error',
style: { display: 'none' }
});
const validate = () => {
if (field.validator) {
const result = field.validator(input.checked, this.values.value);
if (result === true) {
group.classList.remove('has-error');
errorEl.style.display = 'none';
return true;
} else {
group.classList.add('has-error');
errorEl.textContent = result || field.errorMessage || '请选择该选项';
errorEl.style.display = 'block';
return false;
}
}
return true;
};
input.addEventListener('change', (e) => {
const currentValues = { ...this.values.value };
currentValues[field.name] = e.target.checked;
this.values.value = currentValues;
validate();
if (field.onChange) {
field.onChange(e.target.checked, this.values.value);
}
});
label.appendChild(input);
if (field.label) {
label.appendChild(document.createTextNode(field.label));
}
group.appendChild(label);
group.appendChild(errorEl);
return group;
}
renderRadio(field) {
const group = utils.createElement('div', { class: 'ckui-root ckui-form-group' });
if (field.label) {
group.appendChild(utils.createElement('div', {
class: 'ckui-root ckui-label'
}, [field.label]));
}
const errorEl = utils.createElement('span', {
class: 'ckui-form-error',
style: { display: 'none' }
});
const validate = (selectedValue) => {
if (field.validator) {
const result = field.validator(selectedValue, this.values.value);
if (result === true) {
group.classList.remove('has-error');
errorEl.style.display = 'none';
return true;
} else {
group.classList.add('has-error');
errorEl.textContent = result || field.errorMessage || '请选择一项';
errorEl.style.display = 'block';
return false;
}
}
return true;
};
(field.options || []).forEach(opt => {
const label = utils.createElement('label', {
class: 'ckui-root ckui-radio',
style: { display: 'block', marginBottom: '8px' }
});
const input = utils.createElement('input', {
type: 'radio',
name: field.name || '',
value: opt.value
});
if (opt.value === field.value) {
input.checked = true;
}
input.addEventListener('change', (e) => {
if (e.target.checked) {
const currentValues = { ...this.values.value };
currentValues[field.name] = opt.value;
this.values.value = currentValues;
validate(opt.value);
if (field.onChange) {
field.onChange(opt.value, this.values.value);
}
}
});
label.appendChild(input);
label.appendChild(document.createTextNode(opt.label || opt.value));
group.appendChild(label);
});
group.appendChild(errorEl);
return group;
}
renderButton(field) {
const button = utils.createElement('button', {
class: `ckui-root ckui-btn ${field.primary ? 'ckui-btn-primary' : ''}`,
type: 'button',
onclick: () => {
if (field.onClick) {
field.onClick(this.getValues());
}
}
}, [field.label || 'Button']);
return button;
}
renderTags(field) {
const group = utils.createElement('div', { class: 'ckui-root ckui-form-group' });
if (field.label) {
const label = utils.createElement('label', { class: 'ckui-label' }, [field.label]);
group.appendChild(label);
}
const container = utils.createElement('div', {
class: 'ckui-tags-container'
});
const errorEl = utils.createElement('div', {
class: 'ckui-tags-error',
style: { display: 'none' }
});
const tags = field.value || [];
const maxTags = field.maxTags || Infinity;
const validator = field.validator || null;
const updateTags = () => {
const currentTags = this.values.value[field.name] || [];
container.innerHTML = '';
currentTags.forEach(tag => {
const tagEl = utils.createElement('div', { class: 'ckui-tag' });
const tagText = utils.createElement('span', { class: 'ckui-tag-text' }, [tag]);
const removeBtn = utils.createElement('button', {
class: 'ckui-tag-remove',
type: 'button',
onclick: () => {
const newTags = currentTags.filter(t => t !== tag);
this.values.value = { ...this.values.value, [field.name]: newTags };
updateTags();
if (field.onChange) field.onChange(newTags);
setTimeout(() => input.focus(), 0);
}
}, ['×']);
tagEl.appendChild(tagText);
tagEl.appendChild(removeBtn);
container.appendChild(tagEl);
});
if (currentTags.length < maxTags) {
container.appendChild(input);
input.disabled = false;
input.placeholder = field.placeholder || '输入后按空格添加标签';
} else {
input.disabled = true;
input.placeholder = '已达到最大标签数';
}
};
const showError = (message) => {
errorEl.textContent = message;
errorEl.style.display = 'block';
errorEl.classList.remove('warning');
errorEl.classList.add('error');
container.classList.add('error');
setTimeout(() => container.classList.remove('error'), 400);
};
const showWarning = (message) => {
errorEl.textContent = message;
errorEl.style.display = 'block';
errorEl.classList.remove('error');
errorEl.classList.add('warning');
};
const clearError = () => {
errorEl.style.display = 'none';
errorEl.classList.remove('error', 'warning');
};
const input = utils.createElement('input', {
class: 'ckui-tags-input',
type: 'text',
placeholder: field.placeholder || '输入后按空格或回车添加标签'
});
let isComposing = false;
input.addEventListener('compositionstart', () => {
isComposing = true;
});
input.addEventListener('compositionend', () => {
isComposing = false;
});
input.addEventListener('input', () => {
const value = input.value.trim();
if (value) {
showWarning('按空格或回车添加标签');
} else {
clearError();
}
});
const addTag = () => {
const value = input.value.trim();
if (value) {
const currentTags = this.values.value[field.name] || [];
if (currentTags.includes(value)) {
showError('标签已存在,不允许重复');
return;
}
if (currentTags.length >= maxTags) {
showError(`最多只能添加 ${maxTags} 个标签`);
return;
}
if (validator) {
const result = validator(value, currentTags);
if (result !== true) {
showError(typeof result === 'string' ? result : '标签校验失败');
return;
}
}
const newTags = [...currentTags, value];
this.values.value = { ...this.values.value, [field.name]: newTags };
input.value = '';
clearError();
updateTags();
if (field.onChange) field.onChange(newTags);
setTimeout(() => input.focus(), 0);
}
};
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
if (isComposing) {
setTimeout(() => addTag(), 50);
} else {
addTag();
}
}
else if (e.key === ' ' && !isComposing) {
e.preventDefault();
addTag();
}
else if (e.key === 'Backspace' && input.value === '') {
const currentTags = this.values.value[field.name] || [];
if (currentTags.length > 0) {
const newTags = currentTags.slice(0, -1);
this.values.value = { ...this.values.value, [field.name]: newTags };
updateTags();
if (field.onChange) field.onChange(newTags);
setTimeout(() => input.focus(), 0);
}
}
});
this.values.value = { ...this.values.value, [field.name]: tags };
updateTags();
group.appendChild(container);
group.appendChild(errorEl);
return group;
}
renderSelectTags(field) {
const group = utils.createElement('div', { class: 'ckui-root ckui-form-group' });
if (field.label) {
const label = utils.createElement('label', { class: 'ckui-label' }, [field.label]);
group.appendChild(label);
}
const wrapper = utils.createElement('div', {
class: 'ckui-select-tags-wrapper'
});
const container = utils.createElement('div', {
class: 'ckui-tags-container'
});
const dropdown = utils.createElement('div', {
class: 'ckui-select-tags-dropdown'
});
const errorEl = utils.createElement('div', {
class: 'ckui-tags-error',
style: { display: 'none' }
});
const tags = field.value || [];
const options = field.options || [];
const allowCustom = field.allowCustom !== false;
const maxTags = field.maxTags || Infinity;
const validator = field.validator || null;
let highlightedIndex = -1;
let filteredOptions = [];
const updateTags = () => {
const currentTags = this.values.value[field.name] || [];
container.innerHTML = '';
currentTags.forEach(tag => {
const tagEl = utils.createElement('div', { class: 'ckui-tag' });
const tagText = utils.createElement('span', { class: 'ckui-tag-text' }, [tag]);
const removeBtn = utils.createElement('button', {
class: 'ckui-tag-remove',
type: 'button',
onclick: () => {
const newTags = currentTags.filter(t => t !== tag);
this.values.value = { ...this.values.value, [field.name]: newTags };
updateTags();
if (field.onChange) field.onChange(newTags);
}
}, ['×']);
tagEl.appendChild(tagText);
tagEl.appendChild(removeBtn);
container.appendChild(tagEl);
});
if (currentTags.length < maxTags) {
container.appendChild(input);
input.disabled = false;
input.placeholder = field.placeholder || '输入搜索或选择';
} else {
input.disabled = true;
input.placeholder = '已达到最大标签数';
}
};
const showError = (message) => {
errorEl.textContent = message;
errorEl.style.display = 'block';
container.classList.add('error');
setTimeout(() => container.classList.remove('error'), 400);
};
const clearError = () => {
errorEl.style.display = 'none';
};
const updateDropdown = (filterText = '') => {
const currentTags = this.values.value[field.name] || [];
filteredOptions = options.filter(opt =>
opt.toLowerCase().includes(filterText.toLowerCase()) &&
!currentTags.includes(opt)
);
dropdown.innerHTML = '';
highlightedIndex = -1;
if (filteredOptions.length === 0 && filterText && allowCustom) {
const isDuplicate = currentTags.includes(filterText);
const createOption = utils.createElement('div', {
class: isDuplicate ? 'ckui-select-tags-option disabled' : 'ckui-select-tags-option',
onclick: () => {
if (!isDuplicate) {
addTag(filterText);
}
}
}, [isDuplicate ? `⚠️ 已存在: "${filterText}"` : `创建: "${filterText}"`]);
dropdown.appendChild(createOption);
} else {
filteredOptions.forEach((opt, idx) => {
const optEl = utils.createElement('div', {
class: 'ckui-select-tags-option',
onclick: () => addTag(opt)
}, [opt]);
optEl.dataset.index = idx;
dropdown.appendChild(optEl);
});
}
if (dropdown.children.length > 0) {
dropdown.classList.add('active');
} else {
dropdown.classList.remove('active');
}
};
const addTag = (tag) => {
const currentTags = this.values.value[field.name] || [];
if (currentTags.includes(tag)) {
showError('标签已存在,不允许重复');
return;
}
if (currentTags.length >= maxTags) {
showError(`最多只能添加 ${maxTags} 个标签`);
return;
}
if (validator) {
const result = validator(tag, currentTags);
if (result !== true) {
showError(typeof result === 'string' ? result : '标签校验失败');
return;
}
}
const newTags = [...currentTags, tag];
this.values.value = { ...this.values.value, [field.name]: newTags };
input.value = '';
clearError();
updateTags();
updateDropdown('');
if (field.onChange) field.onChange(newTags);
input.focus();
};
const highlightOption = (index) => {
const optionEls = dropdown.querySelectorAll('.ckui-select-tags-option');
optionEls.forEach(el => el.classList.remove('highlighted'));
if (index >= 0 && index < optionEls.length) {
highlightedIndex = index;
optionEls[index].classList.add('highlighted');
optionEls[index].scrollIntoView({ block: 'nearest' });
}
};
const input = utils.createElement('input', {
class: 'ckui-tags-input',
type: 'text',
placeholder: field.placeholder || '输入搜索或选择'
});
let isComposing = false;
input.addEventListener('compositionstart', () => {
isComposing = true;
});
input.addEventListener('compositionend', () => {
isComposing = false;
});
input.addEventListener('input', (e) => {
const value = e.target.value.trim();
if (value) {
showWarning('按回车选择或添加标签');
} else {
clearError();
}
updateDropdown(e.target.value);
});
input.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
const maxIndex = dropdown.children.length - 1;
highlightOption(Math.min(highlightedIndex + 1, maxIndex));
} else if (e.key === 'ArrowUp') {
e.preventDefault();
highlightOption(Math.max(highlightedIndex - 1, 0));
} else if (e.key === 'Enter') {
e.preventDefault();
if (isComposing) {
setTimeout(() => {
if (highlightedIndex >= 0) {
const highlighted = dropdown.children[highlightedIndex];
if (highlighted) highlighted.click();
} else if (input.value.trim()) {
if (allowCustom) {
addTag(input.value.trim());
} else if (filteredOptions.length > 0) {
addTag(filteredOptions[0]);
}
}
}, 50);
} else {
if (highlightedIndex >= 0) {
const highlighted = dropdown.children[highlightedIndex];
if (highlighted) highlighted.click();
} else if (input.value.trim()) {
if (allowCustom) {
addTag(input.value.trim());
} else if (filteredOptions.length > 0) {
addTag(filteredOptions[0]);
}
}
}
} else if (e.key === 'Escape') {
dropdown.classList.remove('active');
input.blur();
} else if (e.key === 'Backspace' && input.value === '') {
const currentTags = this.values.value[field.name] || [];
if (currentTags.length > 0) {
const newTags = currentTags.slice(0, -1);
this.values.value = { ...this.values.value, [field.name]: newTags };
updateTags();
if (field.onChange) field.onChange(newTags);
setTimeout(() => input.focus(), 0);
}
}
});
input.addEventListener('focus', () => {
updateDropdown(input.value);
});
input.addEventListener('blur', () => {
setTimeout(() => dropdown.classList.remove('active'), 200);
});
this.values.value = { ...this.values.value, [field.name]: tags };
updateTags();
wrapper.appendChild(container);
wrapper.appendChild(dropdown);
group.appendChild(wrapper);
group.appendChild(errorEl);
return group;
}
renderHiddenArea(field) {
const container = utils.createElement('div', {
class: 'ckui-root ckui-hidden-area'
});
const contentWrapper = utils.createElement('div');
if (field.fields && Array.isArray(field.fields)) {
field.fields.forEach(innerField => {
const innerGroup = this.renderField(innerField);
if (innerGroup) {
contentWrapper.appendChild(innerGroup);
}
});
} else if (field.content) {
if (typeof field.content === 'string') {
contentWrapper.innerHTML = field.content;
} else if (field.content instanceof Node) {
contentWrapper.appendChild(field.content);
}
}
container.appendChild(contentWrapper);
if (field.visible) {
if (field.visible.value) {
container.classList.add('visible');
}
field.visible.subscribe((value) => {
if (value) {
container.classList.add('visible');
} else {
container.classList.remove('visible');
}
});
}
return container;
}
renderDetail(field) {
const isOpen = field.open !== false;
const container = utils.createElement('div', {
class: isOpen ? 'ckui-root ckui-detail open' : 'ckui-root ckui-detail'
});
const header = utils.createElement('div', {
class: 'ckui-detail-header'
});
const title = utils.createElement('h4', {
class: 'ckui-detail-title'
}, [field.title || '详情']);
const icon = utils.createElement('span', {
class: 'ckui-detail-icon'
}, ['▼']);
header.appendChild(title);
header.appendChild(icon);
const contentWrapper = utils.createElement('div', {
class: 'ckui-detail-content'
});
const body = utils.createElement('div', {
class: 'ckui-detail-body'
});
if (field.fields && Array.isArray(field.fields)) {
field.fields.forEach(innerField => {
const innerGroup = this.renderField(innerField);
if (innerGroup) {
body.appendChild(innerGroup);
}
});
} else if (field.content) {
if (typeof field.content === 'string') {
body.innerHTML = field.content;
} else if (field.content instanceof Node) {
body.appendChild(field.content);
}
}
contentWrapper.appendChild(body);
container.appendChild(header);
container.appendChild(contentWrapper);
let isUpdating = false;
const toggleOpen = (open) => {
if (isUpdating) return;
isUpdating = true;
if (open) {
container.classList.add('open');
} else {
container.classList.remove('open');
}
if (field.openState && field.openState.value !== open) {
field.openState.value = open;
}
isUpdating = false;
};
header.addEventListener('click', () => {
toggleOpen(!container.classList.contains('open'));
});
if (field.openState) {
toggleOpen(field.openState.value);
field.openState.subscribe((value) => {
if (!isUpdating) {
toggleOpen(value);
}
});
}
return container;
}
renderHtml(field) {
const container = utils.createElement('div', {
class: 'ckui-root ckui-form-group'
});
if (field.label) {
const label = utils.createElement('label', {
class: 'ckui-label'
}, [field.label]);
container.appendChild(label);
}
const htmlContainer = utils.createElement('div');
if (typeof field.content === 'string') {
htmlContainer.innerHTML = field.content;
} else if (field.html) {
htmlContainer.innerHTML = field.html;
}
if (field.style) {
Object.entries(field.style).forEach(([key, value]) => {
htmlContainer.style[key] = value;
});
}
if (field.class) {
htmlContainer.className = field.class;
}
container.appendChild(htmlContainer);
return container;
}
renderElement(field) {
const container = utils.createElement('div', {
class: 'ckui-root ckui-form-group'
});
if (field.label) {
const label = utils.createElement('label', {
class: 'ckui-label'
}, [field.label]);
container.appendChild(label);
}
if (field.element) {
if (typeof field.element === 'function') {
const el = field.element();
if (el) {
container.appendChild(el);
}
} else {
container.appendChild(field.element);
}
}
return container;
}
renderElements(field) {
const container = utils.createElement('div', {
class: 'ckui-root ckui-form-group'
});
if (field.label) {
const label = utils.createElement('label', {
class: 'ckui-label'
}, [field.label]);
container.appendChild(label);
}
const elementsContainer = utils.createElement('div');
if (field.style) {
Object.entries(field.style).forEach(([key, value]) => {
elementsContainer.style[key] = value;
});
}
if (field.class) {
elementsContainer.className = field.class;
}
if (field.elements && Array.isArray(field.elements)) {
field.elements.forEach(el => {
if (typeof el === 'function') {
const element = el();
if (element) {
elementsContainer.appendChild(element);
}
} else if (el) {
elementsContainer.appendChild(el);
}
});
}
container.appendChild(elementsContainer);
return container;
}
renderSpace(field) {
const size = field.size || 16;
const direction = field.direction || 'vertical';
const style = direction === 'horizontal'
? `display: inline-block; width: ${typeof size === 'number' ? size + 'px' : size};`
: `display: block; height: ${typeof size === 'number' ? size + 'px' : size};`;
return utils.createElement('div', {
class: 'ckui-root',
style
});
}
getValues() {
return { ...this.values.value };
}
setValues(values) {
this.values.value = { ...this.values.value, ...values };
}
bindValue(name, reactive) {
const unsubscribe = this.values.subscribe((newValues) => {
if (newValues[name] !== undefined) {
reactive.value = newValues[name];
}
});
const reverseUnsubscribe = reactive.subscribe((newValue) => {
const currentValues = { ...this.values.value };
if (currentValues[name] !== newValue) {
currentValues[name] = newValue;
this.values.value = currentValues;
}
});
return () => {
unsubscribe();
reverseUnsubscribe();
};
}
}
const ckui = {
__initialized: true,
version: '2.4.0',
getInstance(type, id) {
return instanceManager.get(type, id);
},
getModal(id) {
return instanceManager.get('modals', id);
},
getFloatWindow(id) {
return instanceManager.get('floatWindows', id);
},
getForm(id) {
return instanceManager.get('forms', id);
},
reactive(value) {
return new Reactive(value);
},
utils,
notification: new NotificationManager(),
notify(options) {
return this.notification.show(options);
},
success(message, title, options = {}) {
if (typeof title === 'object') {
options = title;
title = options.title || '成功';
}
return this.notification.show({ type: 'success', title: title || '成功', message, ...options });
},
error(message, title, options = {}) {
if (typeof title === 'object') {
options = title;
title = options.title || '错误';
}
return this.notification.show({ type: 'error', title: title || '错误', message, ...options });
},
warning(message, title, options = {}) {
if (typeof title === 'object') {
options = title;
title = options.title || '警告';
}
return this.notification.show({ type: 'warning', title: title || '警告', message, ...options });
},
info(message, title, options = {}) {
if (typeof title === 'object') {
options = title;
title = options.title || '提示';
}
return this.notification.show({ type: 'info', title: title || '提示', message, ...options });
},
Modal,
modal(options) {
if (options.id) {
const existing = instanceManager.get('modals', options.id);
if (existing) {
return existing.refresh(options);
}
}
return new Modal(options);
},
async alert(message, title, id, customOptions = {}) {
let options = {};
if (typeof message === 'object') {
options = message;
} else if (typeof title === 'object') {
options = { content: message, title: '提示', ...title };
} else if (id !== null && id !== undefined && typeof id === 'object') {
options = { content: message, title: title || '提示', ...id };
} else {
options = { content: message, title: title || '提示', id, ...customOptions };
}
if (options.id) {
const existing = instanceManager.get('modals', options.id);
if (existing) {
existing.refresh(options);
return existing.show().then(() => undefined).catch(() => undefined);
}
}
return Modal.alert(options);
},
async confirm(message, title, id, customOptions = {}) {
let options = {};
if (typeof message === 'object') {
options = message;
} else if (typeof title === 'object') {
options = { content: message, title: '确认', ...title };
} else if (id !== null && id !== undefined && typeof id === 'object') {
options = { content: message, title: title || '确认', ...id };
} else {
options = { content: message, title: title || '确认', id, ...customOptions };
}
if (options.id) {
const existing = instanceManager.get('modals', options.id);
if (existing) {
existing.refresh(options);
return existing.show().then(() => true).catch(() => false);
}
}
return Modal.confirm(options).then(() => true).catch(() => false);
},
async prompt(message, defaultValue, title, id, customOptions = {}) {
let options = {};
if (typeof message === 'object') {
options = message;
} else if (typeof defaultValue === 'object') {
options = { title: message, ...defaultValue };
} else if (typeof title === 'object') {
options = { title: message, defaultValue: defaultValue, ...title };
} else if (id !== null && id !== undefined && typeof id === 'object') {
options = { title: message, defaultValue: defaultValue, placeholder: title || '', ...id };
} else {
options = {
title: message,
defaultValue: defaultValue,
placeholder: title || '',
id,
...customOptions
};
}
if (options.id) {
const existing = instanceManager.get('modals', options.id);
if (existing) {
options.defaultValue = options.defaultValue || '';
const input = utils.createElement('input', {
class: 'ckui-root ckui-input',
type: 'text',
placeholder: options.placeholder || '',
value: options.defaultValue
});
options.content = input;
existing.refresh(options);
return existing.show().then(() => input.value).catch(() => null);
}
}
return Modal.prompt(options);
},
async select(options, customOptions = {}) {
options = { ...options, ...customOptions };
if (options.id) {
const existing = instanceManager.get('modals', options.id);
if (existing) {
return existing.refresh(options).show();
}
}
return Modal.select(options);
},
async sortableList(options, customOptions = {}) {
options = { ...options, ...customOptions };
if (options.id) {
const existing = instanceManager.get('modals', options.id);
if (existing) {
return existing.refresh(options).show();
}
}
return Modal.sortableList(options);
},
FloatWindow,
floatWindow(options) {
if (options.id) {
const existing = instanceManager.get('floatWindows', options.id);
if (existing) {
return existing.refresh(options);
}
}
return new FloatWindow(options);
},
FormBuilder,
form(options) {
if (options && options.fields) {
const builderId = options.formId || null;
let builder = builderId ? instanceManager.get('forms', builderId) : null;
if (!builder) {
builder = new FormBuilder({ id: builderId });
}
builder.fields = [];
options.fields.forEach(field => {
switch (field.type) {
case 'input':
builder.input(field);
break;
case 'textarea':
builder.textarea(field);
break;
case 'select':
builder.select(field);
break;
case 'checkbox':
builder.checkbox(field);
break;
case 'radio':
builder.radio(field);
break;
}
});
const modalId = options.id || null;
const modal = modalId ? instanceManager.get('modals', modalId) : null;
if (modal) {
modal.refresh({
title: options.title || '表单',
content: builder.render(),
width: options.width || '500px',
onOk: () => true
});
return modal.show()
.then(() => builder.getValues())
.catch(() => null);
} else {
const newModal = new Modal({
id: modalId,
title: options.title || '表单',
content: builder.render(),
width: options.width || '500px',
onOk: () => true
});
return newModal.show()
.then(() => builder.getValues())
.catch(() => null);
}
}
const builderId = options?.id || null;
if (builderId) {
const existing = instanceManager.get('forms', builderId);
if (existing) {
return existing;
}
}
return new FormBuilder(options || {});
},
createElement: utils.createElement,
h: utils.createElement,
row(options, ...children) {
if (!options || options instanceof Node || typeof options === 'string') {
children = [options, ...children].filter(Boolean);
options = {};
}
const props = {
class: 'ckui-root ckui-row',
style: options.style || {}
};
return utils.createElement('div', props, children);
},
col(options, ...children) {
if (!options || options instanceof Node || typeof options === 'string') {
children = [options, ...children].filter(Boolean);
options = {};
}
const props = {
class: 'ckui-root ckui-col',
style: options.style || {}
};
return utils.createElement('div', props, children);
},
space(options, ...children) {
if (!options || options instanceof Node || typeof options === 'string') {
children = [options, ...children].filter(Boolean);
options = {};
}
const props = {
class: 'ckui-root ckui-space',
style: options.style || {}
};
return utils.createElement('div', props, children);
},
card(options = {}) {
const props = {
class: 'ckui-root ckui-card',
style: options.style || {}
};
const card = utils.createElement('div', props);
if (options.title) {
card.appendChild(utils.createElement('div', {
class: 'ckui-card-title'
}, [options.title]));
}
if (options.content) {
if (typeof options.content === 'string') {
const content = document.createElement('div');
content.innerHTML = options.content;
card.appendChild(content);
} else if (options.content instanceof Node) {
card.appendChild(options.content);
}
}
return card;
},
divider() {
return utils.createElement('div', { class: 'ckui-root ckui-divider' });
},
space(size = 16, direction = 'vertical') {
const styles = {
display: 'block',
flexShrink: '0'
};
if (direction === 'vertical') {
styles.height = typeof size === 'number' ? size + 'px' : size;
styles.width = '100%';
} else if (direction === 'horizontal') {
styles.width = typeof size === 'number' ? size + 'px' : size;
styles.height = '100%';
styles.display = 'inline-block';
}
return utils.createElement('div', {
class: 'ckui-root',
style: Object.entries(styles).map(([k, v]) => `${k.replace(/[A-Z]/g, m => '-' + m.toLowerCase())}: ${v}`).join('; ')
});
},
button(options = {}) {
const className = ['ckui-root', 'ckui-btn'];
if (options.primary) className.push('ckui-btn-primary');
if (options.danger) className.push('ckui-btn-danger');
if (options.success) className.push('ckui-btn-success');
return utils.createElement('button', {
class: className.join(' '),
onclick: options.onClick,
disabled: options.disabled
}, [options.label || 'Button']);
},
input(options = {}) {
const input = utils.createElement('input', {
class: 'ckui-root ckui-input',
type: options.type || 'text',
placeholder: options.placeholder || '',
value: options.value || '',
disabled: options.disabled
});
if (options.onChange) {
input.addEventListener('input', (e) => options.onChange(e.target.value));
}
if (options.reactive) {
input.value = options.reactive.value;
input.addEventListener('input', (e) => {
options.reactive.value = e.target.value;
});
options.reactive.subscribe((value) => {
if (input.value !== value) {
input.value = value;
}
});
setTimeout(() => {
if (options.reactive.value !== input.value) {
input.value = options.reactive.value;
}
}, 0);
}
return input;
},
textarea(options = {}) {
const textarea = utils.createElement('textarea', {
class: 'ckui-root ckui-textarea',
placeholder: options.placeholder || '',
disabled: options.disabled
});
textarea.value = options.value || '';
if (options.onChange) {
textarea.addEventListener('input', (e) => options.onChange(e.target.value));
}
if (options.reactive) {
textarea.value = options.reactive.value;
textarea.addEventListener('input', (e) => {
options.reactive.value = e.target.value;
});
options.reactive.subscribe((value) => {
if (textarea.value !== value) {
textarea.value = value;
}
});
setTimeout(() => {
if (options.reactive.value !== textarea.value) {
textarea.value = options.reactive.value;
}
}, 0);
}
return textarea;
},
label(text, options = {}) {
return utils.createElement('label', {
class: 'ckui-root ckui-label',
...options
}, [text]);
},
loading() {
return utils.createElement('div', { class: 'ckui-root ckui-loading' });
},
hiddenarea(options = {}) {
const container = utils.createElement('div', {
class: 'ckui-root ckui-hidden-area'
});
if (options.content) {
if (typeof options.content === 'string') {
container.innerHTML = options.content;
} else if (options.content instanceof Node) {
container.appendChild(options.content);
}
}
if (options.visible) {
if (options.visible.value) {
container.classList.add('visible');
}
options.visible.subscribe((value) => {
if (value) {
container.classList.add('visible');
} else {
container.classList.remove('visible');
}
});
}
return container;
},
detail(options = {}) {
const isOpen = options.open !== false;
const container = utils.createElement('div', {
class: isOpen ? 'ckui-root ckui-detail open' : 'ckui-root ckui-detail'
});
const header = utils.createElement('div', {
class: 'ckui-detail-header'
});
const title = utils.createElement('h4', {
class: 'ckui-detail-title'
}, [options.title || '详情']);
const icon = utils.createElement('span', {
class: 'ckui-detail-icon'
}, ['▼']);
header.appendChild(title);
header.appendChild(icon);
const contentWrapper = utils.createElement('div', {
class: 'ckui-detail-content'
});
const body = utils.createElement('div', {
class: 'ckui-detail-body'
});
if (options.content) {
if (typeof options.content === 'string') {
body.innerHTML = options.content;
} else if (options.content instanceof Node) {
body.appendChild(options.content);
}
}
contentWrapper.appendChild(body);
container.appendChild(header);
container.appendChild(contentWrapper);
let isUpdating = false;
const toggleOpen = (open) => {
if (isUpdating) return;
isUpdating = true;
if (open) {
container.classList.add('open');
} else {
container.classList.remove('open');
}
if (options.openState && options.openState.value !== open) {
options.openState.value = open;
}
isUpdating = false;
};
header.addEventListener('click', () => {
toggleOpen(!container.classList.contains('open'));
});
if (options.openState) {
toggleOpen(options.openState.value);
options.openState.subscribe((value) => {
if (!isUpdating) {
toggleOpen(value);
}
});
}
return container;
},
setTheme(theme) {
globalConfig.currentTheme = theme;
const containers = document.querySelectorAll('.ckui-root');
containers.forEach(container => {
if (theme === 'dark') {
container.classList.add('ckui-dark');
} else {
container.classList.remove('ckui-dark');
}
});
},
html(htmlString) {
const container = document.createElement('div');
container.className = 'ckui-root';
container.innerHTML = htmlString;
return container;
},
setZIndexBase(base) {
globalConfig.zIndexBase = base;
},
getZIndexBase() {
return globalConfig.zIndexBase;
},
setCSSVars(vars, target = null) {
const root = target || document.querySelector('.ckui-root') || document.documentElement;
Object.entries(vars).forEach(([key, value]) => {
const varName = key.startsWith('--') ? key : `--ckui-${key}`;
root.style.setProperty(varName, value);
});
},
trackMouseEvent() {
if (!unsafeWindow.__ckui_mouseTrackingEnabled) {
unsafeWindow.__ckui_mouseTrackingEnabled = true;
unsafeWindow.__ckui_lastMouseX = null;
unsafeWindow.__ckui_lastMouseY = null;
document.addEventListener('mousemove', (e) => {
unsafeWindow.__ckui_lastMouseX = e.clientX;
unsafeWindow.__ckui_lastMouseY = e.clientY;
}, { passive: true });
console.log('[CKUI] 全局鼠标追踪已启用');
}
return ckui;
}
};
utils.injectStyles(coreStyles, 'ckui-core-styles');
unsafeWindow.ckui = ckui;
console.log('[CKUI] Initialized successfully! Version:', ckui.version);
})(unsafeWindow, document);