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"}}
+
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',