mirror of
https://github.com/go-gitea/gitea.git
synced 2026-02-15 07:26:19 +00:00
Fix markup code block layout (#36578)
This commit is contained in:
@@ -46,7 +46,7 @@
|
|||||||
@import "./features/captcha.css";
|
@import "./features/captcha.css";
|
||||||
|
|
||||||
@import "./markup/content.css";
|
@import "./markup/content.css";
|
||||||
@import "./markup/codecopy.css";
|
@import "./markup/codeblock.css";
|
||||||
@import "./markup/codepreview.css";
|
@import "./markup/codepreview.css";
|
||||||
@import "./markup/asciicast.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 {
|
.file-view.markup.orgmode li.indeterminate > p {
|
||||||
display: inline-block;
|
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 {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 {
|
export function makeCodeCopyButton(attrs: Record<string, string> = {}): HTMLButtonElement {
|
||||||
const button = document.createElement('button');
|
const btn = createElementFromAttrs<HTMLButtonElement>('button', {
|
||||||
button.classList.add('code-copy', 'ui', 'button');
|
class: 'ui compact icon button code-copy auto-hide-control',
|
||||||
button.innerHTML = svg('octicon-copy');
|
...attrs,
|
||||||
for (const [key, value] of Object.entries(attrs)) {
|
});
|
||||||
button.setAttribute(key, value);
|
btn.innerHTML = svg('octicon-copy');
|
||||||
}
|
return btn;
|
||||||
return button;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initMarkupCodeCopy(elMarkup: HTMLElement): void {
|
export function initMarkupCodeCopy(elMarkup: HTMLElement): void {
|
||||||
// .markup .code-block code
|
// .markup .code-block code
|
||||||
queryElems(elMarkup, '.code-block code', (el) => {
|
queryElems(elMarkup, '.code-block code', (el) => {
|
||||||
if (!el.textContent) return;
|
if (!el.textContent) return;
|
||||||
const btn = makeCodeCopyButton();
|
|
||||||
// remove final trailing newline introduced during HTML rendering
|
// 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.
|
// 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');
|
const btnContainer = el.closest('.code-block-container') ?? el.closest('.code-block')!;
|
||||||
btnContainer!.append(btn);
|
btnContainer.append(btn);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,54 +1,18 @@
|
|||||||
import {isDarkTheme, parseDom} from '../utils.ts';
|
import {isDarkTheme, parseDom} from '../utils.ts';
|
||||||
import {makeCodeCopyButton} from './codecopy.ts';
|
|
||||||
import {displayError} from './common.ts';
|
import {displayError} from './common.ts';
|
||||||
import {createElementFromAttrs, createElementFromHTML, getCssRootVariablesText, queryElems} from '../utils/dom.ts';
|
import {createElementFromAttrs, createElementFromHTML, queryElems} from '../utils/dom.ts';
|
||||||
import {html} from '../utils/html.ts';
|
import {html, htmlRaw} from '../utils/html.ts';
|
||||||
import {load as loadYaml} from 'js-yaml';
|
import {load as loadYaml} from 'js-yaml';
|
||||||
import type {MermaidConfig} from 'mermaid';
|
import type {MermaidConfig} from 'mermaid';
|
||||||
|
import {svg} from '../svg.ts';
|
||||||
|
|
||||||
const {mermaidMaxSourceCharacters} = window.config;
|
const {mermaidMaxSourceCharacters} = window.config;
|
||||||
|
|
||||||
function getIframeCss(): string {
|
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 `
|
return `
|
||||||
${getCssRootVariablesText()}
|
|
||||||
|
|
||||||
html, body { height: 100%; }
|
html, body { height: 100%; }
|
||||||
body { margin: 0; padding: 0; overflow: hidden; }
|
body { margin: 0; padding: 0; overflow: hidden; }
|
||||||
#mermaid { display: block; margin: 0 auto; }
|
#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 inited = false, isDragging = false;
|
||||||
let currentScale = 1, initLeft = 0, lastLeft = 0, lastTop = 0, lastPageX = 0, lastPageY = 0;
|
let currentScale = 1, initLeft = 0, lastLeft = 0, lastTop = 0, lastPageX = 0, lastPageY = 0;
|
||||||
const container = dragElement.parentElement!;
|
|
||||||
|
|
||||||
const resetView = () => {
|
const resetView = () => {
|
||||||
currentScale = 1;
|
currentScale = 1;
|
||||||
@@ -136,11 +99,12 @@ function initMermaidViewController(dragElement: SVGSVGElement) {
|
|||||||
if (inited) return;
|
if (inited) return;
|
||||||
// if we need to drag or zoom, use absolute position and get the current "left" from the "margin: auto" layout.
|
// if we need to drag or zoom, use absolute position and get the current "left" from the "margin: auto" layout.
|
||||||
inited = true;
|
inited = true;
|
||||||
|
const container = dragElement.parentElement!;
|
||||||
initLeft = container.getBoundingClientRect().width / 2 - dragElement.getBoundingClientRect().width / 2;
|
initLeft = container.getBoundingClientRect().width / 2 - dragElement.getBoundingClientRect().width / 2;
|
||||||
resetView();
|
resetView();
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const el of queryElems(container, '[data-control-action]')) {
|
for (const el of viewController.querySelectorAll('[data-control-action]')) {
|
||||||
el.addEventListener('click', () => {
|
el.addEventListener('click', () => {
|
||||||
initAbsolutePosition();
|
initAbsolutePosition();
|
||||||
switch (el.getAttribute('data-control-action')) {
|
switch (el.getAttribute('data-control-action')) {
|
||||||
@@ -161,7 +125,8 @@ function initMermaidViewController(dragElement: SVGSVGElement) {
|
|||||||
dragElement.addEventListener('mousedown', (e) => {
|
dragElement.addEventListener('mousedown', (e) => {
|
||||||
if (e.button !== 0 || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return; // only left mouse button can drag
|
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;
|
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();
|
initAbsolutePosition();
|
||||||
isDragging = true;
|
isDragging = true;
|
||||||
@@ -239,7 +204,14 @@ export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise<void
|
|||||||
const svgDoc = parseDom(svgText, 'image/svg+xml');
|
const svgDoc = parseDom(svgText, 'image/svg+xml');
|
||||||
const svgNode = (svgDoc.documentElement as unknown) as SVGSVGElement;
|
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
|
// create an iframe to sandbox the svg with styles, and set correct height by reading svg's viewBox height
|
||||||
const iframe = document.createElement('iframe');
|
const iframe = document.createElement('iframe');
|
||||||
@@ -262,17 +234,15 @@ export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise<void
|
|||||||
const iframeBody = iframe.contentDocument!.body;
|
const iframeBody = iframe.contentDocument!.body;
|
||||||
iframeBody.append(svgNode);
|
iframeBody.append(svgNode);
|
||||||
bindFunctions?.(iframeBody); // follow "mermaid.render" doc, attach event handlers to the svg's container
|
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.
|
// 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).
|
// and keep in mind: clientHeight can be 0 if the element is hidden (display: none).
|
||||||
if (!iframeHeightFromViewBox) applyMermaidIframeHeight(iframe, iframeBody.clientHeight);
|
if (!iframeHeightFromViewBox) applyMermaidIframeHeight(iframe, iframeBody.clientHeight);
|
||||||
iframe.classList.remove('is-loading');
|
iframe.classList.remove('is-loading');
|
||||||
|
initMermaidViewController(viewController, svgNode);
|
||||||
initMermaidViewController(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);
|
parentContainer.replaceWith(container);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
displayError(parentContainer, 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 octiconTriangleDown from '../../public/assets/img/svg/octicon-triangle-down.svg';
|
||||||
import octiconX from '../../public/assets/img/svg/octicon-x.svg';
|
import octiconX from '../../public/assets/img/svg/octicon-x.svg';
|
||||||
import octiconXCircleFill from '../../public/assets/img/svg/octicon-x-circle-fill.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 = {
|
const svgs = {
|
||||||
'gitea-double-chevron-left': giteaDoubleChevronLeft,
|
'gitea-double-chevron-left': giteaDoubleChevronLeft,
|
||||||
@@ -161,6 +163,8 @@ const svgs = {
|
|||||||
'octicon-triangle-down': octiconTriangleDown,
|
'octicon-triangle-down': octiconTriangleDown,
|
||||||
'octicon-x': octiconX,
|
'octicon-x': octiconX,
|
||||||
'octicon-x-circle-fill': octiconXCircleFill,
|
'octicon-x-circle-fill': octiconXCircleFill,
|
||||||
|
'octicon-zoom-in': octiconZoomIn,
|
||||||
|
'octicon-zoom-out': octiconZoomOut,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SvgName = keyof typeof svgs;
|
export type SvgName = keyof typeof svgs;
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ export function createElementFromHTML<T extends HTMLElement>(htmlString: string)
|
|||||||
return div.firstChild as T;
|
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);
|
const el = document.createElement(tagName);
|
||||||
for (const [key, value] of Object.entries(attrs || {})) {
|
for (const [key, value] of Object.entries(attrs || {})) {
|
||||||
if (value === undefined || value === null) continue;
|
if (value === undefined || value === null) continue;
|
||||||
@@ -314,7 +314,7 @@ export function createElementFromAttrs(tagName: string, attrs: Record<string, an
|
|||||||
for (const child of children) {
|
for (const child of children) {
|
||||||
el.append(child instanceof Node ? child : document.createTextNode(child));
|
el.append(child instanceof Node ? child : document.createTextNode(child));
|
||||||
}
|
}
|
||||||
return el;
|
return el as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function animateOnce(el: Element, animationClassName: string): Promise<void> {
|
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;
|
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;
|
let elemIdCounter = 0;
|
||||||
export function generateElemId(prefix: string = ''): string {
|
export function generateElemId(prefix: string = ''): string {
|
||||||
return `${prefix}${elemIdCounter++}`;
|
return `${prefix}${elemIdCounter++}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user