|
@@ -70,38 +70,39 @@ Quill.register(SizeAttributor, true);
|
|
|
Quill.register(Quill.import('attributors/style/align') as any, true);
|
|
Quill.register(Quill.import('attributors/style/align') as any, true);
|
|
|
Quill.register(Quill.import('attributors/style/direction') as any, true);
|
|
Quill.register(Quill.import('attributors/style/direction') as any, true);
|
|
|
|
|
|
|
|
-// 缩进改为"行首不间断空格"(首行缩进):曾用内联padding-left,但APP端渲染器把
|
|
|
|
|
-// padding-left作用到整段(所有行整体平移),不符合中文首行缩进观感。
|
|
|
|
|
-// U+00A0任何渲染器都不折叠、且只影响首行,该方案APP端零改动。
|
|
|
|
|
-// 缩进按钮不再走Quill的indent格式(会输出ql-indent class),实现见toolbar handlers.indent
|
|
|
|
|
-const INDENT_SPACES = 2; // 每级缩进的不间断空格数(即首行缩进2字符)
|
|
|
|
|
-const INDENT_MAX = 8; // 缩进层级上限
|
|
|
|
|
-const NBSP = '\u00a0';
|
|
|
|
|
-
|
|
|
|
|
-// 增加/减少缩进:对选区每一行行首插入或移除一级不间断空格(首行缩进,APP端原样显示)
|
|
|
|
|
-function applyIndent(direction: string) {
|
|
|
|
|
- const quill = toRaw(quillEditorRef.value)?.getQuill();
|
|
|
|
|
- if (!quill) return;
|
|
|
|
|
- const range = quill.getSelection();
|
|
|
|
|
- if (!range) return;
|
|
|
|
|
- const lines = quill.getLines(range);
|
|
|
|
|
- if (!lines || !lines.length) return;
|
|
|
|
|
- // 从后往前处理:前行插入/删除文本会让后行的下标移位
|
|
|
|
|
- for (let i = lines.length - 1; i >= 0; i--) {
|
|
|
|
|
- const idx = (lines[i] as any).offset(quill.scroll);
|
|
|
|
|
- const head = quill.getText(idx, INDENT_MAX * INDENT_SPACES);
|
|
|
|
|
- let level = 0;
|
|
|
|
|
- while (level < INDENT_MAX && head.startsWith(NBSP.repeat((level + 1) * INDENT_SPACES))) level++;
|
|
|
|
|
- if (direction === '+1') {
|
|
|
|
|
- if (level < INDENT_MAX) quill.insertText(idx, NBSP.repeat(INDENT_SPACES), 'user');
|
|
|
|
|
- } else if (level > 0) {
|
|
|
|
|
- quill.deleteText(idx, INDENT_SPACES, 'user');
|
|
|
|
|
- } else if (head.startsWith(NBSP)) {
|
|
|
|
|
- // 行首是单个不间断空格(手工敲的)时也允许退掉
|
|
|
|
|
- quill.deleteText(idx, 1, 'user');
|
|
|
|
|
|
|
+// 缩进回退为内联padding-left(整段平移):字符式(行首U+00A0)首行缩进会被APP端渲染组件
|
|
|
|
|
+// 裁剪文本节点首尾空白时裁掉,到不了屏幕;padding-left是APP唯一能渲染的缩进样式,
|
|
|
|
|
+// 且编辑器显示与APP完全一致,两端所见即所得。
|
|
|
|
|
+// Quill默认缩进输出class(ql-indent-N),这里用自定义attributor把缩进层级承载为padding-left
|
|
|
|
|
+const INDENT_EM = 2; // 每级缩进对应的em数
|
|
|
|
|
+const INDENT_MAX = 8; // 与Quill默认缩进层级上限保持一致
|
|
|
|
|
+const StyleAttributorBase: any = (Quill.import('parchment') as any).StyleAttributor;
|
|
|
|
|
+class IndentStyleAttributor extends StyleAttributorBase {
|
|
|
|
|
+ add(node: HTMLElement, value: any) {
|
|
|
|
|
+ // 工具栏按钮和Tab键传的是'+1'/'-1',先换算成绝对层级
|
|
|
|
|
+ if (value === '+1' || value === '-1') {
|
|
|
|
|
+ const cur = this.value(node) || 0;
|
|
|
|
|
+ value = value === '+1' ? cur + 1 : cur - 1;
|
|
|
}
|
|
}
|
|
|
|
|
+ const level = Math.min(Math.max(parseInt(value, 10) || 0, 0), INDENT_MAX);
|
|
|
|
|
+ if (level <= 0) return this.remove(node);
|
|
|
|
|
+ node.style.paddingLeft = `${level * INDENT_EM}em`;
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ value(node: HTMLElement) {
|
|
|
|
|
+ const raw = node.style ? node.style.paddingLeft : '';
|
|
|
|
|
+ if (!raw) return undefined;
|
|
|
|
|
+ const n = parseFloat(raw);
|
|
|
|
|
+ if (!n || n <= 0) return undefined;
|
|
|
|
|
+ const level = Math.round(raw.indexOf('px') >= 0 ? n / (INDENT_EM * 16) : n / INDENT_EM);
|
|
|
|
|
+ return level > 0 ? level : undefined;
|
|
|
|
|
+ }
|
|
|
|
|
+ remove(node: HTMLElement) {
|
|
|
|
|
+ node.style.paddingLeft = '';
|
|
|
|
|
+ return true;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+Quill.register(new IndentStyleAttributor('indent', 'padding-left', { scope: (Quill.import('parchment') as any).Scope.BLOCK }), true);
|
|
|
|
|
|
|
|
// 视频用 <video> 标签插入:Quill默认video格式是iframe嵌入,APP端渲染器对iframe兼容差;
|
|
// 视频用 <video> 标签插入:Quill默认video格式是iframe嵌入,APP端渲染器对iframe兼容差;
|
|
|
// video标签在WebView原生可播、flutter_html等组件渲染也可扩展支持,兼容性严格优于iframe
|
|
// video标签在WebView原生可播、flutter_html等组件渲染也可扩展支持,兼容性严格优于iframe
|
|
@@ -158,8 +159,8 @@ const TOOLBAR_TIPS: [string, string][] = [
|
|
|
['button.ql-code-block', '代码块:选中内容设为代码块'],
|
|
['button.ql-code-block', '代码块:选中内容设为代码块'],
|
|
|
['button.ql-list[value="ordered"]', '有序列表:选中内容加数字编号'],
|
|
['button.ql-list[value="ordered"]', '有序列表:选中内容加数字编号'],
|
|
|
['button.ql-list[value="bullet"]', '无序列表:选中内容加圆点符号'],
|
|
['button.ql-list[value="bullet"]', '无序列表:选中内容加圆点符号'],
|
|
|
- ['button.ql-indent[value="-1"]', '减少缩进:减少段落首行缩进(APP端同步生效)'],
|
|
|
|
|
- ['button.ql-indent[value="+1"]', '增加缩进:段落首行缩进2字符(APP端同步生效)'],
|
|
|
|
|
|
|
+ ['button.ql-indent[value="-1"]', '减少缩进:降低段落缩进层级(APP端同步生效)'],
|
|
|
|
|
+ ['button.ql-indent[value="+1"]', '增加缩进:提高段落缩进层级(APP端同步生效)'],
|
|
|
['.ql-picker.ql-size .ql-picker-label', '字体大小:设置选中文字的字号'],
|
|
['.ql-picker.ql-size .ql-picker-label', '字体大小:设置选中文字的字号'],
|
|
|
['.ql-picker.ql-header .ql-picker-label', '标题:切换段落标题级别'],
|
|
['.ql-picker.ql-header .ql-picker-label', '标题:切换段落标题级别'],
|
|
|
['.ql-picker.ql-color .ql-picker-label', '字体颜色:设置选中文字的颜色'],
|
|
['.ql-picker.ql-color .ql-picker-label', '字体颜色:设置选中文字的颜色'],
|
|
@@ -315,10 +316,6 @@ const options = ref<any>({
|
|
|
videoDialog.uploadedName = '';
|
|
videoDialog.uploadedName = '';
|
|
|
videoDialog.mode = 'url';
|
|
videoDialog.mode = 'url';
|
|
|
videoDialog.visible = true;
|
|
videoDialog.visible = true;
|
|
|
- },
|
|
|
|
|
- // 缩进按钮:行首不间断空格实现首行缩进,不用Quill的indent格式(会输出ql-indent class,APP不认识)
|
|
|
|
|
- indent: (value: string) => {
|
|
|
|
|
- applyIndent(value);
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
@@ -350,13 +347,13 @@ watch(
|
|
|
() => props.modelValue,
|
|
() => props.modelValue,
|
|
|
(v: string) => {
|
|
(v: string) => {
|
|
|
if (v === lastEmitted) return;
|
|
if (v === lastEmitted) return;
|
|
|
- // 旧数据缩进存的是Quill class(ql-indent-N)或上一版内联padding-left,统一转成行首不间断空格(首行缩进);
|
|
|
|
|
- // 再把手工敲的空格/Tab统一成不间断空格,保证不编辑直接保存也能在APP生效。
|
|
|
|
|
|
|
+ // 旧数据缩进可能是Quill class(ql-indent-N)或上一版行首不间断空格,统一归一到padding-left内联样式;
|
|
|
|
|
+ // 再把手工敲的行内空格统一成不间断空格,保证不编辑直接保存也能在APP生效。
|
|
|
// 转换在watch渲染期同步执行,一旦对畸形/超大内容抛错会连累整个Editor挂载失败、
|
|
// 转换在watch渲染期同步执行,一旦对畸形/超大内容抛错会连累整个Editor挂载失败、
|
|
|
// 弹窗打不开(表现为点"修改"没反应),所以异常时降级为原始内容,绝不阻断渲染
|
|
// 弹窗打不开(表现为点"修改"没反应),所以异常时降级为原始内容,绝不阻断渲染
|
|
|
let next: string;
|
|
let next: string;
|
|
|
try {
|
|
try {
|
|
|
- next = stabilizeSpacesHtml(migrateIndentHtml(v || ''));
|
|
|
|
|
|
|
+ next = spacesToPaddingHtml(stabilizeSpacesHtml(migrateIndentHtml(v || '')));
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
console.error('[Editor] 内容转换失败,降级为原始内容:', e);
|
|
console.error('[Editor] 内容转换失败,降级为原始内容:', e);
|
|
|
next = v || '';
|
|
next = v || '';
|
|
@@ -558,11 +555,47 @@ function stabilizeSpacesHtml(html: string): string {
|
|
|
return parts.join('');
|
|
return parts.join('');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 行首空白缩进归一到padding-left:APP端渲染组件会裁掉文本节点首尾空白(U+00A0也被裁),
|
|
|
|
|
+// 字符式缩进到不了屏幕;块级标签行首的连续空白(2字符=1级)在这里并入padding-left内联样式,
|
|
|
|
|
+// 与缩进按钮同一载体,APP端才能渲染出来
|
|
|
|
|
+const LEAD_RUN_RE = /(<(?:p|div|h[1-6]|li|blockquote|pre)\b[^>]*>(?:<(?!br\b|img\b|video\b|\/)[^>]+>)*)([ \t\u00a0]{2,})/gi;
|
|
|
|
|
+function spacesToPaddingHtml(html: string): string {
|
|
|
|
|
+ if (!html || html.indexOf('>') < 0) return html;
|
|
|
|
|
+ return html.replace(LEAD_RUN_RE, (m: string, head: string, run: string) => {
|
|
|
|
|
+ const level = Math.floor(run.length / 2);
|
|
|
|
|
+ if (level < 1) return m;
|
|
|
|
|
+ return addPaddingLevel(head, level);
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 给块级开标签叠加padding-left层级:已有padding-left时累加,上限INDENT_MAX
|
|
|
|
|
+function addPaddingLevel(tag: string, addLevel: number): string {
|
|
|
|
|
+ const sm = /\sstyle\s*=\s*"([^"]*)"/i.exec(tag) || /\sstyle\s*=\s*'([^']*)'/i.exec(tag);
|
|
|
|
|
+ let level = addLevel;
|
|
|
|
|
+ if (sm) {
|
|
|
|
|
+ const pm = /padding-left\s*:\s*([^;"]+)/i.exec(sm[1]);
|
|
|
|
|
+ if (pm) {
|
|
|
|
|
+ const raw = pm[1].trim();
|
|
|
|
|
+ const n = parseFloat(raw);
|
|
|
|
|
+ if (n > 0) level += Math.round(raw.toLowerCase().indexOf('px') >= 0 ? n / (INDENT_EM * 16) : n / INDENT_EM);
|
|
|
|
|
+ }
|
|
|
|
|
+ level = Math.min(level, INDENT_MAX);
|
|
|
|
|
+ const rest = sm[1]
|
|
|
|
|
+ .replace(/padding-left\s*:\s*[^;"]+;?/gi, '')
|
|
|
|
|
+ .replace(/\s+/g, ' ')
|
|
|
|
|
+ .replace(/^;+/, '')
|
|
|
|
|
+ .trim();
|
|
|
|
|
+ return tag.replace(sm[0], ` style="padding-left: ${level * INDENT_EM}em;${rest ? ` ${rest}` : ''}"`);
|
|
|
|
|
+ }
|
|
|
|
|
+ level = Math.min(level, INDENT_MAX);
|
|
|
|
|
+ return tag.slice(0, -1) + ` style="padding-left: ${level * INDENT_EM}em;">`;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// 内容变化统一出口:先做"空格缩进APP化"转换再发给父组件
|
|
// 内容变化统一出口:先做"空格缩进APP化"转换再发给父组件
|
|
|
function handleTextChange() {
|
|
function handleTextChange() {
|
|
|
// 同watch:转换抛错时降级为原始内容,保证编辑/保存链路不因转换异常中断
|
|
// 同watch:转换抛错时降级为原始内容,保证编辑/保存链路不因转换异常中断
|
|
|
try {
|
|
try {
|
|
|
- lastEmitted = stabilizeSpacesHtml(content.value);
|
|
|
|
|
|
|
+ lastEmitted = spacesToPaddingHtml(stabilizeSpacesHtml(content.value));
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
console.error('[Editor] 内容转换失败,降级为原始内容:', e);
|
|
console.error('[Editor] 内容转换失败,降级为原始内容:', e);
|
|
|
lastEmitted = content.value;
|
|
lastEmitted = content.value;
|
|
@@ -570,50 +603,34 @@ function handleTextChange() {
|
|
|
emit('update:modelValue', lastEmitted);
|
|
emit('update:modelValue', lastEmitted);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 历史数据兼容:旧缩进(Quill class ql-indent-N / 上一版内联padding-left)转成行首不间断空格;
|
|
|
|
|
-// 幂等:转换后标记被移除,再次进入不会重复叠加缩进
|
|
|
|
|
|
|
+// 历史数据兼容:旧版缩进是Quill class(ql-indent-N),加载时转成内联padding-left;
|
|
|
|
|
+// 上一版行首不间断空格式缩进由spacesToPaddingHtml归一
|
|
|
function migrateIndentHtml(html: string): string {
|
|
function migrateIndentHtml(html: string): string {
|
|
|
- if (!html || (html.indexOf('ql-indent-') < 0 && html.toLowerCase().indexOf('padding-left') < 0)) return html;
|
|
|
|
|
|
|
+ if (!html || html.indexOf('ql-indent-') < 0) return html;
|
|
|
return html.replace(/<([a-zA-Z][\w-]*)((?:[^>"']|"[^"]*"|'[^']*')*)>/g, (tag: string, name: string, attrs: string) => {
|
|
return html.replace(/<([a-zA-Z][\w-]*)((?:[^>"']|"[^"]*"|'[^']*')*)>/g, (tag: string, name: string, attrs: string) => {
|
|
|
- if (!/^(?:p|div|h[1-6]|li|blockquote|pre)$/i.test(name)) return tag;
|
|
|
|
|
- let level = 0;
|
|
|
|
|
- let newAttrs = attrs;
|
|
|
|
|
const cm = /\sclass\s*=\s*"([^"]*)"/i.exec(attrs) || /\sclass\s*=\s*'([^']*)'/i.exec(attrs);
|
|
const cm = /\sclass\s*=\s*"([^"]*)"/i.exec(attrs) || /\sclass\s*=\s*'([^']*)'/i.exec(attrs);
|
|
|
- if (cm) {
|
|
|
|
|
- const im = /ql-indent-(\d+)/.exec(cm[1]);
|
|
|
|
|
- if (im) {
|
|
|
|
|
- level = Math.min(parseInt(im[1], 10) || 0, INDENT_MAX);
|
|
|
|
|
- const restClass = cm[1]
|
|
|
|
|
- .replace(/ql-indent-\d+/g, '')
|
|
|
|
|
- .replace(/\s+/g, ' ')
|
|
|
|
|
- .trim();
|
|
|
|
|
- newAttrs = newAttrs.replace(cm[0], restClass ? ` class="${restClass}"` : '');
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- const sm = /\sstyle\s*=\s*"([^"]*)"/i.exec(newAttrs) || /\sstyle\s*=\s*'([^']*)'/i.exec(newAttrs);
|
|
|
|
|
|
|
+ if (!cm) return tag;
|
|
|
|
|
+ const im = /ql-indent-(\d+)/.exec(cm[1]);
|
|
|
|
|
+ if (!im) return tag;
|
|
|
|
|
+ const level = Math.min(parseInt(im[1], 10) || 0, INDENT_MAX);
|
|
|
|
|
+ if (level < 1) return tag;
|
|
|
|
|
+ const restClass = cm[1]
|
|
|
|
|
+ .replace(/ql-indent-\d+/g, '')
|
|
|
|
|
+ .replace(/\s+/g, ' ')
|
|
|
|
|
+ .trim();
|
|
|
|
|
+ let newAttrs = attrs.replace(cm[0], restClass ? ` class="${restClass}"` : '');
|
|
|
|
|
+ const pad = `padding-left: ${level * INDENT_EM}em;`;
|
|
|
|
|
+ const sm = /\sstyle\s*=\s*"([^"]*)"/i.exec(newAttrs);
|
|
|
if (sm) {
|
|
if (sm) {
|
|
|
- const pm = /padding-left\s*:\s*([^;"]+)/i.exec(sm[1]);
|
|
|
|
|
- if (pm) {
|
|
|
|
|
- const raw = pm[1].trim();
|
|
|
|
|
- const n = parseFloat(raw);
|
|
|
|
|
- if (n > 0) {
|
|
|
|
|
- const lv = Math.round(raw.toLowerCase().indexOf('px') >= 0 ? n / (INDENT_SPACES * 16) : n / INDENT_SPACES);
|
|
|
|
|
- level = Math.max(level, Math.min(lv, INDENT_MAX));
|
|
|
|
|
- }
|
|
|
|
|
- const restStyle = sm[1]
|
|
|
|
|
- .replace(/padding-left\s*:\s*[^;"]+;?/gi, '')
|
|
|
|
|
- .replace(/\s+/g, ' ')
|
|
|
|
|
- .replace(/^;+/, '')
|
|
|
|
|
- .trim();
|
|
|
|
|
- newAttrs = newAttrs.replace(sm[0], restStyle ? ` style="${restStyle}"` : '');
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ newAttrs = newAttrs.replace(sm[0], ` style="${pad}${sm[1]}"`);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ newAttrs += ` style="${pad}"`;
|
|
|
}
|
|
}
|
|
|
- if (level < 1) return tag;
|
|
|
|
|
- return `<${name}${newAttrs}>` + NBSP.repeat(level * INDENT_SPACES);
|
|
|
|
|
|
|
+ return `<${name}${newAttrs}>`;
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 读缩进层级:兼容内联padding-left与旧版ql-indent-N class(粘贴内容用,转成行首不间断空格)
|
|
|
|
|
|
|
+// 读缩进层级:兼容内联padding-left与旧版ql-indent-N class(粘贴内容用)
|
|
|
function readIndentLevel(el: HTMLElement): number {
|
|
function readIndentLevel(el: HTMLElement): number {
|
|
|
let level = 0;
|
|
let level = 0;
|
|
|
const cm = /ql-indent-(\d+)/.exec(el.getAttribute('class') || '');
|
|
const cm = /ql-indent-(\d+)/.exec(el.getAttribute('class') || '');
|
|
@@ -621,18 +638,20 @@ function readIndentLevel(el: HTMLElement): number {
|
|
|
const pl = el.style.paddingLeft;
|
|
const pl = el.style.paddingLeft;
|
|
|
if (pl) {
|
|
if (pl) {
|
|
|
const n = parseFloat(pl);
|
|
const n = parseFloat(pl);
|
|
|
- if (n > 0) level = Math.max(level, Math.round(pl.indexOf('px') >= 0 ? n / (INDENT_SPACES * 16) : n / INDENT_SPACES));
|
|
|
|
|
|
|
+ if (n > 0) level = Math.max(level, Math.round(pl.indexOf('px') >= 0 ? n / (INDENT_EM * 16) : n / INDENT_EM));
|
|
|
}
|
|
}
|
|
|
return level;
|
|
return level;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 插入一个带块级格式的换行符:Quill的对齐/标题/列表都是"行"格式,只认换行符上的属性
|
|
|
|
|
|
|
+// 插入一个带块级格式的换行符:Quill的缩进/对齐/标题/列表都是"行"格式,只认换行符上的属性
|
|
|
function insertBlockBreak(delta: Delta, el: HTMLElement, listType: 'ordered' | 'bullet' | null) {
|
|
function insertBlockBreak(delta: Delta, el: HTMLElement, listType: 'ordered' | 'bullet' | null) {
|
|
|
const fmt: { [key: string]: any } = {};
|
|
const fmt: { [key: string]: any } = {};
|
|
|
const hm = /^h([1-6])$/.exec(el.tagName.toLowerCase());
|
|
const hm = /^h([1-6])$/.exec(el.tagName.toLowerCase());
|
|
|
if (hm) fmt.header = parseInt(hm[1], 10);
|
|
if (hm) fmt.header = parseInt(hm[1], 10);
|
|
|
if (el.style.textAlign) fmt.align = el.style.textAlign;
|
|
if (el.style.textAlign) fmt.align = el.style.textAlign;
|
|
|
if (listType) fmt.list = listType;
|
|
if (listType) fmt.list = listType;
|
|
|
|
|
+ const level = readIndentLevel(el);
|
|
|
|
|
+ if (level > 0) fmt.indent = level;
|
|
|
delta.insert('\n', Object.keys(fmt).length ? fmt : undefined);
|
|
delta.insert('\n', Object.keys(fmt).length ? fmt : undefined);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -704,14 +723,6 @@ function buildDeltaWithListSupport(
|
|
|
// 是否是块级元素(需要换行)
|
|
// 是否是块级元素(需要换行)
|
|
|
const isBlock = ['p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'br'].includes(tagName);
|
|
const isBlock = ['p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'br'].includes(tagName);
|
|
|
|
|
|
|
|
- // 粘贴内容里的旧缩进(padding-left/ql-indent class)转成行首不间断空格,与编辑器缩进按钮同一方案
|
|
|
|
|
- if (isBlock && tagName !== 'br') {
|
|
|
|
|
- const indentLevel = readIndentLevel(el);
|
|
|
|
|
- if (indentLevel > 0) {
|
|
|
|
|
- delta.insert(NBSP.repeat(indentLevel * INDENT_SPACES));
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
// 遍历子节点
|
|
// 遍历子节点
|
|
|
for (let i = 0; i < el.childNodes.length; i++) {
|
|
for (let i = 0; i < el.childNodes.length; i++) {
|
|
|
const child = el.childNodes[i];
|
|
const child = el.childNodes[i];
|