<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 a-input--discreet" id="message" placeholder="Meddelande" autocomplete=""></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",
  "modifier": "a-input--discreet",
  "has_icon": false
}
  • Content:
    @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);
    		}
    	}
    }
    
  • URL: /components/raw/textarea/_textarea.scss
  • Filesystem Path: src/atoms/textarea/_textarea.scss
  • Size: 1.2 KB
  • Content:
    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();
    
  • URL: /components/raw/textarea/rich-text.js
  • Filesystem Path: src/atoms/textarea/rich-text.js
  • Size: 4.7 KB

NOTE:

The Rich Text editor does not yet have any functionality so it is only an example of how a rich text editor would look.