优化自定义选择框代码

This commit is contained in:
fzk
2024-12-26 10:12:01 +08:00
parent 091b0bfa92
commit f62900a185
5 changed files with 343 additions and 362 deletions

View File

@@ -0,0 +1,67 @@
import {
CheckOutlined,
CloseOutlined,
DownSquareOutlined,
LeftSquareOutlined,
RightSquareOutlined,
UpSquareOutlined
} from "@ant-design/icons"
import { Button, Flex, message, Modal } from "antd"
import React, { useCallback, useEffect, useRef, useState } from "react"
export default function Tooltip(props: any) {
const handleConfirm = () => {
props.onConfirm()
}
const navigateElement = (direction: "parent" | "child" | "prev" | "next") => {
props.onNavigate(direction)
}
const handleCancel = () => {
props.onCancel()
}
return (
<Flex wrap gap="small">
<Button
type="primary"
size="small"
icon={<CheckOutlined />}
onClick={handleConfirm}>
</Button>
<Button
size="small"
icon={<UpSquareOutlined />}
onClick={() => navigateElement("parent")}>
</Button>
<Button
size="small"
icon={<DownSquareOutlined />}
onClick={() => navigateElement("child")}>
</Button>
<Button
size="small"
icon={<LeftSquareOutlined />}
onClick={() => navigateElement("prev")}>
</Button>
<Button
size="small"
icon={<RightSquareOutlined />}
onClick={() => navigateElement("next")}>
</Button>
<Button
danger
size="small"
icon={<CloseOutlined />}
onClick={handleCancel}>
</Button>
</Flex>
)
}

View File

