Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tool bar will be scrolled out of frame #1443

Open
kikouousya opened this issue Jul 3, 2024 · 0 comments
Open

Tool bar will be scrolled out of frame #1443

kikouousya opened this issue Jul 3, 2024 · 0 comments

Comments

@kikouousya
Copy link

The toolbar of this component leaves the view as it scrolls when the content is too long and requires scrolling.

  • Adding a position: sticky style to the customToolbarButton
  • Hiding the original toolbar and adding a custom one above it. when click on my toolbar, the editor's on focus out will be triggered, and there is no option to disable it.

Code here, if you need more info, I will provide.

import React, { useCallback, useEffect, useRef, useState } from "react"
import { CompositeDecorator, ContentBlock, ContentState, EditorState, Modifier, SelectionState } from "draft-js"
import { getSelectionText } from "draftjs-utils"
// import {DraftOffsetKey, DraftEditorLeaf,} from "../../../../../../../node_modules/draft-js/lib"
import DraftOffsetKey from "draft-js/lib/DraftOffsetKey"
// import DraftEditorLeaf from "draft-js/lib/DraftEditorLeaf.react"
import EditorLeaf from "./EditorLeaf"
import { Editor } from "react-draft-wysiwyg"
import "react-draft-wysiwyg/dist/react-draft-wysiwyg.css"
import "./HtmlEditor.less"
import {
  CaretDownFilled,
  CaretRightFilled, CloseOutlined,
  CodeOutlined,
  FolderOpenOutlined,
  FolderOutlined,
  PlusOutlined
} from "@ant-design/icons"
import TextArea from "antd/es/input/TextArea"
import { Map, OrderedMap } from "immutable"


import {
  addLink,
  contextMenuItems, getCurrentLnkData, getEntitySelection, getValidSize,
  handleKeyCommand,
  handleReturn,
  myKeyBindingFn,
  selectSubBlocks,
  setSubBlocksCollapsed
} from "./HtmlEditorActions"
import DraftLinkLeaf from "./DraftLinkLeaf"
import { draftBlocks2Html, findLinkEntities, html2DraftBlocks, selectByBlock } from "./utls"
import _ from "lodash"
import { Button, Dropdown, Form, Input, theme } from "antd"
import { FileData } from "../../../../../../../types/interfaces"
import { ipcRenderer } from "../../../../../utils"
import EditorToolBar from "./EditorToolBar"
import DraggableModal from "../../../../BasicComponent/DraggableModal"
import { useTranslation } from "../../../../Utils/locale"
import { useAppSelector } from "../../../../../store"
import { settingState } from "../../../../../store/settings"
import DevInfo from "../../../../Utils/devInfo"

const AEditor = Editor as any


const isBlockOnSelectionEdge = (selection: SelectionState, key: string): boolean => {
  return selection.getAnchorKey() === key || selection.getFocusKey() === key
}

const LinkEditModal = ({editorState, setEditorState, data, setLinkEditData, entitySelection}: {
  data: { entityKey?, pureUrl?, urlData?, url?, open: boolean, text? }
  setLinkEditData, editorState, setEditorState, entitySelection?
}) => {
  const {t} = useTranslation()
  return <DraggableModal
    width={600}
    open={data.open}
    title={t("设置链接")}
    onCancel={e => {
      setLinkEditData({...data, open: false})
    }}
    onOk={e => {
      addLink(editorState, setEditorState, data.text,
        data.url,
        null, entitySelection || getEntitySelection(editorState, data.entityKey))
      setLinkEditData({...data, open: false})
    }}
  >
    <Form
      labelCol={{span: 8}}
      wrapperCol={{span: 24}}
      labelAlign={"left"}
      colon={false}
    >
      <Form.Item label={t("链接")}>
        {(data.url || "").split("||")?.map((url, i) => {
          return <div style={{
            display: "flex",
            flexDirection: "row",
            alignItems: "center",
            justifyContent: "space-between", gap: 3
          }}>
            <Input
              key={i} value={url}
              onChange={e => {
                setLinkEditData({
                  ...data,
                  url: (data.url || "").split("||").map((u, ii) => ii == i ? e.target.value : u).join("||")
                })
              }}
              suffix={< FolderOpenOutlined onClick={async () => {
                const url = await window.api.showOpenDialog({
                  extFilter: [{
                    name: t("插入文件")
                  }], defaultPath: "../../static/models"
                })
                const detail = await window.api.readFileDetail(url)
                if (url) {
                  setLinkEditData({
                    ...data,
                    url: (data.url || "").split("||").map((u, ii) => ii == i ? `file://${url}?${
                      new URLSearchParams({
                        ...getValidSize(detail)
                      })
                    }` : u).join("||")
                  })
                }
              }} />}
            />
            <a>
              <CloseOutlined
                onClick={e => {
                  setLinkEditData({
                    ...data,
                    url: (data.url || "").split("||").filter((u, ii) => ii != i).join("||")
                  })
                }}
              />
            </a>

          </div>
        })}

        <Button
          style={{width: "100%"}}
          onClick={e => {
            setLinkEditData({
              ...data,
              url: (data.url || "") + "||"
            })
          }}
        ><PlusOutlined /></Button>
      </Form.Item>
      <Form.Item label={t("显示文本")}>
        <Input value={data.text}
               onChange={e => {
                 setLinkEditData({
                   ...data,
                   text: e.target.value
                 })
               }}
        ></Input>
      </Form.Item>

    </Form>
    <DevInfo obj={data} />
  </DraggableModal>
}

