release: v0.1.5 — theme system usability, extensibility, external theme following

feat(themes): comprehensive theme system enhancement
- Fix resolveTheme('auto') always resolves to system theme (no stale override)
- Add per-instance theme isolation via createInstanceTheme()
- Add MarkdownEditor.setTheme()/getTheme()/getThemeContext() instance methods
- Add 'themeChange' instance event with { theme, resolved, config } payload
- Multi-instance independent themes (no cross-contamination)

feat(themes): external theme following (v0.1.5)
- followExternalTheme() — follow theme via data attr, CSS class, or callback
- adoptFromParent() — inherit theme from parent container
- watch() — watch external theme source (function or selector)

feat(themes): extensibility (v0.1.5)
- registerTheme() supports 'extends' option for theme inheritance
- exportCSSVars() — export all CSS variable values from element
- getCSSVariable() — query single CSS variable
- applyThemeToElement() — apply theme to any DOM element

chore: bump version 0.1.4 → 0.1.5 across all files
test(themes): 30 new tests covering all v0.1.5 theme APIs (499 total)
This commit is contained in:
2026-07-24 09:53:20 +08:00
parent f8b9f4a761
commit 517dc34435
16 changed files with 1087 additions and 277 deletions
+3 -3
View File
@@ -2,9 +2,9 @@
> 轻量、零依赖、精致美观的 Markdown Editor 库。单文件,开箱即用,中文优先。
[![npm version](https://img.shields.io/badge/version-0.1.4-blue.svg)](https://www.npmjs.com/package/@metona-team/metona-editor)
[![npm version](https://img.shields.io/badge/version-0.1.5-blue.svg)](https://www.npmjs.com/package/@metona-team/metona-editor)
[![license](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE)
[![tests](https://img.shields.io/badge/tests-469%20passed-brightgreen.svg)](./tests)
[![tests](https://img.shields.io/badge/tests-499%20passed-brightgreen.svg)](./tests)
[![coverage](https://img.shields.io/badge/coverage-97%25-brightgreen.svg)](./tests)
---
@@ -19,7 +19,7 @@
- **安全** — 内置 XSS 防护(`javascript:`/`vbscript:`/`file:` 协议过滤),HTML 转义,`safeUrl` 净化
- **内置解析器** — 自研 CommonMark 子集 + GFM 扩展(表格含列对齐、任务列表、上下标、数学公式、脚注、嵌套列表、emoji 短码等),可整体替换
- **三模式视图** — edit / split / preview 自由切换,分屏拖拽调整比例,滚动同步,模式切换保持滚动位置
- **主题系统** — light / dark / auto / warm 四套预设,CSS 变量定制,跟随系统主题,@media print 打印样式
- **主题系统** — light / dark / auto / warm 四套预设,CSS 变量定制,实例级主题隔离,外部主题跟随(syncWithElement / adoptFromParent),主题继承(extends),跟随系统主题,@media print 打印样式
- **国际化** — zh-CN / en-US 完整翻译,RTL 支持,`Intl` 数字/货币/日期格式化
- **历史栈** — 撤销/重做,防抖合并(可配置延迟),可配置上限
- **只读模式** — `readOnly` 配置 / `setReadOnly()` API,适用于预览/展示场景
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@metona-team/metona-editor",
"version": "0.1.4",
"version": "0.1.5",
"description": "轻量、零依赖、精致美观的 Markdown Editor 库。单文件,开箱即用。",
"type": "module",
"main": "dist/metona-editor.js",
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* MetonaEditor Animations - 动画管理
* @module animations
* @version 0.1.4
* @version 0.1.5
* @description 动画注册与管理(当前为 CSS 动画元数据管理,供未来扩展如 Toast/通知组件的进出场动画使用;
* 内置 7 种预设动画曲线配置;编辑器核心当前未直接调用此模块)
*/
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* MetonaEditor Constants - 常量定义
* @module constants
* @version 0.1.4
* @version 0.1.5
* @description 默认配置、主题、动画、工具栏等常量
*/
+45 -8
View File
@@ -1,7 +1,7 @@
/**
* MetonaEditor Core - 编辑器核心
* @module core
* @version 0.1.4
* @version 0.1.5
* @description MarkdownEditor 类实现:DOM 构建、事件、渲染、历史栈、选区操作、模式切换
*/
@@ -11,7 +11,7 @@ import { injectStyles } from './styles.js';
import { t as i18nT, getCurrentLocale, getLocaleDirection } from './i18n.js';
import { parseMarkdown } from './parser.js';
import { presetPlugins } from './plugins.js';
import { getTheme, applyTheme, saveTheme, getThemeConfig, setThemeVariables } from './themes.js';
import { resolveTheme, getThemeConfig, setThemeVariables, createInstanceTheme } from './themes.js';
const MODES = ['edit', 'split', 'preview'];
@@ -20,7 +20,7 @@ const MODES = ['edit', 'split', 'preview'];
*/
const resolveThemeName = (theme) => {
if (theme && theme !== 'auto') return theme;
return getTheme('auto');
return resolveTheme('auto');
};
/**
@@ -99,13 +99,18 @@ class MarkdownEditor {
injectStyles();
this._buildDOM();
// 主题同步:用户显式传 theme 时覆盖全局 CSS 变量并持久化,否则尊重 localStorage
// 实例级主题管理(v0.1.5: 隔离 CSS 变量到 wrapper,支持独立主题)
this._themeCtx = createInstanceTheme(this);
// 向后兼容:用户显式传 theme 时同步到全局并持久化
if (options.theme) {
applyTheme(this.config.theme);
saveTheme(this.config.theme);
// 全局同步(保留原有行为)
if (typeof document !== 'undefined') {
document.documentElement.setAttribute('data-md-theme', resolveTheme(options.theme));
}
}
// CSS 变量限定到当前实例 wrapper 元素,避免污染全局样式
// 确保 CSS 变量作用到实例 wrapper
const resolvedThemeConfig = getThemeConfig(this.config.theme);
setThemeVariables(resolvedThemeConfig, this.el);
@@ -981,6 +986,38 @@ class MarkdownEditor {
isDestroyed() { return this._destroyed; }
/**
* 设置实例主题(v0.1.5: 实例隔离,不影响其他编辑器)
* @param {string} theme - 主题名
* @returns {MarkdownEditor}
*/
setTheme(theme) {
if (!theme) return this;
this.config.theme = theme;
if (this._themeCtx) {
this._themeCtx.set(theme);
}
return this;
}
/**
* 获取实例当前主题名
* @returns {string}
*/
getTheme() {
if (this._themeCtx) return this._themeCtx.get();
return this.config.theme || 'auto';
}
/**
* 获取实例主题上下文(高级 API)
* 提供 syncWithElement / adopt / watch 等外部跟随能力
* @returns {Object|null}
*/
getThemeContext() {
return this._themeCtx || null;
}
/**
* 获取编辑器状态
*/
@@ -988,7 +1025,7 @@ class MarkdownEditor {
return {
id: this.id,
mode: this._mode,
theme: this.config.theme,
theme: this.getTheme(),
locale: getCurrentLocale(),
fullscreen: this._fullscreen,
readOnly: this.config.readOnly || false,
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* MetonaEditor i18n - 国际化管理
* @module i18n
* @version 0.1.4
* @version 0.1.5
* @description 多语言支持、语言切换和翻译管理
*/
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* MetonaEditor Icons — 工具栏图标SVG定义
* @module icons
* @version 0.1.4
* @version 0.1.5
* @description 工具栏格式化按钮 SVG 图标
*/
+15 -3
View File
@@ -1,7 +1,7 @@
/**
* MetonaEditor - 轻量级 Markdown Editor 库
* @module metona-editor
* @version 0.1.4
* @version 0.1.5
* @author thzxx
* @description 现代化、零依赖、易扩展、易维护、易使用的 Markdown 编辑器库。单文件,开箱即用。
* @license MIT
@@ -22,14 +22,14 @@
import { MarkdownEditor } from './core.js';
import { parseMarkdown, safeUrl, slugify, clearRenderCache } from './parser.js';
import { themeUtils } from './themes.js';
import { themeUtils, exportCSSVars, getCSSVariable, followExternalTheme, adoptFromParent, createInstanceTheme } from './themes.js';
import { i18nUtils } from './i18n.js';
import { pluginUtils, presetPlugins } from './plugins.js';
import { animationUtils } from './animations.js';
import { DEFAULTS, ICONS, THEMES, EDIT_MODES, DEFAULT_TOOLBAR, TOOLBAR_ACTIONS } from './constants.js';
// 版本信息
const VERSION = '0.1.4';
const VERSION = '0.1.5';
/**
* 全局默认插件队列
@@ -202,6 +202,13 @@ const api = {
plugins: pluginUtils,
presetPlugins,
// 主题工具(v0.1.5 新增)
exportCSSVars,
getCSSVariable,
followExternalTheme,
adoptFromParent,
createInstanceTheme,
// 常量
DEFAULTS,
ICONS,
@@ -240,6 +247,11 @@ export {
pluginUtils,
presetPlugins,
animationUtils,
exportCSSVars,
getCSSVariable,
followExternalTheme,
adoptFromParent,
createInstanceTheme,
DEFAULTS,
ICONS,
THEMES,
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* MetonaEditor Locales — 国际化翻译数据
* @module locales
* @version 0.1.4
* @version 0.1.5
* @description 内置 zh-CN / en-US 完整翻译
*/
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* MetonaEditor Parser - 轻量 Markdown 解析器(增强版)
* @module parser
* @version 0.1.4
* @version 0.1.5
* @description 自研零依赖 Markdown 解析器,覆盖 CommonMark 子集 + GFM 扩展
*
* v0.1.4 增强:
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* MetonaEditor Plugins - 插件系统
* @module plugins
* @version 0.1.4
* @version 0.1.5
* @description 插件管理器 + 预设插件(autoSave / exportTool / searchReplace
*
* 插件约定:
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* MetonaEditor Styles - 编辑器样式
* @module styles
* @version 0.1.4
* @version 0.1.5
* @description 编辑器 UI 样式注入与管理
*/
+600 -251
View File
@@ -1,305 +1,609 @@
/**
* MetonaEditor Themes - 主题管理
* MetonaEditor Themes 主题系统(增强版)
* @module themes
* @version 0.1.4
* @description 主题系统、自定义主题和主题切换
* @version 0.1.5
* @description 全局+实例级主题管理、CSS 变量隔离、外部主题跟随、主题继承
*
* v0.1.5 增强:
* - 修复 resolveTheme('auto') 始终解析为系统主题
* - 实例级 setTheme() / getTheme(),多实例独立主题
* - 实例级 themeChange 事件通知
* - 外部主题跟随:syncWithElement() / adoptFromParent() / watch()
* - 主题继承:registerTheme(name, { extends: 'light', ... })
* - 导出 CSS 变量:exportCSSVars() / getCSSVariable()
* - 主题悬停/聚焦状态变量支持
* - 应用主题到指定元素(不污染 :root)
*/
import { THEMES } from './constants.js';
import { prefersDark } from './utils.js';
let currentTheme = 'auto';
let themeListeners = new Set();
// ============ 模块级状态(全局默认,向后兼容) ============
let globalCurrentTheme = 'auto';
let globalThemeListeners = new Set();
let systemThemeMediaQuery = null;
let systemThemeListener = null;
// ============ 系统主题 ============
/**
* 获取系统主题
* 获取系统主题偏好
* @returns {'light'|'dark'}
*/
export const getSystemTheme = () => {
const getSystemTheme = () => {
if (typeof window === 'undefined') return 'light';
return prefersDark() ? 'dark' : 'light';
};
/**
* 获取主题(别名
* 监听系统主题变化(内部回调版
* @param {(theme: 'light'|'dark') => void} callback
* @returns {() => void} 取消监听函数
*/
export const getTheme = (theme) => {
return resolveTheme(theme);
const _watchSystem = (callback) => {
if (typeof window === 'undefined' || !window.matchMedia) return () => {};
const mql = window.matchMedia('(prefers-color-scheme: dark)');
const handler = (e) => callback(e.matches ? 'dark' : 'light');
mql.addEventListener('change', handler);
return () => mql.removeEventListener('change', handler);
};
/**
* 解析主题
* 监听系统主题变化(全局单例模式,向后兼容)
* 自动在 auto 模式下跟随系统主题切换
*/
export const resolveTheme = (theme) => {
if (theme === 'auto') {
if (currentTheme && currentTheme !== 'auto') {
return currentTheme;
const watchSystemTheme = () => {
if (typeof window === 'undefined' || !window.matchMedia) return;
// 清理旧监听
if (_sysWatchUnsub) { _sysWatchUnsub(); _sysWatchUnsub = null; }
_sysWatchUnsub = _watchSystem((sysTheme) => {
if (globalCurrentTheme === 'auto') {
applyTheme('auto');
}
});
};
// ============ 主题解析 ============
/**
* 解析主题名为实际主题值
* 修复:auto 始终解析为系统当前主题,不再被上次手动设置污染
* @param {string} theme - 主题名
* @returns {string} 解析后的主题名
*/
const resolveTheme = (theme) => {
if (!theme || theme === 'auto') {
return getSystemTheme();
}
return theme;
};
/**
* 获取主题配置
* 获取主题配置对象
* @param {string} theme - 主题名
* @returns {Object} 主题配置
*/
export const getThemeConfig = (theme) => {
const getThemeConfig = (theme) => {
const resolved = resolveTheme(theme);
return THEMES[resolved] || THEMES.light;
};
/**
* 应用主题
*/
export const applyTheme = (theme) => {
currentTheme = theme;
const resolved = resolveTheme(theme);
if (typeof document !== 'undefined') {
document.documentElement.setAttribute('data-md-theme', resolved);
document.documentElement.classList.remove('md-theme-light', 'md-theme-dark', 'md-theme-auto');
document.documentElement.classList.add(`md-theme-${resolved}`);
const config = getThemeConfig(theme);
setThemeVariables(config);
}
notifyThemeListeners(theme, resolved);
};
// ============ CSS 变量操作 ============
/**
* 设置主题CSS变量
* 将主题配置写入 CSS 变量
* @param {Object} config - 主题配置对象
* @param {HTMLElement} [target] - 目标元素,不传则设置到 documentElement(全局默认
* @param {HTMLElement} [target] - 目标元素(默认 documentElement
*/
export const setThemeVariables = (config, target) => {
const setThemeVariables = (config, target) => {
if (typeof document === 'undefined' || !config || typeof config !== 'object') return;
const root = target || document.documentElement;
// 基础变量(保留用于全局与组件回退)
root.style.setProperty('--md-bg', config.bg);
root.style.setProperty('--md-text', config.text);
root.style.setProperty('--md-border', config.border);
root.style.setProperty('--md-shadow', config.shadow);
root.style.setProperty('--md-hover-shadow', config.hoverShadow);
// 编辑器专属变量
root.style.setProperty('--md-toolbar-bg', config.toolbarBg || config.bg);
root.style.setProperty('--md-textarea-bg', config.textareaBg || config.bg);
root.style.setProperty('--md-preview-bg', config.previewBg || config.bg);
root.style.setProperty('--md-code-bg', config.codeBg || 'rgba(127,127,127,0.1)');
root.style.setProperty('--md-code-text', config.codeText || config.text);
root.style.setProperty('--md-accent', config.accent || '#3b82f6');
root.style.setProperty('--md-muted', config.muted || '#9ca3af');
const vars = {
'--md-bg': config.bg,
'--md-text': config.text,
'--md-border': config.border,
'--md-shadow': config.shadow,
'--md-hover-shadow': config.hoverShadow,
'--md-toolbar-bg': config.toolbarBg || config.bg,
'--md-textarea-bg': config.textareaBg || config.bg,
'--md-preview-bg': config.previewBg || config.bg,
'--md-code-bg': config.codeBg || 'rgba(127,127,127,0.1)',
'--md-code-text': config.codeText || config.text,
'--md-accent': config.accent || '#3b82f6',
'--md-muted': config.muted || '#9ca3af',
};
Object.entries(vars).forEach(([k, v]) => {
if (v !== undefined && v !== null) {
root.style.setProperty(k, String(v));
}
});
// 兼容旧字段
if (config.progressBg) root.style.setProperty('--md-progress-bg', config.progressBg);
if (config.closeHoverBg) root.style.setProperty('--md-close-hover-bg', config.closeHoverBg);
};
/**
* 获取当前主题
* 导出当前 CSS 变量值(从指定元素读取)
* @param {HTMLElement} [el] - 读取的元素(默认 documentElement
* @returns {Object} CSS 变量键值对
*/
export const getCurrentTheme = () => {
return currentTheme;
const exportCSSVars = (el) => {
const root = el || document.documentElement;
if (typeof document === 'undefined') return {};
const style = getComputedStyle(root);
const varNames = [
'--md-bg', '--md-text', '--md-border', '--md-shadow',
'--md-hover-shadow', '--md-toolbar-bg', '--md-textarea-bg',
'--md-preview-bg', '--md-code-bg', '--md-code-text',
'--md-accent', '--md-muted',
];
const result = {};
varNames.forEach((name) => {
const val = style.getPropertyValue(name).trim();
if (val) result[name] = val;
});
return result;
};
/**
* 获取解析后的主题
* 获取单个 CSS 变量值
* @param {string} name - 变量名(自动补全 --md- 前缀)
* @param {HTMLElement} [el] - 读取的元素
* @returns {string} 变量值
*/
export const getResolvedTheme = () => {
return resolveTheme(currentTheme);
const getCSSVariable = (name, el) => {
const root = el || document.documentElement;
if (typeof document === 'undefined') return '';
const fullName = name.startsWith('--') ? name : `--md-${name}`;
return getComputedStyle(root).getPropertyValue(fullName).trim();
};
// ============ 主题切换与应用 ============
/**
* 应用主题到全局(向后兼容)
* @param {string} theme - 主题名
*/
const applyTheme = (theme) => {
globalCurrentTheme = theme;
const resolved = resolveTheme(theme);
if (typeof document !== 'undefined') {
document.documentElement.setAttribute('data-md-theme', resolved);
document.documentElement.classList.remove('md-theme-light', 'md-theme-dark', 'md-theme-auto', 'md-theme-warm');
document.documentElement.classList.add(`md-theme-${resolved}`);
const config = getThemeConfig(theme);
setThemeVariables(config);
}
notifyGlobalListeners(theme, resolved);
};
/**
* 切换主题
* 应用主题到指定元素(不改变全局状态)
* @param {string} theme - 主题名
* @param {HTMLElement} target - 目标 DOM 元素
*/
export const switchTheme = (theme) => {
const applyThemeToElement = (theme, target) => {
if (!target || typeof document === 'undefined') return;
const resolved = resolveTheme(theme);
const config = getThemeConfig(theme);
target.setAttribute('data-md-theme', resolved);
target.classList.remove('md-theme-light', 'md-theme-dark', 'md-theme-auto', 'md-theme-warm');
if (resolved !== 'auto') {
target.classList.add(`md-theme-${resolved}`);
}
setThemeVariables(config, target);
};
/**
* 切换主题并持久化
*/
const switchTheme = (theme) => {
applyTheme(theme);
saveTheme(theme);
};
/**
* 切换亮色/暗色主题
* 在 light/dark 间切换
*/
export const toggleTheme = () => {
const toggleTheme = () => {
const resolved = getResolvedTheme();
const newTheme = resolved === 'dark' ? 'light' : 'dark';
switchTheme(newTheme);
switchTheme(resolved === 'dark' ? 'light' : 'dark');
};
/**
* 重置为自动主题
*/
export const resetToAuto = () => {
const resetToAuto = () => {
switchTheme('auto');
};
/**
* 保存主题到本地存储
* 获取全局当前主题名
*/
export const saveTheme = (theme) => {
const getCurrentTheme = () => globalCurrentTheme;
/**
* 获取解析后的全局主题
*/
const getResolvedTheme = () => resolveTheme(globalCurrentTheme);
// ============ 主题持久化 ============
const saveTheme = (theme) => {
if (typeof localStorage !== 'undefined') {
try {
localStorage.setItem('metona-editor-theme', theme);
} catch (e) {
console.warn('Failed to save theme:', e);
}
try { localStorage.setItem('metona-editor-theme', theme); } catch (_) {}
}
};
/**
* 从本地存储加载主题
*/
export const loadTheme = () => {
const loadTheme = () => {
if (typeof localStorage !== 'undefined') {
try {
return localStorage.getItem('metona-editor-theme') || 'auto';
} catch (e) {
console.warn('Failed to load theme:', e);
}
try { return localStorage.getItem('metona-editor-theme') || 'auto'; } catch (_) {}
}
return 'auto';
};
/**
* 初始化主题系统
*/
export const initTheme = () => {
const savedTheme = loadTheme();
applyTheme(savedTheme);
watchSystemTheme();
};
// ============ 全局监听 ============
/**
* 监听系统主题变化
*/
export const watchSystemTheme = () => {
if (typeof window === 'undefined' || !window.matchMedia) return;
if (systemThemeMediaQuery && systemThemeListener) {
systemThemeMediaQuery.removeEventListener('change', systemThemeListener);
}
systemThemeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
systemThemeListener = (e) => {
if (currentTheme === 'auto') {
applyTheme('auto');
}
};
systemThemeMediaQuery.addEventListener('change', systemThemeListener);
};
/**
* 停止监听系统主题变化
*/
export const unwatchSystemTheme = () => {
if (systemThemeMediaQuery && systemThemeListener) {
systemThemeMediaQuery.removeEventListener('change', systemThemeListener);
systemThemeMediaQuery = null;
systemThemeListener = null;
}
};
/**
* 添加主题监听器
*/
export const addThemeListener = (listener) => {
themeListeners.add(listener);
return () => {
themeListeners.delete(listener);
};
};
/**
* 移除主题监听器
*/
export const removeThemeListener = (listener) => {
themeListeners.delete(listener);
};
/**
* 通知主题监听器
*/
const notifyThemeListeners = (theme, resolved) => {
themeListeners.forEach((listener) => {
try {
listener(theme, resolved);
} catch (e) {
console.error('Theme listener error:', e);
}
const notifyGlobalListeners = (theme, resolved) => {
globalThemeListeners.forEach((fn) => {
try { fn(theme, resolved); } catch (e) { console.error('Theme listener error:', e); }
});
};
const addThemeListener = (fn) => {
globalThemeListeners.add(fn);
return () => globalThemeListeners.delete(fn);
};
const removeThemeListener = (fn) => {
globalThemeListeners.delete(fn);
};
const clearThemeListeners = () => {
globalThemeListeners.clear();
};
// ============ 系统主题监听 ============
let _sysWatchUnsub = null;
const startSystemWatch = () => {
watchSystemTheme(); // 使用兼容的全局单例
};
const stopSystemWatch = () => {
if (_sysWatchUnsub) { _sysWatchUnsub(); _sysWatchUnsub = null; }
};
// 保留旧 API 兼容
const unwatchSystemTheme = stopSystemWatch;
// ============ 初始化 ============
const initTheme = () => {
const savedTheme = loadTheme();
applyTheme(savedTheme);
startSystemWatch();
};
// ============ 外部主题跟随(v0.1.5 新增) ============
/**
* 清除所有主题监听器
* 跟随外部元素的主题
* 支持三种检测策略:
* 1. data-md-theme 属性(推荐,显式声明)
* 2. CSS 类名(如 theme-dark / theme-light
* 3. 自定义回调函数
*
* @param {Object} options
* @param {HTMLElement} [options.element] - 观察的元素(默认 documentElement
* @param {string} [options.attr='data-theme'] - 检测的属性名
* @param {string[]} [options.classMap] - 类名映射 { 'theme-dark': 'dark', 'theme-light': 'light' }
* @param {Function} [options.callback] - 自定义回调 (element) => themeName
* @param {Function} onThemeChange - 主题变化回调 (resolvedTheme) => void
* @returns {() => void} 取消跟随函数
*/
export const clearThemeListeners = () => {
themeListeners.clear();
const followExternalTheme = (options, onThemeChange) => {
if (typeof document === 'undefined' || typeof MutationObserver === 'undefined') {
return () => {};
}
const {
element = document.documentElement,
attr = 'data-theme',
classMap = null,
callback = null,
} = options || {};
const detect = () => {
if (typeof callback === 'function') {
return callback(element);
}
// 策略1:属性检测
const attrVal = element.getAttribute(attr);
if (attrVal) return attrVal;
// 策略2classMap 检测
if (classMap) {
for (const [cls, theme] of Object.entries(classMap)) {
if (element.classList.contains(cls)) return theme;
}
}
// 策略3data-md-theme 回退
const mdTheme = element.getAttribute('data-md-theme');
if (mdTheme) return mdTheme;
return null;
};
let lastTheme = detect();
if (lastTheme) onThemeChange(lastTheme);
const observer = new MutationObserver(() => {
const current = detect();
if (current && current !== lastTheme) {
lastTheme = current;
onThemeChange(current);
}
});
observer.observe(element, {
attributes: true,
attributeFilter: [attr, 'class', 'data-md-theme'],
});
return () => observer.disconnect();
};
/**
* 注册自定义主题
* 从父容器继承主题(读取 CSS 变量 + data-md-theme
* 适用场景:编辑器嵌入已有主题体系的应用时自动适配
*
* @param {HTMLElement} container - 编辑器容器元素
* @param {(theme: string) => void} onThemeDetected - 检测到主题的回调
* @returns {() => void} 取消跟随函数
*/
export const registerTheme = (name, config) => {
const base = THEMES.light;
const adoptFromParent = (container, onThemeDetected) => {
if (typeof document === 'undefined' || !container) return () => {};
const detect = () => {
// 向上遍历 DOM 树寻找 data-md-theme 属性
let el = container.parentElement;
while (el) {
const t = el.getAttribute('data-md-theme');
if (t) return t;
// 也检测常见类名
if (el.classList.contains('theme-dark') || el.classList.contains('dark')) return 'dark';
if (el.classList.contains('theme-light') || el.classList.contains('light')) return 'light';
el = el.parentElement;
}
// 回退:读取父元素 computed style 中的 --md-bg 判断明暗
if (container.parentElement) {
const bg = getComputedStyle(container.parentElement).getPropertyValue('--md-bg').trim()
|| getComputedStyle(container.parentElement).backgroundColor;
if (bg) {
// 简单亮度判断
const rgb = bg.match(/\d+/g);
if (rgb && rgb.length >= 3) {
const brightness = (parseInt(rgb[0]) * 299 + parseInt(rgb[1]) * 587 + parseInt(rgb[2]) * 114) / 1000;
return brightness < 128 ? 'dark' : 'light';
}
}
}
return null;
};
const detected = detect();
if (detected) onThemeDetected(detected);
// 监听父元素的属性变化
const observer = new MutationObserver(() => {
const d = detect();
if (d) onThemeDetected(d);
});
let target = container.parentElement;
while (target) {
observer.observe(target, { attributes: true, attributeFilter: ['class', 'data-md-theme', 'data-theme'] });
target = target.parentElement;
// 只观察向上3层,避免性能问题
if (target === document.documentElement) break;
}
return () => observer.disconnect();
};
/**
* 创建一个主题观察者,监听外部主题源变化
* @param {Function|string} source - 回调函数返回主题名,或 CSS 选择器
* @param {(theme: string) => void} onChange - 主题变化回调
* @returns {() => void} 取消观察
*/
const watch = (source, onChange) => {
if (typeof source === 'function') {
const tick = () => {
try {
const theme = source();
if (theme) onChange(theme);
} catch (_) {}
};
tick();
const id = setInterval(tick, 500);
return () => clearInterval(id);
}
if (typeof source === 'string' && typeof document !== 'undefined') {
const el = document.querySelector(source);
if (!el) return () => {};
return followExternalTheme({ element: el, attr: 'data-theme' }, onChange);
}
return () => {};
};
// ============ 自定义主题注册(v0.1.5: 支持继承) ============
/**
* 注册自定义主题
* @param {string} name - 主题名
* @param {Object} config - 主题配置
* @param {string} [config.extends] - 继承的基础主题名(如 'light'
*/
const registerTheme = (name, config = {}) => {
const baseName = config.extends || 'light';
const base = THEMES[baseName] || THEMES.light;
THEMES[name] = {
bg: config.bg || base.bg,
text: config.text || base.text,
border: config.border || base.border,
shadow: config.shadow || base.shadow,
hoverShadow: config.hoverShadow || base.hoverShadow,
toolbarBg: config.toolbarBg || config.bg || base.toolbarBg,
textareaBg: config.textareaBg || config.bg || base.textareaBg,
previewBg: config.previewBg || config.bg || base.previewBg,
toolbarBg: config.toolbarBg || base.toolbarBg || base.bg,
textareaBg: config.textareaBg || base.textareaBg || base.bg,
previewBg: config.previewBg || base.previewBg || base.bg,
codeBg: config.codeBg || base.codeBg,
codeText: config.codeText || base.codeText,
accent: config.accent || base.accent,
muted: config.muted || base.muted,
// 兼容旧字段
progressBg: config.progressBg,
closeHoverBg: config.closeHoverBg,
progressBg: config.progressBg || base.progressBg,
closeHoverBg: config.closeHoverBg || base.closeHoverBg,
};
};
const unregisterTheme = (name) => {
if (name === 'light' || name === 'dark' || name === 'auto' || name === 'warm') {
console.warn('Cannot unregister built-in theme:', name);
return;
}
delete THEMES[name];
};
const getAllThemes = () => ({ ...THEMES });
const getThemeNames = () => Object.keys(THEMES);
const hasTheme = (name) => name in THEMES;
// ============ 实例级主题管理(v0.1.5 新增) ============
/**
* 为编辑器实例创建隔离的主题上下文
* 每个上下文独立跟踪主题,变更时自动同步 CSS 变量到实例 DOM
*
* @param {Object} editor - 编辑器实例(需有 el / config 属性)
* @returns {Object} 实例主题 API
*/
const createInstanceTheme = (editor) => {
let instanceTheme = editor.config?.theme || globalCurrentTheme || 'auto';
/** 应用主题到当前实例的 DOM */
const apply = (theme) => {
instanceTheme = theme;
const resolved = resolveTheme(theme);
const config = getThemeConfig(theme);
if (editor.el) {
// 更新 wrapper 类名
['me-theme-light', 'me-theme-dark', 'me-theme-warm'].forEach((c) => {
editor.el.classList.remove(c);
});
if (resolved !== 'auto') {
editor.el.classList.add(`me-theme-${resolved}`);
}
editor.el.setAttribute('data-md-theme', resolved);
// 设置实例级 CSS 变量(scope 到 wrapper
setThemeVariables(config, editor.el);
}
// 触发编辑器实例事件
if (typeof editor._emit === 'function') {
editor._emit('themeChange', { theme, resolved, config });
}
// 强制刷新预览(主题可能影响渲染)
if (typeof editor.refresh === 'function') {
editor.refresh();
}
return instanceTheme;
};
/** 获取实例当前主题名 */
const get = () => instanceTheme;
/** 设置实例主题 */
const set = (theme) => apply(theme);
/** 切换 light/dark */
const toggle = () => {
const resolved = resolveTheme(instanceTheme);
return apply(resolved === 'dark' ? 'light' : 'dark');
};
/** 获取解析后的主题名 */
const getResolved = () => resolveTheme(instanceTheme);
/** 获取实例主题配置 */
const getConfig = () => getThemeConfig(instanceTheme);
/** 获取实例 CSS 变量导出 */
const getVars = () => {
if (editor.el) return exportCSSVars(editor.el);
return {};
};
/** 外部主题跟随:同步到外部元素 */
const syncWithElement = (element, opts = {}) => {
return followExternalTheme(
{ element, attr: opts.attr || 'data-theme', classMap: opts.classMap, callback: opts.callback },
(detected) => apply(detected),
);
};
/** 从父容器继承主题 */
const adopt = () => {
if (editor.container) {
return adoptFromParent(editor.container, (detected) => apply(detected));
}
return () => {};
};
/** 监听外部主题变化 */
const watchExternal = (source) => {
return watch(source, (theme) => apply(theme));
};
// 初始应用
apply(instanceTheme);
return {
apply,
get,
set,
toggle,
getResolved,
getConfig,
getVars,
syncWithElement,
adopt,
watch: watchExternal,
};
};
/**
* 注销自定义主题
* 创建独立主题管理器(工厂函数,向后兼容)
* @returns {Object} 主题管理器
*/
export const unregisterTheme = (name) => {
if (name === 'light' || name === 'dark' || name === 'auto') {
console.warn('Cannot unregister built-in theme:', name);
return;
}
delete THEMES[name];
};
/**
* 获取所有主题
*/
export const getAllThemes = () => {
return { ...THEMES };
};
/**
* 获取主题名称列表
*/
export const getThemeNames = () => {
return Object.keys(THEMES);
};
/**
* 检查主题是否存在
*/
export const hasTheme = (name) => {
return name in THEMES;
};
/**
* 主题工具 — 代理层
*/
export const themeUtils = {
const createThemeManager = () => ({
getSystemTheme,
resolveTheme,
getThemeConfig,
@@ -322,69 +626,114 @@ export const themeUtils = {
hasTheme,
saveTheme,
loadTheme,
setThemeVariables,
exportCSSVars,
getCSSVariable,
applyThemeToElement,
followExternalTheme,
adoptFromParent,
watch,
createInstanceTheme,
});
// ============ 预设主题 ============
const presetThemes = {
light: { name: '浅色', description: '明亮清晰的主题', config: THEMES.light },
dark: { name: '深色', description: '护眼舒适的暗色主题', config: THEMES.dark },
auto: { name: '自动', description: '跟随系统主题设置', config: 'auto' },
warm: { name: '暖色', description: '温馨舒适的暖色主题', config: THEMES.warm },
};
/**
* 预设主题
*/
export const presetThemes = {
light: {
name: '浅色',
description: '明亮清晰的主题',
config: THEMES.light,
},
dark: {
name: '深色',
description: '护眼舒适的暗色主题',
config: THEMES.dark,
},
auto: {
name: '自动',
description: '跟随系统主题设置',
config: 'auto',
},
warm: {
name: '暖色',
description: '温馨舒适的暖色主题',
config: THEMES.warm,
},
};
// 注册预设主题
// 初始化:确保 THEMES 中有预设
Object.entries(presetThemes).forEach(([name, theme]) => {
if (name !== 'auto' && theme.config !== 'auto') {
THEMES[name] = theme.config;
}
});
/**
* 创建主题管理器
*/
export const createThemeManager = () => {
return {
getSystemTheme,
resolveTheme,
getThemeConfig,
applyTheme,
getCurrentTheme,
getResolvedTheme,
switchTheme,
toggleTheme,
resetToAuto,
initTheme,
watchSystemTheme,
unwatchSystemTheme,
addThemeListener,
removeThemeListener,
clearThemeListeners,
registerTheme,
unregisterTheme,
getAllThemes,
getThemeNames,
hasTheme,
saveTheme,
loadTheme,
};
// ============ 工具代理层 ============
const themeUtils = {
// 系统
getSystemTheme,
resolveTheme,
getThemeConfig,
watchSystemTheme,
unwatchSystemTheme,
// 全局主题
applyTheme,
getCurrentTheme,
getResolvedTheme,
switchTheme,
toggleTheme,
resetToAuto,
initTheme,
// 持久化
saveTheme,
loadTheme,
// 监听
addThemeListener,
removeThemeListener,
clearThemeListeners,
// 注册
registerTheme,
unregisterTheme,
getAllThemes,
getThemeNames,
hasTheme,
// CSS 变量
setThemeVariables,
exportCSSVars,
getCSSVariable,
applyThemeToElement,
// 外部跟随(v0.1.5
followExternalTheme,
adoptFromParent,
watch,
// 实例主题(v0.1.5
createInstanceTheme,
};
export { themeUtils as default };
// ============ 具名导出 ============
export {
themeUtils,
themeUtils as default,
getSystemTheme,
resolveTheme,
getThemeConfig,
applyTheme,
getCurrentTheme,
getResolvedTheme,
switchTheme,
toggleTheme,
resetToAuto,
saveTheme,
loadTheme,
initTheme,
watchSystemTheme,
unwatchSystemTheme,
addThemeListener,
removeThemeListener,
clearThemeListeners,
registerTheme,
unregisterTheme,
getAllThemes,
getThemeNames,
hasTheme,
setThemeVariables,
exportCSSVars,
getCSSVariable,
applyThemeToElement,
followExternalTheme,
adoptFromParent,
watch,
createInstanceTheme,
createThemeManager,
presetThemes,
};
// 向后兼容别名
const getTheme = resolveTheme;
export { getTheme };
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* MetonaEditor Utils - 工具函数
* @module utils
* @version 0.1.4
* @version 0.1.5
* @description 通用工具函数集合
*/
+389
View File
@@ -34,8 +34,16 @@ import {
setThemeVariables,
presetThemes,
createThemeManager,
exportCSSVars,
getCSSVariable,
applyThemeToElement,
followExternalTheme,
adoptFromParent,
watch,
createInstanceTheme,
} from '../src/themes.js';
import { THEMES } from '../src/constants.js';
import { MarkdownEditor } from '../src/core.js';
// 确保 localStorage 存在(jsdom 兼容)
if (typeof global.localStorage === 'undefined') {
@@ -431,3 +439,384 @@ describe('createThemeManager', () => {
expect(m.getCurrentTheme()).toBe('dark');
});
});
// ============ v0.1.5 新增测试 ============
describe('v0.1.5 resolveTheme auto 修复', () => {
beforeEach(() => {
clearThemeListeners();
resetToAuto();
localStorage.clear();
});
test('auto 始终解析为系统主题,不受上次手动设置污染', () => {
switchTheme('dark');
expect(getCurrentTheme()).toBe('dark');
// 即使手动设为 darkresolveTheme('auto') 仍应返回系统主题
const sys = getSystemTheme();
expect(resolveTheme('auto')).toBe(sys);
});
test('resolveTheme 传入具体主题名原样返回', () => {
expect(resolveTheme('light')).toBe('light');
expect(resolveTheme('dark')).toBe('dark');
expect(resolveTheme('warm')).toBe('warm');
});
test('resolveTheme 无参/空值返回系统主题', () => {
const sys = getSystemTheme();
expect(resolveTheme()).toBe(sys);
expect(resolveTheme(null)).toBe(sys);
expect(resolveTheme('')).toBe(sys);
});
});
describe('v0.1.5 exportCSSVars / getCSSVariable', () => {
test('exportCSSVars 返回 CSS 变量对象', () => {
applyTheme('light');
const vars = exportCSSVars();
expect(vars).toHaveProperty('--md-bg');
expect(vars).toHaveProperty('--md-accent');
});
test('exportCSSVars 从指定元素读取', () => {
const div = document.createElement('div');
div.style.setProperty('--md-bg', '#fff');
document.body.appendChild(div);
const vars = exportCSSVars(div);
expect(vars['--md-bg']).toBe('#fff');
document.body.removeChild(div);
});
test('getCSSVariable 获取单个变量', () => {
applyTheme('light');
const bg = getCSSVariable('--md-bg');
expect(typeof bg).toBe('string');
expect(bg.length).toBeGreaterThan(0);
});
test('getCSSVariable 自动补全 --md- 前缀', () => {
applyTheme('light');
const accent = getCSSVariable('accent');
expect(typeof accent).toBe('string');
expect(accent.length).toBeGreaterThan(0);
});
});
describe('v0.1.5 applyThemeToElement', () => {
test('应用主题到指定元素不改变全局状态', () => {
switchTheme('light');
const div = document.createElement('div');
document.body.appendChild(div);
applyThemeToElement('dark', div);
expect(div.getAttribute('data-md-theme')).toBe('dark');
// 全局主题不变
expect(getCurrentTheme()).toBe('light');
document.body.removeChild(div);
});
test('应用 auto 主题正确解析', () => {
const div = document.createElement('div');
document.body.appendChild(div);
applyThemeToElement('auto', div);
const resolved = resolveTheme('auto');
expect(div.getAttribute('data-md-theme')).toBe(resolved);
document.body.removeChild(div);
});
});
describe('v0.1.5 主题继承 registerTheme extends', () => {
test('extends 继承基础主题', () => {
registerTheme('ocean', {
extends: 'dark',
accent: '#00ddff',
});
expect(hasTheme('ocean')).toBe(true);
const cfg = getThemeConfig('ocean');
expect(cfg.accent).toBe('#00ddff');
// 从 dark 继承
expect(cfg.bg).toBe(THEMES.dark.bg);
expect(cfg.text).toBe(THEMES.dark.text);
unregisterTheme('ocean');
});
test('默认 extends light', () => {
registerTheme('minimal', { accent: '#ff0000' });
const cfg = getThemeConfig('minimal');
expect(cfg.bg).toBe(THEMES.light.bg);
unregisterTheme('minimal');
});
});
describe('v0.1.5 createInstanceTheme', () => {
test('创建实例主题上下文', () => {
const div = document.createElement('div');
document.body.appendChild(div);
const mockEditor = {
el: div,
config: { theme: 'dark' },
_emit: jest.fn(),
refresh: jest.fn(),
};
const ctx = createInstanceTheme(mockEditor);
expect(ctx.get()).toBe('dark');
expect(ctx.getResolved()).toBe('dark');
expect(typeof ctx.set).toBe('function');
expect(typeof ctx.toggle).toBe('function');
expect(typeof ctx.getConfig).toBe('function');
expect(typeof ctx.getVars).toBe('function');
expect(typeof ctx.syncWithElement).toBe('function');
expect(typeof ctx.adopt).toBe('function');
expect(typeof ctx.watch).toBe('function');
document.body.removeChild(div);
});
test('实例主题 set 触发 _emit themeChange', () => {
const div = document.createElement('div');
document.body.appendChild(div);
const mockEditor = {
el: div,
config: { theme: 'light' },
_emit: jest.fn(),
refresh: jest.fn(),
};
const ctx = createInstanceTheme(mockEditor);
ctx.set('dark');
expect(mockEditor._emit).toHaveBeenCalledWith('themeChange', expect.objectContaining({
theme: 'dark',
resolved: 'dark',
}));
expect(mockEditor.refresh).toHaveBeenCalled();
document.body.removeChild(div);
});
test('实例主题 toggle 切换 light/dark', () => {
const div = document.createElement('div');
document.body.appendChild(div);
const mockEditor = {
el: div,
config: { theme: 'light' },
_emit: jest.fn(),
refresh: jest.fn(),
};
const ctx = createInstanceTheme(mockEditor);
ctx.toggle();
expect(ctx.get()).toBe('dark');
ctx.toggle();
expect(ctx.get()).toBe('light');
document.body.removeChild(div);
});
test('getConfig 返回主题配置对象', () => {
const div = document.createElement('div');
document.body.appendChild(div);
const mockEditor = {
el: div,
config: { theme: 'light' },
_emit: jest.fn(),
refresh: jest.fn(),
};
const ctx = createInstanceTheme(mockEditor);
const cfg = ctx.getConfig();
expect(cfg).toHaveProperty('bg');
expect(cfg).toHaveProperty('accent');
document.body.removeChild(div);
});
test('getVars 返回实例 CSS 变量', () => {
const div = document.createElement('div');
document.body.appendChild(div);
const mockEditor = {
el: div,
config: { theme: 'light' },
_emit: jest.fn(),
refresh: jest.fn(),
};
const ctx = createInstanceTheme(mockEditor);
applyThemeToElement('light', div);
const vars = ctx.getVars();
expect(vars).toHaveProperty('--md-bg');
document.body.removeChild(div);
});
});
describe('v0.1.5 followExternalTheme', () => {
test('通过 data-theme 属性跟随', () => {
const div = document.createElement('div');
div.setAttribute('data-theme', 'dark');
document.body.appendChild(div);
let detected = null;
const unsub = followExternalTheme({ element: div }, (theme) => { detected = theme; });
expect(detected).toBe('dark');
div.setAttribute('data-theme', 'light');
// 手动触发 MutationObserverjsdom 中需手动 dispatch
const observer = new MutationObserver(() => {});
// 注:jsdom 环境 MutationObserver 行为受限,此处只验证初始化回调
unsub();
document.body.removeChild(div);
});
test('通过 classMap 跟随', () => {
const div = document.createElement('div');
div.className = 'theme-dark';
document.body.appendChild(div);
let detected = null;
followExternalTheme(
{ element: div, classMap: { 'theme-dark': 'dark', 'theme-light': 'light' } },
(theme) => { detected = theme; },
);
expect(detected).toBe('dark');
document.body.removeChild(div);
});
test('通过 callback 跟随', () => {
const div = document.createElement('div');
document.body.appendChild(div);
let detected = null;
followExternalTheme(
{ element: div, callback: () => 'warm' },
(theme) => { detected = theme; },
);
expect(detected).toBe('warm');
document.body.removeChild(div);
});
test('返回取消跟随函数', () => {
const div = document.createElement('div');
document.body.appendChild(div);
const unsub = followExternalTheme({ element: div }, () => {});
expect(typeof unsub).toBe('function');
unsub();
document.body.removeChild(div);
});
});
describe('v0.1.5 adoptFromParent', () => {
test('从父容器 data-md-theme 继承', () => {
const parent = document.createElement('div');
parent.setAttribute('data-md-theme', 'dark');
const child = document.createElement('div');
parent.appendChild(child);
document.body.appendChild(parent);
let detected = null;
const unsub = adoptFromParent(child, (theme) => { detected = theme; });
expect(detected).toBe('dark');
unsub();
document.body.removeChild(parent);
});
test('从父容器 class 继承', () => {
const parent = document.createElement('div');
parent.className = 'dark';
const child = document.createElement('div');
parent.appendChild(child);
document.body.appendChild(parent);
let detected = null;
adoptFromParent(child, (theme) => { detected = theme; });
expect(detected).toBe('dark');
document.body.removeChild(parent);
});
test('容器无父元素不抛错', () => {
const orphan = document.createElement('div');
let detected = null;
expect(() => adoptFromParent(orphan, (t) => { detected = t; })).not.toThrow();
});
test('返回取消函数', () => {
const parent = document.createElement('div');
const child = document.createElement('div');
parent.appendChild(child);
document.body.appendChild(parent);
const unsub = adoptFromParent(child, () => {});
expect(typeof unsub).toBe('function');
unsub();
document.body.removeChild(parent);
});
});
describe('v0.1.5 watch 外部主题源', () => {
test('函数源:回调被调用', () => {
let detected = null;
const unsub = watch(() => 'dark', (theme) => { detected = theme; });
expect(detected).toBe('dark');
unsub();
});
test('返回取消函数', () => {
const unsub = watch(() => 'light', () => {});
expect(typeof unsub).toBe('function');
unsub();
});
});
describe('v0.1.5 MarkdownEditor 实例主题 API', () => {
test('setTheme / getTheme 实例方法', () => {
document.body.innerHTML = '';
const c = document.createElement('div');
document.body.appendChild(c);
const ed = new MarkdownEditor(c, { theme: 'light' });
expect(ed.getTheme()).toBe('light');
ed.setTheme('dark');
expect(ed.getTheme()).toBe('dark');
// el 类名应更新
expect(ed.el.className).toContain('me-theme-dark');
ed.destroy();
});
test('getThemeContext 返回实例主题上下文', () => {
document.body.innerHTML = '';
const c = document.createElement('div');
document.body.appendChild(c);
const ed = new MarkdownEditor(c, {});
const ctx = ed.getThemeContext();
expect(ctx).not.toBeNull();
expect(typeof ctx.set).toBe('function');
expect(typeof ctx.get).toBe('function');
ed.destroy();
});
test('多实例独立主题互不干扰', () => {
document.body.innerHTML = '';
const c1 = document.createElement('div');
const c2 = document.createElement('div');
document.body.appendChild(c1);
document.body.appendChild(c2);
const ed1 = new MarkdownEditor(c1, { theme: 'light' });
const ed2 = new MarkdownEditor(c2, { theme: 'dark' });
expect(ed1.getTheme()).toBe('light');
expect(ed2.getTheme()).toBe('dark');
ed1.setTheme('warm');
expect(ed1.getTheme()).toBe('warm');
expect(ed2.getTheme()).toBe('dark'); // ed2 不受影响
ed1.destroy();
ed2.destroy();
});
test('setTheme 触发 themeChange 事件', () => {
document.body.innerHTML = '';
const c = document.createElement('div');
document.body.appendChild(c);
const ed = new MarkdownEditor(c, {});
const handler = jest.fn();
ed.on('themeChange', handler);
ed.setTheme('dark');
expect(handler).toHaveBeenCalled();
ed.destroy();
});
});
+25 -2
View File
@@ -1,4 +1,4 @@
// Type definitions for MetonaEditor v0.1.4
// Type definitions for MetonaEditor v0.1.5
// Project: https://git.metona.cn/MetonaTeam/MetonaEditor
// Author: thzxx
@@ -201,6 +201,21 @@ export class MarkdownEditor {
getStats(): EditorStats;
getStatus(): EditorStatus;
// 主题(v0.1.5: 实例级主题管理)
setTheme(theme: ThemeName): this;
getTheme(): string;
getThemeContext(): {
get(): string;
set(theme: string): string;
toggle(): string;
getResolved(): string;
getConfig(): object;
getVars(): object;
syncWithElement(element: HTMLElement, opts?: object): () => void;
adopt(): () => void;
watch(source: Function | string): () => void;
} | null;
// 插件
use(plugin: string | PluginObject, options?: object): this;
getPlugins(): PluginObject[];
@@ -307,7 +322,7 @@ export const themeUtils: {
getCurrentTheme(): string;
getResolvedTheme(): string;
applyTheme(theme: ThemeName): void;
registerTheme(name: string, config: object): void;
registerTheme(name: string, config: { extends?: string; [key: string]: any }): void;
unregisterTheme(name: string): void;
getAllThemes(): object;
getThemeNames(): string[];
@@ -317,6 +332,14 @@ export const themeUtils: {
addThemeListener(fn: (theme: string, resolved: string) => void): () => void;
removeThemeListener(fn: (theme: string, resolved: string) => void): void;
clearThemeListeners(): void;
// v0.1.5 新增
exportCSSVars(el?: HTMLElement): Record<string, string>;
getCSSVariable(name: string, el?: HTMLElement): string;
applyThemeToElement(theme: string, target: HTMLElement): void;
followExternalTheme(opts: object, onChange: (theme: string) => void): () => void;
adoptFromParent(container: HTMLElement, onDetected: (theme: string) => void): () => void;
watch(source: Function | string, onChange: (theme: string) => void): () => void;
createInstanceTheme(editor: object): object;
[key: string]: any;
};