Fix markup code block layout (#36578)

This commit is contained in:
wxiaoguang
2026-02-11 11:22:33 +08:00
committed by GitHub
parent 018a88590c
commit fd89ceef79
8 changed files with 84 additions and 119 deletions

View File

@@ -46,7 +46,7 @@
@import "./features/captcha.css";
@import "./markup/content.css";
@import "./markup/codecopy.css";
@import "./markup/codeblock.css";
@import "./markup/codepreview.css";
@import "./markup/asciicast.css";

View File

@@ -0,0 +1,10 @@
.markup .ui.button.code-copy {
top: 8px;
right: 6px;
margin: 0;
}
.markup .mermaid-block .view-controller {
right: 6px;
bottom: 5px;
}

View File

@@ -1,40 +0,0 @@
.markup .code-copy {
position: absolute;
top: 8px;
right: 6px;
padding: 9px;
visibility: hidden; /* prevent from click events even opacity=0 */
opacity: 0;
transition: var(--transition-hover-fade);
}
/* adjustments for comment content having only 14px font size */
.repository.view.issue .comment-list .comment .markup .code-copy {
right: 5px;
padding: 8px;
}
/* can not use regular transparent button colors for hover and active states because
we need opaque colors here as code can appear behind the button */
.markup .code-copy:hover {
background: var(--color-secondary) !important;
}
.markup .code-copy:active {
background: var(--color-secondary-dark-1) !important;
}
/* all rendered code-block elements are in their container,
the manually written code-block elements on "packages" pages don't have the container */
.markup .code-block-container:hover .code-copy,
.markup .code-block:hover .code-copy {
visibility: visible;
opacity: 1;
}
@media (hover: none) {
.markup .code-copy {
visibility: visible;
opacity: 1;
}
}

View File

@@ -601,3 +601,40 @@ In markup content, we always use bottom margin for all elements */
.file-view.markup.orgmode li.indeterminate > p {
display: inline-block;
}
/* auto-hide-control is a control element or a container for control elements,
it floats over the code-block and only shows when the code-block is hovered. */
.markup .auto-hide-control {
position: absolute;
z-index: 1;
visibility: hidden; /* prevent from click events even opacity=0 */
opacity: 0;
transition: var(--transition-hover-fade);
}
/* all rendered code-block elements are in their container,
the manually written code-block elements on "packages" pages don't have the container */
.markup .code-block-container:hover .auto-hide-control,
.markup .code-block:hover .auto-hide-control {
visibility: visible;
opacity: 1;
}
@media (hover: none) {
.markup .auto-hide-control {
visibility: visible;
opacity: 1;
}
}
/* can not use regular transparent button colors for hover and active states
because we need opaque colors here as code can appear behind the button */
.markup .auto-hide-control.ui.button:hover,
.markup .auto-hide-control .ui.button:hover {
background: var(--color-secondary) !important;
}
.markup .auto-hide-control.ui.button:active,
.markup .auto-hide-control .ui.button:active {
background: var(--color-secondary-dark-1) !important;
}

View File

@@ -1,25 +1,25 @@
import {svg} from '../svg.ts';
import {queryElems} from '../utils/dom.ts';
import {createElementFromAttrs, queryElems} from '../utils/dom.ts';
export function makeCodeCopyButton(attrs: Record<string, string> = {}): HTMLButtonElement {
const button = document.createElement('button');
button.classList.add('code-copy', 'ui', 'button');
button.innerHTML = svg('octicon-copy');
for (const [key, value] of Object.entries(attrs)) {
button.setAttribute(key, value);
}
return button;
const btn = createElementFromAttrs<HTMLButtonElement>('button', {
class: 'ui compact icon button code-copy auto-hide-control',
...attrs,
});
btn.innerHTML = svg('octicon-copy');
return btn;
}
export function initMarkupCodeCopy(elMarkup: HTMLElement): void {
// .markup .code-block code
queryElems(elMarkup, '.code-block code', (el) => {
if (!el.textContent) return;
const btn = makeCodeCopyButton();
// remove final trailing newline introduced during HTML rendering
btn.setAttribute('data-clipboard-text', el.textContent.replace(/\r?\n$/, ''));
const btn = makeCodeCopyButton({
'data-clipboard-text': el.textContent.replace(/\r?\n$/, ''),
});
// we only want to use `.code-block-container` if it exists, no matter `.code-block` exists or not.
const btnContainer = el.closest('.code-block-container') ?? el.closest('.code-block');
btnContainer!.append(btn);
const btnContainer = el.closest('.code-block-container') ?? el.closest('.code-block')!;
btnContainer.append(btn);
});
}

View File

@@ -1,54 +1,18 @@
import {isDarkTheme, parseDom} from '../utils.ts';
import {makeCodeCopyButton} from './codecopy.ts';
import {displayError} from './common.ts';
import {createElementFromAttrs, createElementFromHTML, getCssRootVariablesText, queryElems} from '../utils/dom.ts';
import {html} from '../utils/html.ts';
import {createElementFromAttrs, createElementFromHTML, queryElems} from '../utils/dom.ts';
import {html, htmlRaw} from '../utils/html.ts';
import {load as loadYaml} from 'js-yaml';
import type {MermaidConfig} from 'mermaid';
import {svg} from '../svg.ts';
const {mermaidMaxSourceCharacters} = window.config;
function getIframeCss(): string {
// Inherit some styles (e.g.: root variables) from parent document.
// The buttons should use the same styles as `button.code-copy`, and align with it.
return `
${getCssRootVariablesText()}
html, body { height: 100%; }
body { margin: 0; padding: 0; overflow: hidden; }
#mermaid { display: block; margin: 0 auto; }
.view-controller {
position: absolute;
z-index: 1;
right: 5px;
bottom: 5px;
display: flex;
gap: 4px;
visibility: hidden;
opacity: 0;
transition: var(--transition-hover-fade);
margin-right: 0.25em;
}
body:hover .view-controller { visibility: visible; opacity: 1; }
@media (hover: none) {
.view-controller { visibility: visible; opacity: 1; }
}
.view-controller button {
cursor: pointer;
display: inline-flex;
justify-content: center;
align-items: center;
line-height: 1;
padding: 7.5px 10px;
border: 1px solid var(--color-light-border);
border-radius: var(--border-radius);
background: var(--color-button);
color: var(--color-text);
user-select: none;
}
.view-controller button:hover { background: var(--color-secondary); }
.view-controller button:active { background: var(--color-secondary-dark-1); }
`;
}
@@ -117,10 +81,9 @@ async function loadMermaid(needElkRender: boolean) {
};
}
function initMermaidViewController(dragElement: SVGSVGElement) {
function initMermaidViewController(viewController: HTMLElement, dragElement: SVGSVGElement) {
let inited = false, isDragging = false;
let currentScale = 1, initLeft = 0, lastLeft = 0, lastTop = 0, lastPageX = 0, lastPageY = 0;
const container = dragElement.parentElement!;
const resetView = () => {
currentScale = 1;
@@ -136,11 +99,12 @@ function initMermaidViewController(dragElement: SVGSVGElement) {
if (inited) return;
// if we need to drag or zoom, use absolute position and get the current "left" from the "margin: auto" layout.
inited = true;
const container = dragElement.parentElement!;
initLeft = container.getBoundingClientRect().width / 2 - dragElement.getBoundingClientRect().width / 2;
resetView();
};
for (const el of queryElems(container, '[data-control-action]')) {
for (const el of viewController.querySelectorAll('[data-control-action]')) {
el.addEventListener('click', () => {
initAbsolutePosition();
switch (el.getAttribute('data-control-action')) {
@@ -161,7 +125,8 @@ function initMermaidViewController(dragElement: SVGSVGElement) {
dragElement.addEventListener('mousedown', (e) => {
if (e.button !== 0 || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return; // only left mouse button can drag
const target = e.target as Element;
if (target.closest('div, p, a, span, button, input')) return; // don't start the drag if the click is on an interactive element (e.g.: link, button) or text element
// don't start the drag if the click is on an interactive element (e.g.: link, button) or text element
if (target.closest('div, p, a, span, button, input, text')) return;
initAbsolutePosition();
isDragging = true;
@@ -239,7 +204,14 @@ export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise<void
const svgDoc = parseDom(svgText, 'image/svg+xml');
const svgNode = (svgDoc.documentElement as unknown) as SVGSVGElement;
const viewControllerHtml = html`<div class="view-controller"><button data-control-action="zoom-in">+</button><button data-control-action="reset">reset</button><button data-control-action="zoom-out">-</button></div>`;
const viewControllerHtml = html`
<div class="view-controller auto-hide-control flex-text-block">
<button type="button" class="ui tiny compact icon button" data-control-action="zoom-in">${htmlRaw(svg('octicon-zoom-in', 12))}</button>
<button type="button" class="ui tiny compact icon button" data-control-action="reset">${htmlRaw(svg('octicon-sync', 12))}</button>
<button type="button" class="ui tiny compact icon button" data-control-action="zoom-out">${htmlRaw(svg('octicon-zoom-out', 12))}</button>
</div>
`;
const viewController = createElementFromHTML(viewControllerHtml);
// create an iframe to sandbox the svg with styles, and set correct height by reading svg's viewBox height
const iframe = document.createElement('iframe');
@@ -262,17 +234,15 @@ export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise<void
const iframeBody = iframe.contentDocument!.body;
iframeBody.append(svgNode);
bindFunctions?.(iframeBody); // follow "mermaid.render" doc, attach event handlers to the svg's container
iframeBody.append(createElementFromHTML(viewControllerHtml));
// according to mermaid, the viewBox height should always exist, here just a fallback for unknown cases.
// and keep in mind: clientHeight can be 0 if the element is hidden (display: none).
if (!iframeHeightFromViewBox) applyMermaidIframeHeight(iframe, iframeBody.clientHeight);
iframe.classList.remove('is-loading');
initMermaidViewController(svgNode);
initMermaidViewController(viewController, svgNode);
});
const container = createElementFromAttrs('div', {class: 'mermaid-block'}, iframe, makeCodeCopyButton({'data-clipboard-text': source}));
const container = createElementFromAttrs('div', {class: 'mermaid-block'}, iframe, viewController);
parentContainer.replaceWith(container);
} catch (err) {
displayError(parentContainer, err);

View File

@@ -80,6 +80,8 @@ import octiconTrash from '../../public/assets/img/svg/octicon-trash.svg';
import octiconTriangleDown from '../../public/assets/img/svg/octicon-triangle-down.svg';
import octiconX from '../../public/assets/img/svg/octicon-x.svg';
import octiconXCircleFill from '../../public/assets/img/svg/octicon-x-circle-fill.svg';
import octiconZoomIn from '../../public/assets/img/svg/octicon-zoom-in.svg';
import octiconZoomOut from '../../public/assets/img/svg/octicon-zoom-out.svg';
const svgs = {
'gitea-double-chevron-left': giteaDoubleChevronLeft,
@@ -161,6 +163,8 @@ const svgs = {
'octicon-triangle-down': octiconTriangleDown,
'octicon-x': octiconX,
'octicon-x-circle-fill': octiconXCircleFill,
'octicon-zoom-in': octiconZoomIn,
'octicon-zoom-out': octiconZoomOut,
};
export type SvgName = keyof typeof svgs;

View File

@@ -301,7 +301,7 @@ export function createElementFromHTML<T extends HTMLElement>(htmlString: string)
return div.firstChild as T;
}
export function createElementFromAttrs(tagName: string, attrs: Record<string, any> | null, ...children: (Node | string)[]): HTMLElement {
export function createElementFromAttrs<T extends HTMLElement>(tagName: string, attrs: Record<string, any> | null, ...children: (Node | string)[]): T {
const el = document.createElement(tagName);
for (const [key, value] of Object.entries(attrs || {})) {
if (value === undefined || value === null) continue;
@@ -314,7 +314,7 @@ export function createElementFromAttrs(tagName: string, attrs: Record<string, an
for (const child of children) {
el.append(child instanceof Node ? child : document.createTextNode(child));
}
return el;
return el as T;
}
export function animateOnce(el: Element, animationClassName: string): Promise<void> {
@@ -352,22 +352,6 @@ export function isPlainClick(e: MouseEvent) {
return e.button === 0 && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey;
}
let cssRootVariablesTextCache: string = '';
export function getCssRootVariablesText(): string {
if (cssRootVariablesTextCache) return cssRootVariablesTextCache;
const style = getComputedStyle(document.documentElement);
let text = ':root {\n';
for (let i = 0; i < style.length; i++) {
const name = style.item(i);
if (name.startsWith('--')) {
text += ` ${name}: ${style.getPropertyValue(name)};\n`;
}
}
text += '}\n';
cssRootVariablesTextCache = text;
return text;
}
let elemIdCounter = 0;
export function generateElemId(prefix: string = ''): string {
return `${prefix}${elemIdCounter++}`;