const HtmlEditor = ({rawData: initialRawData, onChange, parentFolder = null, readOnly}: {
  rawData: string, onChange?: (v: string) => void, parentFolder?: FileData | null,
  readOnly?: boolean,
}) => {
  const [rawData, setRawData] = useState(initialRawData)
  const [editorState, setEditorStateRaw] = useState(EditorState.createEmpty())
  const editorStateRef = useRef(editorState)
  const setEditorState = (editorState) => {
    editorStateRef.current = editorState
    setEditorStateRaw(editorState)
  }
  const [rawTextEditorShowing, setRawTextEditorShowing] = useState(false)
  const [editorScale, setEditorScale] = useState(1)
  const [linkEditData, setLinkEditData] = useState<{
    entityKey?, pureUrl?, urlData?, url?, open: boolean, text?, entitySelection?
  }>({open: false})
  const editorSettings = useAppSelector(state => state.settingState.settings.editor)

  useEffect(() => {
    new Promise(async resolve => {
      const blocks = await html2DraftBlocks(rawData)
      const contentState = ContentState.createFromBlockArray(blocks.toArray() as any)
      const editorState = EditorState.createWithContent(contentState)
      setEditorState(editorState)
      resolve(0)
    })
  }, [])


  const apis = {
    openLinkMenu: (args?) => {
      if (args) {
        const entitySelection = getEntitySelection(editorStateRef.current, args.entityKey)
        entitySelection && setLinkEditData({
          ...args,
          open: true,
          text: getSelectionText(EditorState.forceSelection(editorStateRef.current, entitySelection)),
          entitySelection
        })
      } else {
        const data = getCurrentLnkData(editorState)
        setLinkEditData({
          open: true,
          url: data?.link.target,
          pureUrl: data?.link.target,
          text: data?.link.title || data?.selectionText
        })
      }
    }
  }
  const debounceUpdateEditorState = useCallback(_.debounce(async (rawData) => {
    const blocks = await html2DraftBlocks(rawData, 0)
    const contentState = ContentState.createFromBlockArray(blocks.toArray() as any)
    const editorState = EditorState.createWithContent(contentState)
    setEditorState(editorState)
  }, 500), [])

  const [textInputValue, setTextInputValue] = useState("")
  useEffect(() => {
    ipcRenderer.on("selectBlockByKey", (e, args) => {
      const {blockKey} = args
      const block = editorState.getCurrentContent().getBlockForKey(blockKey)
      const start = 0
      const end = block?.getLength()
      console.log("on selectBlockByKey", {block, start, end, blockKey})
      if (!block) return
      // 创建一个新的selectionState
      const selectionState = new SelectionState({
        anchorKey: blockKey,
        anchorOffset: start,
        focusKey: blockKey,
        focusOffset: end
      })
      // 使用新的selectionState更新editorState
      const newEditorState = EditorState.forceSelection(editorState, selectionState)
      setEditorState(newEditorState)
    })
  }, [])

  const debounceSaveChange = useCallback(_.debounce(async (editorState) => {
    const html = draftBlocks2Html(editorState.getCurrentContent().getBlocksAsArray())
    setTextInputValue(html)
    onChange?.(html)
  }, 3000), [])

  useEffect(() => {
    debounceSaveChange(editorState)
  }, [editorState])
  useEffect(() => {
    debounceUpdateEditorState(rawData)
    return () => {
      debounceUpdateEditorState.cancel()
    }
  }, [rawData])
  const {t} = useTranslation()
  const items = contextMenuItems(editorState, setEditorState, parentFolder)
  // console.log("contextMenuItems", items)
  let olCount = 0
  const BlockComponent = (props: {
    block: ContentBlock,
    decorator: CompositeDecorator,
    contentState: ContentState,
    blockProps: any,
    customStyleMap: any,
    selection: SelectionState,
    onChange: (editorState: EditorState) => void,
    [key: string]: any,
  }) => {
    const blockData = props.block.getData()
    const blockIndent = ((props.indent || 0) + (blockData.get("indent") || 0))

    const blockMarkers = blockData.get("markerTypes")?.split("||") || []
    if (props.block.getType() == "ordered-list-item") {
      olCount++
    } else {
      olCount = 0
    }

    // console.log("blockData", blockData.toJS(), blockMarkers)

    function _renderChildren(): Array<any>{
      const block = props.block
      const blockKey = block.getKey()
      const text = block.getText()
      const lastLeafSet = props.tree?.size - 1
      const hasSelection = isBlockOnSelectionEdge(props.selection, blockKey)

      return props.tree?.map((leafSet, ii) => {
        const leavesForLeafSet = leafSet.get("leaves")
        const lastLeaf = leavesForLeafSet.size - 1
        const leaves = leavesForLeafSet.map((leaf, jj) => {
          const offsetKey = DraftOffsetKey.encode(blockKey, ii, jj)
          const start = leaf.get("start")
          const end = leaf.get("end")
          return <EditorLeaf
            key={offsetKey} offsetKey={offsetKey} block={block} start={start}
            selection={hasSelection ? props.selection : null}
            forceSelection={props.forceSelection} text={text.slice(start, end)}
            styleSet={block.getInlineStyleAt(start)} customStyleMap={props.customStyleMap}
            customStyleFn={props.customStyleFn}
            isLast={ii === lastLeafSet && jj === lastLeaf} />

        }).toArray()

        const decoratorKey = leafSet.get("decoratorKey")
        if (decoratorKey == null) {
          return leaves
        }

        if (!props.decorator) {
          return leaves
        }


        const DecoratorComponent = props.decorator.getComponentForKey(decoratorKey)
        if (!DecoratorComponent) {
          return leaves
        }

        const decoratorProps = props.decorator.getPropsForKey(decoratorKey)
        const decoratorOffsetKey = DraftOffsetKey.encode(blockKey, ii, 0)
        const decoratedText = text.slice(leavesForLeafSet.first().get("start"), leavesForLeafSet.last().get("end"))

        return <DecoratorComponent {...decoratorProps} contentState={props.contentState} decoratedText={decoratedText}
                                   key={decoratorOffsetKey} entityKey={block.getEntityAt(leafSet.get("start"))}
                                   offsetKey={decoratorOffsetKey}>
          {leaves}
        </DecoratorComponent>
      })?.toArray() || []
    }

    return (<div style={{
      display: blockData.get("hidden") ? "none" : "block"
    }}>
      <div
        className={`editorBlock`}
        style={{
          display: "flex", alignItems: "end",
          marginLeft: blockMarkers?.length ? `calc(${blockIndent * 0.35 + "in"} - ${20 * blockMarkers?.length}px)` : blockIndent * 0.35 + "in",
          cursor: "text"
        }}
      >


        <span
          style={{fontSize: 16, cursor: "pointer", alignSelf: "center"}}
          className={blockData.get("collapsed") ? "" : "show-when-hover"}
          onClick={e => {
            if (e.ctrlKey) {
              selectSubBlocks(editorState, setEditorState, [props.block])
            } else {
              setSubBlocksCollapsed(editorState, setEditorState, [props.block])
            }
          }}

          onDoubleClick={(e) => {
            console.log("on caret click", e)
            const blocksAfter: OrderedMap<string, ContentBlock> = editorState.getCurrentContent().getBlockMap().skipUntil(block => block === props.block).rest() as any
            const firstIdentIdentical = blocksAfter.find(block => block?.getData()?.get("indent") == blockData?.get("indent"))
            const blockToCollapse = blocksAfter.takeUntil(block => block === firstIdentIdentical)
            // set Data.hidden
            let newContentState = blockToCollapse.reduce((contentState, b) => {
              return Modifier.setBlockData(contentState!, selectByBlock(b!), b!.getData().set("hidden", !blockData.get("collapsed")))
            }, editorState.getCurrentContent())
            newContentState = Modifier.setBlockData(newContentState, selectByBlock(props.block), blockData.set("collapsed", !blockData.get("collapsed")))
            const newEditorState = EditorState.push(editorState, newContentState, "change-block-data")
            setEditorState(newEditorState)

            console.log("blocksAfter", blocksAfter)
          }}>
          {blockData.get("collapsed") ? <CaretRightFilled /> : <CaretDownFilled />}
        </span>
        {blockMarkers.map(m => <span style={{width: 20}}>
          <i style={{
            fontSize: 16, cursor: "pointer", alignSelf: "center",
            ...m?.split("?")?.[1] ? {color: m?.split("?")?.[1]} : {}
          }}
             className={`bi ${m?.split("?")?.[0]}`} /></span>
        )}
        {props.block.getType() == "unordered-list-item" &&
          <span style={{width: "0.35in"}}>
            <i className="bi bi-dot"></i>
          </span>}
        {props.block.getType() == "ordered-list-item" &&
          <span style={{width: "0.35in"}}>
            {olCount}.
          </span>}

        {_renderChildren()}

      </div>
    </div>)
  }

  const blockRendererFn = (block) => {
    return {
      component: BlockComponent, editable: true, props: {olCount}
    }
  }


  const DraftLink = (props) => {
    return <DraftLinkLeaf
      {...props}
      editorStateRef={editorStateRef}
      setEditorState={setEditorState}
      apis={apis}
    />
  }

  return (

    <div className={"html-editor"}>
      {/*If I click something on this toolbar, OnFucusout will trriger*/}
      {/*{!readOnly && <EditorToolBar editorState={editorState} setEditorState={setEditorState} apis={apis}*/}
      {/*                             toggleRawShowing={() => setRawTextEditorShowing(!rawTextEditorShowing)} />}*/}

      <div
        style={{
          display: "flex",
          // flexDirection: "column",
          height: "100%", // Or whatever height you want
          width: "100%",
          maxHeight: "100vh",
          overflow: "auto"
        }}
      >
        <div
          style={{
            flex: 1,
            scale: editorScale
          }}
          onWheel={e => {
            console.log("on editor Wheel", e)
            if (e.ctrlKey) {
              if (e.deltaY > 0) {
                setEditorScale(editorScale * 1.05)
              } else {
                setEditorScale(editorScale * 0.95)
              }

            }
          }}
        >
          <Dropdown
            menu={{items: items}}
            trigger={["contextMenu"]}
            // open={rightClickContext}
          >
            <div
              onBlur={e => {
                console.log("focusout div")
                e.preventDefault()
                e.stopPropagation()
              }}
            >
              <Editor
                className={"editor-area"}
                // toolbarHidden={true}
                toolbar={{
                  options: []
                }}
                editorState={editorState}
                wrapperClassName="demo-wrapper"
                editorClassName="demo-editor"
                onEditorStateChange={setEditorState}
                handleKeyCommand={(c, e) => handleKeyCommand(c, e, setEditorState, parentFolder, apis, editorSettings)}
                keyBindingFn={(e) => myKeyBindingFn(e, apis)}
                // toolbarCustomButtons={[<LineHeightControl />]} // 添加自定义按钮
                blockRendererFn={blockRendererFn}
                // onChange={handleChange}
                handleReturn={(c, e) => handleReturn(c, e, setEditorState)}
                // blockRenderMap={BlockRenderMap}
                // readOnly={readOnly}
                focusout={e => {
                  console.log("focusout AEditor")
                  e.preventDefault()
                  e.stopPropagation()
                }}
                onBlur={e => {
                  console.log("focusout AEditor")
                  e.preventDefault()
                  e.stopPropagation()
                }}
                editorStyle={{
                  flex: 1,
                  overflow: "auto"
                }}
                customDecorators={[
                  {
                    strategy: findLinkEntities,
                    component: DraftLink
                  }
                ]}
                toolbarCustomButtons={[(<EditorToolBar editorState={editorState} setEditorState={setEditorState} apis={apis}
                                                       toggleRawShowing={() => setRawTextEditorShowing(!rawTextEditorShowing)} />)]}
              />
              {parentFolder && <div style={{height: "50vh"}} />}
            </div>

          </Dropdown>
        </div>

        {rawTextEditorShowing && <TextArea
          style={{
            height: 300,
            flex: 0.8
          }}
          autoSize
          value={textInputValue}
          onChange={e => {
            setRawData(e.target.value)
            setTextInputValue(e.target.value)
            onChange?.(e.target.value)
          }}
        />}
        <LinkEditModal data={linkEditData} setLinkEditData={setLinkEditData} editorState={editorState}
                       setEditorState={setEditorStateRaw} />
      </div>
    </div>
  )
}

export default HtmlEditor
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant