@flun/html-template 4.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +9 -0
- package/LICENSE +15 -0
- package/build.js +3 -0
- package/compile.js +349 -0
- package/copy-files.js +200 -0
- package/customize/account.js +726 -0
- package/customize/data.json +484 -0
- package/customize/functions.js +48 -0
- package/customize/hotReloadInjector.js +25 -0
- package/customize/routes.js +141 -0
- package/customize/users.json +44 -0
- package/customize/variables.js +70 -0
- package/dev-server.js +344 -0
- package/dev.js +4 -0
- package/f-CHANGELOG.md +4 -0
- package/f-README.md +485 -0
- package/index.d.ts +133 -0
- package/index.js +4 -0
- package/package.json +77 -0
- package/restoreDefaults.js +8 -0
- package/services/templateService.js +962 -0
- package/static/about.css +118 -0
- package/static/auth.js +27 -0
- package/static/constants.css +138 -0
- package/static/img/dark.png +0 -0
- package/static/img/favicon.ico +0 -0
- package/static/img/light.png +0 -0
- package/static/img/top.png +0 -0
- package/static/index.css +86 -0
- package/static/mouseOrTouch.js +156 -0
- package/static/public.css +288 -0
- package/static/script.css +318 -0
- package/static/script.js +392 -0
- package/static/styling.css +874 -0
- package/static/styling.js +933 -0
- package/static/themeImg.css +10 -0
- package/static/themeImg.js +19 -0
- package/static/themeModule.js +222 -0
- package/static/topImg.css +19 -0
- package/static/topImg.js +21 -0
- package/static/utils/browser13.js +270 -0
- package/static/utils/closebrackets.js +166 -0
- package/static/utils/css-lint.js +308 -0
- package/static/utils/custom-css-hint.js +876 -0
- package/static/utils/foldgutter.js +141 -0
- package/static/utils/match-highlighter.js +70 -0
- package/templates/about.html +236 -0
- package/templates/account/2fa.html +184 -0
- package/templates/account/forgot-password.html +226 -0
- package/templates/account/login.html +230 -0
- package/templates/account/profile.html +977 -0
- package/templates/account/register.html +224 -0
- package/templates/account/reset-password.html +205 -0
- package/templates/account/verify-email.html +163 -0
- package/templates/base.html +71 -0
- package/templates/footer-content.html +5 -0
- package/templates/index.html +140 -0
- package/templates/script.html +209 -0
- package/templates/test-include.html +11 -0
package/static/script.js
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
// 根目录/static/script.js
|
|
2
|
+
function scriptFun() {
|
|
3
|
+
const modal = document.querySelector('.modal'), cssEditor = document.getElementById('cssEditor'),
|
|
4
|
+
closeBtn = document.querySelector('.close-btn'), saveBtn = document.getElementById('saveBtn'),
|
|
5
|
+
cancelBtn = document.getElementById('cancelBtn'), loader = document.getElementById('loader'), api = '/api/cssEditor',
|
|
6
|
+
previewBtn = document.getElementById('previewBtn'), cancelPreviewBtn = document.getElementById('cancelPreviewBtn'),
|
|
7
|
+
preview = document.getElementById('preview'), previewFrame = document.getElementById('previewFrame'), pApi = '/api/preview',
|
|
8
|
+
urlParams = new URLSearchParams(window.location.search), fileDir = urlParams.get('fileDir'),
|
|
9
|
+
returnUrl = urlParams.get('return'), cm = window.cssEditor; // cm 是 CodeMirror 实例,由外部库创建
|
|
10
|
+
|
|
11
|
+
let isEditingColor = false, editColorRange = null, isPreviewMode = false, globalColorPicker = null;
|
|
12
|
+
|
|
13
|
+
// ----- 阻止 CodeMirror 编辑器区域事件冒泡,避免触发父容器拖拽和阻止默认菜单 -----
|
|
14
|
+
cm.getWrapperElement().addEventListener('mousedown', e => e.stopPropagation());
|
|
15
|
+
cm.getWrapperElement().addEventListener('touchstart', e => e.stopPropagation(), { passive: true });
|
|
16
|
+
cm.getWrapperElement().addEventListener('contextmenu', e => e.stopPropagation());
|
|
17
|
+
|
|
18
|
+
// ---------- 🎨 全局颜色选择器 ----------
|
|
19
|
+
function initGlobalColorPicker() {
|
|
20
|
+
if (globalColorPicker) return;
|
|
21
|
+
globalColorPicker = document.createElement('input');
|
|
22
|
+
globalColorPicker.type = 'color';
|
|
23
|
+
globalColorPicker.style.position = 'fixed';
|
|
24
|
+
globalColorPicker.style.width = '0';
|
|
25
|
+
globalColorPicker.style.height = '0';
|
|
26
|
+
globalColorPicker.style.opacity = '0';
|
|
27
|
+
globalColorPicker.style.pointerEvents = 'none';
|
|
28
|
+
globalColorPicker.style.zIndex = '9999';
|
|
29
|
+
globalColorPicker.style.left = '0px';
|
|
30
|
+
globalColorPicker.style.top = '0px';
|
|
31
|
+
document.body.append(globalColorPicker);
|
|
32
|
+
}
|
|
33
|
+
initGlobalColorPicker();
|
|
34
|
+
|
|
35
|
+
// ---------- 颜色关键字映射 ----------
|
|
36
|
+
const COLOR_KEYWORDS = {
|
|
37
|
+
'aliceblue': '#F0F8FF', 'antiquewhite': '#FAEBD7', 'aqua': '#00FFFF', 'aquamarine': '#7FFFD4', 'azure': '#F0FFFF',
|
|
38
|
+
'beige': '#F5F5DC', 'bisque': '#FFE4C4', 'black': '#000000', 'blanchedalmond': '#FFEBCD', 'blue': '#0000FF',
|
|
39
|
+
'blueviolet': '#8A2BE2', 'brown': '#A52A2A', 'burlywood': '#DEB887', 'cadetblue': '#5F9EA0', 'chartreuse': '#7FFF00',
|
|
40
|
+
'chocolate': '#D2691E', 'coral': '#FF7F50', 'cornflowerblue': '#6495ED', 'cornsilk': '#FFF8DC', 'crimson': '#DC143C',
|
|
41
|
+
'cyan': '#00FFFF', 'darkblue': '#00008B', 'darkcyan': '#008B8B', 'darkgoldenrod': '#B8860B', 'darkgray': '#A9A9A9',
|
|
42
|
+
'darkgreen': '#006400', 'darkgrey': '#A9A9A9', 'darkkhaki': '#BDB76B', 'darkred': '#8B0000', 'dimgray': '#696969',
|
|
43
|
+
'dimgrey': '#696969', 'darkorange': '#FF8C00', 'darkmagenta': '#8B008B', 'deeppink': '#FF1493',
|
|
44
|
+
'darkorchid': '#9932CC', 'darkolivegreen': '#556B2F', 'darksalmon': '#E9967A', 'darkseagreen': '#8FBC8F',
|
|
45
|
+
'darkslateblue': '#483D8B', 'darkslategray': '#2F4F4F', 'darkslategrey': '#2F4F4F', 'darkturquoise': '#00CED1',
|
|
46
|
+
'darkviolet': '#9400D3', 'deepskyblue': '#00BFFF', 'dodgerblue': '#1E90FF', 'firebrick': '#B22222',
|
|
47
|
+
'floralwhite': '#FFFAF0', 'forestgreen': '#228B22', 'fuchsia': '#FF00FF', 'gainsboro': '#DCDCDC', 'gold': '#FFD700',
|
|
48
|
+
'ghostwhite': '#F8F8FF', 'goldenrod': '#DAA520', 'gray': '#808080', 'green': '#008000', 'greenyellow': '#ADFF2F',
|
|
49
|
+
'grey': '#808080', 'honeydew': '#F0FFF0', 'hotpink': '#FF69B4', 'indianred': '#CD5C5C', 'indigo': '#4B0082',
|
|
50
|
+
'ivory': '#FFFFF0', 'khaki': '#F0E68C', 'lavender': '#E6E6FA', 'lavenderblush': '#FFF0F5', 'lawngreen': '#7CFC00',
|
|
51
|
+
'lemonchiffon': '#FFFACD', 'lightblue': '#ADD8E6', 'lightcoral': '#F08080', 'lightcyan': '#E0FFFF',
|
|
52
|
+
'lightgoldenrodyellow': '#FAFAD2', 'lightgray': '#D3D3D3', 'lightgreen': '#90EE90', 'lightgrey': '#D3D3D3',
|
|
53
|
+
'lightpink': '#FFB6C1', 'lightsalmon': '#FFA07A', 'lightseagreen': '#20B2AA', 'lightskyblue': '#87CEFA',
|
|
54
|
+
'lightslategray': '#778899', 'lightslategrey': '#778899', 'lightsteelblue': '#B0C4DE', 'lightyellow': '#FFFFE0',
|
|
55
|
+
'lime': '#00FF00', 'limegreen': '#32CD32', 'linen': '#FAF0E6', 'magenta': '#FF00FF', 'maroon': '#800000',
|
|
56
|
+
'mediumaquamarine': '#66CDAA', 'mediumblue': '#0000CD', 'mediumorchid': '#BA55D3', 'mediumpurple': '#9370DB',
|
|
57
|
+
'mediumseagreen': '#3CB371', 'mediumslateblue': '#7B68EE', 'mediumspringgreen': '#00FA9A', 'moccasin': '#FFE4B5',
|
|
58
|
+
'mediumturquoise': '#48D1CC', 'mediumvioletred': '#C71585', 'midnightblue': '#191970', 'mintcream': '#F5FFFA',
|
|
59
|
+
'mistyrose': '#FFE4E1', 'navajowhite': '#FFDEAD', 'navy': '#000080', 'oldlace': '#FDF5E6', 'olive': '#808000',
|
|
60
|
+
'olivedrab': '#6B8E23', 'orange': '#FFA500', 'orangered': '#FF4500', 'orchid': '#DA70D6', 'peru': '#CD853F',
|
|
61
|
+
'palegoldenrod': '#EEE8AA', 'palegreen': '#98FB98', 'paleturquoise': '#AFEEEE', 'palevioletred': '#DB7093',
|
|
62
|
+
'papayawhip': '#FFEFD5', 'peachpuff': '#FFDAB9', 'pink': '#FFC0CB', 'plum': '#DDA0DD', 'powderblue': '#B0E0E6',
|
|
63
|
+
'purple': '#800080', 'rebeccapurple': '#663399', 'red': '#FF0000', 'rosybrown': '#BC8F8F', 'royalblue': '#4169E1',
|
|
64
|
+
'saddlebrown': '#8B4513', 'salmon': '#FA8072', 'sandybrown': '#F4A460', 'seagreen': '#2E8B57', 'snow': '#FFFAFA',
|
|
65
|
+
'seashell': '#FFF5EE', 'sienna': '#A0522D', 'silver': '#C0C0C0', 'skyblue': '#87CEEB', 'slateblue': '#6A5ACD',
|
|
66
|
+
'slategray': '#708090', 'slategrey': '#708090', 'springgreen': '#00FF7F', 'steelblue': '#4682B4', 'tan': '#D2B48C',
|
|
67
|
+
'teal': '#008080', 'thistle': '#D8BFD8', 'tomato': '#FF6347', 'turquoise': '#40E0D0', 'violet': '#EE82EE',
|
|
68
|
+
'wheat': '#F5DEB3', 'white': '#FFFFFF', 'whitesmoke': '#F5F5F5', 'yellow': '#FFFF00', 'yellowgreen': '#9ACD32',
|
|
69
|
+
'transparent': 'transparent'
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// ---------- 颜色正则(支持所有格式)----------
|
|
73
|
+
const KEYWORDS = Object.keys(COLOR_KEYWORDS).concat('transparent').join('|'),
|
|
74
|
+
HEX = '#(?:[0-9a-fA-F]{3,4}){1,2}\\b', RGB = 'rgba?\\(\\s*\\d+\\s*,\\s*\\d+\\s*,\\s*\\d+\\s*(?:,\\s*[\\d.]+\\s*)?\\)',
|
|
75
|
+
HSL = 'hsla?\\(\\s*\\d+\\s*,\\s*\\d+%\\s*,\\s*\\d+%\\s*(?:,\\s*[\\d.]+\\s*)?\\)',
|
|
76
|
+
COLOR_REGEX = new RegExp(`${HEX}|${RGB}|${HSL}|\\b(${KEYWORDS})\\b`, 'gi'),
|
|
77
|
+
RGB_EXTRACT = /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/i;
|
|
78
|
+
|
|
79
|
+
// ---------- 颜色工具函数 ----------
|
|
80
|
+
function rgbToHex(r, g, b) {
|
|
81
|
+
return '#' + [r, g, b].map(x => {
|
|
82
|
+
const h = parseInt(x).toString(16);
|
|
83
|
+
return h.length === 1 ? '0' + h : h;
|
|
84
|
+
}).join('');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function extractColorAndAlpha(c) {
|
|
88
|
+
const l = c.toLowerCase(), kw = COLOR_KEYWORDS[l];
|
|
89
|
+
let r = 0, g = 0, b = 0, a = 1, t = 'keyword';
|
|
90
|
+
|
|
91
|
+
// 1. 颜色关键字(含 transparent)
|
|
92
|
+
if (kw) {
|
|
93
|
+
if (l === 'transparent') return { r: 0, g: 0, b: 0, a: 0, t, o: c, tr: true };
|
|
94
|
+
const hex = kw.slice(1); // "#ff0000" → "ff0000"
|
|
95
|
+
[r, g, b] = hex.match(/.{2}/g).map(v => parseInt(v, 16)), a = 1;
|
|
96
|
+
}
|
|
97
|
+
// 2. 十六进制 #RRGGBB / #RGB / #RRGGBBAA / #RGBA
|
|
98
|
+
else if (c.startsWith('#')) {
|
|
99
|
+
t = 'hex';
|
|
100
|
+
let h = c.slice(1).toLowerCase();
|
|
101
|
+
if (h.length === 3 || h.length === 4) h = h.split('').map(x => x + x).join('');
|
|
102
|
+
const hexMatch = h.match(/^([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})?$/);
|
|
103
|
+
if (hexMatch)
|
|
104
|
+
[r, g, b] = hexMatch.slice(1, 4).map(v => parseInt(v, 16)), a = hexMatch[4] ? parseInt(hexMatch[4], 16) / 255 : 1;
|
|
105
|
+
}
|
|
106
|
+
// 3. rgb() / rgba()
|
|
107
|
+
else if (c.startsWith('rgb')) {
|
|
108
|
+
const m = RGB_EXTRACT.exec(c);
|
|
109
|
+
if (m) t = 'rgb', [r, g, b] = [m[1], m[2], m[3]].map(Number), a = m[4] ? parseFloat(m[4]) : 1;
|
|
110
|
+
}
|
|
111
|
+
// 4. hsl() / hsla()
|
|
112
|
+
else if (c.startsWith('hsl')) {
|
|
113
|
+
t = 'hsl';
|
|
114
|
+
const d = document.createElement('div');
|
|
115
|
+
d.style.color = c, document.body.append(d);
|
|
116
|
+
const computed = window.getComputedStyle(d).color; // "rgb(r, g, b)" 或 "rgba(r, g, b, a)"
|
|
117
|
+
d.remove();
|
|
118
|
+
|
|
119
|
+
const m = RGB_EXTRACT.exec(computed);
|
|
120
|
+
if (m) [r, g, b] = [m[1], m[2], m[3]].map(Number), a = m[4] ? parseFloat(m[4]) : 1;
|
|
121
|
+
else r = 128, g = 128; b = 128, a = 1; // 解析失败时的默认值
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { r, g, b, a, t, o: c };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function getColorForSwatch(c) {
|
|
128
|
+
const o = extractColorAndAlpha(c);
|
|
129
|
+
if (o.tr || c.toLowerCase() === 'transparent') return 'transparent';
|
|
130
|
+
if (o.a < 1) return `rgba(${o.r},${o.g},${o.b},${o.a})`;
|
|
131
|
+
else if (c.startsWith('#')) return c;
|
|
132
|
+
else if (c.startsWith('rgb')) return `rgb(${o.r},${o.g},${o.b})`;
|
|
133
|
+
else return c;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------- 创建颜色部件 ----------
|
|
137
|
+
function createColorWidget(ct, fr, to) {
|
|
138
|
+
const w = document.createElement('span'), s = document.createElement('span'), t = document.createElement('span');
|
|
139
|
+
w.className = 'cm-color-widget', w.style.display = 'inline-flex', w.style.alignItems = 'center'; w.style.margin = '0 2px',
|
|
140
|
+
w.style.padding = '2px 4px', w.style.borderRadius = '3px', w.style.backgroundColor = 'rgba(0,0,0,0.1)';
|
|
141
|
+
|
|
142
|
+
const bg = getColorForSwatch(ct);
|
|
143
|
+
s.className = 'cm-color-swatch', s.style.backgroundColor = bg, s.title = '点击修改颜色', s.style.display = 'inline-block',
|
|
144
|
+
s.style.width = '16px', s.style.height = '16px', s.style.borderRadius = '3px', s.style.marginRight = '6px',
|
|
145
|
+
s.style.border = bg === 'transparent' ? '1px dashed #999' : '1px solid rgba(255,255,255,1)';
|
|
146
|
+
s.style.cursor = 'pointer', s.style.flexShrink = '0';
|
|
147
|
+
|
|
148
|
+
t.className = 'cm-color-text', t.textContent = ct, t.style.color = '#f8f8f2', t.style.fontSize = '13px';
|
|
149
|
+
t.style.userSelect = 'text', t.style.cursor = 'text', t.style.marginRight = '8px', w.append(s, t);
|
|
150
|
+
|
|
151
|
+
// 透明度滑块
|
|
152
|
+
const o = extractColorAndAlpha(ct), as = document.createElement('input'), av = document.createElement('span');
|
|
153
|
+
if (as) {
|
|
154
|
+
as.type = 'range', as.min = 0, as.max = 100, as.value = Math.round(o.a * 100), as.style.width = '120px',
|
|
155
|
+
as.style.marginRight = '8px', as.title = '调整透明度 (0-100%)', as.className = 'cm-alpha-slider';
|
|
156
|
+
av.className = 'cm-alpha-value', av.textContent = `${Math.round(o.a * 100)}%`, av.style.fontSize = '12px',
|
|
157
|
+
av.style.color = '#ccc', av.style.minWidth = '30px', av.style.textAlign = 'center';
|
|
158
|
+
|
|
159
|
+
// 根据透明度计算新颜色(供 input/change 复用)
|
|
160
|
+
const getUpdatedColor = (alpha) => {
|
|
161
|
+
const na = alpha / 100;
|
|
162
|
+
let nc = ct;
|
|
163
|
+
const lc = ct.toLowerCase();
|
|
164
|
+
if (lc === 'transparent') nc = `rgba(0,0,0,${na})`;
|
|
165
|
+
if (COLOR_KEYWORDS[lc]) {
|
|
166
|
+
const h = COLOR_KEYWORDS[lc], hr = parseInt(h.slice(1, 3), 16), hg = parseInt(h.slice(3, 5), 16),
|
|
167
|
+
hb = parseInt(h.slice(5, 7), 16);
|
|
168
|
+
nc = `rgba(${hr},${hg},${hb},${na})`;
|
|
169
|
+
} else if (ct.includes('rgba') || ct.includes('hsla')) {
|
|
170
|
+
if (ct.includes('rgba')) nc = ct.replace(RGB_EXTRACT, `rgba($1,$2,$3,${na})`);
|
|
171
|
+
else nc = ct.replace(/hsla?\((\d+,\s*\d+%,\s*\d+%)(?:,\s*[\d.]+)?\)/i, `hsla($1,${na})`);
|
|
172
|
+
} else if (ct.startsWith('rgb(')) nc = ct.replace('rgb(', `rgba(`).replace(')', `,${na})`);
|
|
173
|
+
else if (ct.startsWith('hsl(')) nc = ct.replace('hsl(', `hsla(`).replace(')', `,${na})`);
|
|
174
|
+
else if (ct.startsWith('#')) {
|
|
175
|
+
let h = ct.slice(1), hr, hg, hb;
|
|
176
|
+
if (h.length === 3)
|
|
177
|
+
hr = parseInt(h[0] + h[0], 16), hg = parseInt(h[1] + h[1], 16), hb = parseInt(h[2] + h[2], 16);
|
|
178
|
+
else if (h.length === 6)
|
|
179
|
+
hr = parseInt(h.slice(0, 2), 16), hg = parseInt(h.slice(2, 4), 16), hb = parseInt(h.slice(4, 6), 16);
|
|
180
|
+
else hr = 128, hg = 128, hb = 128;
|
|
181
|
+
nc = `rgba(${hr},${hg},${hb},${na})`;
|
|
182
|
+
}
|
|
183
|
+
return nc;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// 实时更新 UI(色块、文本、百分比),但不修改文档
|
|
187
|
+
as.addEventListener('input', function () {
|
|
188
|
+
const na = this.value / 100;
|
|
189
|
+
av.textContent = `${this.value}%`;
|
|
190
|
+
const nc = getUpdatedColor(this.value);
|
|
191
|
+
s.style.backgroundColor = getColorForSwatch(nc);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// 滑块松开或失去焦点时,一次性更新文档
|
|
195
|
+
as.addEventListener('change', function () {
|
|
196
|
+
const nc = getUpdatedColor(this.value);
|
|
197
|
+
cm.replaceRange(nc, fr, to), updateColorWidgets(cm);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// 阻止滑块上的鼠标/触摸事件冒泡,使滑块可拖动
|
|
201
|
+
as.addEventListener('mousedown', e => e.stopPropagation());
|
|
202
|
+
as.addEventListener('touchstart', e => e.stopPropagation(), { passive: true });
|
|
203
|
+
|
|
204
|
+
w.append(as, av);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 色块点击事件
|
|
208
|
+
s.addEventListener('click', e => {
|
|
209
|
+
e.stopPropagation();
|
|
210
|
+
const r = s.getBoundingClientRect(), cc = extractColorAndAlpha(ct), ch = rgbToHex(cc.r, cc.g, cc.b);
|
|
211
|
+
globalColorPicker.style.left = `${r.left}px`, globalColorPicker.style.top = `${r.bottom + 5}px`;
|
|
212
|
+
globalColorPicker.value = ch;
|
|
213
|
+
|
|
214
|
+
if (globalColorPicker._ch) globalColorPicker.removeEventListener('change', globalColorPicker._ch);
|
|
215
|
+
const chf = function () {
|
|
216
|
+
const nh = this.value, na = as ? as.value / 100 : cc.a, hr = parseInt(nh.slice(1, 3), 16),
|
|
217
|
+
hg = parseInt(nh.slice(3, 5), 16), hb = parseInt(nh.slice(5, 7), 16);
|
|
218
|
+
let nc = na < 1 ? `rgba(${hr},${hg},${hb},${na})` : nh;
|
|
219
|
+
const nhu = nh.toUpperCase(), kw = Object.keys(COLOR_KEYWORDS).find(k => COLOR_KEYWORDS[k].toUpperCase() === nhu);
|
|
220
|
+
if (kw && na >= 1) nc = kw;
|
|
221
|
+
cm.replaceRange(nc, fr, to), updateColorWidgets(cm), t.textContent = nc;
|
|
222
|
+
s.style.backgroundColor = getColorForSwatch(nc);
|
|
223
|
+
if (av) av.textContent = `${Math.round(na * 100)}%`;
|
|
224
|
+
to = { line: to.line, ch: fr.ch + nc.length };
|
|
225
|
+
this.removeEventListener('change', chf), globalColorPicker._ch = null;
|
|
226
|
+
};
|
|
227
|
+
globalColorPicker._ch = chf, globalColorPicker.addEventListener('change', chf), globalColorPicker.getBoundingClientRect();
|
|
228
|
+
if (typeof globalColorPicker.showPicker === 'function') globalColorPicker.showPicker();
|
|
229
|
+
else globalColorPicker.click();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return w;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ---------- 扫描编辑器添加部件 ----------
|
|
236
|
+
function updateColorWidgets(cm) {
|
|
237
|
+
isEditingColor = false, editColorRange = null; // 退出编辑模式
|
|
238
|
+
|
|
239
|
+
cm.getAllMarks().forEach(m => { if (m.isColorWidget) m.clear(); });
|
|
240
|
+
const d = cm.getDoc(), lc = d.lineCount();
|
|
241
|
+
for (let i = 0; i < lc; i++) {
|
|
242
|
+
const l = d.getLine(i); let m;
|
|
243
|
+
COLOR_REGEX.lastIndex = 0;
|
|
244
|
+
while ((m = COLOR_REGEX.exec(l)) !== null) {
|
|
245
|
+
const s = m.index, e = s + m[0].length, fr = { line: i, ch: s }, to = { line: i, ch: e };
|
|
246
|
+
if (d.findMarksAt(fr).some(x => x.replacedWith)) continue;
|
|
247
|
+
|
|
248
|
+
const w = createColorWidget(m[0], fr, to),
|
|
249
|
+
mark = cm.markText(fr, to, {
|
|
250
|
+
replacedWith: w, inclusiveLeft: false, inclusiveRight: false, clearOnEnter: true
|
|
251
|
+
});
|
|
252
|
+
mark.isColorWidget = true, w._colorMark = mark;
|
|
253
|
+
|
|
254
|
+
// 为颜色值文本添加双击事件
|
|
255
|
+
const textSpan = w.querySelector('.cm-color-text');
|
|
256
|
+
if (textSpan) {
|
|
257
|
+
textSpan.title = '双击编辑颜色值';
|
|
258
|
+
textSpan.addEventListener('dblclick', (e) => {
|
|
259
|
+
e.stopPropagation();
|
|
260
|
+
const widget = e.target.closest('.cm-color-widget');
|
|
261
|
+
if (!widget) return;
|
|
262
|
+
const mark = widget._colorMark;
|
|
263
|
+
if (mark) {
|
|
264
|
+
const pos = mark.find();
|
|
265
|
+
if (pos) {
|
|
266
|
+
// 记录当前正在编辑的颜色值范围
|
|
267
|
+
isEditingColor = true;
|
|
268
|
+
editColorRange = {
|
|
269
|
+
from: { line: pos.from.line, ch: pos.from.ch },
|
|
270
|
+
to: { line: pos.to.line, ch: pos.to.ch }
|
|
271
|
+
};
|
|
272
|
+
mark.clear(), cm.setCursor(pos.from), cm.focus();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
updateColorWidgets(cm);
|
|
282
|
+
|
|
283
|
+
// 编辑期间不自动重建部件,但允许手动触发重建
|
|
284
|
+
cm.on('change', () => {
|
|
285
|
+
if (isEditingColor) return;
|
|
286
|
+
setTimeout(() => updateColorWidgets(cm), 10);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// 监听光标活动,检测是否离开正在编辑的颜色值区域
|
|
290
|
+
cm.on('cursorActivity', () => {
|
|
291
|
+
if (isEditingColor && editColorRange) {
|
|
292
|
+
const cursor = cm.getCursor(), range = editColorRange,
|
|
293
|
+
// 检查光标是否离开了正在编辑的颜色值范围
|
|
294
|
+
isOutsideRange = cursor.line < range.from.line || cursor.line > range.to.line ||
|
|
295
|
+
(cursor.line === range.from.line && cursor.ch < range.from.ch) ||
|
|
296
|
+
(cursor.line === range.to.line && cursor.ch > range.to.ch);
|
|
297
|
+
|
|
298
|
+
if (isOutsideRange) isEditingColor = false, editColorRange = null, updateColorWidgets(cm);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// ============================== 预览功能 ==============================
|
|
303
|
+
function startPreview() {
|
|
304
|
+
isPreviewMode = true, preview.style.display = 'block';
|
|
305
|
+
const er = cssEditor.getBoundingClientRect();
|
|
306
|
+
preview.style.width = `${er.width}px`, preview.style.height = `${er.height}px`;
|
|
307
|
+
previewBtn.style.display = 'none', cancelPreviewBtn.style.display = 'block';
|
|
308
|
+
previewFrame.src = returnUrl, cm.on('change', updatePreviewStyles);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function updatePreviewStyles() {
|
|
312
|
+
if (!isPreviewMode || !previewFrame.contentWindow?.document) return;
|
|
313
|
+
try {
|
|
314
|
+
const sc = cm.getValue(), id = previewFrame.contentDocument || previewFrame.contentWindow.document,
|
|
315
|
+
os = id.getElementById('dynamic-css'), s = id.createElement('style');
|
|
316
|
+
if (os) os.remove();
|
|
317
|
+
s.id = 'dynamic-css', s.textContent = sc, id.head.append(s);
|
|
318
|
+
} catch (e) { console.log('预览更新失败', e); }
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function cancelPreview() {
|
|
322
|
+
isPreviewMode = false, preview.style.display = 'none', previewBtn.style.display = 'block';
|
|
323
|
+
cancelPreviewBtn.style.display = 'none', previewFrame.src = 'about:blank', cm.off('change', updatePreviewStyles);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
addTapSupport(previewBtn, startPreview), addTapSupport(cancelPreviewBtn, cancelPreview);
|
|
327
|
+
|
|
328
|
+
// 获取储存样式并应用
|
|
329
|
+
getStyle(cssEditor, api), getStyle(preview, pApi);
|
|
330
|
+
modal.append(cssEditor, preview), loadCssContent(fileDir);
|
|
331
|
+
|
|
332
|
+
mouseOrTouch(cssEditor, () => { cssEditor.style.zIndex = '1002', preview.style.zIndex = '1001'; }, api);
|
|
333
|
+
mouseOrTouch(preview, () => { preview.style.zIndex = '1002', cssEditor.style.zIndex = '1001'; }, pApi);
|
|
334
|
+
|
|
335
|
+
addTapSupport(saveBtn, saveCSS), addTapSupport(cancelBtn, cancelEdit), addTapSupport(closeBtn, cancelEdit);
|
|
336
|
+
|
|
337
|
+
// 键盘事件
|
|
338
|
+
document.addEventListener('keydown', e => {
|
|
339
|
+
// Escape 优先处理编辑退出
|
|
340
|
+
if (e.key === 'Escape') {
|
|
341
|
+
if (isEditingColor) {
|
|
342
|
+
e.preventDefault(), isEditingColor = false, editColorRange = null, updateColorWidgets(cm);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (cssEditor.style.display === 'flex') {
|
|
346
|
+
if (isPreviewMode) cancelPreview();
|
|
347
|
+
else cancelEdit();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (e.ctrlKey && e.key === 's') e.preventDefault(), saveCSS();
|
|
351
|
+
if (e.ctrlKey && e.key === 'p') e.preventDefault(), startPreview();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// ---------- 加载CSS内容 ----------
|
|
355
|
+
async function loadCssContent(fd) {
|
|
356
|
+
showLoader();
|
|
357
|
+
try {
|
|
358
|
+
const r = await fetch(`/api/css?fileDir=${encodeURIComponent(fd)}`);
|
|
359
|
+
if (r.ok) { const ct = await r.text(); cm.setValue(ct), updateEditorTitle(fd); }
|
|
360
|
+
else throw new Error(`无法加载CSS文件:${fd}`);
|
|
361
|
+
} catch (err) { alert(`加载CSS失败:${err.message}`), updateEditorTitle(fd); }
|
|
362
|
+
finally { hideLoader(); }
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function updateEditorTitle(fd) {
|
|
366
|
+
const te = document.querySelector('#cssEditor h2');
|
|
367
|
+
if (te) te.textContent = `编辑文件:${fd}`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function saveCSS() {
|
|
371
|
+
showLoader();
|
|
372
|
+
try {
|
|
373
|
+
const r = await fetch('/api/css', {
|
|
374
|
+
method: 'POST',
|
|
375
|
+
headers: { 'Content-Type': 'application/json' },
|
|
376
|
+
body: JSON.stringify({ fileDir, content: cm.getValue() })
|
|
377
|
+
});
|
|
378
|
+
if (r.ok) window.location.href = returnUrl;
|
|
379
|
+
else { const et = await r.text(); throw new Error(et || '保存失败'); }
|
|
380
|
+
} catch (err) { alert(`保存CSS失败:${err.message}`); }
|
|
381
|
+
finally { hideLoader(); }
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function cancelEdit() { if (confirm('确定要取消编辑吗')) window.location.href = returnUrl; }
|
|
385
|
+
|
|
386
|
+
function showLoader() { if (loader) loader.style.display = 'block'; }
|
|
387
|
+
function hideLoader() { if (loader) loader.style.display = 'none'; }
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// 页面加载启动
|
|
391
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', scriptFun);
|
|
392
|
+
else scriptFun();
|