shenwu 6 月之前
父節點
當前提交
d5a3638a01

+ 5 - 2
src/components/TextEditor/Expression.tsx

@@ -1,5 +1,5 @@
 import { MehOutlined } from '@ant-design/icons'
-import { Popover, Tooltip } from 'antd'
+import { Button, Popover, Tooltip } from 'antd'
 import { useCallback, useState } from 'react'
 import style from './index.less'
 export const emoList = [
@@ -90,7 +90,10 @@ function Expression(props: Props) {
             open={isShow}
             onOpenChange={exit}
         >
-            <Tooltip title="表情"><MehOutlined onClick={exit} /></Tooltip>
+            <Tooltip title="表情">
+                {/* <MehOutlined onClick={exit} /> */}
+                <Button onClick={exit} size="small" type="link"><MehOutlined /></Button>
+            </Tooltip>
         </Popover>
     </div>
 }

+ 8 - 0
src/components/TextEditor/index.less

@@ -52,4 +52,12 @@
 
 .myPre img {
   vertical-align: bottom;
+}
+
+.header{
+  display: flex;
+  align-items: center;
+  >button{
+    // margin-left: 10px;
+  }
 }

+ 0 - 232
src/components/TextEditor/index.tsx

@@ -1,232 +0,0 @@
-import { Button, List, Space, Typography } from "antd"
-import { useCallback, useEffect, useRef, useState } from "react"
-import Expression, { emoList } from "./Expression";
-const { Text } = Typography
-import style from './index.less'
-import AddLink from "@/pages/MiniApp/EntWeChat/Welcome/components/link";
-
-type Props = {
-    /**是否展示底部*/
-    footer: null | string | JSX.Element,
-    /**插入变量按钮的按钮名称*/
-    btnNames: string[],
-    /**初始默认插入的按钮*/
-    initBtnNames?: string[],
-    /**是否开启表情 */
-    emo?: boolean,
-    value?: any
-    onChange?: (value: any) => void,
-    maxStr?: number,//最大字数
-    isOnInput?: boolean,
-}
-export function TextEditor(props: Props) {
-    const { footer, btnNames, initBtnNames, emo = true, onChange, value = '', maxStr, isOnInput } = props
-    const ref: { current: any } = useRef(null)
-    const [text, setText] = useState('')
-    const [range, setRange] = useState<Range>()//丢失焦点存放焦点位置
-    const [strLength, setStrLength] = useState(0)
-    /** 回填 */
-    useEffect(() => {
-        backfill()
-    }, [value])
-    //回填处理
-    const backfill = useCallback(() => {
-        if (value) {
-            if (typeof value === 'string') {
-                setStrLength(value.length || 0)
-                let newValue = value || ''
-                let newEmo = emoList.reduce((prev, cur) => {
-                    return [...prev, ...cur]
-                }, [])
-                let emos = newValue.match(/\[[\u4e00-\u9fa5]+\]/g)
-                if (emos && emos?.length) {
-                    emos.forEach(emo => {
-                        let emoData = newEmo.find(o => `[${o.name}]` === emo)
-                        if (emoData) {
-                            newValue = newValue.replace(emo, `<img src="${emoData.url}" alt="${emoData.name}" style="width:20px;height:20px"/>`)
-                        }
-                    })
-                }
-                setText(newValue)//?.replaceAll('\n', '<br/>')
-            } else {
-                setText(value?.innerHTML)
-            }
-        } else {//不存在内容设置焦点
-            ref?.current?.focus()
-        }
-    }, [value])
-
-    // 焦点丢失记录
-    const onBlur = (e: any) => {
-        try {
-            let selection = window.getSelection()
-            let range = selection?.getRangeAt(0)
-            setRange(range)
-            //点击表情不更新
-            if (!e?.relatedTarget?.className?.match(/(myEmo)|(textEditor_btn)/g)) {
-                onChange?.(ref.current?.innerHTML)
-            }
-        } catch (err) {
-        }
-    }
-    // 自定义文本插入按钮
-    const btnClick = useCallback((btnName: any) => {
-        if (range) {
-            let str = btnName //` <span style="display: inline-block;margin:0 1px;position: relative; border: 1px solid ${token.colorBorder}; padding: ${token.paddingXS}px; border-radius: ${token.borderRadius}px;color: ${token.colorTextBase}; background: ${token.colorSuccess};color:${token.colorTextLightSolid}" contenteditable="false">${btnName}<strong data-name="${btnName}" style="padding: 0 6px;cursor:pointer" onclick="let html =document.querySelector('[data-name=${btnName}]').parentElement.parentElement.innerHTML;let span = ' '+document.querySelector('[data-name=${btnName}]').parentElement.outerHTML+' ';console.log('=',html,'=');console.log('=',span,'=');document.execCommand('selectAll');document.execCommand('delete'); document.execCommand('insertHTML', true, html.replace(span,''));">X</strong></span> `
-            let selection = window.getSelection()
-            selection?.empty()//清空选择range
-            selection?.addRange(range as Range)//插入新的range
-            document.execCommand('insertHTML', false, btnName);//插入内容
-            range.collapse()
-            selection?.collapseToEnd()//光标插入到末尾
-        } else {
-            ref.current.focus()
-        }
-    }, [range, ref])
-    /**
-     * 插入表情
-     */
-    const addEmo = useCallback((str: string, range: Range) => {
-        if (range) {
-            let selection = window.getSelection()
-            selection?.empty()//清空选择range
-            selection?.addRange(range)//插入新的range
-            document.execCommand('insertHTML', false, str);
-        }
-    }, [])
-    const addLink = useCallback((str: string, range: Range) => {
-        if (range) {
-            let selection = window.getSelection()
-            selection?.empty()//清空选择range
-            selection?.addRange(range)//插入新的range
-            document.execCommand('insertHTML', false, str);
-        }
-    }, [])
-    //初始化设置
-    useEffect(() => {
-        if (initBtnNames && initBtnNames?.length > 0) {
-            let newText = ''
-            initBtnNames?.map(btnName => {
-                newText += "" //` <span style="display: inline-block;margin:0 1px;position: relative; border: 1px solid ${token.colorBorder}; padding: ${token.paddingXS}px; border-radius: ${token.borderRadius}px;color: ${token.colorTextBase}; background: ${token.colorSuccess};color:${token.colorTextLightSolid}" contenteditable="false">${btnName}<strong data-name="${btnName}" style="padding: 0 6px;cursor:pointer" onclick="let html =document.querySelector('[data-name=${btnName}]').parentElement.parentElement.innerHTML;let span = ' '+document.querySelector('[data-name=${btnName}]').parentElement.outerHTML+' ';console.log('=',html,'=');console.log('=',span,'=');document.execCommand('selectAll');document.execCommand('delete'); document.execCommand('insertHTML', true, html.replace(span,''));">X</strong></span> `
-            })
-            setText(newText)
-        }
-    }, [initBtnNames])
-    //复制只取纯文本
-    function textPaste(event: any) {
-        event.preventDefault();
-        let text;
-        let clp = (event.originalEvent || event).clipboardData;
-        // 兼容chorme或hotfire
-        text = (clp.getData('text/plain') || clp.getData('text'))
-            .replace(/&quot;/ig, '"')
-            .replace(/&amp;/ig, '&')
-            .replace(/&lt;/ig, '<')
-            .replace(/&gt;/ig, '>')
-            .replace(/<\s+/g, '<')
-            .replace(/href="weixin/ig, 'href=weixin')
-        if (text !== "") {
-            document.execCommand('insertHTML', false, text);
-        }
-    }
-    const insertHtmlAtCursor = (html: string) => {
-        const selection = window.getSelection();
-        if (!selection || selection.rangeCount === 0) return;
-
-        const range = selection.getRangeAt(0);
-        range.deleteContents(); // 删除光标处的内容
-
-        const tempDiv = document.createElement('div');
-        tempDiv.innerHTML = html; // 创建包含 HTML 的临时元素
-        const frag = document.createDocumentFragment();
-        
-        // 将临时元素的子元素添加到文档片段中
-        while (tempDiv.firstChild) {
-            frag.appendChild(tempDiv.firstChild);
-        }
-        range.insertNode(frag); // 将内容插入光标位置
-        // 重新设置光标位置
-        range.collapse(false);
-        selection.removeAllRanges();
-        selection.addRange(range);
-    };
-    return <List
-        size="small"
-        header={<div style={{ display: 'flex', justifyContent: 'space-between' }}>
-            <Space wrap size={[20, 0]}>
-                {/* {emo && <Expression addEmo={addEmo} range={range as Range} />} */}
-                {btnNames?.map(name => {
-                    return <Button className="textEditor_btn" size="small" onClick={() => { btnClick(name) }} key={name}>{name}</Button>
-                })}
-                {/* <AddLink onChange={(value) => {
-                    if (value) {
-                        addEmo("1", range as any)
-                    }
-                }} /> */}
-                <button onClick={() => insertHtmlAtCursor('<a href="https://example.com" target="_blank">https://example.com</a>')}>插入链接</button>
-            </Space>
-            {maxStr && <Typography.Text type='secondary'><Typography.Text type={strLength > maxStr ? 'danger' : 'secondary'} >{strLength}</Typography.Text>/{maxStr}</Typography.Text>}
-            {/* <Button type="link">复制</Button> */}
-        </div>}
-        footer={typeof footer === 'string' ? <Text >{footer}</Text> : footer}
-        bordered
-        dataSource={['']}
-        renderItem={(item) => (
-            <List.Item style={{ padding: 5 }}>
-                <pre
-                    className={style.myPre}
-                    style={{
-                        fontFamily: ' sans-serif',
-                        width: '100%',
-                        minHeight: '40px',
-                        padding: 5,
-                        resize: 'none',
-                        whiteSpace: 'pre-wrap',
-                        margin: 0,
-                        textAlign: 'left',
-                        wordBreak: 'break-all'
-                    }}
-                    contentEditable="true"
-                    dangerouslySetInnerHTML={{
-                        __html: text
-                    }}
-                    ref={ref}
-                    // onKeyDown={(e: React.KeyboardEvent<HTMLPreElement>) => {
-                    //     if (e.key === 'Enter') {
-                    //         e.preventDefault()
-                    //         document.execCommand('insertHTML', false, '\n')
-                    //     }
-                    // }}
-                    onInput={(e: any) => {
-
-                    }}
-                    onKeyUp={(e: React.KeyboardEvent<HTMLPreElement>) => {
-                        if (e.key === 'Enter') {
-                        } else {
-                            if (maxStr) {
-                                let childrens = (e.target as any).childNodes
-                                let newText: string = ''
-                                for (const children of childrens) {
-                                    if (children.tagName === 'IMG') {
-                                        newText += `[${children.alt}]`
-                                    } else if (children.tagName === 'BR') {
-                                        newText += `\n`
-                                    } else if (children.tagName === 'SPAN') {
-                                        let name = children.children[0].dataset.name
-                                        newText += `%${name}%`
-                                    } else {
-                                        newText += children.textContent
-                                    }
-                                }
-                                setStrLength(newText.length)
-                            }
-                        }
-                    }}
-                    onBlur={onBlur}//焦点丢失
-                    onPaste={textPaste}
-                />
-            </List.Item>
-        )}
-        style={{ marginTop: 10 }}
-    />
-}

+ 55 - 51
src/components/TextEditor/index1.tsx

@@ -1,6 +1,9 @@
-import { Button } from 'antd';
+import { Button, Tooltip } from 'antd';
 import React, { useState, useRef, useEffect } from 'react';
 import Expression from './Expression';
+import style from './index.less'
+import AddLink from '@/pages/MiniApp/EntWeChat/Welcome/components/link';
+import { UserOutlined } from '@ant-design/icons';
 type Props = {
     value?: any,
     onChange?: (v: any) => void
@@ -11,14 +14,8 @@ const EditablePre: React.FC<Props> = (props) => {
     const [savedRange, setSavedRange] = useState<Range | null>(null); // 用于存储光标位置
     //内容变更
     const handleInput = () => {
-        console.log(11111)
         if (editableDivRef.current) {
-            const newStr = editableDivRef.current.innerHTML
-                .replace(/<div[^>]*>(\s|&nbsp;)*<\/div>/g, '') // 移除空的 <div> 标签
-                .replace(/<div[^>]*>/g, '') // 将 <div> 替换为 ""
-                .replace(/<\/div>/g, '\n') // 将 </div> 替换为 "\n"
-                .replace(/<br\s*\/?>/g, '\n') // 替换 <br> 为换行符
-            console.log(newStr)
+            const newStr = dataOutComp(editableDivRef.current.innerHTML)
             onChange?.(newStr);
         }
     };
@@ -29,69 +26,79 @@ const EditablePre: React.FC<Props> = (props) => {
             setSavedRange(selection.getRangeAt(0)); // 记录当前光标位置
         }
     };
-    const insertAtSavedPosition = (html: string) => {
-        if (!savedRange) return; // 如果没有记录的光标位置,则不执行
-    
+
+    // 插入 HTML 到 savedRange 记录的光标位置
+    const insertHtmlAtCursor = (html: string) => {
+        if (!savedRange) return;  // 如果没有记录的光标位置,则不执行
+
+        // 创建一个临时的 <div> 元素,用于将要插入的 HTML 包裹
         const tempDiv = document.createElement('div');
         tempDiv.innerHTML = html; // 创建包含 HTML 的临时元素
         const frag = document.createDocumentFragment();
-    
+
         // 将临时元素的子元素添加到文档片段中
+        let lastNode: Node | null = null;
         while (tempDiv.firstChild) {
-            frag.appendChild(tempDiv.firstChild);
+            lastNode = frag.appendChild(tempDiv.firstChild);
         }
-    
-        // 插入内容到记录的光标位置
+
+        // 如果 frag 为空,不执行插入操作
+        if (!frag.firstChild) return;
+
+        // 在记录的光标位置插入内容
         savedRange.deleteContents(); // 删除光标处的内容
         savedRange.insertNode(frag); // 将内容插入光标位置
-    
-        // 重新设置光标位置在插入内容之后
-        if (frag.lastChild) {
-            // 设置光标在插入节点后
-            const selection:any = window.getSelection();
-            selection.removeAllRanges();
-            savedRange.setStartAfter(frag.lastChild); // 设置光标在插入节点后
-            savedRange.collapse(true); // 折叠到新的光标位置
-            selection.addRange(savedRange); // 将新的范围添加到选区
-        }
-    };
-    const insertHtmlAtCursor = (html: string) => {
-        const selection = window.getSelection();
-        if (!selection || selection.rangeCount === 0) return;
 
-        const range = selection.getRangeAt(0);
-        range.deleteContents(); // 删除光标处的内容
+        // 重新设置光标位置在插入内容之后
+        if (lastNode) {
+            const range = document.createRange();
+            const selection = window.getSelection();
 
-        const tempDiv = document.createElement('div');
-        tempDiv.innerHTML = html; // 创建包含 HTML 的临时元素
-        const frag = document.createDocumentFragment();
+            range.setStartAfter(lastNode); // 将光标设置在插入内容的后面
+            range.collapse(true); // 折叠范围(即设置为单一光标位置)
 
-        // 将临时元素的子元素添加到文档片段中
-        while (tempDiv.firstChild) {
-            frag.appendChild(tempDiv.firstChild);
+            selection?.removeAllRanges(); // 清除当前的所有选区
+            selection?.addRange(range);   // 设置新的光标范围
         }
-        range.insertNode(frag); // 将内容插入光标位置
 
-        // 重新设置光标位置
-        range.collapse(false);
-        selection.removeAllRanges();
-        selection.addRange(range);
-        handleInput()
+        handleInput();  // 触发内容变更处理
     };
-
+    // 数据导入组件处理
+    const dataInComp = (value: string) => {
+        let newHtml = value?.split('\n')
+            .map((line: any, index: any) => `<div>${line}</div>`)
+            .join('');
+        return newHtml.replace(/<div[^>]*>(\s|&nbsp;)*<\/div>/g, '')
+    }
+    // 数据导出出组件处理
+    const dataOutComp = (value: string) => {
+        const newStr = value
+            .replace(/<div[^>]*>(\s|&nbsp;)*<\/div>/g, '') // 移除空的 <div> 标签
+            .replace(/<div[^>]*>/g, '') // 将 <div> 替换为 ""
+            .replace(/<\/div>/g, '\n') // 将 </div> 替换为 "\n"
+        // .replace(/<br\s*\/?>/g, '\n') // 替换 <br> 为换行符
+        return newStr
+    }
     useEffect(() => {
         if (value) {
-            let newHtml = value?.split('\n')
-                .map((line: any, index: any) => `<div>${line}</div>`)
-                .join('');
             if (editableDivRef.current) {
-                editableDivRef.current.innerHTML = newHtml.replace(/<div[^>]*>(\s|&nbsp;)*<\/div>/g, '')
+                editableDivRef.current.innerHTML = dataInComp(value)
             }
         }
     }, [])
     return (
         <div>
+            <div style={{ display: 'flex', alignItems: 'center' }} className={style.header}>
+                <Expression addEmo={insertHtmlAtCursor} range={savedRange as Range} />
+                <AddLink showIcon onChange={(value) => {
+                    if (value) {
+                        insertHtmlAtCursor(`<a href="${value?.pagePath}" target="_blank">${value?.pagePath}</a>`)
+                    }
+                }} />
+                <div>  <Tooltip title="@用户昵称"><Button size="small" type="link" onClick={() => insertHtmlAtCursor('@用户昵称')}><UserOutlined /></Button></Tooltip></div>
+            </div>
             <div
+                id="editText"
                 ref={editableDivRef}
                 contentEditable
                 onInput={handleInput}
@@ -107,9 +114,6 @@ const EditablePre: React.FC<Props> = (props) => {
             >
                 {/* 初始内容可以在此处放置 */}
             </div>
-            <Expression addEmo={insertAtSavedPosition} range={savedRange as Range} />
-            <Button onClick={() => insertHtmlAtCursor('<a href="https://example.com" target="_blank">https://example.com</a>')}>插入链接</Button>
-            <Button onClick={() => insertHtmlAtCursor('@用户昵称')}>@用户昵称</Button>
         </div>
     );
 };

+ 5 - 5
src/pages/MiniApp/EntWeChat/Welcome/components/link.tsx

@@ -1,10 +1,10 @@
-import { PlusOutlined } from "@ant-design/icons"
-import { Button, Card, Modal, Tabs } from "antd"
+import { LinkOutlined, PlusOutlined } from "@ant-design/icons"
+import { Button, Card, Modal, Tabs, Tooltip } from "antd"
 import { useEffect, useState } from "react"
 import Book from '@/pages/MiniApp/Extend/Book'
 import Page from '@/pages/MiniApp/Extend/Page'
-function AddLink(props: { value?: any, onChange?: (v: any) => void }) {
-    let { value, onChange } = props
+function AddLink(props: { value?: any, onChange?: (v: any) => void, showIcon?: boolean }) {
+    let { value, onChange, showIcon } = props
     let [open, setOpen] = useState(false)
     let [activeKey, setActiveKey] = useState("book")
     const close = () => {
@@ -24,7 +24,7 @@ function AddLink(props: { value?: any, onChange?: (v: any) => void }) {
             {value?.bookChapterName && <div>章节名称:<span style={{ color: "#777" }}>{value?.bookChapterName}</span></div>}
             {value?.pagePath && <div>路径地址:<span style={{ color: "#777" }}>{value?.pagePath}</span></div>}
 
-        </Card> : <a onClick={() => { setOpen(true) }}><PlusOutlined /> 插入链接</a>
+        </Card> : showIcon ? <Tooltip title="插入链接"><Button size="small" type="link" onClick={() => { setOpen(true) }}><LinkOutlined /></Button></Tooltip> : <a onClick={() => { setOpen(true) }}><PlusOutlined /> 插入链接</a>
         }
         <Modal
             title="插入链接"

+ 3 - 3
src/pages/MiniApp/Extend/Book/index.tsx

@@ -20,7 +20,7 @@ const Page: React.FC<Props> = (props) => {
     // 当作为组件使用时首次打开存在链接名称以链接名称搜索
     useEffect(() => {
         if (value?.linkName && value?.bookName) {
-            form?.current?.setFieldValue("linkName",value?.linkName)
+            form?.current?.setFieldValue("linkName", value?.linkName)
             form?.current?.submit()
         }
     }, [])
@@ -55,15 +55,15 @@ const Page: React.FC<Props> = (props) => {
                 type: 'radio',
                 selectedRowKeys: [props?.value?.linkId || props?.value],
                 onSelect: (record, selected) => {
+                    props?.onClose?.(false)
                     props?.onChange?.(record)
-                    // props?.onClose?.(false)
                 },
             } : false}
             // 点击行
             onRow={(record) => ({
                 onClick: () => {
-                    props?.onChange?.(record)
                     props?.onClose?.(false)
+                    props?.onChange?.(record)
                 }
             })}
             //多选展示按钮