loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Improve "URL" handling in markdown editor (#7006)

The button to insert an URL now opens a dialog prompting for the two
components, the URL and the description.
Any existing text selection is taken into account to pre-fill the
description field.

Closes: #6731

![image](/attachments/ca1f7767-5fd6-4c38-8c7a-83d893523de2)

Co-authored-by: Otto Richter <otto@codeberg.org>
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/7006
Reviewed-by: Otto <otto@codeberg.org>
Co-authored-by: Lucas Schwiderski <lucas@lschwiderski.de>
Co-committed-by: Lucas Schwiderski <lucas@lschwiderski.de>

authored by

Lucas Schwiderski
Otto Richter
Lucas Schwiderski
and committed by
Otto
6dad4575 3372db66

+114 -1
+5
options/locale/locale_en-US.ini
··· 232 232 table_modal.label.rows = Rows 233 233 table_modal.label.columns = Columns 234 234 235 + link_modal.header = Add a link 236 + link_modal.url = Url 237 + link_modal.description = Description 238 + link_modal.paste_reminder = Hint: With a URL in your clipboard, you can paste directly into the editor to create a link. 239 + 235 240 [filter] 236 241 string.asc = A - Z 237 242 string.desc = Z - A
+27 -1
templates/shared/combomarkdowneditor.tmpl
··· 29 29 <div class="markdown-toolbar-group"> 30 30 <md-quote class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.quote.tooltip"}}">{{svg "octicon-quote"}}</md-quote> 31 31 <md-code class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.code.tooltip"}}">{{svg "octicon-code"}}</md-code> 32 - <md-link class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.link.tooltip"}}">{{svg "octicon-link"}}</md-link> 32 + <button class="markdown-toolbar-button show-modal button" data-md-button data-md-action="new-link" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.link.tooltip"}}">{{svg "octicon-link"}}</button> 33 33 </div> 34 34 <div class="markdown-toolbar-group"> 35 35 <md-unordered-list class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.list.unordered.tooltip"}}">{{svg "octicon-list-unordered"}}</md-unordered-list> ··· 87 87 <button class="ui cancel button" data-selector-name="cancel-button">{{ctx.Locale.Tr "cancel"}}</button> 88 88 <button class="ui primary button" data-selector-name="ok-button">{{ctx.Locale.Tr "ok"}}</button> 89 89 </div> 90 + </div> 91 + 92 + <div class="ui small modal" data-modal-name="new-markdown-link"> 93 + <div class="header">{{ctx.Locale.Tr "editor.link_modal.header"}}</div> 94 + 95 + <fieldset class="content"> 96 + <div class="ui form" data-selector-name="form"> 97 + <label> 98 + {{ctx.Locale.Tr "editor.link_modal.url"}} 99 + <input name="link-url" required dir="auto" autocomplete="off"> 100 + </label> 101 + <label> 102 + {{ctx.Locale.Tr "editor.link_modal.description"}} 103 + <input name="link-description" required dir="auto" autocomplete="off"> 104 + </label> 105 + 106 + <div class="help"> 107 + {{ctx.Locale.Tr "editor.link_modal.paste_reminder"}} 108 + </div> 109 + 110 + <div class="text right actions"> 111 + <button class="ui cancel button" data-selector-name="cancel-button">{{ctx.Locale.Tr "cancel"}}</button> 112 + <button class="ui primary button" data-selector-name="ok-button">{{ctx.Locale.Tr "ok"}}</button> 113 + </div> 114 + </div> 115 + </fieldset> 90 116 </div> 91 117 </div>
+28
tests/e2e/markdown-editor.test.e2e.ts
··· 5 5 // @watch end 6 6 7 7 import {expect} from '@playwright/test'; 8 + import {accessibilityCheck} from './shared/accessibility.ts'; 8 9 import {save_visual, test} from './utils_e2e.ts'; 9 10 10 11 test.use({user: 'user2'}); ··· 221 222 222 223 const textarea = page.locator('textarea[name=content]'); 223 224 await expect(textarea).toHaveValue('| Header | Header |\n|---------|---------|\n| Content | Content |\n| Content | Content |\n| Content | Content |\n'); 225 + await save_visual(page); 226 + }); 227 + 228 + test('markdown insert link', async ({page}) => { 229 + const response = await page.goto('/user2/repo1/issues/new'); 230 + expect(response?.status()).toBe(200); 231 + 232 + const newLinkButton = page.locator('button[data-md-action="new-link"]'); 233 + await newLinkButton.click(); 234 + 235 + const newLinkModal = page.locator('div[data-markdown-link-modal-id="0"]'); 236 + await expect(newLinkModal).toBeVisible(); 237 + await accessibilityCheck({page}, ['[data-modal-name="new-markdown-link"]'], [], []); 238 + await save_visual(page); 239 + 240 + const url = 'https://example.com'; 241 + const description = 'Where does this lead?'; 242 + 243 + await newLinkModal.locator('input[name="link-url"]').fill(url); 244 + await newLinkModal.locator('input[name="link-description"]').fill(description); 245 + 246 + await newLinkModal.locator('button[data-selector-name="ok-button"]').click(); 247 + 248 + await expect(newLinkModal).toBeHidden(); 249 + 250 + const textarea = page.locator('textarea[name=content]'); 251 + await expect(textarea).toHaveValue(`[${description}](${url})`); 224 252 await save_visual(page); 225 253 }); 226 254
+54
web_src/js/features/comp/ComboMarkdownEditor.js
··· 49 49 this.setupDropzone(); 50 50 this.setupTextarea(); 51 51 this.setupTableInserter(); 52 + this.setupLinkInserter(); 52 53 53 54 await this.switchToUserPreference(); 54 55 ··· 93 94 this.indentSelection(true); 94 95 }); 95 96 this.textareaMarkdownToolbar.querySelector('button[data-md-action="new-table"]')?.setAttribute('data-modal', `div[data-markdown-table-modal-id="${elementIdCounter}"]`); 97 + this.textareaMarkdownToolbar.querySelector('button[data-md-action="new-link"]')?.setAttribute('data-modal', `div[data-markdown-link-modal-id="${elementIdCounter}"]`); 96 98 97 99 this.textarea.addEventListener('keydown', (e) => { 98 100 if (e.shiftKey) { ··· 226 228 const button = newTableModal.querySelector('button[data-selector-name="ok-button"]'); 227 229 button.setAttribute('data-element-id', elementIdCounter); 228 230 button.addEventListener('click', this.addNewTable); 231 + } 232 + 233 + addNewLink(event) { 234 + const elementId = event.target.getAttribute('data-element-id'); 235 + const newLinkModal = document.querySelector(`div[data-markdown-link-modal-id="${elementId}"]`); 236 + const form = newLinkModal.querySelector('div[data-selector-name="form"]'); 237 + 238 + // Validate input fields 239 + for (const currentInput of form.querySelectorAll('input')) { 240 + if (!currentInput.checkValidity()) { 241 + currentInput.reportValidity(); 242 + return; 243 + } 244 + } 245 + 246 + const url = form.querySelector('input[name="link-url"]').value; 247 + const description = form.querySelector('input[name="link-description"]').value; 248 + 249 + const code = `[${description}](${url})`; 250 + 251 + replaceTextareaSelection(document.getElementById(`_combo_markdown_editor_${elementId}`), code); 252 + 253 + // Close the modal then clear its fields in case the user wants to add another one. 254 + newLinkModal.querySelector('button[data-selector-name="cancel-button"]').click(); 255 + form.querySelector('input[name="link-url"]').value = ''; 256 + form.querySelector('input[name="link-description"]').value = ''; 257 + } 258 + 259 + setupLinkInserter() { 260 + const newLinkModal = this.container.querySelector('div[data-modal-name="new-markdown-link"]'); 261 + newLinkModal.setAttribute('data-markdown-link-modal-id', elementIdCounter); 262 + const textarea = document.getElementById(`_combo_markdown_editor_${elementIdCounter}`); 263 + 264 + $(newLinkModal).modal({ 265 + // Pre-fill the description field from the selection to create behavior similar 266 + // to pasting an URL over selected text. 267 + onShow: () => { 268 + const start = textarea.selectionStart; 269 + const end = textarea.selectionEnd; 270 + 271 + if (start !== end) { 272 + const selection = textarea.value.slice(start ?? undefined, end ?? undefined); 273 + newLinkModal.querySelector('input[name="link-description"]').value = selection; 274 + } else { 275 + newLinkModal.querySelector('input[name="link-description"]').value = ''; 276 + } 277 + }, 278 + }); 279 + 280 + const button = newLinkModal.querySelector('button[data-selector-name="ok-button"]'); 281 + button.setAttribute('data-element-id', elementIdCounter); 282 + button.addEventListener('click', this.addNewLink); 229 283 } 230 284 231 285 prepareEasyMDEToolbarActions() {