fix(css-editor): handle copy and paste get the plain text
Signed-off-by: Innei <i@innei.in>
This commit is contained in:
parent
cda226420f
commit
febc678834
|
|
@ -6,6 +6,7 @@ import { useLayoutEffect, useRef } from "react"
|
|||
import css from "shiki/langs/css.mjs"
|
||||
import githubDark from "shiki/themes/github-dark.mjs"
|
||||
import githubLight from "shiki/themes/github-light.mjs"
|
||||
import { useEventCallback } from "usehooks-ts"
|
||||
|
||||
import { shiki } from "~/components/ui/code-highlighter/shiki/shared"
|
||||
|
||||
|
|
@ -19,6 +20,7 @@ export const CSSEditor: Component<{
|
|||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
const isDark = useIsDark()
|
||||
const updateFnRef = useRef<() => void>()
|
||||
useLayoutEffect(() => {
|
||||
let dispose: () => void
|
||||
if (ref.current) {
|
||||
|
|
@ -32,7 +34,7 @@ export const CSSEditor: Component<{
|
|||
selection?.addRange(range)
|
||||
|
||||
ref.current.textContent = defaultValue ?? ""
|
||||
const { dispose: disposeShiki } = createPlainShiki(shiki).mount(ref.current, {
|
||||
const { dispose: disposeShiki, update } = createPlainShiki(shiki).mount(ref.current, {
|
||||
lang: "css",
|
||||
themes: {
|
||||
light: "github-light",
|
||||
|
|
@ -40,6 +42,7 @@ export const CSSEditor: Component<{
|
|||
},
|
||||
defaultTheme: isDark ? "dark" : "light",
|
||||
})
|
||||
updateFnRef.current = update
|
||||
dispose = disposeShiki
|
||||
}
|
||||
return () => dispose?.()
|
||||
|
|
@ -67,6 +70,50 @@ export const CSSEditor: Component<{
|
|||
})
|
||||
},
|
||||
})
|
||||
const handleCopy = useEventCallback((e: React.ClipboardEvent<HTMLDivElement>) => {
|
||||
e.preventDefault()
|
||||
e.clipboardData.setData("text/plain", ref.current?.textContent ?? "")
|
||||
})
|
||||
const handlePaste = useEventCallback((e: React.ClipboardEvent<HTMLDivElement>) => {
|
||||
e.preventDefault()
|
||||
const text = e.clipboardData.getData("text/plain")
|
||||
|
||||
const selection = window.getSelection()
|
||||
if (selection && selection.rangeCount > 0) {
|
||||
const range = selection.getRangeAt(0)
|
||||
// Delete the current selection
|
||||
range.deleteContents()
|
||||
// Insert the text at the current cursor position
|
||||
range.insertNode(document.createTextNode(text))
|
||||
|
||||
range.setStartAfter(range.endContainer)
|
||||
range.setEndAfter(range.endContainer)
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
}
|
||||
updateFnRef.current?.()
|
||||
onChange(ref.current?.textContent ?? "")
|
||||
// Focus the editor and set cursor to the end
|
||||
if (ref.current) {
|
||||
ref.current.focus()
|
||||
const selection = window.getSelection()
|
||||
const range = document.createRange()
|
||||
range.selectNodeContents(ref.current)
|
||||
range.collapse(false) // collapse to end
|
||||
selection?.removeAllRanges()
|
||||
selection?.addRange(range)
|
||||
|
||||
// Get selection position
|
||||
const rect = range.getBoundingClientRect()
|
||||
const containerRect = ref.current.getBoundingClientRect()
|
||||
const relativeBottom = rect.bottom - containerRect.top
|
||||
|
||||
// If out of view, scroll to the position
|
||||
if (relativeBottom > ref.current.clientHeight) {
|
||||
ref.current.scrollTop += relativeBottom - ref.current.clientHeight
|
||||
}
|
||||
}
|
||||
})
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
|
@ -76,10 +123,12 @@ export const CSSEditor: Component<{
|
|||
"focus:!bg-accent/5",
|
||||
"border border-border",
|
||||
"placeholder:text-theme-placeholder-text dark:bg-zinc-700/[0.15] dark:text-zinc-200",
|
||||
"hover:border-accent/60",
|
||||
"overflow-auto whitespace-pre hover:border-accent/60",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
onCopy={handleCopy}
|
||||
onPaste={handlePaste}
|
||||
contentEditable
|
||||
tabIndex={0}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -383,7 +383,9 @@ const CustomCSSModal = () => {
|
|||
variant="outline"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
|
||||
setUISetting("customCSS", initialCSS.current)
|
||||
|
||||
forceUpdate()
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@
|
|||
"appearance.theme.light": "亮色",
|
||||
"appearance.theme.system": "跟随系统",
|
||||
"appearance.thumbnail_ratio.description": "文章列表缩略图比例",
|
||||
"appearance.thumbnail_ratio.original": "原始图像",
|
||||
"appearance.thumbnail_ratio.original": "原始比例",
|
||||
"appearance.thumbnail_ratio.square": "正方形",
|
||||
"appearance.thumbnail_ratio.title": "缩略比例",
|
||||
"appearance.title": "外观",
|
||||
|
|
@ -155,7 +155,7 @@
|
|||
"integration.instapaper.password.label": "密码",
|
||||
"integration.instapaper.title": "Instapaper",
|
||||
"integration.instapaper.username.label": "Instapaper 用户名",
|
||||
"integration.obsidian.enable.description": "显示保存到Obsidian按钮(如果可用)",
|
||||
"integration.obsidian.enable.description": "显示保存到 Obsidian 按钮(如果可用)",
|
||||
"integration.obsidian.enable.label": "启用",
|
||||
"integration.obsidian.title": "Obsidian",
|
||||
"integration.obsidian.vaultPath.description": "你的 Obsidian 仓库的路径",
|
||||
|
|
@ -284,8 +284,8 @@
|
|||
"wallet.ranking.rank": "排名",
|
||||
"wallet.ranking.title": "Power 排名",
|
||||
"wallet.rewardDescription.description1": "每日奖励基于「用户等级」和「活跃度」两部分计算",
|
||||
"wallet.rewardDescription.description2": "用户等级: 由 Power 排行榜的排名决定",
|
||||
"wallet.rewardDescription.description3": "用户活跃度: 使用 Follow 功能可以提升活跃度,活跃度的奖励倍数范围是 1x ~ 10x",
|
||||
"wallet.rewardDescription.description2": "用户等级:由 Power 排行榜的排名决定",
|
||||
"wallet.rewardDescription.description3": "用户活跃度:使用 Follow 功能可以提升活跃度,活跃度的奖励倍数范围是 1x ~ 10x",
|
||||
"wallet.rewardDescription.level": "用户等级",
|
||||
"wallet.rewardDescription.percentage": "排名比例",
|
||||
"wallet.rewardDescription.reward": "奖励倍数",
|
||||
|
|
|
|||
Loading…
Reference in New Issue