diff --git a/bootstrap/bootstrap.php b/bootstrap/bootstrap.php
index acaebc3bf11..6bbe169c789 100644
--- a/bootstrap/bootstrap.php
+++ b/bootstrap/bootstrap.php
@@ -224,6 +224,7 @@
// Other
Craft::setAlias('@appicons/github.svg', "$brandIconsPath/github.svg");
+Craft::setAlias('@appicons/markdown.svg', "$brandIconsPath/markdown.svg");
Craft::setAlias('@appicons/globe.svg', "$regularIconsPath/globe.svg");
// Renamed icon aliases
diff --git a/scripts/copyicons.php b/scripts/copyicons.php
index 0f1c407a4b9..b5d23bf12f6 100644
--- a/scripts/copyicons.php
+++ b/scripts/copyicons.php
@@ -7,6 +7,7 @@
$icons = [
'brands/github.svg',
+ 'brands/markdown.svg',
'custom-icons/asterisk-slash.svg',
'custom-icons/diamond-slash.svg',
'custom-icons/element-card-slash.svg',
diff --git a/src/fieldlayoutelements/Markdown.php b/src/fieldlayoutelements/Markdown.php
new file mode 100644
index 00000000000..4148230fc6b
--- /dev/null
+++ b/src/fieldlayoutelements/Markdown.php
@@ -0,0 +1,113 @@
+
+ * @since 5.5.0
+ */
+class Markdown extends BaseUiElement
+{
+ /**
+ * @var string The Markdown content
+ */
+ public string $content = '';
+
+ /**
+ * @var bool Whether the content should be displayed in a pane.
+ */
+ public bool $displayInPane = true;
+
+ /**
+ * @inheritdoc
+ */
+ protected function selectorLabel(): string
+ {
+ return StringHelper::firstLine($this->content) ?: 'Markdown';
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function selectorIcon(): ?string
+ {
+ return 'markdown';
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function selectorLabelAttributes(): array
+ {
+ $attr = parent::selectorLabelAttributes();
+ if ($this->content) {
+ $attr['class'][] = 'code';
+ }
+ return $attr;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function hasCustomWidth(): bool
+ {
+ return true;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function hasSettings()
+ {
+ return true;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function settingsHtml(): ?string
+ {
+ return
+ Cp::textareaFieldHtml([
+ 'label' => Craft::t('app', 'Content'),
+ 'class' => ['code', 'nicetext'],
+ 'id' => 'content',
+ 'name' => 'content',
+ 'value' => $this->content,
+ ]) .
+ Cp::lightswitchFieldHtml([
+ 'label' => Craft::t('app', 'Display content in a pane'),
+ 'id' => 'display-in-pane',
+ 'name' => 'displayInPane',
+ 'on' => $this->displayInPane,
+ ]);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function formHtml(?ElementInterface $element = null, bool $static = false): ?string
+ {
+ $content = MarkdownHelper::process(Html::encode($this->content));
+ if ($this->displayInPane) {
+ $content = Html::tag('div', $content, [
+ 'class' => 'pane',
+ ]);
+ }
+ return Html::tag('div', $content, $this->containerAttributes($element, $static));
+ }
+}
diff --git a/src/helpers/StringHelper.php b/src/helpers/StringHelper.php
index 397ec54168f..e7469082450 100644
--- a/src/helpers/StringHelper.php
+++ b/src/helpers/StringHelper.php
@@ -918,6 +918,18 @@ public static function lines(string $str): array
return array_map(fn(BaseStringy $line) => (string)$line, $lines);
}
+ /**
+ * Returns the first line of a string.
+ *
+ * @param string $str
+ * @return string
+ * @since 5.5.0
+ */
+ public static function firstLine(string $str): string
+ {
+ return (string)BaseStringy::create($str)->lines()[0];
+ }
+
/**
* Converts the first character of the supplied string to lower case.
*
diff --git a/src/icons/brands/markdown.svg b/src/icons/brands/markdown.svg
new file mode 100644
index 00000000000..f1166de13a6
--- /dev/null
+++ b/src/icons/brands/markdown.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/models/FieldLayout.php b/src/models/FieldLayout.php
index 8f6e9701ea7..871da240459 100644
--- a/src/models/FieldLayout.php
+++ b/src/models/FieldLayout.php
@@ -22,6 +22,7 @@
use craft\fieldlayoutelements\Heading;
use craft\fieldlayoutelements\HorizontalRule;
use craft\fieldlayoutelements\LineBreak;
+use craft\fieldlayoutelements\Markdown;
use craft\fieldlayoutelements\Template;
use craft\fieldlayoutelements\Tip;
use craft\helpers\ArrayHelper;
@@ -457,6 +458,7 @@ public function getAvailableUiElements(): array
new Heading(),
new Tip(['style' => Tip::STYLE_TIP]),
new Tip(['style' => Tip::STYLE_WARNING]),
+ new Markdown(),
new Template(),
];
diff --git a/src/translations/en/app.php b/src/translations/en/app.php
index 404676e89f1..7d19593e29c 100644
--- a/src/translations/en/app.php
+++ b/src/translations/en/app.php
@@ -552,6 +552,7 @@
'Display Settings' => 'Display Settings',
'Display as cards' => 'Display as cards',
'Display as thumbnails' => 'Display as thumbnails',
+ 'Display content in a pane' => 'Display content in a pane',
'Display hierarchically' => 'Display hierarchically',
'Display in a structured table' => 'Display in a structured table',
'Display in a table' => 'Display in a table',
diff --git a/tests/unit/helpers/StringHelperTest.php b/tests/unit/helpers/StringHelperTest.php
index 64de0c12932..ee12ccc89b5 100644
--- a/tests/unit/helpers/StringHelperTest.php
+++ b/tests/unit/helpers/StringHelperTest.php
@@ -738,6 +738,16 @@ public function testLines(int $expected, string $string): void
self::assertCount($expected, $actual);
}
+ /**
+ * @dataProvider firstLineDataProvider
+ * @param string $expected
+ * @param string $string
+ */
+ public function testFirstLine(string $expected, string $string): void
+ {
+ self::assertEquals($expected, StringHelper::firstLine($string));
+ }
+
/**
*
*/
@@ -2173,6 +2183,41 @@ public static function linesDataProvider(): array
+ ',
+ ],
+ ];
+ }
+
+ /**
+ * @return array
+ */
+ public static function firstLineDataProvider(): array
+ {
+ return [
+ [
+ 'test',
+ 'test
+
+
+ test',
+ ],
+ ['test
test', 'test
test'],
+ ['thesearetabs notspaces', 'thesearetabs notspaces'],
+ [
+ '😂', '😂
+ 😁',
+ ],
+ [
+ '', '
+
+
+
+
+
+
+
+
+
',
],
];