Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lang/en/fieldtypes.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@
'list.title' => 'List',
'markdown.config.automatic_line_breaks' => 'Enables automatic line breaks.',
'markdown.config.automatic_links' => 'Enables automatic linking of any URLs.',
'markdown.config.buttons' => 'Choose which buttons to show in the toolbar. Drag to rearrange.',
'markdown.config.container' => 'Choose which asset container to use for this field.',
'markdown.config.escape_markup' => 'Escapes inline HTML markup (e.g. `<div>` to `&lt;div&gt;`).',
'markdown.config.folder' => 'Specify the initial directory.',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
<button
v-for="button in buttons"
:key="button.name"
type="button"
v-tooltip="button.text"
class="relative hover:bg-white/10!"
:class="{ active: enabled(button.name) }"
:aria-label="button.text"
:aria-pressed="enabled(button.name)"
@click="toggleButton(button.name)"
>
<ui-icon :name="button.svg" />
Expand Down Expand Up @@ -91,6 +95,17 @@ export default {
swapAnimation: { horizontal: true },
plugins: [Plugins.SwapAnimation],
distance: 10,
classes: {
'source:dragging': [
'draggable-source--is-dragging',
'bg-gray-800!',
'opacity-100!',
'shadow-lg',
'ring-1',
'ring-white/15',
'z-10',
],
},
})
.on('sortable:stop', (e) => {
this.buttons.splice(e.newIndex, 0, this.buttons.splice(e.oldIndex, 1)[0]);
Expand Down
20 changes: 20 additions & 0 deletions resources/js/components/fieldtypes/markdown/MarkdownFieldtype.vue
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,26 @@ export default {
return text.slice(delimiter.length, -delimiter.length);
},

toggleHeading(level) {
const startPoint = this.codemirror.getCursor('start');
const endPoint = this.codemirror.getCursor('end');
const pattern = /^(\s*)(#{1,6})(?:\s+|$)/;
const delimiter = '#'.repeat(level);
const prefix = delimiter + ' ';

for (let i = startPoint.line; i <= endPoint.line; i++) {
const text = this.codemirror.getLine(i);
const heading = text.match(pattern);
const newText = heading
? text.replace(pattern, heading[2] === delimiter ? '$1' : '$1' + prefix)
: text.replace(/^(\s*)/, '$1' + prefix);

this.codemirror.replaceRange(newText, { line: i, ch: 0 }, { line: i, ch: Infinity });
}

this.codemirror.focus();
},

toggleLine(type) {
const startPoint = this.codemirror.getCursor('start');
const endPoint = this.codemirror.getCursor('end');
Expand Down
36 changes: 36 additions & 0 deletions resources/js/components/fieldtypes/markdown/buttons.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,40 @@
const availableButtons = () => [
{
name: 'h1',
text: __('Heading 1'),
command: (editor) => editor.toggleHeading(1),
svg: 'h1',
},
{
name: 'h2',
text: __('Heading 2'),
command: (editor) => editor.toggleHeading(2),
svg: 'h2',
},
{
name: 'h3',
text: __('Heading 3'),
command: (editor) => editor.toggleHeading(3),
svg: 'h3',
},
{
name: 'h4',
text: __('Heading 4'),
command: (editor) => editor.toggleHeading(4),
svg: 'h4',
},
{
name: 'h5',
text: __('Heading 5'),
command: (editor) => editor.toggleHeading(5),
svg: 'h5',
},
{
name: 'h6',
text: __('Heading 6'),
command: (editor) => editor.toggleHeading(6),
svg: 'h6',
},
{ name: 'bold', text: __('Bold'), command: (editor) => editor.toggleInline('bold'), svg: 'text-bold' },
{ name: 'italic', text: __('Italic'), command: (editor) => editor.toggleInline('italic'), svg: 'text-italic' },
{ name: 'strikethrough', text: __('Strikethrough'), command: (editor) => editor.toggleInline('strikethrough'), svg: 'text-strike-through' },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { mount } from '@vue/test-utils';
import { expect, test } from 'vitest';
import { expect, test, vi } from 'vitest';
import CodeMirror from 'codemirror';
import { computed, ref } from 'vue';
import * as Globals from '@/bootstrap/globals';
import MarkdownFieldtype from '@/components/fieldtypes/markdown/MarkdownFieldtype.vue';
import { containerContextKey } from '@/components/ui/Publish/Container.vue';
import { availableButtons } from '@/components/fieldtypes/markdown/buttons';

Object.keys(Globals).forEach((fn) => (window[fn] = Globals[fn]));
window.cp_url = (url) => url;
Expand Down Expand Up @@ -59,3 +61,32 @@ test('the preview pane gets the content direction', async () => {
// dir attribute can be asserted without switching into preview mode.
expect(wrapper.find('.markdown-preview').attributes('dir')).toBe('rtl');
});

function headingEditor(text) {
const codemirror = new CodeMirror.Doc(text);
codemirror.focus = vi.fn();

return { codemirror, toggleHeading: MarkdownFieldtype.methods.toggleHeading };
}

test.each([1, 2, 3, 4, 5, 6])('the H%i button toggles its heading level', (level) => {
const editor = headingEditor('hello world');
const button = availableButtons().find((button) => button.name === `h${level}`);

editor.codemirror.setCursor({ line: 0, ch: 5 });
button.command(editor);
expect(editor.codemirror.getValue()).toBe(`${'#'.repeat(level)} hello world`);

button.command(editor);
expect(editor.codemirror.getValue()).toBe('hello world');

expect(editor.codemirror.focus).toHaveBeenCalledTimes(2);
});

test('heading buttons replace existing levels across selected lines', () => {
const editor = headingEditor('# First\n ### Second\nPlain text\n####### Not a heading');
editor.codemirror.setSelection({ line: 0, ch: 0 }, { line: 3, ch: 24 });
editor.toggleHeading(2);

expect(editor.codemirror.getValue()).toBe('## First\n ## Second\n## Plain text\n## ####### Not a heading');
});
4 changes: 2 additions & 2 deletions src/Fieldtypes/Markdown.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,9 @@ protected function configFieldItems(): array
'fields' => [
'buttons' => [
'display' => __('Buttons'),
'instructions' => __('statamic::fieldtypes.bard.config.buttons'),
'instructions' => __('statamic::fieldtypes.markdown.config.buttons'),
'type' => 'markdown_buttons_setting',
'full_width_setting' => true,
'default' => [
'bold',
'italic',
Expand All @@ -65,7 +66,6 @@ protected function configFieldItems(): array
'image',
'table',
],
'width' => 66,
],
'toolbar_mode' => [
'display' => __('Toolbar Mode'),
Expand Down
22 changes: 22 additions & 0 deletions tests/Fieldtypes/MarkdownTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,28 @@ class MarkdownTest extends TestCase
{
use PreventSavingStacheItemsToDisk;

#[Test]
public function heading_buttons_are_opt_in()
{
$buttons = (new Markdown)->configFields()->get('buttons');

$this->assertSame([
'bold', 'italic', 'unorderedlist', 'orderedlist', 'quote', 'link', 'image', 'table',
], $buttons->defaultValue());
$this->assertTrue($buttons->get('full_width_setting'));
}

#[Test]
public function existing_button_selections_and_order_are_preserved()
{
foreach ([[], ['table', 'bold'], ['h6', 'h1', 'italic']] as $buttons) {
$fieldtype = $this->fieldtype(['buttons' => $buttons]);

$this->assertSame($buttons, $fieldtype->config('buttons'));
$this->assertSame($buttons, $fieldtype->config()['buttons']);
}
}

#[Test]
public function it_augments_to_html()
{
Expand Down
Loading