diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 0700ee1624..a7b10fe7fd 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -225,6 +225,13 @@ buttons.enable_monospace_font = Enable monospace font buttons.disable_monospace_font = Disable monospace font buttons.indent.tooltip = Nest items by one level buttons.unindent.tooltip = Unnest items by one level +buttons.new_table.tooltip = Add table + +table_modal.header = Add table +table_modal.placeholder.header = Header +table_modal.placeholder.content = Content +table_modal.label.rows = Rows +table_modal.label.columns = Columns [filter] string.asc = A - Z diff --git a/templates/shared/combomarkdowneditor.tmpl b/templates/shared/combomarkdowneditor.tmpl index 48f72b6991..9fcab8a9ae 100644 --- a/templates/shared/combomarkdowneditor.tmpl +++ b/templates/shared/combomarkdowneditor.tmpl @@ -37,6 +37,7 @@ Template Attributes: {{svg "octicon-tasklist"}} +
{{svg "octicon-mention"}} @@ -61,4 +62,30 @@ Template Attributes:
{{ctx.Locale.Tr "loading"}}
+ +
diff --git a/tests/e2e/markdown-editor.test.e2e.js b/tests/e2e/markdown-editor.test.e2e.js index 37b8fecf36..36b35d7e8e 100644 --- a/tests/e2e/markdown-editor.test.e2e.js +++ b/tests/e2e/markdown-editor.test.e2e.js @@ -3,6 +3,7 @@ // @watch start // web_src/js/features/comp/ComboMarkdownEditor.js // web_src/css/editor/combomarkdowneditor.css +// templates/shared/combomarkdowneditor.tmpl // @watch end import {expect} from '@playwright/test'; @@ -181,3 +182,27 @@ test('markdown list continuation', async ({browser}, workerInfo) => { await expect(textarea).toHaveValue(`${prefix}one\n${prefix}two`); } }); + +test('markdown insert table', async ({browser}, workerInfo) => { + const context = await load_logged_in_context(browser, workerInfo, 'user2'); + + const page = await context.newPage(); + const response = await page.goto('/user2/repo1/issues/new'); + expect(response?.status()).toBe(200); + + const newTableButton = page.locator('button[data-md-action="new-table"]'); + await newTableButton.click(); + + const newTableModal = page.locator('div[data-markdown-table-modal-id="0"]'); + await expect(newTableModal).toBeVisible(); + + await newTableModal.locator('input[name="table-rows"]').fill('3'); + await newTableModal.locator('input[name="table-columns"]').fill('2'); + + await newTableModal.locator('button[data-selector-name="ok-button"]').click(); + + await expect(newTableModal).toBeHidden(); + + const textarea = page.locator('textarea[name=content]'); + await expect(textarea).toHaveValue('| Header | Header |\n|---------|---------|\n| Content | Content |\n| Content | Content |\n| Content | Content |\n'); +}); diff --git a/web_src/js/features/comp/ComboMarkdownEditor.js b/web_src/js/features/comp/ComboMarkdownEditor.js index 65ca7db882..56628f83b0 100644 --- a/web_src/js/features/comp/ComboMarkdownEditor.js +++ b/web_src/js/features/comp/ComboMarkdownEditor.js @@ -2,7 +2,7 @@ import '@github/markdown-toolbar-element'; import '@github/text-expander-element'; import $ from 'jquery'; import {attachTribute} from '../tribute.js'; -import {hideElem, showElem, autosize, isElemVisible} from '../../utils/dom.js'; +import {hideElem, showElem, autosize, isElemVisible, replaceTextareaSelection} from '../../utils/dom.js'; import {initEasyMDEPaste, initTextareaPaste} from './Paste.js'; import {handleGlobalEnterQuickSubmit} from './QuickSubmit.js'; import {renderPreviewPanelContent} from '../repo-editor.js'; @@ -48,8 +48,11 @@ class ComboMarkdownEditor { this.setupTab(); this.setupDropzone(); this.setupTextarea(); + this.setupTableInserter(); await this.switchToUserPreference(); + + elementIdCounter++; } applyEditorHeights(el, heights) { @@ -67,7 +70,7 @@ class ComboMarkdownEditor { setupTextarea() { this.textarea = this.container.querySelector('.markdown-text-editor'); this.textarea._giteaComboMarkdownEditor = this; - this.textarea.id = `_combo_markdown_editor_${String(elementIdCounter++)}`; + this.textarea.id = `_combo_markdown_editor_${elementIdCounter}`; this.textarea.addEventListener('input', (e) => this.options?.onContentChanged?.(this, e)); this.applyEditorHeights(this.textarea, this.options.editorHeights); @@ -89,6 +92,7 @@ class ComboMarkdownEditor { this.textareaMarkdownToolbar.querySelector('button[data-md-action="unindent"]')?.addEventListener('click', () => { this.indentSelection(true); }); + this.textareaMarkdownToolbar.querySelector('button[data-md-action="new-table"]')?.setAttribute('data-modal', `div[data-markdown-table-modal-id="${elementIdCounter}"]`); this.textarea.addEventListener('keydown', (e) => { if (e.shiftKey) { @@ -155,7 +159,6 @@ class ComboMarkdownEditor { 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}`); - elementIdCounter++; tabEditor.addEventListener('click', () => { requestAnimationFrame(() => { @@ -181,6 +184,48 @@ class ComboMarkdownEditor { }); } + addNewTable(event) { + const elementId = event.target.getAttribute('data-element-id'); + const newTableModal = document.querySelector(`div[data-markdown-table-modal-id="${elementId}"]`); + const form = newTableModal.querySelector('div[data-selector-name="form"]'); + + // Vaildate input fields + for (const currentInput of form.querySelectorAll('input')) { + if (!currentInput.checkValidity()) { + currentInput.reportValidity(); + return; + } + } + + let headerText = form.querySelector('input[name="table-header"]').value; + let contentText = form.querySelector('input[name="table-content"]').value; + const rowCount = parseInt(form.querySelector('input[name="table-rows"]').value); + const columnCount = parseInt(form.querySelector('input[name="table-columns"]').value); + + headerText = headerText.padEnd(contentText.length); + contentText = contentText.padEnd(headerText.length); + + let code = `| ${(new Array(columnCount)).fill(headerText).join(' | ')} |\n`; + code += `|-${(new Array(columnCount)).fill('-'.repeat(headerText.length)).join('-|-')}-|\n`; + for (let i = 0; i < rowCount; i++) { + code += `| ${(new Array(columnCount)).fill(contentText).join(' | ')} |\n`; + } + + replaceTextareaSelection(document.getElementById(`_combo_markdown_editor_${elementId}`), code); + + // Close the modal + newTableModal.querySelector('button[data-selector-name="cancel-button"]').click(); + } + + setupTableInserter() { + const newTableModal = this.container.querySelector('div[data-modal-name="new-markdown-table"]'); + newTableModal.setAttribute('data-markdown-table-modal-id', elementIdCounter); + + const button = newTableModal.querySelector('button[data-selector-name="ok-button"]'); + button.setAttribute('data-element-id', elementIdCounter); + button.addEventListener('click', this.addNewTable); + } + prepareEasyMDEToolbarActions() { this.easyMDEToolbarDefault = [ 'bold', 'italic', 'strikethrough', '|', 'heading-1', 'heading-2', 'heading-3',