ui: use switch for markdown editor modes (#7481)

Replaces https://codeberg.org/forgejo/forgejo/pulls/5478
Closes https://codeberg.org/forgejo/forgejo/issues/244

Use switch for preview mode switching instead of tabs. It is placed in line with the toolbar buttons.

Preview:
* https://codeberg.org/attachments/38910747-c14c-41d1-9935-c35f3e17033b
* https://codeberg.org/attachments/ff8ea47a-f157-424f-8b7f-af1008d5e8b5

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/7481
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Reviewed-by: Beowulf <beowulf@beocode.eu>
This commit is contained in:
0ko 2025-04-07 13:47:13 +00:00
parent 8296a23d79
commit afffbe2982
5 changed files with 94 additions and 42 deletions

View file

@ -13,43 +13,44 @@ Template Attributes:
* EasyMDE: whether to display button for switching to legacy editor * EasyMDE: whether to display button for switching to legacy editor
*/}} */}}
<div {{if .ContainerId}}id="{{.ContainerId}}"{{end}} class="combo-markdown-editor {{.ContainerClasses}}" data-dropzone-parent-container="{{.DropzoneParentContainer}}"> <div {{if .ContainerId}}id="{{.ContainerId}}"{{end}} class="combo-markdown-editor {{.ContainerClasses}}" data-dropzone-parent-container="{{.DropzoneParentContainer}}">
{{if .MarkdownPreviewUrl}}
<div class="ui top tabular menu"> <markdown-toolbar>
<a href="#" class="active item" data-tab-for="markdown-writer">{{ctx.Locale.Tr "write"}}</a> {{if .MarkdownPreviewUrl}}
<a href="#" class="item" data-tab-for="markdown-previewer" data-preview-url="{{.MarkdownPreviewUrl}}" data-preview-context="{{.MarkdownPreviewContext}}">{{ctx.Locale.Tr "preview"}}</a> <div class="switch">
</div> <a href="#" class="active item" data-tab-for="markdown-writer">{{ctx.Locale.Tr "write"}}</a>
{{end}} <a href="#" class="item" data-tab-for="markdown-previewer" data-preview-url="{{.MarkdownPreviewUrl}}" data-preview-context="{{.MarkdownPreviewContext}}">{{ctx.Locale.Tr "preview"}}</a>
</div>
{{end}}
<div class="markdown-toolbar-group">
<md-header class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.heading.tooltip"}}">{{svg "octicon-heading"}}</md-header>
<md-bold class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.bold.tooltip"}}">{{svg "octicon-bold"}}</md-bold>
<md-italic class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.italic.tooltip"}}">{{svg "octicon-italic"}}</md-italic>
</div>
<div class="markdown-toolbar-group">
<md-quote class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.quote.tooltip"}}">{{svg "octicon-quote"}}</md-quote>
<md-code class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.code.tooltip"}}">{{svg "octicon-code"}}</md-code>
<button class="markdown-toolbar-button show-modal button" data-md-button data-md-action="new-link" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.link.tooltip"}}">{{svg "octicon-link"}}</button>
</div>
<div class="markdown-toolbar-group">
<md-unordered-list class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.list.unordered.tooltip"}}">{{svg "octicon-list-unordered"}}</md-unordered-list>
<md-ordered-list class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.list.ordered.tooltip"}}">{{svg "octicon-list-ordered"}}</md-ordered-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="indent" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.indent.tooltip"}}">{{svg "octicon-arrow-right"}}</button>
</div>
<div class="markdown-toolbar-group">
<button type="button" class="markdown-toolbar-button show-modal button" data-md-button data-md-action="new-table" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.new_table.tooltip"}}">{{svg "octicon-table"}}</button>
<md-mention class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.mention.tooltip"}}">{{svg "octicon-mention"}}</md-mention>
<md-ref class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.ref.tooltip"}}">{{svg "octicon-cross-reference"}}</md-ref>
</div>
<div class="markdown-toolbar-group">
<button class="markdown-toolbar-button markdown-switch-monospace" data-md-button role="switch" data-enable-text="{{ctx.Locale.Tr "editor.buttons.enable_monospace_font"}}" data-disable-text="{{ctx.Locale.Tr "editor.buttons.disable_monospace_font"}}">{{svg "octicon-typography"}}</button>
{{if .EasyMDE}}
<button class="markdown-toolbar-button markdown-switch-easymde" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.switch_to_legacy.tooltip"}}">{{svg "octicon-arrow-switch"}}</button>
{{end}}
</div>
</markdown-toolbar>
<div class="ui tab active" data-tab-panel="markdown-writer"> <div class="ui tab active" data-tab-panel="markdown-writer">
<markdown-toolbar>
<div class="markdown-toolbar-group">
<md-header class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.heading.tooltip"}}">{{svg "octicon-heading"}}</md-header>
<md-bold class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.bold.tooltip"}}">{{svg "octicon-bold"}}</md-bold>
<md-italic class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.italic.tooltip"}}">{{svg "octicon-italic"}}</md-italic>
</div>
<div class="markdown-toolbar-group">
<md-quote class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.quote.tooltip"}}">{{svg "octicon-quote"}}</md-quote>
<md-code class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.code.tooltip"}}">{{svg "octicon-code"}}</md-code>
<button class="markdown-toolbar-button show-modal button" data-md-button data-md-action="new-link" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.link.tooltip"}}">{{svg "octicon-link"}}</button>
</div>
<div class="markdown-toolbar-group">
<md-unordered-list class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.list.unordered.tooltip"}}">{{svg "octicon-list-unordered"}}</md-unordered-list>
<md-ordered-list class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.list.ordered.tooltip"}}">{{svg "octicon-list-ordered"}}</md-ordered-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="indent" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.indent.tooltip"}}">{{svg "octicon-arrow-right"}}</button>
</div>
<div class="markdown-toolbar-group">
<button type="button" class="markdown-toolbar-button show-modal button" data-md-button data-md-action="new-table" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.new_table.tooltip"}}">{{svg "octicon-table"}}</button>
<md-mention class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.mention.tooltip"}}">{{svg "octicon-mention"}}</md-mention>
<md-ref class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.ref.tooltip"}}">{{svg "octicon-cross-reference"}}</md-ref>
</div>
<div class="markdown-toolbar-group">
<button class="markdown-toolbar-button markdown-switch-monospace" data-md-button role="switch" data-enable-text="{{ctx.Locale.Tr "editor.buttons.enable_monospace_font"}}" data-disable-text="{{ctx.Locale.Tr "editor.buttons.disable_monospace_font"}}">{{svg "octicon-typography"}}</button>
{{if .EasyMDE}}
<button class="markdown-toolbar-button markdown-switch-easymde" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.switch_to_legacy.tooltip"}}">{{svg "octicon-arrow-switch"}}</button>
{{end}}
</div>
</markdown-toolbar>
<text-expander keys=": @" suffix=""> <text-expander keys=": @" suffix="">
<textarea class="markdown-text-editor js-quick-submit"{{if .TextareaName}} name="{{.TextareaName}}"{{end}}{{if .TextareaPlaceholder}} placeholder="{{.TextareaPlaceholder}}"{{end}}{{if .TextareaAriaLabel}} aria-label="{{.TextareaAriaLabel}}"{{end}}{{if .DisableAutosize}} data-disable-autosize="{{.DisableAutosize}}"{{end}}>{{.TextareaContent}}</textarea> <textarea class="markdown-text-editor js-quick-submit"{{if .TextareaName}} name="{{.TextareaName}}"{{end}}{{if .TextareaPlaceholder}} placeholder="{{.TextareaPlaceholder}}"{{end}}{{if .TextareaAriaLabel}} aria-label="{{.TextareaAriaLabel}}"{{end}}{{if .DisableAutosize}} data-disable-autosize="{{.DisableAutosize}}"{{end}}>{{.TextareaContent}}</textarea>
</text-expander> </text-expander>

