Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: open code block as modal #3877

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions docs/en/guide/markdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,51 @@ const line3 = 'This is line 3'
const line4 = 'This is line 4'
```

## Code Modal

You can enable codeModals for each code blocks via config:

```js
export default {
markdown: {
codeModal: true
}
}
```

Please see [`markdown` options](../reference/site-config#markdown) for more details.

You can add `:modal` / `:no-modal` mark in your fenced code blocks to override the value set in config.


**Input**

````md:modal
```js:modal
export default {
data () {
return {
msg: 'Code with very long lines can often be hard to read in code blocks. Code modals can make seeing the whole content possible.',
lorem: 'ipsum',
}
}
}
```
````

**Output**

```js:modal
export default {
data () {
return {
msg: 'Code with very long lines can often be hard to read in code blocks. Code modals can make seeing the whole content possible.',
lorem: 'ipsum',
}
}
}
```

## Import Code Snippets

You can import code snippets from existing files via following syntax:
Expand Down
51 changes: 51 additions & 0 deletions src/client/app/composables/codeModal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { inBrowser } from 'vitepress'

export function useCodeModal() {
if (inBrowser) {
window.addEventListener('click', (e) => {
const el = e.target as HTMLElement

if (el.matches('div[class*="language-"] > button.modal')) {
//remove focus from button
el.blur()

const parent = el.parentElement
const sibling = el.nextElementSibling
if (!parent || !sibling) {
return
}

sibling.classList.add('open')
}

if (
el.matches('div[class*="language-"] div.modal-container button.close')
) {
const parent = el.parentElement?.parentElement
if (!parent) {
return
}

parent.classList.remove('open')
}

if (el.matches('div[class*="language-"] > div.modal-container')) {
el.classList.remove('open')
}
})

window.addEventListener('keydown', (ev) => {
if (ev.key == 'Escape') {
let modal = window.document.querySelector(
'div[class*="language-"] > div.modal-container.open'
)

if (!modal) {
return
}

modal.classList.remove('open')
}
})
}
}
3 changes: 3 additions & 0 deletions src/client/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { usePrefetch } from './composables/preFetch'
import { dataSymbol, initData, siteDataRef, useData } from './data'
import { RouterSymbol, createRouter, scrollTo, type Router } from './router'
import { inBrowser, pathToFile } from './utils'
import { useCodeModal } from './composables/codeModal'

function resolveThemeExtends(theme: typeof RawTheme): typeof RawTheme {
if (theme.extends) {
Expand Down Expand Up @@ -57,6 +58,8 @@ const VitePressApp = defineComponent({
useCopyCode()
// setup global code groups handler
useCodeGroups()
// setup global code modal handler
useCodeModal()

if (Theme.setup) Theme.setup()
return () => h(Theme.Layout!)
Expand Down
43 changes: 43 additions & 0 deletions src/client/theme-default/styles/components/vp-code.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,46 @@
html:not(.dark) .vp-code span {
color: var(--shiki-light, inherit);
}

.vp-code ~ .modal-container:not(.open) {
display: none;
}

.vp-code ~ .modal-container {
position: fixed;

z-index: 100;

top: 0;
left: 0;
width: 100vw;
height: 100vh;

display: flex;
justify-content: center;
align-items: center;

background-color: rgba(255, 255, 255, 0.02);
backdrop-filter: blur(2px);
}

html:not(.dark) .vp-code ~ .modal-container {
background-color: rgba(0, 0, 0, 0.1);
}

.vp-code ~ .modal-container [class*='language-'] {
position: relative;
margin: 20px;

max-width: 90%;
width: min-content;

border-radius: 8px;

background-color: var(--vp-code-block-bg);
box-shadow: var(--vp-shadow-2);
}

.vp-code ~ .modal-container [class*='language-']:hover span.lang {
opacity: 0;
}
44 changes: 37 additions & 7 deletions src/client/theme-default/styles/components/vp-doc.css
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,10 @@
opacity 0.35s;
}

.vp-doc [class*='language-']:hover .has-focused-lines .line:not(.has-focus) {
.vp-doc
[class*='language-']:hover:not(:has(.modal-container:hover))
.has-focused-lines
.line:not(.has-focus) {
filter: blur(0);
opacity: 1;
}
Expand Down Expand Up @@ -437,7 +440,9 @@
color 0.5s;
}

.vp-doc [class*='language-'] > button.copy {
.vp-doc [class*='language-'] > button.copy,
.vp-doc [class*='language-'] > button.modal,
.vp-doc [class*='language-'] > button.close {
/*rtl:ignore*/
direction: ltr;
position: absolute;
Expand All @@ -462,13 +467,35 @@
opacity 0.25s;
}

.vp-doc [class*='language-']:hover > button.copy,
.vp-doc [class*='language-'] > button.copy:focus {
.vp-doc [class*='language-'] > button.modal {
top: 64px;
background-image: var(--vp-icon-expand);
}

.vp-doc [class*='language-'] > button.close {
top: 64px;
background-image: var(--vp-icon-close);
}

.vp-doc
[class*='language-']:hover:not(:has(.modal-container:hover))
> button.copy,
.vp-doc [class*='language-'] > button.copy:focus,
.vp-doc
[class*='language-']:hover:not(:has(.modal-container:hover))
> button.modal,
.vp-doc [class*='language-'] > button.modal:focus,
.vp-doc
[class*='language-']:hover:not(:has(.modal-container:hover))
> button.close,
.vp-doc [class*='language-'] > button.close:focus {
opacity: 1;
}

.vp-doc [class*='language-'] > button.copy:hover,
.vp-doc [class*='language-'] > button.copy.copied {
.vp-doc [class*='language-'] > button.copy.copied,
.vp-doc [class*='language-'] > button.modal:hover,
.vp-doc [class*='language-'] > button.close:hover {
border-color: var(--vp-code-copy-code-hover-border-color);
background-color: var(--vp-code-copy-code-hover-bg);
}
Expand Down Expand Up @@ -506,7 +533,7 @@
content: var(--vp-code-copy-copied-text-content);
}

.vp-doc [class*='language-'] > span.lang {
.vp-doc [class*='language-'] span.lang {
position: absolute;
top: 2px;
/*rtl:ignore*/
Expand All @@ -520,7 +547,10 @@
opacity 0.4s;
}

.vp-doc [class*='language-']:hover > button.copy + span.lang,
.vp-doc
[class*='language-']:hover:not(:has(.modal-container:hover))
> button.copy
+ span.lang,
.vp-doc [class*='language-'] > button.copy:focus + span.lang {
opacity: 0;
}
Expand Down
4 changes: 4 additions & 0 deletions src/client/theme-default/styles/icons.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions src/node/markdown/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { linkPlugin } from './plugins/link'
import { preWrapperPlugin } from './plugins/preWrapper'
import { restoreEntities } from './plugins/restoreEntities'
import { snippetPlugin } from './plugins/snippet'
import { codeModalPlugin } from './plugins/codeModal'

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

Expand Down Expand Up @@ -116,6 +117,16 @@ export interface MarkdownOptions extends Options {
* @default 'Copy Code'
*/
codeCopyButtonTitle?: string
/**
* Show an additional button to open a fullscreen modal in code blocks
* @default false
*/
codeModal?: boolean
/**
* The tooltip text for the modal button in code blocks
* @default 'Open Modal'
*/
codeModalButtonTitle?: string

/* ==================== Markdown It Plugins ==================== */

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

const md = MarkdownIt({
Expand Down Expand Up @@ -230,6 +242,7 @@ export const createMarkdownRenderer = async (
base
)
.use(lineNumberPlugin, options.lineNumbers)
.use(codeModalPlugin, options.codeModal, { codeModalButtonTitle })

md.renderer.rules.table_open = function (tokens, idx, options, env, self) {
return '<table tabindex="0">\n'
Expand Down
41 changes: 41 additions & 0 deletions src/node/markdown/plugins/codeModal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// markdown-it plugin for generating line numbers.
// It depends on preWrapper plugin.

import type MarkdownIt from 'markdown-it'
import type { MarkdownOptions } from '../markdown'

export const codeModalPlugin = (
md: MarkdownIt,
enable = false,
options: MarkdownOptions = {}
) => {
const fence = md.renderer.rules.fence!
md.renderer.rules.fence = (...args) => {
const rawCode = fence(...args)

const [tokens, idx] = args
const info = tokens[idx].info

if (
(!enable && !/:modal($| |=)/.test(info)) ||
(enable && /:no-modal($| )/.test(info))
) {
return rawCode
}

let end = rawCode.lastIndexOf('</div>')

let innerCode =
rawCode.substring(0, end) +
`<button title="${options.codeModalButtonTitle}" class="close"></button>` +
'</div>'

const modal =
`<button title="${options.codeModalButtonTitle}" class="modal"></button>` +
'<div class="modal-container">' +
innerCode +
'</div>'

return rawCode.substring(0, end) + modal + '</div>'
}
}
2 changes: 2 additions & 0 deletions src/node/markdown/plugins/highlight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export async function highlight(
const vueRE = /-vue$/
const lineNoStartRE = /=(\d*)/
const lineNoRE = /:(no-)?line-numbers(=\d*)?$/
const modalNoRE = /:(no-)?modal(=\d*)?$/
const mustacheRE = /\{\{.*?\}\}/g

return (str: string, lang: string, attrs: string) => {
Expand All @@ -101,6 +102,7 @@ export async function highlight(
lang
.replace(lineNoStartRE, '')
.replace(lineNoRE, '')
.replace(modalNoRE, '')
.replace(vueRE, '')
.toLowerCase() || defaultLang

Expand Down
1 change: 1 addition & 0 deletions src/node/markdown/plugins/preWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ function extractLang(info: string) {
.trim()
.replace(/=(\d*)/, '')
.replace(/:(no-)?line-numbers({| |$|=\d*).*/, '')
.replace(/:(no-)?modal({| |$|=\d*).*/, '')
.replace(/(-vue|{| ).*$/, '')
.replace(/^vue-html$/, 'template')
.replace(/^ansi$/, '')
Expand Down