@ -76,7 +76,9 @@ const navs = ref([
const mentionList = ref ( [ ] )
const currentMentionQuery = ref ( '' )
setTimeout ( ( ) => {
console . log ( 'props.members' , props . members )
} , 1000 )
/ / 编 辑 器 内 容
const editorContent = ref ( '' )
const editorHtml = ref ( '' )
@ -100,6 +102,7 @@ const handleInput = (event) => {
const editorClone = target . cloneNode ( true )
const quoteElements = editorClone . querySelectorAll ( '.editor-quote' )
quoteElements . forEach ( quote => quote . remove ( ) )
quoteElements . forEach ( quote => quote . remove ( ) )
/ / 处 理 表 情 图 片 , 将 其 a l t 属 性 ( 表 情 文 本 ) 添 加 到 文 本 内 容 中
const emojiImages = editorClone . querySelectorAll ( 'img.editor-emoji' )
@ -119,7 +122,8 @@ const handleInput = (event) => {
editorContent . value = textContent
/ / 检 查 是 否 需 要 清 空 编 辑 器 以 显 示 p l a c e h o l d e r
const isEmpty = textContent . trim ( ) === '' &&
/ / 只 有 当 编 辑 器 中 没 有 任 何 内 容 ( 包 括 空 格 ) 且 没 有 其 他 元 素 时 才 清 空
const isEmpty = textContent === '' &&
! target . querySelector ( 'img, .editor-file, .mention' )
if ( isEmpty && target . innerHTML !== '' ) {
@ -163,11 +167,30 @@ const showMentionList = () => {
mentionList . value = props . members . filter ( member => {
return member . value . toLowerCase ( ) . startsWith ( query )
} )
if ( dialogueStore . groupInfo . is _manager ) {
mentionList . value . unshift ( { id : 0 , nickname : '全体成员' , avatar : defAvatar , value : '全体成员' } )
}
showMention . value = mentionList . value . length > 0
selectedMentionIndex . value = 0
}
/ / 处 理 鼠 标 点 击 选 择 m e n t i o n
const handleMentionSelectByMouse = ( member ) => {
const selection = window . getSelection ( ) ;
if ( selection && selection . rangeCount > 0 ) {
insertMention ( member , selection . getRangeAt ( 0 ) . cloneRange ( ) ) ;
} else {
/ / 如 果 没 有 有 效 的 选 区 , 尝 试 聚 焦 编 辑 器 并 获 取 选 区
editorRef . value ? . focus ( ) ;
nextTick ( ( ) => {
const newSelection = window . getSelection ( ) ;
if ( newSelection && newSelection . rangeCount > 0 ) {
insertMention ( member , newSelection . getRangeAt ( 0 ) . cloneRange ( ) ) ;
}
} ) ;
}
} ;
/ / 隐 藏 m e n t i o n 列 表
const hideMentionList = ( ) => {
showMention . value = false
@ -187,65 +210,73 @@ const updateMentionPosition = (range) => {
}
/ / 插 入 m e n t i o n
const insertMention = ( member ) => {
const selection = window . getSelection ( )
if ( ! selection . rangeCount ) return
const insertMention = ( member , clonedRange ) => {
console . log ( '插入mention' , member ) ;
const selection = window . getSelection ( ) ;
if ( ! clonedRange || ! selection ) return ;
const range = selection . getRangeAt ( 0 )
const textNode = range . startContainer
const offset = range . startOffset
const range = clonedRange ; / / 使 用 传 入 的 克 隆 r a n g e
/ / 找 到 @ 符 号 的 位 置
const textContent = textNode . textContent || ''
const atIndex = textContent . lastIndexOf ( '@' , offset - 1 )
const textNode = range . startContainer ;
const offset = range . startOffset ;
const textContent = textNode . nodeType === Node . TEXT _NODE ? textNode . textContent || '' : '' ;
/ / @ 符 号 的 查 找 逻 辑 仅 当 光 标 在 文 本 节 点 内 且 不 在 开 头 时 才 有 意 义
const atIndex = ( textNode . nodeType === Node . TEXT _NODE && offset > 0 ) ? textContent . lastIndexOf ( '@' , offset - 1 ) : - 1 ;
/ / 创 建 m e n t i o n 元 素
const mentionSpan = document . createElement ( 'span' )
mentionSpan . className = 'mention'
mentionSpan . setAttribute ( 'data-user-id' , member . id || member . user _id )
mentionSpan . textContent = ` @ ${ member . value || member . nickname } `
mentionSpan . contentEditable = 'false'
const mentionSpan = document . createElement ( 'span' ) ;
mentionSpan . className = 'mention' ;
mentionSpan . setAttribute ( 'data-user-id' , String ( member . id ) ) ;
mentionSpan . textContent = ` @ ${ member . value || member . nickname } ` ;
mentionSpan . contentEditable = 'false' ;
if ( atIndex !== - 1 ) {
/ / 如 果 找 到 @ 符 号 , 替 换 文 本
const beforeText = textContent . substring ( 0 , atIndex )
const afterText = textContent . substring ( offset )
if ( atIndex !== - 1 && textNode . nodeType === Node . TEXT _NODE ) {
const parent = textNode . parentNode ;
if ( ! parent ) return ; / / S a n i t y c h e c k
/ / 创 建 新 的 文 本 节 点
const beforeNode = document . createTextNode ( beforeText )
const afterNode = document . createTextNode ( ' ' + afterText )
/ / 从 @ 符 号 开 始 删 除 , 直 到 当 前 光 标 位 置
range . setStart ( textNode , atIndex ) ;
range . setEnd ( textNode , offset ) ;
range . deleteContents ( ) ;
/ / 替 换 内 容
const parent = textNode . parentNode
parent . insertBefore ( beforeNode , textNode )
parent . insertBefore ( mentionSpan , textNode )
parent . insertBefore ( afterNode , textNode )
parent . removeChild ( textNode )
/ / 插 入 m e n t i o n 元 素
range . insertNode ( mentionSpan ) ;
} else {
/ / 如 果 没 有 找 到 @ 符 号 , 直 接 在 光 标 位 置 插 入
range . deleteContents ( )
/ / 插 入 @ 提 及 元 素
range . insertNode ( mentionSpan )
/ / 在 @ 提 及 元 素 后 添 加 空 格
const spaceNode = document . createTextNode ( ' ' )
range . setStartAfter ( mentionSpan )
range . insertNode ( spaceNode )
/ / 将 光 标 移 动 到 空 格 后
range . setStartAfter ( spaceNode )
range . collapse ( true )
selection . removeAllRanges ( )
selection . addRange ( range )
/ / 如 果 没 有 找 到 @ 符 号 , 或 者 光 标 不 在 合 适 的 文 本 节 点 内 , 直 接 在 当 前 光 标 位 置 插 入
if ( ! range . collapsed ) {
range . deleteContents ( ) ;
}
range . insertNode ( mentionSpan ) ;
}
/ / 触 发 输 入 事 件 以 更 新 编 辑 器 内 容
handleInput ( { target : editorRef . value } )
/ / 在 m e n t i o n 之 后 插 入 一 个 空 格 , 并 将 光 标 移 到 空 格 之 后
const spaceNode = document . createTextNode ( '\u00A0' ) ; / / 使 用 不 间 断 空 格
const currentParent = mentionSpan . parentNode ;
if ( currentParent ) {
/ / 将 空 格 节 点 插 入 到 m e n t i o n S p a n 之 后
if ( mentionSpan . nextSibling ) {
currentParent . insertBefore ( spaceNode , mentionSpan . nextSibling ) ;
} else {
currentParent . appendChild ( spaceNode ) ;
}
/ / 设 置 光 标 到 空 格 之 后
range . setStartAfter ( spaceNode ) ;
range . collapse ( true ) ;
} else {
/ / F a l l b a c k : 如 果 m e n t i o n S p a n 没 有 父 节 点 ( 理 论 上 不 应 该 发 生 ) , 则 将 光 标 设 置 在 m e n t i o n S p a n 之 后
range . setStartAfter ( mentionSpan ) ;
range . collapse ( true ) ;
}
/ / 隐 藏 m e n t i o n 列 表
hideMentionList ( )
}
selection . removeAllRanges ( ) ;
selection . addRange ( range ) ;
editorRef . value ? . focus ( ) ; / / 确 保 编 辑 器 在 操 作 后 仍 有 焦 点
nextTick ( ( ) => {
handleInput ( { target : editorRef . value } ) ;
hideMentionList ( ) ;
} ) ;
} ;
/ / 处 理 粘 贴 事 件
const handlePaste = ( event ) => {
@ -343,12 +374,15 @@ const handleKeydown = (event) => {
break
case 'Enter' :
case 'Tab' :
event . preventDefault ( )
const selectedMember = mentionList . value [ selectedMentionIndex . value ]
event . preventDefault ( ) ;
const selectedMember = mentionList . value [ selectedMentionIndex . value ] ;
if ( selectedMember ) {
insertMention ( selectedMember )
const selection = window . getSelection ( ) ;
if ( selection && selection . rangeCount > 0 ) {
insertMention ( selectedMember , selection . getRangeAt ( 0 ) . cloneRange ( ) ) ;
}
break
}
break ;
case 'Escape' :
hideMentionList ( )
break
@ -391,6 +425,25 @@ const handleKeydown = (event) => {
}
prevSibling = prevSibling . previousSibling
}
} else {
/ / 如 果 光 标 在 文 本 节 点 中 间 或 末 尾 , 且 当 前 文 本 节 点 只 包 含 空 格
/ / 检 查 前 一 个 兄 弟 节 点 是 否 是 m e n t i o n
if ( ! container . textContent . trim ( ) ) {
let prevSibling = container . previousSibling
while ( prevSibling ) {
if ( prevSibling . nodeType === Node . ELEMENT _NODE &&
prevSibling . classList &&
prevSibling . classList . contains ( 'mention' ) ) {
targetMention = prevSibling
break
}
/ / 如 果 是 文 本 节 点 且 不 为 空 , 停 止 查 找
if ( prevSibling . nodeType === Node . TEXT _NODE && prevSibling . textContent . trim ( ) ) {
break
}
prevSibling = prevSibling . previousSibling
}
}
}
} else if ( container . nodeType === Node . ELEMENT _NODE ) {
/ / 如 果 光 标 在 元 素 节 点 中 , 检 查 前 一 个 子 节 点
@ -525,24 +578,29 @@ const sendMessage = () => {
if ( messageData . items . length === 0 ||
( messageData . items . length === 1 &&
messageData . items [ 0 ] . type === 1 &&
! messageData . items [ 0 ] . content . trim ( ) ) ) {
! messageData . items [ 0 ] . content . trim End ( ) ) ) {
return / / 没 有 内 容 , 不 发 送
}
/ / 处 理 不 同 类 型 的 消 息
messageData . items . forEach ( item => {
/ / 处 理 文 本 内 容
if ( item . type === 1 && item . content . trim ( ) ) {
if ( item . type === 1 && item . content . trim End ( ) ) {
const data = {
items : [ {
content : item . content ,
type : 1
} ] ,
mentionUids : messageData . mentionUids ,
mentions : [ ] ,
mentions : messageData . mentionUids . map ( uid => {
return {
atid : uid ,
name : mentionList . value . find ( member => member . id === uid ) ? . nickname || ''
}
} ) ,
quoteId : messageData . quoteId ,
}
console . log ( 'data' , data )
emit (
'editor-event' ,
emitCall ( 'text_event' , data )
@ -602,7 +660,7 @@ const parseEditorContent = () => {
/ / 处 理 @ 提 及
const userId = node . getAttribute ( 'data-user-id' )
if ( userId ) {
mentionUids . push ( parseInt ( userId ) )
mentionUids . push ( Number ( userId ) )
}
textContent += node . textContent
} else if ( node . tagName === 'IMG' ) {
@ -634,7 +692,7 @@ const parseEditorContent = () => {
if ( textContent . trim ( ) ) {
items . push ( {
type : 1 ,
content : textContent . trim ( )
content : textContent . trim End ( )
} )
textContent = ''
}
@ -674,7 +732,7 @@ const parseEditorContent = () => {
if ( textContent . trim ( ) ) {
items . push ( {
type : 1 ,
content : textContent . trim ( )
content : textContent . trim End ( )
} )
textContent = ''
}
@ -696,7 +754,7 @@ const parseEditorContent = () => {
if ( textContent . trim ( ) ) {
items . push ( {
type : 1 ,
content : textContent . trim ( )
content : textContent . trim End ( )
} )
}
@ -918,27 +976,44 @@ const insertImageEmoji = (imgSrc, altText) => {
}
/ / 事 件 监 听
const onSubscribeMention = ( data ) => {
/ / 确 保 编 辑 器 获 得 焦 点
editorRef . value ? . focus ( )
const onSubscribeMention = async ( data ) => {
const editorNode = editorRef . value ;
if ( ! editorNode ) return ;
/ / 如 果 编 辑 器 为 空 或 者 光 标 不 在 编 辑 器 内 , 将 光 标 移 动 到 编 辑 器 末 尾
const selection = window . getSelection ( )
if ( ! selection . rangeCount || ! editorRef . value . contains ( selection . anchorNode ) ) {
const range = document . createRange ( )
if ( editorRef . value . lastChild ) {
range . setStartAfter ( editorRef . value . lastChild )
editorNode . focus ( ) ;
await nextTick ( ) ; / / 确 保 焦 点 已 设 置
let selection = window . getSelection ( ) ;
if ( ! selection || selection . rangeCount === 0 ) {
const range = document . createRange ( ) ;
if ( editorNode . lastChild ) {
range . setStartAfter ( editorNode . lastChild ) ;
} else {
range . setStart ( editorRef . value , 0 )
range . setStart ( editor Node, 0 ) ;
}
range . collapse ( true )
selection . removeAllRanges ( )
selection . addRange ( range )
range . collapse ( true ) ;
if ( selection ) selection . removeAllRanges ( ) ;
selection ? . addRange ( range ) ;
await nextTick ( ) ;
selection = window . getSelection ( ) ;
} else if ( ! editorNode . contains ( selection . anchorNode ) ) {
const range = document . createRange ( ) ;
if ( editorNode . lastChild ) {
range . setStartAfter ( editorNode . lastChild ) ;
} else {
range . setStart ( editorNode , 0 ) ;
}
range . collapse ( true ) ;
selection . removeAllRanges ( ) ;
selection . addRange ( range ) ;
await nextTick ( ) ;
selection = window . getSelection ( ) ;
}
/ / 插 入 @ 提 及
insertMention ( data )
}
if ( selection && selection . rangeCount > 0 ) {
insertMention ( data , selection . getRangeAt ( 0 ) . cloneRange ( ) ) ;
}
} ;
const onSubscribeQuote = ( data ) => {
/ / 保 存 引 用 数 据 , 但 不 保 存 到 草 稿 中
@ -1431,7 +1506,7 @@ const handleEditorClick = (event) => {
< n -icon size = "18" class = "icon" :component ="nav.icon" / >
< p class = "tip-title" > { { nav . title } } < / p >
< / div >
< n -button class = "w-80px h-30px ml-auto" type = "primary" >
< n -button class = "w-80px h-30px ml-auto" type = "primary" @click ="sendMessage" >
< template # icon >
< n -icon >
< IosSend / >
@ -1483,7 +1558,7 @@ const handleEditorClick = (event) => {
: key = "member.user_id || member.id"
class = "cursor-pointer px-14px h-42px"
: class = "{ 'bg-#EEE9F9': index === selectedMentionIndex }"
@ mousedown . prevent = " insertMention (member)"
@ mousedown . prevent = " handleMentionSelectByMouse (member)"
@ mouseover = "selectedMentionIndex = index"
>
< div class = "flex items-center border-b-1px border-b-solid border-b-#F8F8F8 h-full" >