mirror of
https://github.com/go-gitea/gitea.git
synced 2026-02-11 14:28:57 +00:00
Fix markup code block layout (#36578)
This commit is contained in:
@@ -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";
|
||||
|
||||
|
||||
10
web_src/css/markup/codeblock.css
Normal file
10
web_src/css/markup/codeblock.css
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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++}`;
|
||||
|
||||
Reference in New Issue
Block a user