diff --git a/tests/e2e/declare_repos_test.go b/tests/e2e/declare_repos_test.go index 93f69faf4c..836c2c6552 100644 --- a/tests/e2e/declare_repos_test.go +++ b/tests/e2e/declare_repos_test.go @@ -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{ diff --git a/tests/e2e/markdown-editor.test.e2e.ts b/tests/e2e/markdown-editor.test.e2e.ts index 2b5f0d80a0..7983907343 100644 --- a/tests/e2e/markdown-editor.test.e2e.ts +++ b/tests/e2e/markdown-editor.test.e2e.ts @@ -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); +}); diff --git a/web_src/js/features/comp/ComboMarkdownEditor.js b/web_src/js/features/comp/ComboMarkdownEditor.js index 3c94274175..4edd13bfc2 100644 --- a/web_src/js/features/comp/ComboMarkdownEditor.js +++ b/web_src/js/features/comp/ComboMarkdownEditor.js @@ -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); }