AI應(yīng)用越來(lái)越多,為了實(shí)現(xiàn)更好地實(shí)現(xiàn)打字機(jī)效果,尤其是輸出對(duì)象為富文本的場(chǎng)景,特模仿element-plus-x的typewriter的能力,實(shí)現(xiàn)相關(guān)功能,其中forCKEditor5.ts 是針對(duì)輸出對(duì)象為CKEditor5富文本的情景,forDom.ts是針對(duì)輸出對(duì)象為普通元素(如div)的情景。
特點(diǎn):
1、兩種方案基于typescript實(shí)現(xiàn),可用于react、vue等框架中。
2、屬性有:
- container:定義輸出對(duì)象,在forDom.ts中是html元素,如某個(gè)div元素;在forCKEditor5.ts中是
CKEditor對(duì)象 - mode:定義輸入類型,支持純文本(text)、markdwon和html字符串
- interval:每次打字的間隔時(shí)間 單位( ms )
- step:每次打字吐多少字符
- cursor:
僅forDomts支持,定義末尾光標(biāo),默認(rèn)為"|" - doneMarker和doneMarker:doneMarker用于定義結(jié)束符(默認(rèn)為[DONE]),當(dāng)檢測(cè)到結(jié)束符后,聽(tīng)停止輸出并隱藏光標(biāo),detectDoneMarker定義是否開(kāi)啟檢測(cè)
3、方法有:
- append:核心防范,用于追加內(nèi)容,支持鏈?zhǔn)讲僮?/li>
- pause:暫停輸出
- resume: 恢復(fù)輸出
- stop:停止打字
- destroy:銷(xiāo)毀操作
- clear:清空內(nèi)容
- showCursor/hideCursor:
僅forDomts支持,顯示/隱藏末尾光標(biāo)(cursor定義末尾光標(biāo)) - onComplete:
僅forCKEditor5.ts支持,用于傳遞回調(diào),在完成輸出時(shí)調(diào)用
4、簡(jiǎn)單示例
// forCKEditor5.ts的使用示例
const typewriter = new CKEditorTypewriterEngine({
editor, // 某CKEditor5實(shí)例
mode: 'html',
interval: 40,
step: 2,
detectDoneMarker: true,
doneMarker: '[done]'
});
typewriter.append(content).append('[done]');
typewriter.onComplete(() => {
// 處理一些邏輯
})
// forDom.ts的使用示例
const tw = new TypewriterEngine({
container: typeWiriterRef.current!,
mode: 'markdown',
interval: 40,
step: 2,
cursor: '|',
detectDoneMarker: true,
doneMarker: '[done]'
});
const timeout = setTimeout(() => {
tw.append('# 關(guān)于中國(guó)蘋(píng)果產(chǎn)業(yè)發(fā)展的調(diào)研報(bào)告\n\n**報(bào)送單位:農(nóng)業(yè)農(nóng)村部發(fā)展規(guī)劃司** \n**報(bào)送時(shí)間:xxxx年xx月xx日**\n\n為全面掌握我國(guó)蘋(píng)果產(chǎn)業(yè)發(fā)展現(xiàn)狀,深入分析存在問(wèn)題與發(fā)展趨勢(shì),進(jìn)一步優(yōu)化產(chǎn)業(yè)布局、提升產(chǎn)品質(zhì)量、增強(qiáng)市場(chǎng)競(jìng)爭(zhēng)力,我司組織開(kāi)展了全國(guó)蘋(píng)果產(chǎn)業(yè)專題調(diào)研工作。現(xiàn)將有關(guān)情況報(bào)告如下:\n\n---\n\n## 一、產(chǎn)業(yè)發(fā)展總體情況\n\n近年來(lái),我國(guó)蘋(píng)果產(chǎn)業(yè)穩(wěn)步發(fā)展,種植面積和產(chǎn)量持續(xù)增長(zhǎng),已成為全球最大的蘋(píng)果生產(chǎn)國(guó)和消費(fèi)國(guó)。2023年數(shù)據(jù)顯示,全國(guó)蘋(píng)果種植面積約為**4500萬(wàn)畝**,總產(chǎn)量達(dá)到**4800萬(wàn)噸**,占全球總產(chǎn)量的**55%以上**。主要產(chǎn)區(qū)集中在陜西、山東、甘肅、河南、山西等省份。\n\n| 地區(qū) | 種植面積(萬(wàn)畝) | 年產(chǎn)量(萬(wàn)噸) | 占全國(guó)比重 |\n|------|------------------|----------------|-------------|\n| 陜西省 | 1200 | 1200 | 25% |\n| 山東省 | 900 | 1000 | 20.8% |\n| 甘肅省 | 600 | 700 | 14.6% |\n| 河南省 | 500 | 550 | 11.5% |\n| 山西省 | 400 | 450 | 9.4% |\n| 其他地區(qū) | 900 | 900 | 18.7% |\n\n從品種結(jié)構(gòu)來(lái)看,富士系蘋(píng)果占據(jù)主導(dǎo)地位,占比超過(guò)**70%**,其他如嘎啦、金帥、秦冠等傳統(tǒng)品種逐步被優(yōu)質(zhì)高產(chǎn)新品種替代。\n\n---\n\n## 二、產(chǎn)業(yè)發(fā)展中存在的問(wèn)題\n\n### (一)產(chǎn)業(yè)結(jié)構(gòu)不合理,區(qū)域集中度高\(yùn)n\n蘋(píng)果主產(chǎn)區(qū)高度集中于黃土高原及華北平原地區(qū),易受氣候、病蟲(chóng)害等自然因素影響,抗風(fēng)險(xiǎn)能力較弱。部分老果園老化嚴(yán)重,缺乏更新?lián)Q代機(jī)制,導(dǎo)致品質(zhì)下降、產(chǎn)量波動(dòng)大。\n\n### (二)產(chǎn)業(yè)鏈條短,加工轉(zhuǎn)化率低\n\n目前,我國(guó)蘋(píng)果主要用于鮮果銷(xiāo)售,深加工比例較低。2023年數(shù)據(jù)顯示,蘋(píng)果加工轉(zhuǎn)化率僅為**12%**,遠(yuǎn)低于發(fā)達(dá)國(guó)家**30%以上**的平均水平。果汁、果干、果酒等產(chǎn)品仍處于起步階段,品牌化程度不高。\n\n### (三)銷(xiāo)售渠道單一,流通體系不健全\n\n蘋(píng)果銷(xiāo)售以批發(fā)市場(chǎng)為主,電商渠道發(fā)展較快但尚未形成規(guī)模效應(yīng)。冷鏈物流體系建設(shè)滯后,部分地區(qū)存在“賣(mài)難”“爛市”現(xiàn)象,價(jià)格波動(dòng)頻繁,農(nóng)民收入不穩(wěn)定。\n\n### (四)科技創(chuàng)新能力不足,標(biāo)準(zhǔn)化水平偏低\n\n盡管我國(guó)在蘋(píng)果育種、栽培技術(shù)等方面取得一定進(jìn)展,但整體科技支撐能力仍顯薄弱,優(yōu)質(zhì)新品種推廣速度慢,標(biāo)準(zhǔn)化生產(chǎn)覆蓋率不足**30%**,制約了產(chǎn)業(yè)高質(zhì)量發(fā)展。\n\n---\n\n## 三、典型地區(qū)調(diào)研情況\n\n### (一)陜西省渭南市\(zhòng)n\n渭南市是全國(guó)重要的蘋(píng)果生產(chǎn)基地之一,種植面積達(dá)**200萬(wàn)畝**,年產(chǎn)量**250萬(wàn)噸**。該市通過(guò)“龍頭企業(yè)+合作社+農(nóng)戶”的模式推動(dòng)產(chǎn)業(yè)升級(jí),建設(shè)高標(biāo)準(zhǔn)示范園,引進(jìn)優(yōu)良品種,并建立蘋(píng)果質(zhì)量追溯系統(tǒng),有效提升了產(chǎn)品質(zhì)量和市場(chǎng)競(jìng)爭(zhēng)力。\n\n### (二)山東省煙臺(tái)市\(zhòng)n\n煙臺(tái)市擁有百年蘋(píng)果種植歷史,是中國(guó)蘋(píng)果出口的重要口岸。近年來(lái),煙臺(tái)市依托地理標(biāo)志保護(hù),打造“煙臺(tái)蘋(píng)果”區(qū)域公用品牌,積極拓展國(guó)際市場(chǎng)。2023年出口量達(dá)**45萬(wàn)噸**,占全國(guó)出口總量的**25%**。\n\n### (三)甘肅省慶陽(yáng)市\(zhòng)n\n慶陽(yáng)市地處黃土高原腹地,光照充足、晝夜溫差大,適宜蘋(píng)果生長(zhǎng)。當(dāng)?shù)卣雠_(tái)多項(xiàng)扶持政策,推動(dòng)蘋(píng)果產(chǎn)業(yè)規(guī)?;⒓s化發(fā)展,蘋(píng)果種植面積達(dá)**300萬(wàn)畝**,年產(chǎn)值超**50億元**。但仍面臨基礎(chǔ)設(shè)施薄弱、人才短缺等問(wèn)題。\n\n---\n\n## 四、對(duì)策建議\n\n### (一)優(yōu)化產(chǎn)業(yè)布局,推進(jìn)結(jié)構(gòu)調(diào)整\n\n科學(xué)規(guī)劃產(chǎn)區(qū)布局,引導(dǎo)優(yōu)勢(shì)產(chǎn)區(qū)適度擴(kuò)張,非優(yōu)勢(shì)產(chǎn)區(qū)有序退出,鼓勵(lì)發(fā)展山地、丘陵等地塊種植蘋(píng)果,提高土地利用效率。\n\n### (二)加強(qiáng)科技創(chuàng)新,提升標(biāo)準(zhǔn)化水平\n\n加大科研投入,加快新品種選育和配套栽培技術(shù)推廣,推動(dòng)蘋(píng)果種植標(biāo)準(zhǔn)化、機(jī)械化、智能化發(fā)展,提高生產(chǎn)效率和果實(shí)品質(zhì)。\n\n### (三)完善加工體系,延伸產(chǎn)業(yè)鏈條\n\n支持建設(shè)蘋(píng)果深加工項(xiàng)目,重點(diǎn)發(fā)展果汁、果醋、果酒等產(chǎn)品,培育一批具有自主知識(shí)產(chǎn)權(quán)的品牌企業(yè),提升附加值和市場(chǎng)占有率。\n\n### (四)健全流通體系,拓寬銷(xiāo)售渠道\n\n加快推進(jìn)冷鏈物流體系建設(shè),鼓勵(lì)發(fā)展電商、社區(qū)團(tuán)購(gòu)等新型銷(xiāo)售模式,構(gòu)建覆蓋全國(guó)、連接國(guó)際的現(xiàn)代蘋(píng)果流通網(wǎng)絡(luò),增強(qiáng)產(chǎn)業(yè)抗風(fēng)險(xiǎn)能力。\n\n### (五)強(qiáng)化政策扶持,保障農(nóng)民利益\n\n加大對(duì)蘋(píng)果產(chǎn)業(yè)的財(cái)政、金融支持力度,完善農(nóng)業(yè)保險(xiǎn)制度,建立健全產(chǎn)銷(xiāo)對(duì)接機(jī)制,確保農(nóng)民穩(wěn)定增收。\n\n---\n\n## 附件\n\n1. 中國(guó)蘋(píng)果產(chǎn)業(yè)主要產(chǎn)區(qū)分布圖 \n2. 2023年全國(guó)蘋(píng)果產(chǎn)量及結(jié)構(gòu)統(tǒng)計(jì)表 \n3. 典型地區(qū)蘋(píng)果產(chǎn)業(yè)發(fā)展情況匯總表 \n\n---\n\n**聯(lián)系人:XXX** \n**聯(lián)系電話:XXXX-XXXXXXXX** \n**電子郵箱:XXXX@agri.gov.cn**\n\n農(nóng)業(yè)農(nóng)村部發(fā)展規(guī)劃司 \nxxxx年xx月xx日');
tw.append('[done]');
}, 1000);
一、forCKEditor5.ts
// forCKEditor5.ts
import { marked } from 'marked';
// import type { Editor, ViewDocumentFragment, ViewElement, ViewNode, ViewText } from 'ckeditor5';
import type { Editor, ModelElement, ModelText, ModelRootElement, ModelDocumentFragment as ModelFragment } from 'ckeditor5';
type TypewriterMode = 'text' | 'markdown' | 'html';
interface CKEditorTypewriterOptions {
container: Editor;
mode?: TypewriterMode;
interval?: number; // ms
step?: number;
detectDoneMarker?: boolean;
doneMarker?: string;
}
interface QueueItem {
node: ModelText | ModelElement;
parent: ModelElement | ModelFragment;
offset?: number; // 對(duì)文本節(jié)點(diǎn),表示已輸出長(zhǎng)度
}
export class CKEditorTypewriterEngine {
private container: Editor;
private mode: TypewriterMode;
private interval: number;
private step: number;
private queue: QueueItem[] = [];
private timer: number | null = null;
private isPaused = false;
private detectDoneMarker: boolean;
private doneMarker: string;
private isCompleted = false;
private onCompleteCallback?: () => void;
constructor(options: CKEditorTypewriterOptions) {
// 檢查編輯器上是否已有打字機(jī)實(shí)例
const existingInstance = (options.container as any).__typewriterInstance;
if (existingInstance) {
existingInstance.destroy();
}
// 將當(dāng)前實(shí)例存儲(chǔ)在編輯器上
(options.container as any).__typewriterInstance = this;
this.container = options.container;
this.mode = options.mode ?? 'html';
this.interval = options.interval ?? 40;
this.step = options.step ?? 1;
this.detectDoneMarker = options.detectDoneMarker ?? false;
this.doneMarker = options.doneMarker ?? '[DONE]';
}
/** 簡(jiǎn)單 Markdown 轉(zhuǎn) HTML */
private markdownToHtml(md: string): string {
return marked.parse(md) as string;
}
/** 將 HTML / Markdown / Text 轉(zhuǎn)成模型隊(duì)列 */
public append(input: string): CKEditorTypewriterEngine {
const pos = this.container?.model?.document?.selection?.getFirstPosition();
if (!pos) {
return this;
}
let processedInput = input;
let shouldHideCursorAfter = false;
// 檢測(cè)并處理 doneMarker
if (this.detectDoneMarker) {
const doneIndex = processedInput.indexOf(this.doneMarker);
if (doneIndex !== -1) {
processedInput = processedInput.substring(0, doneIndex);
shouldHideCursorAfter = true;
}
}
let html = processedInput;
if (this.mode === 'text') {
html = this.escapeHtml(processedInput);
} else if (this.mode === 'markdown') {
html = this.markdownToHtml(processedInput);
}
// 將 HTML 轉(zhuǎn)到到模型 fragment
const modelFragment: ModelFragment = this.container.data.parse(html);
// 將 fragment 的頂層子節(jié)點(diǎn)壓入隊(duì)列
this.enqueueFragment(modelFragment, pos.parent);
// 啟動(dòng)打字機(jī)
if (!this.timer) this.start();
// 完成后隱藏光標(biāo)
if (shouldHideCursorAfter) {
const checkCompletion = () => {
if (!this.queue.length && !this.timer) {
this.isCompleted = true;
this.onCompleteCallback?.();
} else {
requestAnimationFrame(checkCompletion);
}
};
checkCompletion();
}
return this;
}
/** 將 fragment 子節(jié)點(diǎn)壓入隊(duì)列,保留父引用 */
private enqueueFragment(frag: ModelFragment | ModelElement, parent: ModelElement | ModelFragment) {
for (const child of frag.getChildren()) {
this.queue.push({ node: child as ModelText | ModelElement, parent });
}
}
/** 啟動(dòng)定時(shí)器 */
private start() {
if (this.timer) return;
this.isPaused = false;
this.timer = window.setInterval(() => {
if (this.isPaused) return;
this.processNext();
}, this.interval);
}
/** 逐步處理隊(duì)列 */
private processNext() {
if (!this.queue.length) {
this.stop();
return;
}
const item = this.queue[0];
this.container.model.change((writer) => {
if (item.node.is('$text')) {
const textNode = item.node as ModelText;
const offset = item.offset ?? 0;
const chunk = textNode.data.slice(offset, offset + this.step);
if (chunk) {
// 獲取textNode的屬性
const textAttributes = textNode.getAttributes();
// 插入文本并保留原有屬性
writer.insertText(chunk, textAttributes, item.parent as ModelElement, 'end');
item.offset = offset + chunk.length;
} else {
this.queue.shift();
}
} else if (item.node.is('element')) {
const el = item.node as ModelElement;
// 克隆元素但不帶子節(jié)點(diǎn)
const clone = writer.createElement(el.name, el.getAttributes());
writer.insert(clone, item.parent, 'end');
this.queue.shift();
// 將子節(jié)點(diǎn)壓入隊(duì)列,父節(jié)點(diǎn)為 clone
for (const child of Array.from(el.getChildren()).reverse()) {
this.queue.unshift({ node: child as ModelText | ModelElement, parent: clone });
}
} else {
this.queue.shift();
}
// 光標(biāo)移到最后
writer.setSelection(this.container.model.document.getRoot() as ModelRootElement, 'end');
});
// 滾動(dòng)到編輯器底部,確保新內(nèi)容可見(jiàn)
if (!this.isCompleted) {
this.container.editing.view.scrollToTheSelection();
}
}
/** 暫停 */
public pause() {
this.isPaused = true;
}
/** 恢復(fù) */
public resume() {
this.isPaused = false;
}
/** 設(shè)置完成回調(diào)函數(shù) */
public onComplete(callback: () => void): CKEditorTypewriterEngine {
this.onCompleteCallback = callback;
return this;
}
/** 停止打字機(jī) */
public stop() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
this.queue = [];
// 解決輸出后,點(diǎn)擊輸出內(nèi)容任意位置,光標(biāo)會(huì)被強(qiáng)制跳轉(zhuǎn)到末尾的問(wèn)題(比較hack)
this.container.setData(this.container.getData() + '');
}
/** 銷(xiāo)毀 */
public destroy() {
this.stop();
this.clear();
// 從編輯器上移除實(shí)例引用
delete (this.containeras any).__typewriterInstance;
}
public clear() {
// 通過(guò)model方式清空container的內(nèi)容
this.container.model.change((writer) => {
const root = this.container.model.document.getRoot() as ModelRootElement;
const range = writer.createRangeIn(root);
// 不要直接remove(root),這樣會(huì)破壞結(jié)構(gòu)
writer.remove(range);
});
this.queue = [];
}
/** 簡(jiǎn)單轉(zhuǎn)義 HTML */
private escapeHtml(text: string) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
二、forDom.ts
// forDom.ts
import { marked } from 'marked';
type TypewriterMode = 'text' | 'markdown' | 'html';
interface TypewriterOptions {
container: HTMLElement;
mode?: TypewriterMode;
interval?: number; // ms
step?: number;
cursor?: string;
detectDoneMarker?: boolean;
doneMarker?: string;
}
interface QueueItem {
node: Node;
parent: HTMLElement;
offset?: number; // 對(duì)文本節(jié)點(diǎn),表示已輸出長(zhǎng)度
}
export class TypewriterEngine {
private container: HTMLElement;
private mode: TypewriterMode;
private interval: number;
private step: number;
private cursorChar: string;
private detectDoneMarker: boolean;
private doneMarker: string;
private textQueue: string[] = [];
private htmlQueue: QueueItem[] = [];
private timer: number | null = null;
private isPaused = false;
private cursorEl: HTMLElement;
constructor(options: TypewriterOptions) {
this.container = options.container;
this.mode = options.mode ?? 'text';
this.interval = options.interval ?? 40;
this.step = options.step ?? 1;
this.cursorChar = options.cursor ?? '|';
this.detectDoneMarker = options.detectDoneMarker ?? false;
this.doneMarker = options.doneMarker ?? '[DONE]';
// 檢查容器是否已有打字機(jī)實(shí)例,若有則銷(xiāo)毀
const existingInstance = (this.container as any).__typewriterInstance;
if (existingInstance) {
existingInstance.destroy();
}
// 將當(dāng)前實(shí)例存儲(chǔ)在容器上
(this.container as any).__typewriterInstance = this;
// 清除容器現(xiàn)有內(nèi)容
while (this.container.firstChild) {
this.container.removeChild(this.container.firstChild);
}
// 初始化光標(biāo)
this.cursorEl = document.createElement('span');
this.cursorEl.className = 'tw-cursor';
this.cursorEl.textContent = this.cursorChar;
this.container.appendChild(this.cursorEl);
this.startCursorBlink();
}
/** 光標(biāo)閃爍 */
private startCursorBlink() {
setInterval(() => {
this.cursorEl.style.visibility =
this.cursorEl.style.visibility === 'hidden' ? 'visible' : 'hidden';
}, 500);
}
/** 簡(jiǎn)單 Markdown 轉(zhuǎn) HTML */
private markdownToHtml(md: string): string {
return marked.parse(md) as string;
}
/** 追加內(nèi)容 */
public append(input: string): TypewriterEngine {
let processedInput = input;
let shouldHideCursorAfter = false;
// 檢測(cè)并處理 doneMarker
if (this.detectDoneMarker) {
const doneIndex = processedInput.indexOf(this.doneMarker);
if (doneIndex !== -1) {
processedInput = processedInput.substring(0, doneIndex);
shouldHideCursorAfter = true;
}
}
if (this.mode === 'text') {
const html = this.escapeHtml(processedInput);
this.textQueue.push(html);
} else if (this.mode === 'markdown') {
const html = this.markdownToHtml(processedInput);
const fragment = this.parseHTML(html);
this.enqueueFragment(fragment, this.container);
} else {
const fragment = this.parseHTML(processedInput);
this.enqueueFragment(fragment, this.container);
}
if (!this.timer) this.start();
// 完成后隱藏光標(biāo)
if (shouldHideCursorAfter) {
const checkCompletion = () => {
if (!this.textQueue.length && !this.htmlQueue.length && !this.timer) {
this.hideCursor();
} else {
requestAnimationFrame(checkCompletion);
}
};
checkCompletion();
}
return this;
}
/** HTML 字符串解析成 fragment */
private parseHTML(html: string): DocumentFragment {
const doc = document.implementation.createHTMLDocument('');
doc.body.innerHTML = html;
const frag = document.createDocumentFragment();
while (doc.body.firstChild) frag.appendChild(doc.body.firstChild);
return frag;
}
/** 將 fragment 壓入 htmlQueue */
private enqueueFragment(frag: DocumentFragment, parent: HTMLElement) {
frag.childNodes.forEach((child) => {
this.htmlQueue.push({ node: child, parent });
});
}
/** 啟動(dòng)打字 */
private start() {
if (this.timer) return;
this.isPaused = false;
this.timer = window.setInterval(() => {
if (this.isPaused) return;
if (this.mode === 'text') {
this.processTextMode();
} else {
this.processHtmlMode();
}
}, this.interval);
}
/** 轉(zhuǎn)義HTML字符 */
private escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/** 文本/Markdown模式處理 */
private processTextMode() {
if (!this.textQueue.length) {
this.stop();
return;
}
const current = this.textQueue[0];
const chunk = current.slice(0, this.step);
this.textQueue[0] = current.slice(this.step);
this.cursorEl.remove();
const temp = document.createElement('div');
temp.innerHTML = chunk;
// 由于DOM節(jié)點(diǎn)只能有一個(gè)父節(jié)點(diǎn),每次調(diào)用 appendChild 都會(huì)將節(jié)點(diǎn)從 temp 中移除;
// 當(dāng) temp 的所有子節(jié)點(diǎn)都被轉(zhuǎn)移后, temp.firstChild 變?yōu)?null ,循環(huán)終止
while (temp.firstChild) this.container.appendChild(temp.firstChild);
this.container.appendChild(this.cursorEl);
if (this.textQueue[0].length === 0) this.textQueue.shift();
}
/** HTML模式逐節(jié)點(diǎn)打字 */
private processHtmlMode() {
if (!this.htmlQueue.length) {
this.stop();
return;
}
const item = this.htmlQueue[0];
if (item.node.nodeType === Node.TEXT_NODE) {
const text = item.node.textContent ?? '';
const offset = item.offset ?? 0;
const chunk = text.slice(offset, offset + this.step);
if (chunk) {
const tn = document.createTextNode(chunk);
item.parent.appendChild(tn); // ? appendChild 替代 insertBefore
this.container.appendChild(this.cursorEl);
item.offset = offset + this.step;
return;
} else {
this.htmlQueue.shift(); // 文本輸出完
}
} else if (item.node.nodeType === Node.ELEMENT_NODE) {
const el = item.node as HTMLElement;
const clone = el.cloneNode(false) as HTMLElement;
// 復(fù)制所有屬性
for (const attr of Array.from(el.attributes)) {
clone.setAttribute(attr.name, attr.value);
}
item.parent.appendChild(clone); // ? appendChild 替代 insertBefore
this.htmlQueue.shift();
for (let i = el.childNodes.length - 1; i >= 0; i--) {
this.htmlQueue.unshift({ node: el.childNodes[i], parent: clone });
}
} else {
this.htmlQueue.shift();
}
}
/** 暫停 */
public pause() {
this.isPaused = true;
}
/** 恢復(fù) */
public resume() {
this.isPaused = false;
// 繼續(xù)輸出時(shí)重新添加光標(biāo)
this.container.appendChild(this.cursorEl);
}
/** 隱藏光標(biāo) */
public hideCursor() {
this.cursorEl.remove();
}
/** 顯示光標(biāo) */
public showCursor() {
if (!this.container.contains(this.cursorEl)) {
this.container.appendChild(this.cursorEl);
}
}
/** 停止打字 */
public stop() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
}
/** 銷(xiāo)毀 */
public destroy() {
this.stop();
this.cursorEl?.remove();
// 清空容器內(nèi)容,避免重復(fù)輸出
this.clear();
// 清空隊(duì)列,防止殘留的任務(wù)繼續(xù)執(zhí)行
this.textQueue = [];
this.htmlQueue = [];
// 移除容器上的實(shí)例引用
delete (this.container as any).__typewriterInstance;
}
public clear() {
this.container.innerHTML = '';
}
}