From e7f4da686d62fa123f617d0bda8e32a0113b686f Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 24 Sep 2025 18:16:36 +0200 Subject: [PATCH 1/4] Add markdown code blog wrap toggle Fixes: https://github.com/go-gitea/gitea/issues/35314 --- options/locale/locale_en-US.ini | 2 + public/assets/img/svg/material-wrap-text.svg | 1 + templates/base/head_script.tmpl | 2 + web_src/css/index.css | 2 +- web_src/css/markup/codeblocks.css | 35 ++++++++++++++ web_src/css/markup/codecopy.css | 30 ------------ web_src/js/globals.d.ts | 2 +- web_src/js/markup/codeblocks.ts | 51 ++++++++++++++++++++ web_src/js/markup/codecopy.ts | 22 --------- web_src/js/markup/content.ts | 4 +- web_src/js/markup/mermaid.ts | 4 +- web_src/js/svg.ts | 2 + web_src/svg/material-wrap-text.svg | 1 + 13 files changed, 100 insertions(+), 58 deletions(-) create mode 100644 public/assets/img/svg/material-wrap-text.svg create mode 100644 web_src/css/markup/codeblocks.css delete mode 100644 web_src/css/markup/codecopy.css create mode 100644 web_src/js/markup/codeblocks.ts delete mode 100644 web_src/js/markup/codecopy.ts create mode 100644 web_src/svg/material-wrap-text.svg diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index b3eb7b1f4a4fd..7fdfcf98bfa43 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -115,6 +115,8 @@ preview = Preview loading = Loading… files = Files +code_toggle_wrap = Toggle Code Wrap + error = Error error404 = The page you are trying to reach either does not exist or you are not authorized to view it. error503 = The server could not complete your request. Please try again later. diff --git a/public/assets/img/svg/material-wrap-text.svg b/public/assets/img/svg/material-wrap-text.svg new file mode 100644 index 0000000000000..106d309a3f86b --- /dev/null +++ b/public/assets/img/svg/material-wrap-text.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/base/head_script.tmpl b/templates/base/head_script.tmpl index f6648b59d8c53..ccf740adafb21 100644 --- a/templates/base/head_script.tmpl +++ b/templates/base/head_script.tmpl @@ -33,6 +33,7 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly. mermaidMaxSourceCharacters: {{MermaidMaxSourceCharacters}}, {{/* this global i18n object should only contain general texts. for specialized texts, it should be provided inside the related modules by: (1) API response (2) HTML data-attribute (3) PageData */}} i18n: { + copy: {{ctx.Locale.Tr "copy"}}, copy_success: {{ctx.Locale.Tr "copy_success"}}, copy_error: {{ctx.Locale.Tr "copy_error"}}, error_occurred: {{ctx.Locale.Tr "error.occurred"}}, @@ -41,6 +42,7 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly. modal_confirm: {{ctx.Locale.Tr "modal.confirm"}}, modal_cancel: {{ctx.Locale.Tr "modal.cancel"}}, more_items: {{ctx.Locale.Tr "more_items"}}, + code_toggle_wrap: {{ctx.Locale.Tr "code_toggle_wrap"}}, }, }; {{/* in case some pages don't render the pageData, we make sure it is an object to prevent null access */}} diff --git a/web_src/css/index.css b/web_src/css/index.css index 291cd04b2b95c..7007d7928f715 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -44,7 +44,7 @@ @import "./features/console.css"; @import "./markup/content.css"; -@import "./markup/codecopy.css"; +@import "./markup/codeblocks.css"; @import "./markup/codepreview.css"; @import "./markup/asciicast.css"; diff --git a/web_src/css/markup/codeblocks.css b/web_src/css/markup/codeblocks.css new file mode 100644 index 0000000000000..2236f0b083cb9 --- /dev/null +++ b/web_src/css/markup/codeblocks.css @@ -0,0 +1,35 @@ +.markup .code-block-buttons { + position: absolute; + top: 8px; + right: 6px; + display: flex; + gap: 4px; + visibility: hidden; + animation: fadeout 0.2s both; +} + +/* adjustments for comment content having only 14px font size */ +.repository.view.issue .comment-list .comment .markup .code-copy { + right: 5px; + padding: 8px; +} + +.markup .code-block-buttons .btn { + background: var(--color-button) !important; + padding: 9px; + border: 1px solid var(--color-light-border); +} + +.markup .code-block-buttons .btn:hover { + background: var(--color-hover-opaque) !important; +} + +.markup .code-block-buttons .btn[data-active="false"] { + color: var(--color-text-light-3); +} + +.markup .code-block-container:hover .code-block-buttons, +.markup .code-block:hover .code-block-buttons { + visibility: visible; + animation: fadein 0.2s both; +} diff --git a/web_src/css/markup/codecopy.css b/web_src/css/markup/codecopy.css deleted file mode 100644 index 5a7b9955e7ad2..0000000000000 --- a/web_src/css/markup/codecopy.css +++ /dev/null @@ -1,30 +0,0 @@ -.markup .code-copy { - position: absolute; - top: 8px; - right: 6px; - padding: 9px; - visibility: hidden; - animation: fadeout 0.2s both; -} - -/* 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; -} - -.markup .code-block-container:hover .code-copy, -.markup .code-block:hover .code-copy { - visibility: visible; - animation: fadein 0.2s both; -} diff --git a/web_src/js/globals.d.ts b/web_src/js/globals.d.ts index 00f1744a957b2..19caca6fc71fc 100644 --- a/web_src/js/globals.d.ts +++ b/web_src/js/globals.d.ts @@ -52,7 +52,7 @@ interface Element { interface Window { __webpack_public_path__: string; - config: import('./web_src/js/types.ts').Config; + config: import('./types.ts').Config; $: typeof import('@types/jquery'), jQuery: typeof import('@types/jquery'), htmx: typeof import('htmx.org').default, diff --git a/web_src/js/markup/codeblocks.ts b/web_src/js/markup/codeblocks.ts new file mode 100644 index 0000000000000..26b2260ad4985 --- /dev/null +++ b/web_src/js/markup/codeblocks.ts @@ -0,0 +1,51 @@ +import {svg, type SvgName} from '../svg.ts'; +import {queryElems, type DOMEvent} from '../utils/dom.ts'; + +export function makeCodeBlockButton(className: string, name: SvgName): HTMLButtonElement { + const button = document.createElement('button'); + button.classList.add(className, 'btn'); + button.innerHTML = svg(name, 14); + return button; +} + +const getMarkupCodeWrap = () => (localStorage.getItem('wrap-markup-code') || 'false') === 'true'; +const setMarkupCodeWrap = (value: boolean) => localStorage.setItem('wrap-markup-code', String(value)); + +function updateWrap(container: Element, wrap: boolean) { + container.classList.remove(wrap ? 'code-overflow-scroll' : 'code-overflow-wrap'); + container.classList.add(wrap ? 'code-overflow-wrap' : 'code-overflow-scroll'); +} + +export function initMarkupCodeBlocks(elMarkup: HTMLElement): void { + // .markup .code-block code + queryElems(elMarkup, '.code-block code', (el) => { + if (!el.textContent) return; + + const copyBtn = makeCodeBlockButton('code-copy', 'octicon-copy'); + copyBtn.setAttribute('data-tooltip-content', window.config.i18n.copy); + // remove final trailing newline introduced during HTML rendering + copyBtn.setAttribute('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 container = el.closest('.code-block-container') ?? el.closest('.code-block'); + + const wrapBtn = makeCodeBlockButton('code-wrap', 'material-wrap-text'); + const wrap = getMarkupCodeWrap(); + wrapBtn.setAttribute('data-active', String(wrap)); + updateWrap(container, wrap); + + wrapBtn.setAttribute('data-tooltip-content', window.config.i18n.code_toggle_wrap); + wrapBtn.addEventListener('click', (e) => { + const wrap = !getMarkupCodeWrap(); + updateWrap(container, wrap); + (e.currentTarget as HTMLButtonElement).setAttribute('data-active', String(wrap)); + setMarkupCodeWrap(wrap); + }); + + const btnContainer = document.createElement('div'); + btnContainer.classList.add('code-block-buttons'); + btnContainer.append(wrapBtn); + btnContainer.append(copyBtn); + container.append(btnContainer); + }); +} diff --git a/web_src/js/markup/codecopy.ts b/web_src/js/markup/codecopy.ts deleted file mode 100644 index b37aa3a2369a2..0000000000000 --- a/web_src/js/markup/codecopy.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {svg} from '../svg.ts'; -import {queryElems} from '../utils/dom.ts'; - -export function makeCodeCopyButton(): HTMLButtonElement { - const button = document.createElement('button'); - button.classList.add('code-copy', 'ui', 'button'); - button.innerHTML = svg('octicon-copy'); - return button; -} - -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$/, '')); - // 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); - }); -} diff --git a/web_src/js/markup/content.ts b/web_src/js/markup/content.ts index 55db4aa8102b3..3d583eb7500d6 100644 --- a/web_src/js/markup/content.ts +++ b/web_src/js/markup/content.ts @@ -1,6 +1,6 @@ import {initMarkupCodeMermaid} from './mermaid.ts'; import {initMarkupCodeMath} from './math.ts'; -import {initMarkupCodeCopy} from './codecopy.ts'; +import {initMarkupCodeBlocks} from './codeblocks.ts'; import {initMarkupRenderAsciicast} from './asciicast.ts'; import {initMarkupTasklist} from './tasklist.ts'; import {registerGlobalSelectorFunc} from '../modules/observer.ts'; @@ -8,7 +8,7 @@ import {registerGlobalSelectorFunc} from '../modules/observer.ts'; // code that runs for all markup content export function initMarkupContent(): void { registerGlobalSelectorFunc('.markup', (el: HTMLElement) => { - initMarkupCodeCopy(el); + initMarkupCodeBlocks(el); initMarkupTasklist(el); initMarkupCodeMermaid(el); initMarkupCodeMath(el); diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index 33d9a1ed9bd83..6927ecdb224fe 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -1,5 +1,5 @@ import {isDarkTheme} from '../utils.ts'; -import {makeCodeCopyButton} from './codecopy.ts'; +import {makeCodeBlockButton} from './codeblocks.ts'; import {displayError} from './common.ts'; import {queryElems} from '../utils/dom.ts'; import {html, htmlRaw} from '../utils/html.ts'; @@ -53,7 +53,7 @@ export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise \ No newline at end of file From b7ab5af97952e479e7fd411394b937bb74e366af Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 24 Sep 2025 18:24:33 +0200 Subject: [PATCH 2/4] tweak viewBox --- public/assets/img/svg/material-wrap-text.svg | 2 +- web_src/svg/material-wrap-text.svg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/assets/img/svg/material-wrap-text.svg b/public/assets/img/svg/material-wrap-text.svg index 106d309a3f86b..295e79da6d74e 100644 --- a/public/assets/img/svg/material-wrap-text.svg +++ b/public/assets/img/svg/material-wrap-text.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/web_src/svg/material-wrap-text.svg b/web_src/svg/material-wrap-text.svg index e5135a015becd..7d467506dae88 100644 --- a/web_src/svg/material-wrap-text.svg +++ b/web_src/svg/material-wrap-text.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file From f8bcf2f86b14512dd1ed5ccdeea8518e15c69ba8 Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 24 Sep 2025 18:26:33 +0200 Subject: [PATCH 3/4] fix lint --- web_src/js/markup/codeblocks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/markup/codeblocks.ts b/web_src/js/markup/codeblocks.ts index 26b2260ad4985..dd3a48fdf7770 100644 --- a/web_src/js/markup/codeblocks.ts +++ b/web_src/js/markup/codeblocks.ts @@ -1,5 +1,5 @@ import {svg, type SvgName} from '../svg.ts'; -import {queryElems, type DOMEvent} from '../utils/dom.ts'; +import {queryElems} from '../utils/dom.ts'; export function makeCodeBlockButton(className: string, name: SvgName): HTMLButtonElement { const button = document.createElement('button'); From b2ee29d9fc79af88854fc10f7ad695365db37465 Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 24 Sep 2025 18:42:43 +0200 Subject: [PATCH 4/4] misc tweaks --- options/locale/locale_en-US.ini | 2 +- web_src/js/markup/codeblocks.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 7fdfcf98bfa43..92f58469f0bf8 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -115,7 +115,7 @@ preview = Preview loading = Loading… files = Files -code_toggle_wrap = Toggle Code Wrap +code_toggle_wrap = Toggle code wrap error = Error error404 = The page you are trying to reach either does not exist or you are not authorized to view it. diff --git a/web_src/js/markup/codeblocks.ts b/web_src/js/markup/codeblocks.ts index dd3a48fdf7770..152a8a1de69d1 100644 --- a/web_src/js/markup/codeblocks.ts +++ b/web_src/js/markup/codeblocks.ts @@ -8,8 +8,8 @@ export function makeCodeBlockButton(className: string, name: SvgName): HTMLButto return button; } -const getMarkupCodeWrap = () => (localStorage.getItem('wrap-markup-code') || 'false') === 'true'; -const setMarkupCodeWrap = (value: boolean) => localStorage.setItem('wrap-markup-code', String(value)); +const getWrap = () => (localStorage.getItem('wrap-markup-code') || 'false') === 'true'; +const saveWrap = (value: boolean) => localStorage.setItem('wrap-markup-code', String(value)); function updateWrap(container: Element, wrap: boolean) { container.classList.remove(wrap ? 'code-overflow-scroll' : 'code-overflow-wrap'); @@ -30,16 +30,16 @@ export function initMarkupCodeBlocks(elMarkup: HTMLElement): void { const container = el.closest('.code-block-container') ?? el.closest('.code-block'); const wrapBtn = makeCodeBlockButton('code-wrap', 'material-wrap-text'); - const wrap = getMarkupCodeWrap(); + const wrap = getWrap(); wrapBtn.setAttribute('data-active', String(wrap)); updateWrap(container, wrap); wrapBtn.setAttribute('data-tooltip-content', window.config.i18n.code_toggle_wrap); wrapBtn.addEventListener('click', (e) => { - const wrap = !getMarkupCodeWrap(); + const wrap = !getWrap(); updateWrap(container, wrap); (e.currentTarget as HTMLButtonElement).setAttribute('data-active', String(wrap)); - setMarkupCodeWrap(wrap); + saveWrap(wrap); }); const btnContainer = document.createElement('div');