cwd-widget 0.0.1

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.
@@ -0,0 +1,406 @@
1
+ /**
2
+ * CommentForm 评论表单组件
3
+ */
4
+
5
+ import { Component } from './Component.js';
6
+ import { AdminAuthModal } from './AdminAuthModal.js';
7
+ import { auth } from '../utils/auth.js';
8
+ import { renderMarkdown } from '../utils/markdown.js';
9
+
10
+ export class CommentForm extends Component {
11
+ /**
12
+ * @param {HTMLElement|string} container - 容器元素或选择器
13
+ * @param {Object} props - 组件属性
14
+ * @param {Object} props.form - 表单数据
15
+ * @param {Object} props.formErrors - 表单错误
16
+ * @param {boolean} props.submitting - 是否正在提交
17
+ * @param {Function} props.onSubmit - 提交回调
18
+ * @param {Function} props.onFieldChange - 字段变化回调
19
+ * @param {string} props.adminEmail - 管理员邮箱
20
+ * @param {Function} props.onVerifyAdmin - 验证管理员回调 (returns Promise)
21
+ */
22
+ constructor(container, props = {}) {
23
+ super(container, props);
24
+ // 确保 localForm 的各个字段都有初始值
25
+ const initialForm = props.form || {};
26
+ this.state = {
27
+ localForm: {
28
+ name: initialForm.name || '',
29
+ email: initialForm.email || '',
30
+ url: initialForm.url || '',
31
+ content: initialForm.content || '',
32
+ },
33
+ activeTab: 'write', // 'write' | 'preview'
34
+ showPreview: false,
35
+ };
36
+ this.modal = null;
37
+ }
38
+
39
+ render() {
40
+ const { formErrors, submitting } = this.props;
41
+ const { localForm } = this.state;
42
+
43
+ const canSubmit = localForm.name.trim() && localForm.email.trim() && localForm.content.trim();
44
+ const isAdmin = this.props.adminEmail && localForm.email.trim() === this.props.adminEmail;
45
+ const isVerified = isAdmin && auth.hasToken();
46
+
47
+ const root = this.createElement('form', {
48
+ className: 'cwd-comment-form',
49
+ attributes: {
50
+ novalidate: true,
51
+ onSubmit: (e) => this.handleSubmit(e),
52
+ },
53
+ children: [
54
+ // 标题
55
+ this.createTextElement('h3', '发表评论', 'cwd-form-title'),
56
+
57
+ // 表单字段
58
+ this.createElement('div', {
59
+ className: 'cwd-form-fields',
60
+ children: [
61
+ // 第一行:昵称和邮箱
62
+ this.createElement('div', {
63
+ className: 'cwd-form-row',
64
+ children: [
65
+ // 昵称
66
+ this.createFormField('昵称 *', 'text', 'name', localForm.name, formErrors.name),
67
+ // 邮箱
68
+ this.createElement('div', {
69
+ className: 'cwd-form-field-wrapper',
70
+ children: [
71
+ this.createFormField('邮箱 *', 'email', 'email', localForm.email, formErrors.email),
72
+ isVerified
73
+ ? this.createElement('div', {
74
+ className: 'cwd-admin-controls',
75
+ children: [
76
+ this.createElement('button', {
77
+ className: 'cwd-btn-text',
78
+ text: '退出验证',
79
+ attributes: {
80
+ type: 'button',
81
+ title: '清除管理员凭证',
82
+ onClick: () => {
83
+ auth.clearToken();
84
+ this.render();
85
+ },
86
+ },
87
+ }),
88
+ ],
89
+ })
90
+ : null,
91
+ ],
92
+ }),
93
+ // 网址
94
+ this.createFormField('网址', 'url', 'url', localForm.url, formErrors.url),
95
+ ],
96
+ }),
97
+
98
+ // 评论内容
99
+ this.createElement('div', {
100
+ className: 'cwd-form-field',
101
+ children: [
102
+ this.createTextElement('label', '写下你的评论...', 'cwd-form-label'),
103
+ this.createElement('textarea', {
104
+ className: `cwd-form-textarea ${formErrors.content ? 'cwd-input-error' : ''}`,
105
+ attributes: {
106
+ rows: 4,
107
+ disabled: submitting,
108
+ onInput: (e) => this.handleFieldChange('content', e.target.value),
109
+ },
110
+ }),
111
+ ...(formErrors.content ? [this.createTextElement('span', formErrors.content, 'cwd-error-text')] : []),
112
+ ],
113
+ }),
114
+ ],
115
+ }),
116
+
117
+ // 操作按钮
118
+ this.createElement('div', {
119
+ className: 'cwd-form-actions',
120
+ children: [
121
+ this.createElement('button', {
122
+ className: `cwd-btn cwd-btn-secondary cwd-btn-preview ${this.state.showPreview ? 'cwd-btn-active' : ''}`,
123
+ attributes: {
124
+ type: 'button',
125
+ disabled: submitting || !localForm.content?.trim(),
126
+ style: localForm.content?.trim() ? '' : 'display:none;',
127
+ onClick: () => this.togglePreview(),
128
+ },
129
+ text: this.state.showPreview ? '关闭' : '预览',
130
+ }),
131
+ this.createElement('button', {
132
+ className: 'cwd-btn cwd-btn-primary',
133
+ attributes: {
134
+ type: 'submit',
135
+ disabled: submitting || !canSubmit,
136
+ },
137
+ text: submitting ? '提交中...' : '提交评论',
138
+ }),
139
+ ],
140
+ }),
141
+
142
+ // 预览区域
143
+ ...(this.state.showPreview && localForm.content
144
+ ? [
145
+ this.createElement('div', {
146
+ className: 'cwd-preview-container',
147
+ children: [
148
+ this.createElement('div', {
149
+ className: 'cwd-preview-content cwd-comment-content',
150
+ // 直接设置 innerHTML
151
+ html: renderMarkdown(localForm.content),
152
+ }),
153
+ ],
154
+ }),
155
+ ]
156
+ : []),
157
+ ],
158
+ });
159
+
160
+ // 设置输入框的值
161
+ this.setInputValues(root, localForm);
162
+
163
+ this.elements.root = root;
164
+ this.empty(this.container);
165
+ this.container.appendChild(root);
166
+ }
167
+
168
+ updateProps(prevProps) {
169
+ // 只在非提交状态时同步表单数据(避免覆盖用户正在输入的内容)
170
+ if (!this.props.submitting && this.props.form !== prevProps.form) {
171
+ // 保留当前正在输入的内容
172
+ const currentName = this.state.localForm.name || '';
173
+ const currentEmail = this.state.localForm.email || '';
174
+ const currentUrl = this.state.localForm.url || '';
175
+ const currentContent = this.state.localForm.content || '';
176
+
177
+ this.state.localForm = {
178
+ name: this.props.form.name || currentName,
179
+ email: this.props.form.email || currentEmail,
180
+ url: this.props.form.url || currentUrl,
181
+ content: this.props.form.content !== undefined ? this.props.form.content : currentContent,
182
+ };
183
+
184
+ // 同步更新 DOM 值(不重新渲染)
185
+ if (this.elements.root) {
186
+ this.setInputValues(this.elements.root, this.state.localForm);
187
+ }
188
+ }
189
+
190
+ // 更新提交按钮状态和错误提示
191
+ if (this.elements.root) {
192
+ this.updateFormState();
193
+ }
194
+ }
195
+
196
+ /**
197
+ * 更新表单状态(按钮、错误提示等)
198
+ */
199
+ updateFormState() {
200
+ const { formErrors, submitting } = this.props;
201
+ const { localForm } = this.state;
202
+
203
+ const canSubmit = localForm.name.trim() && localForm.email.trim() && localForm.content.trim();
204
+
205
+ // 更新提交按钮状态
206
+ const submitBtn = this.elements.root.querySelector('button[type="submit"]');
207
+ if (submitBtn) {
208
+ submitBtn.disabled = submitting || !canSubmit;
209
+ submitBtn.textContent = submitting ? '提交中...' : '提交评论';
210
+ }
211
+
212
+ // 更新预览按钮状态
213
+ const previewBtn = this.elements.root.querySelector('.cwd-btn-preview');
214
+ if (previewBtn) {
215
+ const hasContent = !!localForm.content?.trim();
216
+ previewBtn.disabled = submitting || !hasContent;
217
+ previewBtn.style.display = hasContent ? '' : 'none';
218
+ if (!hasContent) {
219
+ this.state.showPreview = false;
220
+ const previewContainer = this.elements.root.querySelector('.cwd-preview-container');
221
+ if (previewContainer) {
222
+ previewContainer.remove();
223
+ }
224
+ previewBtn.textContent = '预览';
225
+ } else {
226
+ previewBtn.textContent = this.state.showPreview ? '关闭' : '预览';
227
+ }
228
+ }
229
+
230
+ // 更新输入框禁用状态
231
+ const inputs = this.elements.root.querySelectorAll('input, textarea');
232
+ inputs.forEach((input) => {
233
+ input.disabled = submitting;
234
+ });
235
+
236
+ // 更新错误提示
237
+ this.updateErrors(formErrors);
238
+ }
239
+
240
+ /**
241
+ * 更新错误提示
242
+ */
243
+ updateErrors(formErrors) {
244
+ if (!this.elements.root) return;
245
+
246
+ const nameInput = this.elements.root.querySelector('input[name="name"]');
247
+ this.updateFieldError(nameInput, formErrors?.name);
248
+
249
+ const emailInput = this.elements.root.querySelector('input[name="email"]');
250
+ this.updateFieldError(emailInput, formErrors?.email);
251
+
252
+ const urlInput = this.elements.root.querySelector('input[name="url"]');
253
+ this.updateFieldError(urlInput, formErrors?.url);
254
+
255
+ const contentTextarea = this.elements.root.querySelector('textarea');
256
+ this.updateFieldError(contentTextarea, formErrors?.content);
257
+ }
258
+
259
+ /**
260
+ * 更新单个字段的错误状态
261
+ */
262
+ updateFieldError(element, error) {
263
+ if (!element) return;
264
+
265
+ // 移除或添加错误样式
266
+ if (error) {
267
+ element.classList.add('cwd-input-error');
268
+ } else {
269
+ element.classList.remove('cwd-input-error');
270
+ }
271
+
272
+ // 查找并更新/移除错误提示元素
273
+ const parent = element.parentElement;
274
+ let errorSpan = parent.querySelector('.cwd-error-text');
275
+ if (error) {
276
+ if (!errorSpan) {
277
+ errorSpan = document.createElement('span');
278
+ errorSpan.className = 'cwd-error-text';
279
+ parent.appendChild(errorSpan);
280
+ }
281
+ errorSpan.textContent = error;
282
+ } else if (errorSpan) {
283
+ errorSpan.remove();
284
+ }
285
+ }
286
+
287
+ /**
288
+ * 创建表单字段
289
+ */
290
+ createFormField(label, type, fieldName, value, error, placeholder = '') {
291
+ return this.createElement('div', {
292
+ className: 'cwd-form-field',
293
+ children: [
294
+ this.createTextElement('label', label, 'cwd-form-label'),
295
+ this.createElement('input', {
296
+ className: `cwd-form-input ${error ? 'cwd-input-error' : ''}`,
297
+ attributes: {
298
+ type,
299
+ name: fieldName,
300
+ value: value || '',
301
+ disabled: this.props.submitting,
302
+ onInput: (e) => this.handleFieldChange(fieldName, e.target.value),
303
+ onBlur: (e) => {
304
+ if (fieldName === 'email') this.handleEmailBlur(e.target.value);
305
+ },
306
+ },
307
+ }),
308
+ ...(error ? [this.createTextElement('span', error, 'cwd-error-text')] : []),
309
+ ],
310
+ });
311
+ }
312
+
313
+ /**
314
+ * 设置输入框的值
315
+ */
316
+ setInputValues(root, form) {
317
+ const nameInput = root.querySelector('input[name="name"]');
318
+ const emailInput = root.querySelector('input[name="email"]');
319
+ const urlInput = root.querySelector('input[name="url"]');
320
+ const contentTextarea = root.querySelector('textarea');
321
+
322
+ if (nameInput) nameInput.value = form.name || '';
323
+ if (emailInput) emailInput.value = form.email || '';
324
+ if (urlInput) urlInput.value = form.url || '';
325
+ if (contentTextarea) contentTextarea.value = form.content || '';
326
+ }
327
+
328
+ togglePreview() {
329
+ this.state.showPreview = !this.state.showPreview;
330
+ this.render();
331
+ }
332
+
333
+ handleFieldChange(field, value) {
334
+ this.state.localForm[field] = value;
335
+ if (this.props.onFieldChange) {
336
+ this.props.onFieldChange(field, value);
337
+ }
338
+ // 实时更新按钮状态
339
+ if (this.elements.root) {
340
+ this.updateFormState();
341
+ // 实时更新预览内容
342
+ if (field === 'content' && this.state.showPreview) {
343
+ this.updatePreviewContent(value);
344
+ }
345
+ }
346
+ }
347
+
348
+ updatePreviewContent(content) {
349
+ const previewContent = this.elements.root.querySelector('.cwd-preview-content');
350
+ if (previewContent) {
351
+ previewContent.innerHTML = renderMarkdown(content);
352
+ }
353
+ }
354
+
355
+ handleSubmit(e) {
356
+ e.preventDefault();
357
+ const email = this.state.localForm.email?.trim();
358
+ const adminEmail = this.props.adminEmail;
359
+ if (adminEmail && email && email === adminEmail && !auth.hasToken()) {
360
+ this.showAuthModal();
361
+ return;
362
+ }
363
+ if (this.props.onSubmit) {
364
+ this.props.onSubmit(this.state.localForm);
365
+ }
366
+ }
367
+
368
+ async handleEmailBlur(email) {
369
+ if (!email || !this.props.adminEmail) return;
370
+ if (email.trim() === this.props.adminEmail) {
371
+ // Check local storage
372
+ if (auth.hasToken()) {
373
+ // Already valid
374
+ return;
375
+ }
376
+ // Show modal
377
+ this.showAuthModal();
378
+ }
379
+ }
380
+
381
+ showAuthModal() {
382
+ // Create modal container if not exists
383
+ let modalContainer = this.elements.root.querySelector('.cwd-modal-container');
384
+ if (!modalContainer) {
385
+ modalContainer = document.createElement('div');
386
+ modalContainer.className = 'cwd-modal-container';
387
+ this.elements.root.appendChild(modalContainer);
388
+ }
389
+
390
+ this.modal = new AdminAuthModal(modalContainer, {
391
+ onCancel: () => {
392
+ this.modal.destroy();
393
+ this.modal = null;
394
+ },
395
+ onSubmit: async (key) => {
396
+ if (this.props.onVerifyAdmin) {
397
+ await this.props.onVerifyAdmin(key);
398
+ auth.saveToken(key);
399
+ this.modal.destroy();
400
+ this.modal = null;
401
+ }
402
+ },
403
+ });
404
+ this.modal.render();
405
+ }
406
+ }