<style media="screen">
:root {
--m-modal-button-primary-color: #e0bff5;
--m-modal-button-primary-hover-color: #c27fec;
--m-modal-button-primary-border-color: #c27fec;
--m-modal-button-primary-text-color: #1f2a36;
--m-modal-button-secondary-color: #ff9fb4;
--m-modal-button-secondary-hover-color: #ff4069;
--m-modal-button-secondary-border-color: #ff4069;
--m-modal-button-secondary-text-color: #1f2a36;
}
</style>
<div class="field-group">
<label for="message">Meddelande</label>
<textarea required rows="5" class="a-textarea" id="message" placeholder="Meddelande" autocomplete="" data-max="300" data-min="30"></textarea>
</div>
<style media="screen">
:root {
--m-modal-button-primary-color: {{button_primary_color}};
--m-modal-button-primary-hover-color: {{button_primary_hover_color}};
--m-modal-button-primary-border-color: {{button_primary_border_color}};
--m-modal-button-primary-text-color: {{button_primary_text_color}};
--m-modal-button-secondary-color: {{button_secondary_color}};
--m-modal-button-secondary-hover-color: {{button_secondary_hover_color}};
--m-modal-button-secondary-border-color: {{button_secondary_border_color}};
--m-modal-button-secondary-text-color: {{button_secondary_text_color}};
}
</style>
<div class="field-group{{#if is_invalid}} is-invalid{{/if}}{{#if disabled}} disabled{{/if}}">
{{> @label for="message" label="Meddelande" }}
<textarea required rows="5" class="a-textarea{{#if modifier}} {{modifier}}{{/if}}" id="{{id}}" {{#if is_invalid}}aria-invalid="true"{{/if}} {{#if is_richtext}}data-rich-text{{/if}} {{#if required}}required {{/if}} {{#if is_disabled}}disabled {{/if}} placeholder="{{placeholder}}" autocomplete="{{autocomplete}}"{{#if has_help}} aria-describedby="textarea-help"{{/if}} {{#if required}} aria-describedby="textarea-help"{{/if}}{{#if max}} data-max="{{max}}"{{/if}}{{#if min}} data-min="{{min}}"{{/if}}>{{#if is_richtext}}
<p>Lorem <a href="https://internetstiftelsen.se" target="_blank">ipsum</a> <strong>dolor</strong> sit amet, consectetur adipiscing elit. Praesent mollis suscipit felis, eu ullamcorper quam. Nunc eleifend sapien velit, vitae <em>tincidunt</em> urna mollis ac.</p>
<ul>
<li>aliquet</li>
<li>vehicula</li>
<li>turpis</li>
</ul>
{{/if}}</textarea>
{{#if has_help}}
<div class="input-help" id="textarea-help">
Välj det som passar dig bäst
</div>
{{else if required}}
<div class="input-help" id="textarea-help">
Fätet är obligatoriskt
</div>
{{/if}}
</div>
{
"label": "Meddelande",
"placeholder": "Meddelande",
"id": "message",
"required": false,
"disabled": false,
"button_primary_color": "#e0bff5",
"button_primary_hover_color": "#c27fec",
"button_primary_border_color": "#c27fec",
"button_primary_text_color": "#1f2a36",
"button_secondary_color": "#ff9fb4",
"button_secondary_hover_color": "#ff4069",
"button_secondary_border_color": "#ff4069",
"button_secondary_text_color": "#1f2a36",
"is_richtext": false,
"has_help": false,
"min": 30,
"max": 300
}
@charset "UTF-8";
@use "sass:color";
@use '../../configurations/mixins' as mixin;
@use '../../configurations/extends';
@use '../../configurations/bem' as bem;
@use '../../configurations/variables' as var;
@use '../../configurations/functions' as func;
@use '../../configurations/colors/colors' as colors;
@use '../../configurations/colors/colors-functions' as colorFunc;
@use '../../vendor/baseline/plumber' as plumber;
@include mixin.atom(textarea) {
@extend %input;
min-width: 100%;
max-width: 100%;
min-height: func.rhythm(12);
&:focus {
@extend %input-focus;
}
&[disabled] {
@extend %disabled;
}
@include bem.m(discreet) {
@extend %discreet;
}
@include bem.m(hidden) {
@include mixin.visuallyhidden;
min-height: 0;
min-width: 0;
width: 100%;
padding: 0;
}
@include bem.m(rich-text) {
min-height: auto;
> div {
min-height: func.rhythm(12);
p,
ul,
ol {
@include plumber.plumber(
var.$font-size: 1.75,
$leading-bottom: 2
);
}
li p {
display: inline;
}
}
}
@include bem.e(toolbar) {
button {
@extend %button-toolbar;
margin-right: func.rhythm(0.5);
margin-bottom: func.rhythm(1);
}
}
}
import { Editor } from '@tiptap/core';
import Document from '@tiptap/extension-document';
import Paragraph from '@tiptap/extension-paragraph';
import Text from '@tiptap/extension-text';
import BulletList from '@tiptap/extension-bullet-list';
import ListItem from '@tiptap/extension-list-item';
import Bold from '@tiptap/extension-bold';
import Italic from '@tiptap/extension-italic';
import { Link } from '@tiptap/extension-link';
import History from '@tiptap/extension-history';
import className from '../../assets/js/className';
import { open } from '../../molecules/modal/modal';
function clearAndCapitalize(str) {
return str.replace(/-/, '').toUpperCase();
}
function kebabToCamel(str) {
return str.replace(/-\w/g, clearAndCapitalize);
}
function kebabToPascal(str) {
return str.replace(/(^\w|-\w)/g, clearAndCapitalize);
}
function insertLink(el, editor) {
const addLink = (e, modal, close) => {
e.preventDefault();
let value = modal.querySelector('input').value.trim();
if (!value.length) {
editor.commands.unsetLink();
} else {
const isAbsolute = new RegExp('(?:^[a-z][a-z0-9+.-]*:|//)', 'i');
if (!isAbsolute.test(value)) {
value = `https://${value}`;
}
editor.commands.toggleLink({ href: value });
}
close();
};
if (editor.view.state.selection.empty) {
return;
}
const currentValue = (editor.getAttributes('link').href) ? editor.getAttributes('link').href : '';
open({
title: 'Lägg till länk',
content: `
<p class="u-m-b-0 u-m-t-default"><input type="url" value="${currentValue}" class="${className('a-input')}"></p>
`,
actions: [
{
text: 'Avbryt',
color: 'transparent',
attrs: {
'data-modal-close': null,
},
},
{
text: 'Spara',
modifier: 'primary',
key: 'enter',
onClick: addLink,
},
],
}, {
onOpen(id, modal) {
modal.querySelector('input').focus();
},
});
}
function createToolbarButton(el, control, editor) {
const button = document.createElement('button');
const iconId = (['link'].includes(control)) ? control : `richtext-${control}`;
button.setAttribute('data-rich-text-control', control);
button.value = kebabToCamel(control);
button.innerHTML = `
<span class="u-visuallyhidden">${control.replace('-', ' ')}</span>
<svg class="icon">
<use xlink:href="#icon-${iconId}"></use>
</svg>
`;
el.appendChild(button);
button.addEventListener('click', (e) => {
e.preventDefault();
if (control === 'link') {
insertLink(el, editor);
} else {
const method = `toggle${kebabToPascal(control)}`;
editor.chain()
.focus()[method]()
.run();
}
});
}
function toogleButtonState(editor, el) {
[].forEach.call(el.parentNode.querySelectorAll('[data-rich-text-control]'), (control) => {
if (editor.isActive(control.value)) {
control.classList.add('is-active');
} else {
control.classList.remove('is-active');
}
if (control.value === 'link' && editor.view.state.selection.empty) {
control.disabled = true;
} else if (control.value === 'link') {
control.disabled = false;
}
});
}
function createToolbar(el, editor) {
const toolbar = document.createElement('div');
toolbar.className = className('a-textarea__toolbar');
el.parentNode.insertBefore(toolbar, el);
['bold', 'italic', 'link', 'bullet-list'].forEach((control) => {
createToolbarButton(toolbar, control, editor);
});
}
function getHTML(editor) {
const html = editor.getHTML();
return html
.replace(/<li><p>/g, '<li>')
.replace(/<\/p><\/li>/g, '</li>');
}
export function setupTextArea(el, onChange = () => {}) {
const editorEl = document.createElement('div');
const editor = new Editor({
element: editorEl,
extensions: [
Document,
Paragraph,
Text,
ListItem,
BulletList,
Bold,
Italic,
Link.configure({
openOnClick: false,
HTMLAttributes: {
class: 'u-link',
},
}),
History.configure({
depth: 10,
}),
],
content: el.value,
onTransaction(props) {
toogleButtonState(props.editor, editorEl);
},
onUpdate(props) {
const html = getHTML(props.editor);
el.value = html;
onChange(html);
},
});
editorEl.className = el.className;
editorEl.classList.add(className('a-textarea--rich-text'));
el.classList.add(className('a-textarea--hidden'));
el.editor = editor;
el.parentNode.insertBefore(editorEl, el);
createToolbar(editorEl, editor);
const event = new CustomEvent('editor-ready', {
detail: {
editor,
},
});
el.dispatchEvent(event);
}
export function init() {
const els = document.querySelectorAll('textarea[data-rich-text]');
if (els.length) {
[].forEach.call(els, (el) => setupTextArea(el));
}
}
init();
The Rich Text editor does not yet have any functionality so it is only an example of how a rich text editor would look.