如何在Angular中实现社交平台式#/@标签高亮及联想列表功能
Angular 实现#/@文本高亮+联想列表方案
1. 选用contenteditable容器替代原生input
原生input无法内嵌span这类富文本元素,必须用contenteditable="true"的div作为输入容器。在Angular组件中可以这样定义:
<div #editableContainer contenteditable="true" class="input-container" (input)="onInput($event)" (keydown)="onKeyDown($event)" ></div> <!-- 联想列表容器 --> <div #suggestionList class="suggestion-box" *ngIf="showSuggestions"> <div *ngFor="let item of suggestions" (click)="selectSuggestion(item)">{{item.name}}</div> </div>
2. 实时解析文本并高亮#/@内容
监听输入事件,每次输入后解析文本中的#和@匹配项,将其替换为带样式的span。关键是处理光标位置,避免替换后光标错位:
@ViewChild('editableContainer') editableContainer!: ElementRef<HTMLDivElement>; private lastCursorPosition: number = 0; onInput(event: Event) { const container = this.editableContainer.nativeElement; // 保存当前光标位置 this.lastCursorPosition = this.getCursorPosition(container); // 解析文本,替换#/@为高亮span const text = container.innerText; const formattedText = text.replace(/(#\w+)|(@\w+)/g, (match) => { const className = match.startsWith('#') ? 'hashtag' : 'mention'; return `<span class="${className}">${match}</span>`; }); // 重新设置内容 container.innerHTML = formattedText; // 恢复光标位置 this.setCursorPosition(container, this.lastCursorPosition); } // 获取光标位置的工具方法 private getCursorPosition(element: HTMLElement): number { let position = 0; const selection = window.getSelection(); if (selection?.focusNode === element) { position = selection.focusOffset; } else if (selection?.focusNode?.parentElement === element) { position = Array.from(element.childNodes).indexOf(selection.focusNode) + selection.focusOffset; } return position; } // 设置光标位置的工具方法 private setCursorPosition(element: HTMLElement, position: number): void { const selection = window.getSelection(); const range = document.createRange(); let currentNode: Node | null = element.firstChild; let currentLength = 0; while (currentNode) { if (currentNode.nodeType === Node.TEXT_NODE) { if (currentLength + currentNode.length >= position) { range.setStart(currentNode, position - currentLength); range.collapse(true); selection?.removeAllRanges(); selection?.addRange(range); break; } currentLength += currentNode.length; } currentNode = currentNode.nextSibling; } }
对应的CSS样式:
.hashtag { color: #1da1f2; cursor: pointer; } .mention { color: #1da1f2; cursor: pointer; } .input-container { border: 1px solid #ccc; padding: 8px; min-height: 40px; }
3. 联想列表的触发与定位
当用户输入#或@时,触发联想请求,根据输入的关键词过滤数据,并将联想框定位在光标下方:
showSuggestions = false; suggestions: Array<{name: string, id: string}> = []; currentTrigger: '#' | '@' | null = null; currentKeyword = ''; onInput(event: Event) { // 先执行之前的高亮逻辑... // 检测是否触发联想 const text = this.editableContainer.nativeElement.innerText; const lastTriggerIndex = Math.max(text.lastIndexOf('#'), text.lastIndexOf('@')); if (lastTriggerIndex !== -1 && lastTriggerIndex >= this.lastCursorPosition - this.currentKeyword.length -1) { this.currentTrigger = text[lastTriggerIndex] as '#' | '@'; this.currentKeyword = text.slice(lastTriggerIndex + 1, this.lastCursorPosition); // 模拟请求数据,实际项目中调用API this.suggestions = this.getMockSuggestions(this.currentTrigger, this.currentKeyword); this.showSuggestions = true; // 定位联想框 this.positionSuggestionBox(); } else { this.showSuggestions = false; } } private positionSuggestionBox() { const container = this.editableContainer.nativeElement; const suggestionBox = this.suggestionList.nativeElement; const range = window.getSelection()?.getRangeAt(0); if (range) { const rect = range.getBoundingClientRect(); suggestionBox.style.position = 'absolute'; suggestionBox.style.top = `${rect.bottom + window.scrollY}px`; suggestionBox.style.left = `${rect.left + window.scrollX}px`; } } // 模拟获取联想数据 private getMockSuggestions(trigger: '#' | '@', keyword: string): Array<{name: string, id: string}> { const hashtags = [{name: '#Angular', id: '1'}, {name: '#WebDev', id: '2'}, {name: '#Frontend', id: '3'}]; const mentions = [{name: '@JohnDoe', id: 'a'}, {name: '@JaneSmith', id: 'b'}, {name: '@AngularTeam', id: 'c'}]; const data = trigger === '#' ? hashtags : mentions; return data.filter(item => item.name.toLowerCase().includes(keyword.toLowerCase())); }
4. 联想项选择与插入
点击联想项时,将选中的内容插入到输入容器中,替换当前正在输入的关键词部分:
selectSuggestion(item: {name: string, id: string}) { const container = this.editableContainer.nativeElement; const text = container.innerText; const lastTriggerIndex = text.lastIndexOf(this.currentTrigger!); // 替换关键词为选中的项 const newText = text.slice(0, lastTriggerIndex) + item.name + text.slice(this.lastCursorPosition); // 重新设置内容并恢复光标 container.innerText = newText; this.onInput(new Event('input')); // 触发重新高亮 this.showSuggestions = false; // 将光标移到插入内容的末尾 this.setCursorPosition(container, lastTriggerIndex + item.name.length); }
注意事项
- 处理退格键:当删除#/@内容时,要及时关闭联想列表
- 兼容性:contenteditable在不同浏览器中的光标处理可能有差异,需要做兼容
- 性能优化:输入事件触发频繁,可加入防抖处理,避免频繁解析和请求
内容的提问来源于stack exchange,提问作者user19845343
相关产品推荐
相关产品推荐