@@ -0,0 +1,272 @@
import { Modal } from "antd"
import React, { useEffect, useRef, useState } from "react"
import { createRoot } from "react-dom/client"
import { useMessage } from "@plasmohq/messaging/hook"
import { useStorage } from "@plasmohq/storage/hook"
import { addCss, saveHtml, saveMarkdown, scrollToTop, setIcon } from "~tools"
import { savePdf } from "~utils/downloadPdf"
import { useContent } from "~utils/editMarkdownHook"
import Turndown from "~utils/turndown"
import Tooltip from "./Tooltip"
const turndownService = Turndown()
export default function CustomDomSelector() {
const isReady = useRef(false)
const isSelect = useRef(false)
const downloadType = useRef("")
const [content, setContent] = useContent()
const [validTime] = useStorage("app-validTime", "1730390400")
const [isModalOpen, setIsModalOpen] = useState(false)
const selectorRef = useRef<HTMLElement | null>(null)
const tooltipRef = useRef<HTMLElement | null>(null)
const articleTitle = document
.querySelector<HTMLElement>("head title")
?.innerText.trim()
useEffect(() => {
addEventListeners()
setIcon(true)
addCss(`.codebox-current { outline: 2px solid #42b88350 !important; }`)
return () => {
removeEventListeners()
// removeSelector()
removeTooltip()
removeHighlight()
}
}, [])
const createSelector = () => {
const selector = document.createElement("div")
selector.classList.add("codebox-selector")
selector.style.position = "absolute"
selector.style.pointerEvents = "none"
selector.style.zIndex = "2147483640"
selector.style.backgroundColor = "#42b88325"
selector.style.border = "2px solid #42b88350"
selector.style.borderRadius = "2px"
selector.style.transition = "all 0.1s ease-in"
selector.style.display = "none"
document.body.appendChild(selector)
selectorRef.current = selector
}
const createTooltip = () => {
const tooltip = document.createElement("div")
tooltip.classList.add("codebox-tooltip")
tooltip.style.position = "absolute"
tooltip.style.zIndex = "2147483641"
tooltip.style.backgroundColor = "#fff"
tooltip.style.border = "1px solid #eee"
tooltip.style.borderRadius = "5px"
tooltip.style.padding = "8px"
tooltip.style.boxShadow = "0 2px 8px rgba(0, 0, 0, 0.15)"
// tooltip.style.display = "none"
document.body.appendChild(tooltip)
const root = createRoot(tooltip)
root.render(
<Tooltip
onConfirm={handleConfirm}
onCancel={handleCancel}
onNavigate={navigateElement}
/>
)
tooltipRef.current = tooltip
}
const removeSelector = () => {
if (selectorRef.current) {
document.body.removeChild(selectorRef.current)
}
}
const removeTooltip = () => {
if (tooltipRef.current) {
document.body.removeChild(tooltipRef.current)
tooltipRef.current = null
}
}
const addEventListeners = () => {
document.addEventListener("mousemove", handleMouseMove)
document.addEventListener("click", handleClick)
}
const removeEventListeners = () => {
document.removeEventListener("mousemove", handleMouseMove)
document.removeEventListener("click", handleClick)
}
const handleMouseMove = (event: MouseEvent) => {
if (isReady.current && !isSelect.current) {
const target = event.target as HTMLElement
highlightElement(target)
// updateSelectorPosition(target)
}
}
const handleClick = (event: MouseEvent) => {
if (isReady.current) {
const target = event.target as HTMLElement
const tooltip = target.closest(".codebox-tooltip")
const modal = target.closest(".codebox-modal")
const submit = target.closest(".valid-submit")
if (!tooltip && !modal && !submit) {
removeTooltip()
createTooltip()
isSelect.current = true
highlightElement(target)
// updateSelectorPosition(target)
updateTooltipPosition(target)
event.stopPropagation()
event.preventDefault()
}
}
}
const highlightElement = (element: HTMLElement) => {
removeHighlight()
element.classList.add("codebox-current")
selectorRef.current = element
}
const removeHighlight = () => {
const currentList = document.querySelectorAll(".codebox-current")
currentList.forEach((el) => el.classList.remove("codebox-current"))
selectorRef.current = null
}
const updateSelectorPosition = (element: HTMLElement) => {
if (selectorRef.current) {
const rect = element.getBoundingClientRect()
selectorRef.current.style.top = `${rect.top + window.scrollY}px`
selectorRef.current.style.left = `${rect.left + window.scrollX}px`
selectorRef.current.style.width = `${rect.width}px`
selectorRef.current.style.height = `${rect.height}px`
selectorRef.current.style.display = "block"
}
}
const updateTooltipPosition = (element: HTMLElement) => {
const rect = element.getBoundingClientRect()
const distanceTop = rect.top + window.scrollY
const distanceLeft = rect.left + window.scrollX
const top =
distanceTop < 50 ? distanceTop + rect.height + 5 : distanceTop - 40
tooltipRef.current.style.top = `${top}px`
tooltipRef.current.style.left = `${distanceLeft + 5}px`
scrollToTop(tooltipRef.current)
}
useMessage(async (req: any, res: any) => {
if (req.name == "custom-downloadHtml") {
setCustom("html")
}
if (req.name == "custom-downloadMarkdown") {
setCustom("downloadMarkdown")
}
if (req.name == "custom-editMarkdown") {
setCustom("editMarkdown")
}
if (req.name == "custom-downloadPdf") {
setCustom("pdf")
}
if (req.name == "custom-downloadImg") {
setCustom("img")
}
})
const setCustom = (type: string) => {
downloadType.current = type
isReady.current = true
isSelect.current = false
}
const handleConfirm = () => {
if (!selectorRef.current || !downloadType.current) return
switch (downloadType.current) {
case "html":
saveHtml(selectorRef.current, articleTitle)
break
case "downloadMarkdown":
const markdown = turndownService.turndown(selectorRef.current)
saveMarkdown(markdown, articleTitle)
break
case "editMarkdown":
setContent(".codebox-current")
break
case "pdf":
savePdf(selectorRef.current, articleTitle)
break
}
resetState()
}
const handleCancel = () => {
resetState()
}
const resetState = () => {
removeHighlight()
// removeSelector()
removeTooltip()
isReady.current = false
isSelect.current = false
}
const navigateElement = (direction: "parent" | "child" | "prev" | "next") => {
if (!selectorRef.current) return
let newElement: HTMLElement | null = null
switch (direction) {
case "parent":
newElement = selectorRef.current.parentElement
break
case "child":
newElement = selectorRef.current.firstElementChild as HTMLElement
break
case "prev":
newElement = selectorRef.current.previousElementSibling as HTMLElement
break
case "next":
newElement = selectorRef.current.nextElementSibling as HTMLElement
break
}
if (newElement) {
highlightElement(newElement)
// updateSelectorPosition(newElement)
updateTooltipPosition(newElement)
}
}
const handleOkModal = () => {
setIsModalOpen(false)
}
const handleCancelModal = () => {
setIsModalOpen(false)
}
return (
<>
<Modal
title="Basic Modal"
className="codebox-modal"
open={isModalOpen}
onOk={handleOkModal}
onCancel={handleCancelModal}>
<p>Some contents...</p>
<p>Some contents...</p>
<p>Some contents...</p>
</Modal>
</>
)
}

