Add Read "raw_content" contentEditable Element & Edit
This commit is contained in:
parent
99d63d4912
commit
945f8ce431
|
|
@ -100,7 +100,7 @@ input[type="checkbox"][role="switch"] {
|
||||||
background-color: var(--lighter-gray);
|
background-color: var(--lighter-gray);
|
||||||
transition: border .3s, background-color .3s;
|
transition: border .3s, background-color .3s;
|
||||||
|
|
||||||
&::before{
|
&::before {
|
||||||
width: 2em;
|
width: 2em;
|
||||||
height: 2em;
|
height: 2em;
|
||||||
content: '';
|
content: '';
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { sendToBackground } from "@plasmohq/messaging";
|
import { sendToBackground } from "@plasmohq/messaging";
|
||||||
import useForm from "~hooks/useForm";
|
import useForm from "~hooks/useForm";
|
||||||
import Tab from "~components/ui/tab";
|
import Tab from "~components/ui/tab";
|
||||||
import Form from "~components/ui/form";
|
import Form from "~components/ui/form";
|
||||||
|
import Article from "~components/ui/article";
|
||||||
import { add } from "~components/ui/message/utils";
|
import { add } from "~components/ui/message/utils";
|
||||||
|
|
||||||
|
import styles from "./read.module.less";
|
||||||
|
|
||||||
interface FormValue {
|
interface FormValue {
|
||||||
title: string;
|
title: string;
|
||||||
link: string;
|
link: string;
|
||||||
|
|
@ -14,6 +17,7 @@ interface FormValue {
|
||||||
image: string;
|
image: string;
|
||||||
sitename: string;
|
sitename: string;
|
||||||
tags: string;
|
tags: string;
|
||||||
|
raw_content?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getInfo = async () => {
|
const getInfo = async () => {
|
||||||
|
|
@ -46,6 +50,7 @@ const submitForm = (body: FormValue) => {
|
||||||
|
|
||||||
function Read() {
|
function Read() {
|
||||||
const formRef = useRef<HTMLFormElement>();
|
const formRef = useRef<HTMLFormElement>();
|
||||||
|
const [htmlContent, setHtmlContent] = useState("");
|
||||||
|
|
||||||
const { bindInput, setValues, onSubmit } = useForm<FormValue>({
|
const { bindInput, setValues, onSubmit } = useForm<FormValue>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
|
|
@ -60,13 +65,14 @@ function Read() {
|
||||||
}
|
}
|
||||||
|
|
||||||
setValues(res);
|
setValues(res);
|
||||||
|
res.raw_content && setHtmlContent(res.raw_content);
|
||||||
})
|
})
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tab>
|
<Tab className={styles.read}>
|
||||||
<Tab.Body>
|
<Tab.Body>
|
||||||
<Form ref={formRef} onSubmit={onSubmit(submitForm)}>
|
<Form ref={formRef} onSubmit={onSubmit((values) => submitForm({ ...values, raw_content: htmlContent }))}>
|
||||||
<input {...bindInput("title")} required placeholder="标题" />
|
<input {...bindInput("title")} required placeholder="标题" />
|
||||||
<input {...bindInput("link")} required placeholder="链接" />
|
<input {...bindInput("link")} required placeholder="链接" />
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -94,6 +100,7 @@ function Read() {
|
||||||
<input {...bindInput("tags")} placeholder="标签" />
|
<input {...bindInput("tags")} placeholder="标签" />
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
<Article className={styles.article} value={htmlContent} onChange={setHtmlContent} />
|
||||||
</Tab.Body>
|
</Tab.Body>
|
||||||
<Tab.Footer>
|
<Tab.Footer>
|
||||||
<button onClick={() => formRef.current?.requestSubmit()}>提交</button>
|
<button onClick={() => formRef.current?.requestSubmit()}>提交</button>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
.read {
|
||||||
|
.article {
|
||||||
|
padding: .75em;
|
||||||
|
margin-top: 1em;
|
||||||
|
border-radius: .5em;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { sendToBackground } from "@plasmohq/messaging";
|
import { sendToBackground } from "@plasmohq/messaging";
|
||||||
import useForm from "~hooks/useForm";
|
import useForm from "~hooks/useForm";
|
||||||
import Tab from "~components/ui/tab";
|
import Tab from "~components/ui/tab";
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
.article {
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #28b9be;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 1.5em;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6, p {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
color: #fff;
|
||||||
|
padding: .5em;
|
||||||
|
overflow: auto;
|
||||||
|
border-radius: .5em;
|
||||||
|
background-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
:not(pre) > code {
|
||||||
|
color: brown;
|
||||||
|
padding: 0 .5em;
|
||||||
|
border-radius: .5em;
|
||||||
|
display: inline-block;
|
||||||
|
background-color: antiquewhite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { clsn } from "~utils";
|
||||||
|
|
||||||
|
import styles from "./article.module.less";
|
||||||
|
|
||||||
|
interface ArticleProps {
|
||||||
|
className?: string;
|
||||||
|
value: string | number;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Article({ className, value, onChange }: ArticleProps) {
|
||||||
|
const articleRef = useRef<HTMLElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!articleRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (articleRef.current.innerHTML !== String(value)) {
|
||||||
|
articleRef.current.innerHTML = String(value);
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
ref={articleRef}
|
||||||
|
className={clsn(styles.article, className)}
|
||||||
|
contentEditable
|
||||||
|
onInput={(ev) => onChange(articleRef.current.innerHTML)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Article;
|
||||||
|
|
@ -1,16 +1,21 @@
|
||||||
|
import { clsn } from "~utils";
|
||||||
import { IconBack } from "~assets/icons";
|
import { IconBack } from "~assets/icons";
|
||||||
import type { ReactNode, PropsWithChildren } from "react";
|
import type { ReactNode, PropsWithChildren } from "react";
|
||||||
|
|
||||||
import styles from "./tab.module.less";
|
import styles from "./tab.module.less";
|
||||||
|
|
||||||
|
interface TabProps extends PropsWithChildren {
|
||||||
|
className: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
title: ReactNode;
|
title: ReactNode;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Tab({ children }: PropsWithChildren) {
|
function Tab({ className, children }: TabProps) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.tab}>
|
<div className={clsn(styles.tab, className)}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { formatHTML } from "~utils/html";
|
||||||
|
|
||||||
const getTitle = () => {
|
const getTitle = () => {
|
||||||
const title = document.title;
|
const title = document.title;
|
||||||
const metaTitle = document.querySelector<HTMLMetaElement>(`meta[name="title"]`);
|
const metaTitle = document.querySelector<HTMLMetaElement>(`meta[name="title"]`);
|
||||||
|
|
@ -114,6 +116,92 @@ const getTags = () => {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getHTML = () => {
|
||||||
|
// 微信
|
||||||
|
if (location.host === "mp.weixin.qq.com") {
|
||||||
|
console.log("微信");
|
||||||
|
|
||||||
|
return document.querySelector(".rich_media_content").innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 腾讯云开发者社区
|
||||||
|
if (location.host === "cloud.tencent.com") {
|
||||||
|
console.log("腾讯云");
|
||||||
|
|
||||||
|
return formatHTML(document.querySelector(".rno-markdown").innerHTML, (doc) => {
|
||||||
|
// 替换掉腾讯云自己的链接
|
||||||
|
const qcloudLinkEl = doc.querySelectorAll("a");
|
||||||
|
qcloudLinkEl.forEach((el) => {
|
||||||
|
if (el.href.includes("qcloud")) {
|
||||||
|
el.parentNode.replaceChild(document.createTextNode(el.innerText), el);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mdTextEl = doc.querySelectorAll(".rno-markdown__textlink-new") as NodeListOf<HTMLElement>;
|
||||||
|
mdTextEl.forEach((el) => {
|
||||||
|
el.parentNode.replaceChild(document.createTextNode(el.innerText), el);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 掘金
|
||||||
|
const itemPropArticle = document.querySelector("[itemprop=articleBody] .markdown-body");
|
||||||
|
|
||||||
|
if (itemPropArticle) {
|
||||||
|
console.log("掘金");
|
||||||
|
|
||||||
|
return formatHTML(itemPropArticle.innerHTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSDN
|
||||||
|
const contentViews = document.getElementById("content_views");
|
||||||
|
|
||||||
|
if (contentViews) {
|
||||||
|
console.log("CSDN");
|
||||||
|
|
||||||
|
return formatHTML(contentViews.innerHTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用模式
|
||||||
|
const articleEl = document.querySelector("article");
|
||||||
|
|
||||||
|
if (articleEl) {
|
||||||
|
console.log("文章标签");
|
||||||
|
|
||||||
|
return formatHTML(articleEl.innerHTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试匹配到文章
|
||||||
|
const firstParagraph = document.querySelector("p");
|
||||||
|
|
||||||
|
let continueFindCorrectParent = true;
|
||||||
|
let parentEl;
|
||||||
|
let limitCount = 0;
|
||||||
|
|
||||||
|
if (firstParagraph) {
|
||||||
|
while (continueFindCorrectParent) {
|
||||||
|
if (limitCount > 20) {
|
||||||
|
continueFindCorrectParent = false;
|
||||||
|
throw new Error("DOM 层级检测遍历次数过多");
|
||||||
|
}
|
||||||
|
|
||||||
|
limitCount += 1;
|
||||||
|
|
||||||
|
parentEl = parentEl ? parentEl.parentElement : firstParagraph.parentElement;
|
||||||
|
|
||||||
|
// 这个父下面有多个疑似正文的元素
|
||||||
|
if (Array.from(parentEl.querySelectorAll("h2, h3, h4, p")).length > 1) {
|
||||||
|
console.log(parentEl, `匹配结束,向上遍历 ${limitCount} 次`);
|
||||||
|
continueFindCorrectParent = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatHTML(parentEl.innerHTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener((req, sender, send) => {
|
chrome.runtime.onMessage.addListener((req, sender, send) => {
|
||||||
if (req.type === "toolbox:getInfo") {
|
if (req.type === "toolbox:getInfo") {
|
||||||
send({
|
send({
|
||||||
|
|
@ -124,6 +212,7 @@ chrome.runtime.onMessage.addListener((req, sender, send) => {
|
||||||
image: getImage(),
|
image: getImage(),
|
||||||
sitename: getSiteName(),
|
sitename: getSiteName(),
|
||||||
tags: getTags(),
|
tags: getTags(),
|
||||||
|
raw_content: getHTML(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
// 删除标题里面没用的内容
|
||||||
|
const pickTextNode = (el: HTMLElement) => {
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
|
||||||
|
while (el.firstChild) {
|
||||||
|
// 检查当前子节点是否是文本节点
|
||||||
|
if (el.firstChild.nodeType === Node.TEXT_NODE) {
|
||||||
|
// 如果是文本节点,将其移动到 DocumentFragment 中
|
||||||
|
fragment.appendChild(el.firstChild);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
el.removeChild(el.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fragment.textContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除里面的属性
|
||||||
|
const removeAttributes = (el: HTMLElement) => {
|
||||||
|
const attrs = el.attributes;
|
||||||
|
|
||||||
|
Array.from(attrs).forEach((attr) => {
|
||||||
|
el.removeAttribute(attr.name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化 HTML 内容
|
||||||
|
export const formatHTML = (html: string, extraFormatter?: (doc: Document) => void) => {
|
||||||
|
const nextDocument = document.implementation.createHTMLDocument();
|
||||||
|
|
||||||
|
nextDocument.documentElement.innerHTML = html;
|
||||||
|
|
||||||
|
// 删除标题内无效元素
|
||||||
|
const titleEl = nextDocument.querySelectorAll("h2, h3, h4, h5, h6") as NodeListOf<HTMLElement>;
|
||||||
|
titleEl.forEach((el) => {
|
||||||
|
// 遍历并移除所有属性
|
||||||
|
removeAttributes(el);
|
||||||
|
|
||||||
|
el.innerHTML = pickTextNode(el);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 优化 p 标签
|
||||||
|
const paraEl = nextDocument.querySelectorAll("p") as NodeListOf<HTMLElement>;
|
||||||
|
paraEl.forEach((el) => {
|
||||||
|
// 删除空的 p 标签
|
||||||
|
if (!el.innerHTML.trim()) {
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 遍历并移除所有属性
|
||||||
|
removeAttributes(el);
|
||||||
|
|
||||||
|
// 删除 p 里面的 span 替换成普通 Text,应该没用的
|
||||||
|
const spanEl = el.querySelectorAll("span");
|
||||||
|
spanEl.forEach((el) => {
|
||||||
|
el.parentNode.replaceChild(document.createTextNode(el.innerText), el);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 删除 figure 标签
|
||||||
|
const figureEl = nextDocument.querySelectorAll("figure") as NodeListOf<HTMLElement>;
|
||||||
|
figureEl.forEach((el) => {
|
||||||
|
const img = el.querySelector("img");
|
||||||
|
|
||||||
|
if (img) {
|
||||||
|
el.innerHTML = "";
|
||||||
|
el.appendChild(img);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 删除 style 标签
|
||||||
|
const stylesEl = nextDocument.querySelectorAll("style");
|
||||||
|
stylesEl.forEach((el) => {
|
||||||
|
el.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 提取 pre 下面的内容
|
||||||
|
const preEl = nextDocument.querySelectorAll("pre");
|
||||||
|
preEl.forEach((el) => {
|
||||||
|
removeAttributes(el);
|
||||||
|
|
||||||
|
// hljs / prism
|
||||||
|
const codeEl = el.querySelector("code") as HTMLElement;
|
||||||
|
|
||||||
|
if (codeEl) {
|
||||||
|
const nextCodeEl = document.createElement("code");
|
||||||
|
nextCodeEl.innerText = codeEl.innerText;
|
||||||
|
|
||||||
|
el.innerHTML = null;
|
||||||
|
el.appendChild(nextCodeEl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (extraFormatter) {
|
||||||
|
extraFormatter(nextDocument);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextDocument.documentElement.innerHTML;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue