mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-04-22 11:57:29 +08:00
Merge pull request #244 from nagisa77/codex/add-copy-button-to-markdown-code-blocks
This commit is contained in:
@@ -101,6 +101,24 @@ body {
|
|||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
line-height: 0.8;
|
line-height: 0.8;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-code-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-code-btn:hover {
|
||||||
|
background-color: var(--primary-color-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-content-text code {
|
.info-content-text code {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-content-text" v-html="renderMarkdown(comment.text)" @click="handleImageClick"></div>
|
<div class="info-content-text" v-html="renderMarkdown(comment.text)" @click="handleContentClick"></div>
|
||||||
<div class="article-footer-container">
|
<div class="article-footer-container">
|
||||||
<ReactionsGroup v-model="comment.reactions" content-type="comment" :content-id="comment.id">
|
<ReactionsGroup v-model="comment.reactions" content-type="comment" :content-id="comment.id">
|
||||||
<div class="make-reaction-item comment-reaction" @click="toggleEditor">
|
<div class="make-reaction-item comment-reaction" @click="toggleEditor">
|
||||||
@@ -69,7 +69,7 @@ import { ref, watch, computed } from 'vue'
|
|||||||
import VueEasyLightbox from 'vue-easy-lightbox'
|
import VueEasyLightbox from 'vue-easy-lightbox'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import CommentEditor from './CommentEditor.vue'
|
import CommentEditor from './CommentEditor.vue'
|
||||||
import { renderMarkdown } from '../utils/markdown'
|
import { renderMarkdown, handleMarkdownClick } from '../utils/markdown'
|
||||||
import TimeManager from '../utils/time'
|
import TimeManager from '../utils/time'
|
||||||
import BaseTimeline from './BaseTimeline.vue'
|
import BaseTimeline from './BaseTimeline.vue'
|
||||||
import { API_BASE_URL, toast } from '../main'
|
import { API_BASE_URL, toast } from '../main'
|
||||||
@@ -196,7 +196,8 @@ const CommentItem = {
|
|||||||
toast.success('已复制')
|
toast.success('已复制')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const handleImageClick = e => {
|
const handleContentClick = e => {
|
||||||
|
handleMarkdownClick(e)
|
||||||
if (e.target.tagName === 'IMG') {
|
if (e.target.tagName === 'IMG') {
|
||||||
const container = e.target.parentNode
|
const container = e.target.parentNode
|
||||||
const imgs = [...container.querySelectorAll('img')].map(i => i.src)
|
const imgs = [...container.querySelectorAll('img')].map(i => i.src)
|
||||||
@@ -205,7 +206,7 @@ const CommentItem = {
|
|||||||
lightboxVisible.value = true
|
lightboxVisible.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { showReplies, toggleReplies, showEditor, toggleEditor, submitReply, copyCommentLink, renderMarkdown, isWaitingForReply, commentMenuItems, deleteComment, lightboxVisible, lightboxIndex, lightboxImgs, handleImageClick, loggedIn }
|
return { showReplies, toggleReplies, showEditor, toggleEditor, submitReply, copyCommentLink, renderMarkdown, isWaitingForReply, commentMenuItems, deleteComment, lightboxVisible, lightboxIndex, lightboxImgs, handleContentClick, loggedIn }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,39 @@
|
|||||||
import MarkdownIt from 'markdown-it'
|
import MarkdownIt from 'markdown-it'
|
||||||
|
import hljs from 'highlight.js'
|
||||||
|
import 'highlight.js/styles/github.css'
|
||||||
|
import { toast } from '../main'
|
||||||
|
|
||||||
const md = new MarkdownIt({
|
const md = new MarkdownIt({
|
||||||
html: false,
|
html: false,
|
||||||
linkify: true,
|
linkify: true,
|
||||||
breaks: true
|
breaks: true,
|
||||||
|
highlight: (str, lang) => {
|
||||||
|
let code = ''
|
||||||
|
if (lang && hljs.getLanguage(lang)) {
|
||||||
|
code = hljs.highlight(str, { language: lang, ignoreIllegals: true }).value
|
||||||
|
} else {
|
||||||
|
code = hljs.highlightAuto(str).value
|
||||||
|
}
|
||||||
|
return `<pre class="code-block"><button class="copy-code-btn">复制</button><code class="hljs language-${lang || ''}">${code}</code></pre>`
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export function renderMarkdown(text) {
|
export function renderMarkdown(text) {
|
||||||
return md.render(text || '')
|
return md.render(text || '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function handleMarkdownClick(e) {
|
||||||
|
if (e.target.classList.contains('copy-code-btn')) {
|
||||||
|
const pre = e.target.closest('pre')
|
||||||
|
const codeEl = pre && pre.querySelector('code')
|
||||||
|
if (codeEl) {
|
||||||
|
navigator.clipboard.writeText(codeEl.innerText).then(() => {
|
||||||
|
toast.success('已复制')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function stripMarkdown(text) {
|
export function stripMarkdown(text) {
|
||||||
const html = md.render(text || '')
|
const html = md.render(text || '')
|
||||||
const el = document.createElement('div')
|
const el = document.createElement('div')
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<div class="about-tabs-item-label">{{ tab.label }}</div>
|
<div class="about-tabs-item-label">{{ tab.label }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="about-content" v-html="renderMarkdown(content)"></div>
|
<div class="about-content" v-html="renderMarkdown(content)" @click="handleContentClick"></div>
|
||||||
<div class="about-loading" v-if="isFetching">
|
<div class="about-loading" v-if="isFetching">
|
||||||
<l-hatch-spinner size="100" stroke="10" speed="1" color="var(--primary-color)" />
|
<l-hatch-spinner size="100" stroke="10" speed="1" color="var(--primary-color)" />
|
||||||
</div>
|
</div>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { renderMarkdown } from '../utils/markdown'
|
import { renderMarkdown, handleMarkdownClick } from '../utils/markdown'
|
||||||
import { hatch } from 'ldrs'
|
import { hatch } from 'ldrs'
|
||||||
hatch.register()
|
hatch.register()
|
||||||
|
|
||||||
@@ -61,7 +61,11 @@ export default {
|
|||||||
loadContent(tabs[0].file)
|
loadContent(tabs[0].file)
|
||||||
})
|
})
|
||||||
|
|
||||||
return { tabs, selectedTab, content, renderMarkdown, selectTab, isFetching }
|
const handleContentClick = e => {
|
||||||
|
handleMarkdownClick(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { tabs, selectedTab, content, renderMarkdown, selectTab, isFetching, handleContentClick }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
<div class="user-name">{{ author.username }}</div>
|
<div class="user-name">{{ author.username }}</div>
|
||||||
<div class="post-time">{{ postTime }}</div>
|
<div class="post-time">{{ postTime }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-content-text" v-html="renderMarkdown(postContent)" @click="handleImageClick"></div>
|
<div class="info-content-text" v-html="renderMarkdown(postContent)" @click="handleContentClick"></div>
|
||||||
|
|
||||||
<div class="article-footer-container">
|
<div class="article-footer-container">
|
||||||
<ReactionsGroup v-model="postReactions" content-type="post" :content-id="postId">
|
<ReactionsGroup v-model="postReactions" content-type="post" :content-id="postId">
|
||||||
@@ -113,7 +113,7 @@ import ArticleTags from '../components/ArticleTags.vue'
|
|||||||
import ArticleCategory from '../components/ArticleCategory.vue'
|
import ArticleCategory from '../components/ArticleCategory.vue'
|
||||||
import ReactionsGroup from '../components/ReactionsGroup.vue'
|
import ReactionsGroup from '../components/ReactionsGroup.vue'
|
||||||
import DropdownMenu from '../components/DropdownMenu.vue'
|
import DropdownMenu from '../components/DropdownMenu.vue'
|
||||||
import { renderMarkdown } from '../utils/markdown'
|
import { renderMarkdown, handleMarkdownClick } from '../utils/markdown'
|
||||||
import { API_BASE_URL, toast } from '../main'
|
import { API_BASE_URL, toast } from '../main'
|
||||||
import { getToken, authState } from '../utils/auth'
|
import { getToken, authState } from '../utils/auth'
|
||||||
import TimeManager from '../utils/time'
|
import TimeManager from '../utils/time'
|
||||||
@@ -237,7 +237,8 @@ export default {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleImageClick = e => {
|
const handleContentClick = e => {
|
||||||
|
handleMarkdownClick(e)
|
||||||
if (e.target.tagName === 'IMG') {
|
if (e.target.tagName === 'IMG') {
|
||||||
const container = e.target.parentNode
|
const container = e.target.parentNode
|
||||||
const imgs = [...container.querySelectorAll('img')].map(i => i.src)
|
const imgs = [...container.querySelectorAll('img')].map(i => i.src)
|
||||||
@@ -511,7 +512,7 @@ export default {
|
|||||||
lightboxVisible,
|
lightboxVisible,
|
||||||
lightboxIndex,
|
lightboxIndex,
|
||||||
lightboxImgs,
|
lightboxImgs,
|
||||||
handleImageClick,
|
handleContentClick,
|
||||||
isMobile
|
isMobile
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user