Skip to content

Commit 8f29aa0

Browse files
committed
basic code modals
1 parent 99c0cec commit 8f29aa0

File tree

9 files changed

+172
-6
lines changed

9 files changed

+172
-6
lines changed

docs/guide/markdown.md

+23
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,29 @@ const line3 = 'This is line 3'
597597
const line4 = 'This is line 4'
598598
```
599599

600+
## Code Modal
601+
602+
[//]: # (todo - expand on docs )
603+
604+
````md:modal
605+
```ts {1}
606+
// line-numbers is disabled by default
607+
const line2 = 'This is line 2'
608+
const line3 = 'This is line 3'
609+
```
610+
611+
```ts:line-numbers {1}
612+
// line-numbers is enabled
613+
const line2 = 'This is line 2'
614+
const line3 = 'This is line 3'
615+
```
616+
617+
```ts:line-numbers=2 {1}
618+
// line-numbers is enabled and start from 2
619+
const line3 = 'This is line 3'
620+
const line4 = 'This is line 4'
621+
```
622+
````
600623
## Import Code Snippets
601624

602625
You can import code snippets from existing files via following syntax:
+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { inBrowser } from 'vitepress'
2+
3+
export function useCodeModal() {
4+
if (inBrowser) {
5+
window.addEventListener('click', (e) => {
6+
const el = e.target as HTMLElement
7+
8+
if (el.matches('div[class*="language-"] > button.modal')) {
9+
const parent = el.parentElement
10+
const sibling = el.nextElementSibling
11+
if (!parent || !sibling) {
12+
return
13+
}
14+
15+
sibling.classList.add('open')
16+
}
17+
18+
if (el.matches('div[class*="language-"] > div.modal-container')) {
19+
el.classList.remove('open')
20+
}
21+
})
22+
23+
window.addEventListener('keydown', (ev) => {
24+
if (ev.key == 'Escape') {
25+
let modal = window.document.querySelector(
26+
'div[class*="language-"] > div.modal-container.open'
27+
)
28+
29+
if (!modal) {
30+
return
31+
}
32+
33+
modal.classList.remove('open')
34+
}
35+
})
36+
}
37+
}

src/client/app/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { usePrefetch } from './composables/preFetch'
1717
import { dataSymbol, initData, siteDataRef, useData } from './data'
1818
import { RouterSymbol, createRouter, scrollTo, type Router } from './router'
1919
import { inBrowser, pathToFile } from './utils'
20+
import { useCodeModal } from './composables/codeModal'
2021

2122
function resolveThemeExtends(theme: typeof RawTheme): typeof RawTheme {
2223
if (theme.extends) {
@@ -57,6 +58,8 @@ const VitePressApp = defineComponent({
5758
useCopyCode()
5859
// setup global code groups handler
5960
useCodeGroups()
61+
// setup global code modal handler
62+
useCodeModal()
6063

6164
if (Theme.setup) Theme.setup()
6265
return () => h(Theme.Layout!)

src/client/theme-default/styles/components/vp-code.css

+43
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,46 @@
55
html:not(.dark) .vp-code span {
66
color: var(--shiki-light, inherit);
77
}
8+
9+
.vp-code ~ .modal-container:not(.open) {
10+
display: none;
11+
}
12+
13+
.vp-code ~ .modal-container {
14+
position: fixed;
15+
16+
z-index: 100;
17+
18+
top: 0;
19+
left: 0;
20+
width: 100vw;
21+
height: 100vh;
22+
23+
display: flex;
24+
justify-content: center;
25+
align-items: center;
26+
27+
background-color: rgba(255, 255, 255, 0.02);
28+
backdrop-filter: blur(2px);
29+
}
30+
31+
html:not(.dark) .vp-code ~ .modal-container {
32+
background-color: rgba(0, 0, 0, 0.1);
33+
}
34+
35+
.vp-code ~ .modal-container [class*='language-'] {
36+
position: relative;
37+
margin: 20px;
38+
39+
max-width: 90%;
40+
width: min-content;
41+
42+
border-radius: 8px;
43+
44+
background-color: var(--vp-code-block-bg);
45+
box-shadow: var(--vp-shadow-2);
46+
}
47+
48+
.vp-code ~ .modal-container [class*='language-']:hover span.lang {
49+
opacity: 0;
50+
}

src/client/theme-default/styles/components/vp-doc.css

+14-6
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,8 @@
429429
color 0.5s;
430430
}
431431

432-
.vp-doc [class*='language-'] > button.copy {
432+
.vp-doc [class*='language-'] > button.copy,
433+
.vp-doc [class*='language-'] > button.modal {
433434
/*rtl:ignore*/
434435
direction: ltr;
435436
position: absolute;
@@ -454,13 +455,20 @@
454455
opacity 0.25s;
455456
}
456457

458+
.vp-doc [class*='language-'] > button.modal {
459+
top: 64px;
460+
}
461+
457462
.vp-doc [class*='language-']:hover > button.copy,
458-
.vp-doc [class*='language-'] > button.copy:focus {
463+
.vp-doc [class*='language-'] > button.copy:focus,
464+
.vp-doc [class*='language-']:hover > button.modal,
465+
.vp-doc [class*='language-'] > button.modal:focus {
459466
opacity: 1;
460467
}
461468

462469
.vp-doc [class*='language-'] > button.copy:hover,
463-
.vp-doc [class*='language-'] > button.copy.copied {
470+
.vp-doc [class*='language-'] > button.copy.copied,
471+
.vp-doc [class*='language-'] > button.modal:hover {
464472
border-color: var(--vp-code-copy-code-hover-border-color);
465473
background-color: var(--vp-code-copy-code-hover-bg);
466474
}
@@ -498,7 +506,7 @@
498506
content: var(--vp-code-copy-copied-text-content);
499507
}
500508

501-
.vp-doc [class*='language-'] > span.lang {
509+
.vp-doc [class*='language-'] span.lang {
502510
position: absolute;
503511
top: 2px;
504512
/*rtl:ignore*/
@@ -512,8 +520,8 @@
512520
opacity 0.4s;
513521
}
514522

515-
.vp-doc [class*='language-']:hover > button.copy + span.lang,
516-
.vp-doc [class*='language-'] > button.copy:focus + span.lang {
523+
.vp-doc [class*='language-']:hover button.copy + span.lang,
524+
.vp-doc [class*='language-'] button.copy:focus + span.lang {
517525
opacity: 0;
518526
}
519527

src/node/markdown/markdown.ts

+13
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { linkPlugin } from './plugins/link'
3737
import { preWrapperPlugin } from './plugins/preWrapper'
3838
import { restoreEntities } from './plugins/restoreEntities'
3939
import { snippetPlugin } from './plugins/snippet'
40+
import { codeModalPlugin } from './plugins/codeModal'
4041

4142
export type { Header } from '../shared'
4243

@@ -116,6 +117,16 @@ export interface MarkdownOptions extends Options {
116117
* @default 'Copy Code'
117118
*/
118119
codeCopyButtonTitle?: string
120+
/**
121+
* Show an additional button to open a fullscreen modal in code blocks
122+
* @default false
123+
*/
124+
codeModal?: boolean
125+
/**
126+
* The tooltip text for the modal button in code blocks
127+
* @default 'Open Modal'
128+
*/
129+
codeModalButtonTitle?: string
119130

120131
/* ==================== Markdown It Plugins ==================== */
121132

@@ -201,6 +212,7 @@ export const createMarkdownRenderer = async (
201212
): Promise<MarkdownRenderer> => {
202213
const theme = options.theme ?? { light: 'github-light', dark: 'github-dark' }
203214
const codeCopyButtonTitle = options.codeCopyButtonTitle || 'Copy Code'
215+
const codeModalButtonTitle = options.codeModalButtonTitle || 'Open Modal'
204216
const hasSingleTheme = typeof theme === 'string' || 'name' in theme
205217

206218
const md = MarkdownIt({
@@ -230,6 +242,7 @@ export const createMarkdownRenderer = async (
230242
base
231243
)
232244
.use(lineNumberPlugin, options.lineNumbers)
245+
.use(codeModalPlugin, options.codeModal, { codeModalButtonTitle })
233246

234247
if (options.gfmAlerts !== false) {
235248
md.use(gitHubAlertsPlugin)
+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// markdown-it plugin for generating line numbers.
2+
// It depends on preWrapper plugin.
3+
4+
import type MarkdownIt from 'markdown-it'
5+
import type { MarkdownOptions } from '../markdown'
6+
7+
export const codeModalPlugin = (
8+
md: MarkdownIt,
9+
enable = false,
10+
options: MarkdownOptions = {}
11+
) => {
12+
const fence = md.renderer.rules.fence!
13+
md.renderer.rules.fence = (...args) => {
14+
const rawCode = fence(...args)
15+
16+
const [tokens, idx] = args
17+
const info = tokens[idx].info
18+
19+
if (
20+
(!enable && !/:modal($| |=)/.test(info)) ||
21+
(enable && /:no-modal($| )/.test(info))
22+
) {
23+
return rawCode
24+
}
25+
26+
const code =
27+
`<button title="${options.codeModalButtonTitle}" class="modal"></button>` +
28+
'<div class="modal-container">' +
29+
fence(...args) +
30+
'</div>'
31+
32+
let end = rawCode.lastIndexOf('</div>')
33+
34+
return rawCode.substring(0, end) + code + '</div>'
35+
}
36+
}

src/node/markdown/plugins/highlight.ts

+2
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export async function highlight(
9494
const vueRE = /-vue$/
9595
const lineNoStartRE = /=(\d*)/
9696
const lineNoRE = /:(no-)?line-numbers(=\d*)?$/
97+
const modalNoRE = /:(no-)?modal(=\d*)?$/
9798
const mustacheRE = /\{\{.*?\}\}/g
9899

99100
return (str: string, lang: string, attrs: string) => {
@@ -102,6 +103,7 @@ export async function highlight(
102103
lang
103104
.replace(lineNoStartRE, '')
104105
.replace(lineNoRE, '')
106+
.replace(modalNoRE, '')
105107
.replace(vueRE, '')
106108
.toLowerCase() || defaultLang
107109

src/node/markdown/plugins/preWrapper.ts

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ function extractLang(info: string) {
4747
.trim()
4848
.replace(/=(\d*)/, '')
4949
.replace(/:(no-)?line-numbers({| |$|=\d*).*/, '')
50+
.replace(/:(no-)?modal({| |$|=\d*).*/, '')
5051
.replace(/(-vue|{| ).*$/, '')
5152
.replace(/^vue-html$/, 'template')
5253
.replace(/^ansi$/, '')

0 commit comments

Comments
 (0)