View file

@ -39,7 +39,7 @@ test('Markdown image preview behaviour', async ({page}, workerInfo) => {
await save_visual(page); await save_visual(page);
}); });
test('markdown indentation', async ({page}) => { test('Markdown indentation', async ({page}) => {
const initText = `* first\n* second\n* third\n* last`; const initText = `* first\n* second\n* third\n* last`;
const response = await page.goto('/user2/repo1/issues/new'); const response = await page.goto('/user2/repo1/issues/new');
@ -109,7 +109,7 @@ test('markdown indentation', async ({page}) => {
await expect(textarea).toHaveValue(initText); await expect(textarea).toHaveValue(initText);
}); });
test('markdown list continuation', async ({page}) => { test('Markdown list continuation', async ({page}) => {
const initText = `* first\n* second`; const initText = `* first\n* second`;
const response = await page.goto('/user2/repo1/issues/new'); const response = await page.goto('/user2/repo1/issues/new');
@ -202,7 +202,7 @@ test('markdown list continuation', async ({page}) => {
} }
}); });
test('markdown insert table', async ({page}) => { test('Markdown insert table', async ({page}) => {
const response = await page.goto('/user2/repo1/issues/new'); const response = await page.goto('/user2/repo1/issues/new');
expect(response?.status()).toBe(200); expect(response?.status()).toBe(200);
@ -225,7 +225,7 @@ test('markdown insert table', async ({page}) => {
await save_visual(page); await save_visual(page);
}); });
test('markdown insert link', async ({page}) => { test('Markdown insert link', async ({page}) => {
const response = await page.goto('/user2/repo1/issues/new'); const response = await page.goto('/user2/repo1/issues/new');
expect(response?.status()).toBe(200); expect(response?.status()).toBe(200);
@ -277,3 +277,43 @@ test('text expander has higher prio then prefix continuation', async ({page}) =>
await textarea.press('Enter'); await textarea.press('Enter');
await expect(textarea).toHaveValue(`* first\n* 😸\n* @user2 \n* `); await expect(textarea).toHaveValue(`* first\n* 😸\n* @user2 \n* `);
}); });
test('Combo Markdown: preview mode switch', async ({page}) => {
// Load page with editor
const response = await page.goto('/user2/repo1/issues/new');
expect(response?.status()).toBe(200);
const toolbarItem = page.locator('md-header');
const editorPanel = page.locator('[data-tab-panel="markdown-writer"]');
const previewPanel = page.locator('[data-tab-panel="markdown-previewer"]');
// Verify correct visibility of related UI elements
await expect(toolbarItem).toBeVisible();
await expect(editorPanel).toBeVisible();
await expect(previewPanel).toBeHidden();
// Fill some content
const textarea = page.locator('textarea.markdown-text-editor');
await textarea.fill('**Content** :100: _100_');
// Switch to preview mode
await page.locator('a[data-tab-for="markdown-previewer"]').click();
// Verify that the related UI elements were switched correctly
await expect(toolbarItem).toBeHidden();
await expect(editorPanel).toBeHidden();
await expect(previewPanel).toBeVisible();
await save_visual(page);
// Verify that some content rendered
await expect(page.locator('[data-tab-panel="markdown-previewer"] .emoji[data-alias="100"]')).toBeVisible();
// Switch back to edit mode
await page.locator('a[data-tab-for="markdown-writer"]').click();
// Verify that the related UI elements were switched back correctly
await expect(toolbarItem).toBeVisible();
await expect(editorPanel).toBeVisible();
await expect(previewPanel).toBeHidden();
await save_visual(page);
});

View file

@ -11,6 +11,14 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
markdown-toolbar .switch .item {
padding: 0.25em 1em;
}
.markdown-toolbar-hidden .markdown-toolbar-button {
display: none;
}
.combo-markdown-editor .markdown-toolbar-group { .combo-markdown-editor .markdown-toolbar-group {
display: flex; display: flex;
} }

View file

@ -151,7 +151,7 @@ class ComboMarkdownEditor {
setupTab() { setupTab() {
const $container = $(this.container); const $container = $(this.container);
const tabs = $container[0].querySelectorAll('.tabular.menu > .item'); const tabs = $container[0].querySelectorAll('.switch > .item');
// Fomantic Tab requires the "data-tab" to be globally unique. // Fomantic Tab requires the "data-tab" to be globally unique.
// So here it uses our defined "data-tab-for" and "data-tab-panel" to generate the "data-tab" attribute for Fomantic. // So here it uses our defined "data-tab-for" and "data-tab-panel" to generate the "data-tab" attribute for Fomantic.
@ -159,12 +159,14 @@ class ComboMarkdownEditor {
const tabPreviewer = Array.from(tabs).find((tab) => tab.getAttribute('data-tab-for') === 'markdown-previewer'); const tabPreviewer = Array.from(tabs).find((tab) => tab.getAttribute('data-tab-for') === 'markdown-previewer');
tabEditor.setAttribute('data-tab', `markdown-writer-${elementIdCounter}`); tabEditor.setAttribute('data-tab', `markdown-writer-${elementIdCounter}`);
tabPreviewer.setAttribute('data-tab', `markdown-previewer-${elementIdCounter}`); tabPreviewer.setAttribute('data-tab', `markdown-previewer-${elementIdCounter}`);
const toolbar = $container[0].querySelector('markdown-toolbar');
const panelEditor = $container[0].querySelector('.ui.tab[data-tab-panel="markdown-writer"]'); const panelEditor = $container[0].querySelector('.ui.tab[data-tab-panel="markdown-writer"]');
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}`);
tabEditor.addEventListener('click', () => { tabEditor.addEventListener('click', () => {
toolbar.classList.remove('markdown-toolbar-hidden');
requestAnimationFrame(() => { requestAnimationFrame(() => {
this.focus(); this.focus();
}); });
@ -177,6 +179,7 @@ class ComboMarkdownEditor {
this.previewMode = this.options.previewMode ?? 'comment'; this.previewMode = this.options.previewMode ?? 'comment';
this.previewWiki = this.options.previewWiki ?? false; this.previewWiki = this.options.previewWiki ?? false;
tabPreviewer.addEventListener('click', async () => { tabPreviewer.addEventListener('click', async () => {
toolbar.classList.add('markdown-toolbar-hidden');
const formData = new FormData(); const formData = new FormData();
formData.append('mode', this.previewMode); formData.append('mode', this.previewMode);
formData.append('context', this.previewContext); formData.append('context', this.previewContext);

View file

@ -469,7 +469,7 @@ async function onEditContent(event) {
editContentZone.querySelector('button[data-button-name="cancel-edit"]').addEventListener('click', cancelAndReset); editContentZone.querySelector('button[data-button-name="cancel-edit"]').addEventListener('click', cancelAndReset);
editContentZone.querySelector('button[data-button-name="save-edit"]').addEventListener('click', saveAndRefresh); editContentZone.querySelector('button[data-button-name="save-edit"]').addEventListener('click', saveAndRefresh);
} else { } else {
const tabEditor = editContentZone.querySelector('.combo-markdown-editor').querySelector('.tabular.menu > a[data-tab-for=markdown-writer]'); const tabEditor = editContentZone.querySelector('.combo-markdown-editor').querySelector('.switch > a[data-tab-for=markdown-writer]');
tabEditor?.click(); tabEditor?.click();
} }