View File

@@ -1,41 +1,13 @@
import { StyleProvider } from "@ant-design/cssinjs"
import {
CheckOutlined,
CloseOutlined,
DownSquareOutlined,
LeftSquareOutlined,
RightSquareOutlined,
UpSquareOutlined
} from "@ant-design/icons"
import { Button, Flex, message, Modal } from "antd"
import antdResetCssText from "data-text:antd/dist/reset.css"
import dayjs from "dayjs"
import type {
PlasmoCSConfig,
PlasmoGetShadowHostId,
PlasmoGetStyle
} from "plasmo"
import { useEffect, useRef, useState } from "react"
import { sendToBackground } from "@plasmohq/messaging"
import { useMessage } from "@plasmohq/messaging/hook"
import { useStorage } from "@plasmohq/storage/hook"
import ValidateContent from "~component/contents/validateContent"
import CustomDomSelector from "~component/customDomSelector"
import { ThemeProvider } from "~theme"
import { addCss, saveHtml, saveMarkdown, scrollToTop, setIcon } from "~tools"
import { getSummary } from "~utils/coze"
import useCssCodeHook from "~utils/cssCodeHook"
import { downloadAllImagesAsZip } from "~utils/downloadAllImg"
import { savePdf } from "~utils/downloadPdf"
import DrawImages from "~utils/drawImages"
import { useContent } from "~utils/editMarkdownHook"
import Turndown from "~utils/turndown"
const turndownService = Turndown()
const articleTitle = document
.querySelector<HTMLElement>("head title")
?.innerText.trim()
const HOST_ID = "codebox-csui"
@@ -48,341 +20,11 @@ export const getStyle: PlasmoGetStyle = () => {
return style
}
let isDownloadType = "markdown"
let isReady = false
let isSelect = false
let instance = null
export default function CustomOverlay() {
const [cssCode, runCss] = useCssCodeHook("custom")
const [content, setContent] = useContent()
const [summary, setSummary] = useStorage("app-summary", "")
const [validTime, setValidTime] = useStorage("app-validTime", "1730390400")
const [isCurrentDom, setIsCurrentDom] = useState<boolean>(false)
const [rect, setRect] = useState<any>(() => {
return { top: 0, right: 0 }
})
const [messageApi, contextHolder] = message.useMessage()
useEffect(() => {
getSelection()
setIcon(true)
}, [])
useMessage(async (req: any, res: any) => {
if (req.name == "custom-isShow") {
res.send({ isShow: true })
}
if (req.name == "custom-downloadHtml") {
setCustom("html")
}
if (req.name == "custom-downloadMarkdown") {
setCustom("downloadMarkdown")
}
if (req.name == "custom-downloadPdf") {
setCustom("pdf")
}
if (req.name == "custom-downloadImg") {
setCustom("img")
}
if (req.name == "custom-editMarkdown") {
setCustom("editMarkdown")
}
if (req.name == "app-downloadImages") {
await downloadImages(req.body?.onProgress)
}
if (req.name == "app-get-summary") {
setSummary("")
const res = await getSummary(location.href)
if (res.code == 0) {
const result = JSON.parse(res.data)
setSummary(result)
}
}
if (req.name == "app-full-page-screenshot") {
if (confirm("确认下载?")) {
const { scrollHeight, clientHeight } = document.documentElement
const devicePixelRatio = window.devicePixelRatio || 1
let capturedHeight = 0
let capturedImages = []
const captureAndScroll = async () => {
const scrollAmount = clientHeight * devicePixelRatio
const res = await sendToBackground({ name: "screenshot" })
const dataUrl = res.dataUrl
capturedHeight += scrollAmount
if (capturedHeight < scrollHeight * devicePixelRatio) {
capturedImages.push(dataUrl)
window.scrollTo(0, capturedHeight)
setTimeout(captureAndScroll, 2000) // Adjust the delay as needed
} else {
DrawImages(capturedImages, articleTitle)
}
}
captureAndScroll()
}
}
})
async function downloadImages(
onProgress?: (current: number, total: number) => void
) {
const imageUrls = Array.from(document.images).map((img) => img.src)
try {
const res = await sendToBackground({
name: "download",
body: {
action: "downloadAllImages",
imageUrls: imageUrls,
title: articleTitle,
onProgress: onProgress
}
})
if (res.code == 0) {
alert("下载失败")
}
} catch (error) {
console.error(`Failed to download images:`, error)
}
}
function setCustom(type) {
isReady = true
isSelect = false
isDownloadType = type
messageApi.info("请在页面选择要下载区域!")
}
function getSelection() {
addCss(`.codebox-current { border: 1px solid #7983ff!important; }`)
document.addEventListener("mousemove", (event) => {
const target = event.target as HTMLElement
if (isReady && target && !isSelect) {
removeCurrentDom()
target.classList.add("codebox-current")
}
})
document.addEventListener("click", (event) => {
const target = event.target as HTMLElement
const tooltip = target.closest("#codebox-csui")
const modal = target.closest(".ant-modal")
const submit = target.closest(".valid-submit")
if (isReady && target && !tooltip && !modal && !submit) {
isSelect = true
setIsCurrentDom(true)
removeCurrentDom()
target.classList.add("codebox-current")
setTooltip()
event.stopPropagation()
event.preventDefault()
}
})
}
function setTooltip() {
const currentDom = document.querySelector<HTMLElement>(".codebox-current")
const rect = currentDom.getBoundingClientRect()
const distanceTop = rect.top + window.scrollY
const distanceLeft = rect.left + window.scrollX
const top = distanceTop < 50 ? distanceTop + 15 : distanceTop - 40
const left = distanceLeft + 5
setRect({ top, left })
scrollToTop(currentDom)
}
function removeCurrentDom() {
const currentList = document.querySelectorAll(".codebox-current")
currentList.forEach((el) => {
el.classList.remove("codebox-current")
})
}
function downloadHtml(currentDom) {
saveHtml(currentDom, articleTitle)
}
function downloadMarkdown(currentDom) {
const markdown = turndownService.turndown(currentDom)
saveMarkdown(markdown, articleTitle)
}
function handleOk() {
const currentDom = document.querySelector(".codebox-current")
if (isDownloadType == "html") {
removeCurrentDom()
downloadHtml(currentDom)
} else if (isDownloadType == "downloadMarkdown") {
removeCurrentDom()
downloadMarkdown(currentDom)
} else if (isDownloadType == "editMarkdown") {
setContent(".codebox-current")
removeCurrentDom()
} else if (isDownloadType == "pdf") {
removeCurrentDom()
savePdf(currentDom, articleTitle)
}
isReady = false
isSelect = false
setIsCurrentDom(false)
instance.destroy()
}
function handleConfirm() {
instance = Modal.confirm({
title: "提示",
content: (
<>
<div style={{ fontSize: "18px" }}></div>
{Number(validTime) > dayjs().unix() ? (
<></>
) : (
<ValidateContent handleOk={handleOk}></ValidateContent>
)}
</>
),
okText: "确认",
okButtonProps: {
disabled: Number(validTime) <= dayjs().unix()
},
onOk: () => {
handleOk()
},
cancelText: "取消",
onCancel: () => {
handleCancel()
}
})
}
function handleCancel() {
removeCurrentDom()
isReady = false
isSelect = false
setIsCurrentDom(false)
instance.destroy()
}
function handleSetParent(event) {
const currentDom = document.querySelector(".codebox-current")
const parent = currentDom.parentElement
if (parent) {
removeCurrentDom()
parent.classList.add("codebox-current")
setTooltip()
}
event.stopPropagation()
}
function handleSetChild(event) {
const currentDom = document.querySelector(".codebox-current")
const child = currentDom.firstElementChild
if (child) {
removeCurrentDom()
child.classList.add("codebox-current")
setTooltip()
}
event.stopPropagation()
}
function handleSetPrev(event) {
const currentDom = document.querySelector(".codebox-current")
const previousSibling = currentDom.previousElementSibling
if (previousSibling) {
removeCurrentDom()
previousSibling.classList.add("codebox-current")
setTooltip()
}
event.stopPropagation()
}
function handleSetNext(event) {
const currentDom = document.querySelector(".codebox-current")
const nextSibling = currentDom.nextElementSibling
if (nextSibling) {
removeCurrentDom()
nextSibling.classList.add("codebox-current")
setTooltip()
}
event.stopPropagation()
}
return (
<ThemeProvider>
{contextHolder}
<StyleProvider container={document.getElementById(HOST_ID).shadowRoot}>
{isCurrentDom ? (
<div
className="codebox-tooltip"
style={{
position: "absolute",
top: `${rect.top}px`,
left: `${rect.left}px`,
width: "520px",
background: "#fff",
border: "1px solid #eee",
borderRadius: "5px"
}}>
<Flex wrap gap="small">
<Button
color="primary"
variant="text"
size="small"
icon={<CheckOutlined />}
onClick={handleConfirm}>
</Button>
<Button
color="default"
variant="text"
size="small"
icon={<UpSquareOutlined />}
onClick={handleSetParent}>
</Button>
<Button
color="default"
variant="text"
size="small"
icon={<DownSquareOutlined />}
onClick={handleSetChild}>
</Button>
<Button
color="default"
variant="text"
size="small"
icon={<LeftSquareOutlined />}
onClick={handleSetPrev}>
</Button>
<Button
color="default"
variant="text"
size="small"
icon={<RightSquareOutlined />}
onClick={handleSetNext}>
</Button>
<Button
color="danger"
variant="text"
size="small"
icon={<CloseOutlined />}
onClick={handleCancel}>
</Button>
</Flex>
</div>
) : (
<></>
)}
<CustomDomSelector />
</StyleProvider>
</ThemeProvider>
)

View File

@@ -1,7 +1,7 @@
{
"name": "code-box",
"displayName": "__MSG_extensionName__",
"version": "0.9.23",
"version": "0.9.24",
"description": "__MSG_extensionDescription__",
"author": "027xiguapi. <458813868@qq.com>",
"scripts": {

View File

@@ -5,7 +5,7 @@ import { sendToBackground } from "@plasmohq/messaging"
export function scrollToTop(element) {
window.scrollTo({
top: element.offsetTop,
top: element.offsetTop + 50,
behavior: "smooth" // 可选,平滑滚动
})
}