EasyMDE
EasyMDE Markdown text editor powered by ALpineJS and Laravel Blade View Components.
Usage
Add the style and script file of EasyMDE.
@push('styles')
<link rel="stylesheet" href="https://unpkg.com/easymde/dist/easymde.min.css">
@endpush
@push('scripts')
<script src="https://unpkg.com/easymde/dist/easymde.min.js" defer></script>
@endpush
// in blade view
<x-easymde-editor
label="Body"
name="body" />
Add this custom css for the component that uses svg icons instead of the default FontAwesome icons.
.editor-toolbar {
border: none;
position: sticky;
top: 0;
left: 0;
z-index: 10;
background: white;
opacity: 1;
margin-left: 1px;
margin-right: 1px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
border-top-right-radius: 0.5em;
border-top-left-radius: 0.5em;
overflow: hidden;
}
.editor-toolbar:hover {
opacity: 1;
}
.editor-preview {
top: 50px;
}
.CodeMirror, .CodeMirror-scroll {
min-height: 100px;
border-radius: 0.5rem;
}
.CodeMirror {
overflow: hidden;
margin-top: -50px;
padding-top: 60px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
border-color: #e2e8f0;
}
.CodeMirror-focused {
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
}
.simplemde-haserror .CodeMirror,
.easymde-haserror .CodeMirror {
border: 1px solid rgb(245,101,101);
background-color: rgb(255,245,245);
}
.editor-toolbar a.active,
.editor-toolbar a:hover {
background-color: none !important;
}
.editor-toolbar a,
.editor-toolbar button {
margin-right: 10px;
}
.editor-toolbar.disabled-for-preview button:not(.no-disable) {
opacity: .2;
}
.button-bold:before,
.button-heading-2:before,
.button-italic:before,
.button-strike:before,
.button-code:before,
.button-link:before,
.button-unordered-list:before,
.button-ordered-list:before,
.button-image:before,
.button-preview:before,
.button-quote:before,
.button-columns:before,
.button-fullscreen:before,
.button-clean-block::before {
content: '';
display: flex;
width: 24px;
height: 24px;
background-size: 20px;
background-position: 3px 2px;
background-repeat: no-repeat;
}
.editor-toolbar button.heading-2:after {
content: "";
}
.editor-toolbar:after,
.editor-toolbar:before {
display: block;
content: '';
height: 0;
}
.button-clean-block::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='icon icon-tabler icon-tabler-eraser' width='44' height='44' viewBox='0 0 24 24' stroke-width='1.5' stroke='%232c3e50' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z'/%3E%3Cpath d='M19 19h-11l-4 -4a1 1 0 0 1 0 -1.41l10 -10a1 1 0 0 1 1.41 0l5 5a1 1 0 0 1 0 1.41l-9 9' /%3E%3Cpath d='M18 12.3l-6.3 -6.3' /%3E%3C/svg%3E");
}
.button-preview:before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='icon icon-tabler icon-tabler-eye' width='44' height='44' viewBox='0 0 24 24' stroke-width='1.5' stroke='%232c3e50' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z'/%3E%3Ccircle cx='12' cy='12' r='2' /%3E%3Cpath d='M2 12l1.5 2a11 11 0 0 0 17 0l1.5 -2' /%3E%3Cpath d='M2 12l1.5 -2a11 11 0 0 1 17 0l1.5 2' /%3E%3C/svg%3E");
}
.button-bold:before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='icon icon-tabler icon-tabler-bold' width='44' height='44' viewBox='0 0 24 24' stroke-width='1.5' stroke='%232c3e50' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z'/%3E%3Cpath d='M7 5h6a3.5 3.5 0 0 1 0 7h-6z' /%3E%3Cpath d='M13 12h1a3.5 3.5 0 0 1 0 7h-7v-7' /%3E%3C/svg%3E");
}
.button-italic:before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='icon icon-tabler icon-tabler-italic' width='44' height='44' viewBox='0 0 24 24' stroke-width='1.5' stroke='%232c3e50' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z'/%3E%3Cline x1='11' y1='5' x2='17' y2='5' /%3E%3Cline x1='7' y1='19' x2='13' y2='19' /%3E%3Cline x1='14' y1='5' x2='10' y2='19' /%3E%3C/svg%3E");
}
.button-strike:before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='icon icon-tabler icon-tabler-strikethrough' width='44' height='44' viewBox='0 0 24 24' stroke-width='1.5' stroke='%232c3e50' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z'/%3E%3Cline x1='5' y1='12' x2='19' y2='12' /%3E%3Cpath d='M16 6.5a4 2 0 0 0 -4 -1.5h-1a3.5 3.5 0 0 0 0 7' /%3E%3Cpath d='M16.5 16a3.5 3.5 0 0 1 -3.5 3h-1.5a4 2 0 0 1 -4 -1.5' /%3E%3C/svg%3E");
}
.button-code:before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='icon icon-tabler icon-tabler-code' width='44' height='44' viewBox='0 0 24 24' stroke-width='1.5' stroke='%232c3e50' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z'/%3E%3Cpolyline points='7 8 3 12 7 16' /%3E%3Cpolyline points='17 8 21 12 17 16' /%3E%3Cline x1='14' y1='4' x2='10' y2='20' /%3E%3C/svg%3E");
}
.button-link:before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='icon icon-tabler icon-tabler-link' width='44' height='44' viewBox='0 0 24 24' stroke-width='1.5' stroke='%232c3e50' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z'/%3E%3Cpath d='M10 14a3.5 3.5 0 0 0 5 0l4 -4a3.5 3.5 0 0 0 -5 -5l-.5 .5' /%3E%3Cpath d='M14 10a3.5 3.5 0 0 0 -5 0l-4 4a3.5 3.5 0 0 0 5 5l.5 -.5' /%3E%3C/svg%3E");
}
.button-unordered-list:before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='icon icon-tabler icon-tabler-list' width='44' height='44' viewBox='0 0 24 24' stroke-width='1.5' stroke='%232c3e50' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z'/%3E%3Cline x1='9' y1='6' x2='20' y2='6' /%3E%3Cline x1='9' y1='12' x2='20' y2='12' /%3E%3Cline x1='9' y1='18' x2='20' y2='18' /%3E%3Cline x1='5' y1='6' x2='5' y2='6.01' /%3E%3Cline x1='5' y1='12' x2='5' y2='12.01' /%3E%3Cline x1='5' y1='18' x2='5' y2='18.01' /%3E%3C/svg%3E");
}
.button-image:before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='icon icon-tabler icon-tabler-photo' width='44' height='44' viewBox='0 0 24 24' stroke-width='1.5' stroke='%232c3e50' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z'/%3E%3Cline x1='15' y1='8' x2='15.01' y2='8' /%3E%3Crect x='4' y='4' width='16' height='16' rx='3' /%3E%3Cpath d='M4 15l4 -4a3 5 0 0 1 3 0l 5 5' /%3E%3Cpath d='M14 14l1 -1a3 5 0 0 1 3 0l 2 2' /%3E%3C/svg%3E");
}
.button-quote:before {
background-image: url("data:image/svg+xml,%3Csvg width='1em' height='1em' viewBox='0 0 16 16' class='bi bi-blockquote-left' fill='%232c3e50' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' d='M2 3.5a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5zm5 3a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5zm0 3a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5zm-5 3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5z'/%3E%3Cpath d='M3.734 6.352a6.586 6.586 0 0 0-.445.275 1.94 1.94 0 0 0-.346.299 1.38 1.38 0 0 0-.252.369c-.058.129-.1.295-.123.498h.282c.242 0 .431.06.568.182.14.117.21.29.21.521a.697.697 0 0 1-.187.463c-.12.14-.289.21-.503.21-.336 0-.577-.108-.721-.327C2.072 8.619 2 8.328 2 7.969c0-.254.055-.485.164-.692.11-.21.242-.398.398-.562.16-.168.33-.31.51-.428.18-.117.33-.213.451-.287l.211.352zm2.168 0a6.588 6.588 0 0 0-.445.275 1.94 1.94 0 0 0-.346.299c-.113.12-.199.246-.257.375a1.75 1.75 0 0 0-.118.492h.282c.242 0 .431.06.568.182.14.117.21.29.21.521a.697.697 0 0 1-.187.463c-.12.14-.289.21-.504.21-.335 0-.576-.108-.72-.327-.145-.223-.217-.514-.217-.873 0-.254.055-.485.164-.692.11-.21.242-.398.398-.562.16-.168.33-.31.51-.428.18-.117.33-.213.451-.287l.211.352z'/%3E%3C/svg%3E");
}
.button-ordered-list:before {
background-image: url("data:image/svg+xml,%3Csvg width='1em' height='1em' viewBox='0 0 16 16' class='bi bi-list-ol' fill='%232c3e50' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' d='M5 11.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5z'/%3E%3Cpath d='M1.713 11.865v-.474H2c.217 0 .363-.137.363-.317 0-.185-.158-.31-.361-.31-.223 0-.367.152-.373.31h-.59c.016-.467.373-.787.986-.787.588-.002.954.291.957.703a.595.595 0 0 1-.492.594v.033a.615.615 0 0 1 .569.631c.003.533-.502.8-1.051.8-.656 0-1-.37-1.008-.794h.582c.008.178.186.306.422.309.254 0 .424-.145.422-.35-.002-.195-.155-.348-.414-.348h-.3zm-.004-4.699h-.604v-.035c0-.408.295-.844.958-.844.583 0 .96.326.96.756 0 .389-.257.617-.476.848l-.537.572v.03h1.054V9H1.143v-.395l.957-.99c.138-.142.293-.304.293-.508 0-.18-.147-.32-.342-.32a.33.33 0 0 0-.342.338v.041zM2.564 5h-.635V2.924h-.031l-.598.42v-.567l.629-.443h.635V5z'/%3E%3C/svg%3E");
}
.button-heading-2:before {
background-image: url("data:image/svg+xml,%3Csvg width='1em' height='1em' viewBox='0 0 16 16' class='bi bi-type-h2' fill='%232c3e50' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M7.638 13V3.669H6.38V7.62H1.759V3.67H.5V13h1.258V8.728h4.62V13h1.259zm3.022-6.733v-.048c0-.889.63-1.668 1.716-1.668.957 0 1.675.608 1.675 1.572 0 .855-.554 1.504-1.067 2.085l-3.513 3.999V13H15.5v-1.094h-4.245v-.075l2.481-2.844c.875-.998 1.586-1.784 1.586-2.953 0-1.463-1.155-2.556-2.919-2.556-1.941 0-2.966 1.326-2.966 2.74v.049h1.223z'/%3E%3C/svg%3E");
}
.button-columns:before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='icon icon-tabler icon-tabler-layout-columns' width='44' height='44' viewBox='0 0 24 24' stroke-width='1.5' stroke='%232c3e50' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z'/%3E%3Crect x='4' y='4' width='16' height='16' rx='2' /%3E%3Cline x1='12' y1='4' x2='12' y2='20' /%3E%3C/svg%3E");
}
.button-fullscreen:before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='icon icon-tabler icon-tabler-maximize' width='44' height='44' viewBox='0 0 24 24' stroke-width='1.5' stroke='%232c3e50' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z'/%3E%3Cpath d='M4 8v-2a2 2 0 0 1 2 -2h2' /%3E%3Cpath d='M4 16v2a2 2 0 0 0 2 2h2' /%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v2' /%3E%3Cpath d='M16 20h2a2 2 0 0 0 2 -2v-2' /%3E%3C/svg%3E");
}
.editor-preview-active pre {
border-radius: 0.5rem;
padding: 10px;
}
Syntax Highlighting
For syntax highlighting in blade view include the following files.
@push('styles')
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.18.3/styles/atom-one-dark.min.css">
@endpush
@push('scripts')
<script src="https://cdn.jsdelivr.net/highlight.js/latest/highlight.min.js" defer></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('pre code').forEach((block) => {
hljs.highlightBlock(block);
block.classList.add('rounded-lg');
});
});
</script>
@endpush
Markdown Styles
Add the following css styles for styling the markdown content in your view
.markdown-content {
max-width: 65ch; // change according to need...
}
.markdown-content pre code {
padding: 1rem;
font-size: 0.9rem;
}
.markdown-content div,
.markdown-content p,
.markdown-content pre,
.markdown-content blockquote,
.markdown-content ol,
.markdown-content ul {
margin-bottom: 1.5rem;
}
.markdown-content h1,
.markdown-content h2,
.markdown-content h3 {
color: #1a202c;
font-size: 2rem;
font-weight: bold;
margin-top: 0;
margin-bottom: 0.75rem;
line-height: 1.2;
}
.markdown-content strong {
font-weight: bold;
}
.markdown-content blockquote {
display: block;
border-left: 4px solid #667eea;
padding-left: 0.8em;
font-size: 1.25rem;
font-style: italic;
font-weight: 400;
}
.markdown-content a {
color: #667eea;
text-decoration: underline;
text-decoration-color: hsla(229,76%,66%, 0.3);
-moz-text-decoration-color: hsla(229,76%,66%, 0.3);
}
.markdown-content a:hover {
text-decoration-color: hsla(229,76%,66%, 1);
-moz-text-decoration-color: hsla(229,76%,66%,1);
}
.markdown-content ol,
.markdown-content ul {
display: block;
}
.markdown-content li {
position: relative;
padding-left: 1.5em;
margin-bottom: 1rem;
}
.markdown-content ul li:before {
position: absolute;
top: 10px;
left: 0;
content: "";
width: 0.4em;
height: 0.4em;
background-color: #667eea;
border-radius: 50%;
display: inline-block;
}
.markdown-content ol {
counter-reset: custom-counter;
}
.markdown-content ol li:before {
counter-increment: custom-counter;
position: absolute;
top: 2px;
left: 0;
content: counter(custom-counter) ".";
display: inline-block;
font-size: 0.85em;
font-weight: 500;
color: #667eea;
text-align: right;
}
Component
// components/easymde-editor.blade.php
<div class="mb-5">
@if($label ?? null)
<label for="{{ $name }}" class="form-label block mb-1 font-semibold text-gray-700">
{{ $label }}
@if($optional ?? null)
<span class="text-sm text-gray-500 font-normal">(optional)</span>
@endif
</label>
@endif
@php $id = $name . Str::random(8); @endphp
<div
x-data="{ error: '', showToolbar: true }"
x-init="
showToolbar = Boolean('{{ $toolbar ?? true }}');
document.addEventListener('DOMContentLoaded', () => {
toolbarSettings = [
{
name: 'bold',
action: EasyMDE.toggleBold,
className: 'button-bold',
title: 'Bold'
},
{
name: 'italic',
action: EasyMDE.toggleItalic,
className: 'button-italic',
title: 'Italic'
},
{
name: 'strikethrough',
action: EasyMDE.toggleStrikethrough,
className: 'button-strike',
title: 'Strikethrough'
},
{
name: 'heading-2',
action: EasyMDE.toggleHeading2,
className: 'button-heading-2',
title: 'Heading'
},
{
name: 'quote',
action: EasyMDE.toggleBlockquote,
className: 'button-quote',
title: 'Quote'
},
{
name: 'unordered-list',
action: EasyMDE.toggleUnorderedList,
className: 'button-unordered-list',
title: 'Unordered list'
},
{
name: 'ordered-list',
action: EasyMDE.toggleOrderedList,
className: 'button-ordered-list',
title: 'Ordered list'
},
{
name: 'link',
action: EasyMDE.drawLink,
className: 'button-link',
title: 'Create Link'
},
{
name: 'code',
action: EasyMDE.toggleCodeBlock,
className: 'button-code',
title: 'Code'
},
{
name: 'image',
action: EasyMDE.drawImage,
className: 'button-image',
title: 'Insert Image'
},
{
name: 'clean-block',
action: EasyMDE.cleanBlock,
className: 'button-clean-block',
title: 'Clean block'
},
{
name: 'preview',
action: EasyMDE.togglePreview,
className: 'button-preview no-disable',
title: 'Toggle Preview'
},
{
name: 'side-by-side',
action: EasyMDE.toggleSideBySide,
className: 'button-columns no-disable no-mobile',
title: 'Toggle Side by Side'
},
{
name: 'fullscreen',
action: EasyMDE.toggleFullScreen,
className: 'button-fullscreen no-disable no-mobile',
title: 'Toggle Fullscreen'
}
];
new EasyMDE({
hideIcons: {{ json_encode($hideIcons ?? []) }},
status: false,
autoDownloadFontAwesome: false,
forceSync: true,
element: $refs.input,
toolbar: showToolbar == true ? toolbarSettings : false,
renderingConfig: {
codeSyntaxHighlighting: true
},
indentWithTabs: true,
lineWrapping: true,
tabSize: 4,
placeholder: '{{ $placeholder ?? 'Write something...' }}'
});
});
"
@js-errors.window="error = $event.detail.errors.{{ $name }} || ''"
class="relative"
x-cloak>
<div
class="relative"
:class="{'easymde-haserror' : error.length || '{{ $errors->has($name) }}'}">
<textarea
id="{{ $id }}"
placeholder="{{ $placeholder ?? '' }}"
{{ $attributes->merge() }}
x-ref="input">{{ old($name, $value ?? '') }}</textarea>
@isset($hint)
<div class="text-sm text-gray-500 my-2 leading-tight help-text">{{ $hint }}</div>
@endisset
<div x-show="error.length > 0">
<svg class="z-20 absolute text-red-600 fill-current w-5 h-5" style="top: 12px; right: 12px"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M11.953,2C6.465,2,2,6.486,2,12s4.486,10,10,10s10-4.486,10-10S17.493,2,11.953,2z M13,17h-2v-2h2V17z M13,13h-2V7h2V13z" />
</svg>
<div class="text-red-600 mt-2 text-sm block leading-tight error-text" x-html="error"></div>
</div>
@error($name)
<svg class="z-20 absolute text-red-600 fill-current w-5 h-5" style="top: 12px; right: 12px"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M11.953,2C6.465,2,2,6.486,2,12s4.486,10,10,10s10-4.486,10-10S17.493,2,11.953,2z M13,17h-2v-2h2V17z M13,13h-2V7h2V13z" />
</svg>
<div class="text-red-600 mt-2 text-sm block leading-tight error-text">{{ $message }}</div>
@enderror
</div>
</div>
</div>