feat: add button to create Markdown table (#5589)
This adds a new button to the Markdown toolbar, which allows creating the structure of a Markdown table. This makes it easier to wok with tables, as creating the structure by hand is annoying. Screenshots: https://codeberg.org/attachments/8bb00059-caa7-4453-b26c-15e4b7b93c83 https://codeberg.org/attachments/581e695c-33eb-4b81-9c63-a944aab443d9 Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5589 Reviewed-by: Otto <otto@codeberg.org> Co-authored-by: JakobDev <jakobdev@gmx.de> Co-committed-by: JakobDev <jakobdev@gmx.de>
This commit is contained in:
parent
e5e2860221
commit
4615891b9d
4 changed files with 107 additions and 3 deletions
|
@ -225,6 +225,13 @@ buttons.enable_monospace_font = Enable monospace font
|
||||||
buttons.disable_monospace_font = Disable monospace font
|
buttons.disable_monospace_font = Disable monospace font
|
||||||
buttons.indent.tooltip = Nest items by one level
|
buttons.indent.tooltip = Nest items by one level
|
||||||
buttons.unindent.tooltip = Unnest 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]
|
[filter]
|
||||||
string.asc = A - Z
|
string.asc = A - Z
|
||||||
|
|
|
@ -37,6 +37,7 @@ Template Attributes:
|
||||||
<md-task-list class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.list.task.tooltip"}}">{{svg "octicon-tasklist"}}</md-task-list>
|
<md-task-list class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.list.task.tooltip"}}">{{svg "octicon-tasklist"}}</md-task-list>
|
||||||
<button type="button" class="markdown-toolbar-button" data-md-button data-md-action="unindent" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.unindent.tooltip"}}">{{svg "octicon-arrow-left"}}</button>
|
<button type="button" class="markdown-toolbar-button" data-md-button data-md-action="unindent" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.unindent.tooltip"}}">{{svg "octicon-arrow-left"}}</button>
|
||||||
<button type="button" class="markdown-toolbar-button" data-md-button data-md-action="indent" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.indent.tooltip"}}">{{svg "octicon-arrow-right"}}</button>
|
<button type="button" class="markdown-toolbar-button" data-md-button data-md-action="indent" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.indent.tooltip"}}">{{svg "octicon-arrow-right"}}</button>
|
||||||
|
<button type="button" class="markdown-toolbar-button show-modal button" data-md-action="new-table" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.new_table.tooltip"}}">{{svg "octicon-table"}}</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="markdown-toolbar-group">
|
<div class="markdown-toolbar-group">
|
||||||
<md-mention class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.mention.tooltip"}}">{{svg "octicon-mention"}}</md-mention>
|
<md-mention class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.mention.tooltip"}}">{{svg "octicon-mention"}}</md-mention>
|
||||||
|
@ -61,4 +62,30 @@ Template Attributes:
|
||||||
<div class="ui tab markup" data-tab-panel="markdown-previewer">
|
<div class="ui tab markup" data-tab-panel="markdown-previewer">
|
||||||
{{ctx.Locale.Tr "loading"}}
|
{{ctx.Locale.Tr "loading"}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="ui small modal tw-w-fit" data-modal-name="new-markdown-table">
|
||||||
|
<div class="header">{{ctx.Locale.Tr "editor.table_modal.header"}}</div>
|
||||||
|
|
||||||
|
<div class="ui form content" data-selector-name="form">
|
||||||
|
<input type="hidden" name="table-header" value="{{ctx.Locale.Tr "editor.table_modal.placeholder.header"}}">
|
||||||
|
<input type="hidden" name="table-content" value="{{ctx.Locale.Tr "editor.table_modal.placeholder.content"}}">
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><label>{{ctx.Locale.Tr "editor.table_modal.label.rows"}}</label></td>
|
||||||
|
<td><input type="number" name="table-rows" min="1" value="2" required></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><label>{{ctx.Locale.Tr "editor.table_modal.label.columns"}}</label></td>
|
||||||
|
<td><input type="number" name="table-columns" min="1" value="2" required></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text right actions">
|
||||||
|
<button class="ui cancel button" data-selector-name="cancel-button">{{ctx.Locale.Tr "cancel"}}</button>
|
||||||
|
<button class="ui blue button" data-selector-name="ok-button">{{ctx.Locale.Tr "ok"}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
// @watch start
|
// @watch start
|
||||||
// web_src/js/features/comp/ComboMarkdownEditor.js
|
// web_src/js/features/comp/ComboMarkdownEditor.js
|
||||||
// web_src/css/editor/combomarkdowneditor.css
|
// web_src/css/editor/combomarkdowneditor.css
|
||||||
|
// templates/shared/combomarkdowneditor.tmpl
|
||||||
// @watch end
|
// @watch end
|
||||||
|
|
||||||
import {expect} from '@playwright/test';
|
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`);
|
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');
|
||||||
|
});
|
||||||
|
|
|
@ -2,7 +2,7 @@ import '@github/markdown-toolbar-element';
|
||||||
import '@github/text-expander-element';
|
import '@github/text-expander-element';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import {attachTribute} from '../tribute.js';
|
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 {initEasyMDEPaste, initTextareaPaste} from './Paste.js';
|
||||||
import {handleGlobalEnterQuickSubmit} from './QuickSubmit.js';
|
import {handleGlobalEnterQuickSubmit} from './QuickSubmit.js';
|
||||||
import {renderPreviewPanelContent} from '../repo-editor.js';
|
import {renderPreviewPanelContent} from '../repo-editor.js';
|
||||||
|
@ -48,8 +48,11 @@ class ComboMarkdownEditor {
|
||||||
this.setupTab();
|
this.setupTab();
|
||||||
this.setupDropzone();
|
this.setupDropzone();
|
||||||
this.setupTextarea();
|
this.setupTextarea();
|
||||||
|
this.setupTableInserter();
|
||||||
|
|
||||||
await this.switchToUserPreference();
|
await this.switchToUserPreference();
|
||||||
|
|
||||||
|
elementIdCounter++;
|
||||||
}
|
}
|
||||||
|
|
||||||
applyEditorHeights(el, heights) {
|
applyEditorHeights(el, heights) {
|
||||||
|
@ -67,7 +70,7 @@ class ComboMarkdownEditor {
|
||||||
setupTextarea() {
|
setupTextarea() {
|
||||||
this.textarea = this.container.querySelector('.markdown-text-editor');
|
this.textarea = this.container.querySelector('.markdown-text-editor');
|
||||||
this.textarea._giteaComboMarkdownEditor = this;
|
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.textarea.addEventListener('input', (e) => this.options?.onContentChanged?.(this, e));
|
||||||
this.applyEditorHeights(this.textarea, this.options.editorHeights);
|
this.applyEditorHeights(this.textarea, this.options.editorHeights);
|
||||||
|
|
||||||
|
@ -89,6 +92,7 @@ class ComboMarkdownEditor {
|
||||||
this.textareaMarkdownToolbar.querySelector('button[data-md-action="unindent"]')?.addEventListener('click', () => {
|
this.textareaMarkdownToolbar.querySelector('button[data-md-action="unindent"]')?.addEventListener('click', () => {
|
||||||
this.indentSelection(true);
|
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) => {
|
this.textarea.addEventListener('keydown', (e) => {
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
|
@ -155,7 +159,6 @@ class ComboMarkdownEditor {
|
||||||
const panelPreviewer = $container[0].querySelector('.ui.tab[data-tab-panel="markdown-previewer"]');
|
const panelPreviewer = $container[0].querySelector('.ui.tab[data-tab-panel="markdown-previewer"]');
|
||||||
panelEditor.setAttribute('data-tab', `markdown-writer-${elementIdCounter}`);
|
panelEditor.setAttribute('data-tab', `markdown-writer-${elementIdCounter}`);
|
||||||
panelPreviewer.setAttribute('data-tab', `markdown-previewer-${elementIdCounter}`);
|
panelPreviewer.setAttribute('data-tab', `markdown-previewer-${elementIdCounter}`);
|
||||||
elementIdCounter++;
|
|
||||||
|
|
||||||
tabEditor.addEventListener('click', () => {
|
tabEditor.addEventListener('click', () => {
|
||||||
requestAnimationFrame(() => {
|
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() {
|
prepareEasyMDEToolbarActions() {
|
||||||
this.easyMDEToolbarDefault = [
|
this.easyMDEToolbarDefault = [
|
||||||
'bold', 'italic', 'strikethrough', '|', 'heading-1', 'heading-2', 'heading-3',
|
'bold', 'italic', 'strikethrough', '|', 'heading-1', 'heading-2', 'heading-3',
|
||||||
|
|
Loading…
Reference in a new issue