1
0
Fork 0
mirror of https://codeberg.org/forgejo/forgejo.git synced 2025-07-19 01:29:40 +02:00

fix(ui): multiple ComboMarkdownEditors on one page interfere (#8417)

When there are multiple combo-markdown-editors, then only the first will get changes from the toolbar buttons.
Fixes: #6742

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8417
Reviewed-by: Beowulf <beowulf@beocode.eu>
Co-authored-by: zokki <zokki.softwareschmiede@gmail.com>
Co-committed-by: zokki <zokki.softwareschmiede@gmail.com>
This commit is contained in:
zokki 2025-07-14 13:24:45 +02:00 committed by Beowulf
parent 81e59014da
commit 1937fcf476
3 changed files with 91 additions and 17 deletions

View file

@ -79,6 +79,22 @@ func DeclareGitRepos(t *testing.T) func() {
Filename: "a-file",
Versions: []string{"{a}{а}"},
}}, nil),
newRepo(t, 2, "multiple-combo-boxes", nil, []FileChanges{{
Filename: ".forgejo/issue_template/multi-combo-boxes.yaml",
Versions: []string{`
name: "Multiple combo-boxes"
description: "To show something"
body:
- type: textarea
id: textarea-one
attributes:
label: one
- type: textarea
id: textarea-two
attributes:
label: two
`},
}}, nil),
newRepo(t, 11, "dependency-test", &tests.DeclarativeRepoOptions{
UnitConfig: optional.Some(map[unit_model.Type]convert.Conversion{
unit_model.TypeIssues: &repo_model.IssuesConfig{

View file

@ -456,3 +456,62 @@ test('Combo Markdown: preview mode switch', async ({page}) => {
await expect(previewPanel).toBeHidden();
await save_visual(page);
});
test('Multiple combo markdown: insert table', async ({page}) => {
const response = await page.goto('/user2/multiple-combo-boxes/issues/new?template=.forgejo%2fissue_template%2fmulti-combo-boxes.yaml');
expect(response?.status()).toBe(200);
// check that there are two textareas
const textareaOne = page.locator('textarea[name=form-field-textarea-one]');
const comboboxOne = page.locator('textarea#_combo_markdown_editor_0');
await expect(textareaOne).toBeVisible();
await expect(comboboxOne).toBeHidden();
const textareaTwo = page.locator('textarea[name=form-field-textarea-two]');
const comboboxTwo = page.locator('textarea#_combo_markdown_editor_1');
await expect(textareaTwo).toBeVisible();
await expect(comboboxTwo).toBeHidden();
// focus first one and add table to it
await textareaOne.click();
await expect(comboboxOne).toBeVisible();
await expect(comboboxTwo).toBeHidden();
const newTableButtonOne = page.locator('[for="_combo_markdown_editor_0"] button[data-md-action="new-table"]');
await newTableButtonOne.click();
const newTableModalOne = page.locator('div[data-markdown-table-modal-id="0"]');
await expect(newTableModalOne).toBeVisible();
await newTableModalOne.locator('input[name="table-rows"]').fill('3');
await newTableModalOne.locator('input[name="table-columns"]').fill('2');
await newTableModalOne.locator('button[data-selector-name="ok-button"]').click();
await expect(newTableModalOne).toBeHidden();
await expect(comboboxOne).toHaveValue('| Header | Header |\n|---------|---------|\n| Content | Content |\n| Content | Content |\n| Content | Content |\n');
await expect(comboboxTwo).toBeEmpty();
await save_visual(page);
// focus second one and add table to it
await textareaTwo.click();
await expect(comboboxOne).toBeHidden();
await expect(comboboxTwo).toBeVisible();
const newTableButtonTwo = page.locator('[for="_combo_markdown_editor_1"] button[data-md-action="new-table"]');
await newTableButtonTwo.click();
const newTableModalTwo = page.locator('div[data-markdown-table-modal-id="1"]');
await expect(newTableModalTwo).toBeVisible();
await newTableModalTwo.locator('input[name="table-rows"]').fill('2');
await newTableModalTwo.locator('input[name="table-columns"]').fill('3');
await newTableModalTwo.locator('button[data-selector-name="ok-button"]').click();
await expect(newTableModalTwo).toBeHidden();
await expect(comboboxOne).toHaveValue('| Header | Header |\n|---------|---------|\n| Content | Content |\n| Content | Content |\n| Content | Content |\n');
await expect(comboboxTwo).toHaveValue('| Header | Header | Header |\n|---------|---------|---------|\n| Content | Content | Content |\n| Content | Content | Content |\n');
await save_visual(page);
});

View file

@ -11,8 +11,6 @@ import {initTextExpander} from './TextExpander.js';
import {showErrorToast, showHintToast} from '../../modules/toast.js';
import {POST} from '../../modules/fetch.js';
let elementIdCounter = 0;
/**
* validate if the given textarea is non-empty.
* @param {HTMLElement} textarea - The textarea element to be validated.
@ -39,10 +37,13 @@ export function validateTextareaNonEmpty(textarea) {
const listPrefixRegex = /^\s*((\d+)[.)]\s|[-*+]\s{1,4}\[[ x]\]\s?|[-*+]\s|(>\s?)+)?/;
class ComboMarkdownEditor {
static idSuffixCounter = 0;
constructor(container, options = {}) {
container._giteaComboMarkdownEditor = this;
this.options = options;
this.container = container;
this.elementIdSuffix = ComboMarkdownEditor.idSuffixCounter++;
}
async init() {
@ -55,8 +56,6 @@ class ComboMarkdownEditor {
this.setupLinkInserter();
await this.switchToUserPreference();
elementIdCounter++;
}
applyEditorHeights(el, heights) {
@ -74,7 +73,7 @@ class ComboMarkdownEditor {
setupTextarea() {
this.textarea = this.container.querySelector('.markdown-text-editor');
this.textarea._giteaComboMarkdownEditor = this;
this.textarea.id = `_combo_markdown_editor_${elementIdCounter}`;
this.textarea.id = `_combo_markdown_editor_${this.elementIdSuffix}`;
this.textarea.addEventListener('input', (e) => this.options?.onContentChanged?.(this, e));
this.applyEditorHeights(this.textarea, this.options.editorHeights);
@ -96,8 +95,8 @@ class ComboMarkdownEditor {
this.textareaMarkdownToolbar.querySelector('button[data-md-action="unindent"]')?.addEventListener('click', () => {
this.indentSelection(true, false);
});
this.textareaMarkdownToolbar.querySelector('button[data-md-action="new-table"]')?.setAttribute('data-modal', `div[data-markdown-table-modal-id="${elementIdCounter}"]`);
this.textareaMarkdownToolbar.querySelector('button[data-md-action="new-link"]')?.setAttribute('data-modal', `div[data-markdown-link-modal-id="${elementIdCounter}"]`);
this.textareaMarkdownToolbar.querySelector('button[data-md-action="new-table"]')?.setAttribute('data-modal', `div[data-markdown-table-modal-id="${this.elementIdSuffix}"]`);
this.textareaMarkdownToolbar.querySelector('button[data-md-action="new-link"]')?.setAttribute('data-modal', `div[data-markdown-link-modal-id="${this.elementIdSuffix}"]`);
// Track whether any actual input or pointer action was made after focusing, and only intercept Tab presses after that.
this.tabEnabled = false;
@ -195,7 +194,7 @@ class ComboMarkdownEditor {
setupDropzone() {
const dropzoneParentContainer = this.container.getAttribute('data-dropzone-parent-container');
if (dropzoneParentContainer) {
this.dropzone = this.container.closest(this.container.getAttribute('data-dropzone-parent-container'))?.querySelector('.dropzone');
this.dropzone = this.container.closest(dropzoneParentContainer)?.querySelector('.dropzone');
}
}
@ -207,13 +206,13 @@ class ComboMarkdownEditor {
// So here it uses our defined "data-tab-for" and "data-tab-panel" to generate the "data-tab" attribute for Fomantic.
const tabEditor = Array.from(tabs).find((tab) => tab.getAttribute('data-tab-for') === 'markdown-writer');
const tabPreviewer = Array.from(tabs).find((tab) => tab.getAttribute('data-tab-for') === 'markdown-previewer');
tabEditor.setAttribute('data-tab', `markdown-writer-${elementIdCounter}`);
tabPreviewer.setAttribute('data-tab', `markdown-previewer-${elementIdCounter}`);
tabEditor.setAttribute('data-tab', `markdown-writer-${this.elementIdSuffix}`);
tabPreviewer.setAttribute('data-tab', `markdown-previewer-${this.elementIdSuffix}`);
const toolbar = $container[0].querySelector('markdown-toolbar');
const panelEditor = $container[0].querySelector('.ui.tab[data-tab-panel="markdown-writer"]');
const panelPreviewer = $container[0].querySelector('.ui.tab[data-tab-panel="markdown-previewer"]');
panelEditor.setAttribute('data-tab', `markdown-writer-${elementIdCounter}`);
panelPreviewer.setAttribute('data-tab', `markdown-previewer-${elementIdCounter}`);
panelEditor.setAttribute('data-tab', `markdown-writer-${this.elementIdSuffix}`);
panelPreviewer.setAttribute('data-tab', `markdown-previewer-${this.elementIdSuffix}`);
tabEditor.addEventListener('click', () => {
toolbar.classList.remove('markdown-toolbar-hidden');
@ -276,10 +275,10 @@ class ComboMarkdownEditor {
setupTableInserter() {
const newTableModal = this.container.querySelector('div[data-modal-name="new-markdown-table"]');
newTableModal.setAttribute('data-markdown-table-modal-id', elementIdCounter);
newTableModal.setAttribute('data-markdown-table-modal-id', this.elementIdSuffix);
const button = newTableModal.querySelector('button[data-selector-name="ok-button"]');
button.setAttribute('data-element-id', elementIdCounter);
button.setAttribute('data-element-id', this.elementIdSuffix);
button.addEventListener('click', this.addNewTable);
}
@ -311,8 +310,8 @@ class ComboMarkdownEditor {
setupLinkInserter() {
const newLinkModal = this.container.querySelector('div[data-modal-name="new-markdown-link"]');
newLinkModal.setAttribute('data-markdown-link-modal-id', elementIdCounter);
const textarea = document.getElementById(`_combo_markdown_editor_${elementIdCounter}`);
newLinkModal.setAttribute('data-markdown-link-modal-id', this.elementIdSuffix);
const textarea = document.getElementById(`_combo_markdown_editor_${this.elementIdSuffix}`);
$(newLinkModal).modal({
// Pre-fill the description field from the selection to create behavior similar
@ -331,7 +330,7 @@ class ComboMarkdownEditor {
});
const button = newLinkModal.querySelector('button[data-selector-name="ok-button"]');
button.setAttribute('data-element-id', elementIdCounter);
button.setAttribute('data-element-id', this.elementIdSuffix);
button.addEventListener('click', this.addNewLink);
}