|
@@ -0,0 +1,198 @@
|
|
|
|
+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'
|
|
|
|
+
|
|
|
|
+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)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }, [value])
|
|
|
|
+
|
|
|
|
+ // 焦点丢失记录
|
|
|
|
+ const onBlur = (e: any) => {
|
|
|
|
+ try {
|
|
|
|
+ let selection = window.getSelection()
|
|
|
|
+ let range = selection?.getRangeAt(0)
|
|
|
|
+ setRange(range)
|
|
|
|
+ //点击表情不更新
|
|
|
|
+ if (!e.relatedTarget.className.includes('myEmo')) {
|
|
|
|
+ onChange?.(ref.current)
|
|
|
|
+ }
|
|
|
|
+ } catch (err) {
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ // 插入按钮
|
|
|
|
+ const btnClick = useCallback((btnName: string) => {
|
|
|
|
+ if (range) {
|
|
|
|
+ let str = "" //` <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, str);//插入内容
|
|
|
|
+ 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);
|
|
|
|
+ }
|
|
|
|
+ }, [])
|
|
|
|
+
|
|
|
|
+ //初始化设置
|
|
|
|
+ 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(/"/ig, '"')
|
|
|
|
+ .replace(/&/ig, '&')
|
|
|
|
+ .replace(/</ig, '<')
|
|
|
|
+ .replace(/>/ig, '>')
|
|
|
|
+ .replace(/<\s+/g, '<')
|
|
|
|
+ .replace(/href="weixin/ig, 'href=weixin')
|
|
|
|
+ if (text !== "") {
|
|
|
|
+ document.execCommand('insertHTML', false, text);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ return <List
|
|
|
|
+ header={<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
|
|
+ <Space wrap>
|
|
|
|
+ {emo && <Expression addEmo={addEmo} range={range as Range} />}
|
|
|
|
+ {btnNames?.map(name => {
|
|
|
|
+ return <Button onClick={() => { btnClick(name) }} key={name}>{name}</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>
|
|
|
|
+ <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'
|
|
|
|
+ }}
|
|
|
|
+ onInput={(e) => {
|
|
|
|
+ if (isOnInput) {
|
|
|
|
+ onChange?.(e.target)
|
|
|
|
+ // setText((e.target as any).innerHTML)
|
|
|
|
+ }
|
|
|
|
+ }}
|
|
|
|
+ contentEditable="true"
|
|
|
|
+ dangerouslySetInnerHTML={{
|
|
|
|
+ __html: text
|
|
|
|
+ }}
|
|
|
|
+ ref={ref}
|
|
|
|
+ onKeyDown={(e: React.KeyboardEvent<HTMLPreElement>) => {
|
|
|
|
+ if (e.key === 'Enter') {
|
|
|
|
+ document.execCommand('insertHTML', false, '<br/>\n')
|
|
|
|
+ e.preventDefault()
|
|
|
|
+ }
|
|
|
|
+ }}
|
|
|
|
+ onKeyUp={(e: React.KeyboardEvent<HTMLPreElement>) => {
|
|
|
|
+ if (e.key === 'Enter') {
|
|
|
|
+ e.preventDefault()
|
|
|
|
+ } 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 }}
|
|
|
|
+ />
|
|
|
|
+}
|