diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index b3eb7b1f4a4fd..92f58469f0bf8 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..295e79da6d74e --- /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..152a8a1de69d1 --- /dev/null +++ b/web_src/js/markup/codeblocks.ts @@ -0,0 +1,51 @@ +import {svg, type SvgName} from '../svg.ts'; +import {queryElems} 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 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'); + 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 = 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 = !getWrap(); + updateWrap(container, wrap); + (e.currentTarget as HTMLButtonElement).setAttribute('data-active', String(wrap)); + saveWrap(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