Skip to content

Commit

Permalink
feat(richt-text-input): introduce soft-linebreak (br-tags) (#2850)
Browse files Browse the repository at this point in the history
  • Loading branch information
misama-ct authored Jul 15, 2024
1 parent 46b3447 commit 2e60131
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 40 deletions.
6 changes: 6 additions & 0 deletions .changeset/healthy-poems-approve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@commercetools-uikit/rich-text-input': minor
'@commercetools-uikit/rich-text-utils': minor
---

rich-text-inputs are supporting soft-linebreaks (shift + enter) now
10 changes: 8 additions & 2 deletions packages/components/inputs/rich-text-input/src/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
HiddenInput,
Element,
Leaf,
Softbreaker,
toggleMark,
resetEditor,
focusEditor,
Expand Down Expand Up @@ -132,10 +133,10 @@ const Editor = forwardRef((props: TEditorProps, forwardedRef) => {
},
[editor]
);
/*
/*
Resetting the editor requires access to `editor` object returned from `useSlate` hook.
Therefore, `reset` function is attached to the passed `ref` object via `useImperativeHandle` hook
to be called from the parent component.
to be called from the parent component.
e.g. <button onMouseDown={() => ref.current?.resetValue("<p><strong>Value after reset</strong></p>")}>Reset</button>
*/
useImperativeHandle(forwardedRef, () => {
Expand Down Expand Up @@ -205,6 +206,11 @@ const Editor = forwardRef((props: TEditorProps, forwardedRef) => {
break;
}
}

if (event.shiftKey && event.key === 'Enter') {
event.preventDefault();
editor.insertText(Softbreaker.placeholderCharacter);
}
}}
/>
{props.children}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import Readme from '../README.md';

// Create our initial value...

const initialValue = '<h1>H1 <u>heading</u></h1>';
const initialValue =
'<h1>Heading</h1><p>This is a paragraph.<br/><br/>New sentence, <strong>same</strong> paragraph, but with <u>soft</u>-linebreak (<em>Shift + Enter</em>).</p>';

storiesOf('Components|Inputs', module)
.addDecorator(withKnobs)
Expand Down Expand Up @@ -92,8 +93,12 @@ storiesOf('Components|Inputs', module)
onChange={onChange}
value={value}
/>
<Text.Headline as="h3">Output</Text.Headline>
<pre>{value}</pre>
<Text.Headline as="h3">
{value === initialValue ? 'Initial HTML-Input:' : 'HTML-Output:'}
</Text.Headline>
<pre style={{ whiteSpace: 'pre-wrap', wordWrap: 'break-word' }}>
{value}
</pre>
</Spacings.Stack>
</Section>
);
Expand Down
90 changes: 62 additions & 28 deletions packages/components/inputs/rich-text-utils/src/html/html.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ describe('html', () => {
'<ol><li><span style="font-weight: bold; font-family: &quot;Comic Sans MS&quot;;">Computermouse for <span style="text-decoration-line: underline;">controlling</span></span></li></ol><table class="table table-bordered"><tbody><tr><td>hello</td></tr><tr><td><p>world<img src="https://www.rollingstone.com/wp-content/uploads/2019/01/shutterstock_10010937aj.jpg" style="width: 100%; float: right;" class="pull-right img-circle"></p></td></tr></tbody></table><ol><li><span style="font-weight: bold; font-family: &quot;Comic Sans MS&quot;;"><span style="text-decoration-line: underline;"><br></span></span></li></ol>';

expect(html.serialize(html.deserialize(htmlValue))).toEqual(
'<ol><li><strong>Computermouse for </strong><u><strong>controlling</strong></u></li></ol>hello<p>world</p><ol><li><u><strong></strong></u></li></ol>'
'<ol><li><strong>Computermouse for </strong><u><strong>controlling</strong></u></li></ol>hello<p>world</p><ol><li><u><strong><br/></strong></u></li></ol>'
);
});
});
Expand All @@ -124,35 +124,69 @@ describe('html', () => {
describe('with invalid markup passed to text node', () => {
it('should properly serialize and replace with information', () => {
const htmlValue = `<p>\r\n<em>Bitte beachten Sie: Die Lieferung erfolgt nach Verf&uuml;gbarkeit - Jahrgangs- und Designw&uuml;nschen k&ouml;nnen wir leider nicht nachkommen. Wir danken Ihnen f&uuml;r Ihr Verst&auml;ndnis.</em>\r\n</p>\r\n\r\n<h3>Informationen zum Gold K&auml;nguru 1/4</h3>\r\n<p>\r\nDer <strong>Australian Nugget (Gold K&auml;nguru)</strong> ist eine australische Goldm&uuml;nze, die erstmals 1986 von der Perth Mint gepr&auml;gt wurde und sich vor allem dank der j&auml;hrlich wechselnden Motive auf der Vorderseite weltweiter Beliebtheit erfreut. Die M&uuml;nze hat einen Feingehalt von 999,9 und gilt als offizielles Zahlungsmittel.\r\n</p>\r\n\r\n<p >\r\nSeit 1987 wird der <strong>Australian Nugget</strong> in den St&uuml;ckelungen 1/20 Unze, 1/10 Unze, 1/4 Unze, 1/2 Unze und 1 Unze gepr&auml;gt, der Nennwert liegt zwischen 5 und 100 australischen Dollar. Mittlerweile existieren aber auch gr&ouml;&szlig;ere St&uuml;ckelungen mit einem Gewicht von 2 Unzen, 10 Unzen sowie 1 Kilogramm. Im Jahr 2010 wurde eine 2-Dollar-Version der M&uuml;nze mit einem Gewicht von 0,5 Gramm aufgelegt. 2011 stellte die Perth Mint zudem eine Ausgabe der M&uuml;nze mit einem Gewicht von 1 Tonne vor &ndash; damit ist der <strong>Gold K&auml;nguru</strong> zugleich die kleinste und gr&ouml;sste Anlagem&uuml;nze der Welt.\r\n</p>\r\n\r\n<p>\r\nBis 1989 wurden auf der Vorderseite der M&uuml;nze au&szlig;ergew&ouml;hnliche, in Australien gefundene Goldnuggets abgebildet &ndash; daher auch der Name <strong>Australian Nugget</strong>. Seit 1990 zeigt die Goldm&uuml;nze j&auml;hrlich wechselnde Motive von K&auml;ngurus, der offizielle Name lautet seitdem &bdquo;Australian Kangaroo&ldquo;. Gr&ouml;&szlig;ere Einheiten des <strong>Gold K&auml;nguru</strong>, wie die 2-Unzen-, 10-Unzen- und 1-Kilogramm-Version tragen j&auml;hrlich das gleiche Motiv des &bdquo;Red Kangaroo&ldquo;. Auf der R&uuml;ckseite der Goldm&uuml;nze findet sich - wie bei allen Commonwealth-M&uuml;nzen - das Portr&auml;t von K&ouml;nigin Elizabeth II.\r\n</p>\r\n<p>\r\n</p>\r\n<h3>\r\nGold K&auml;nguru 1/4 kaufen bei philoro kaufen\r\n</h3>\r\n<p>\r\n\tNeben der <strong>Gold Känguru 1/4/strong> finden Sie bei uns eine breite Auswahl renommierter <a href="https://philoro.ch/shop/goldmuenzen" target="_blank" title="Goldmünzen kaufen" style="text-decoration: none; color: #86754f;">Goldmünzen</a> aus aller Welt. Gerne bieten wir Ihnen zum Thema Anlagemünzen unsere umfassende Beratung an. Wir garantieren für die von uns vertriebenen Produkte höchste Qualität. Deshalb arbeiten wir ausschliesslich mit international anerkannten und etablierten Herstellern zusammen.\r\n</p>\r\n\r\n<p>\r\nBesuchen Sie uns in einer unserer <a href="https://philoro.ch/filialen" target="_blank" title="Unsere Filialen" style="text-decoration: none; color: #86754f;">Filialen</a> und überzeugen Sie sich selbst, oder bestellen Sie einfach und bequem online.\r\n</p>\r\n`;
expect(html.serialize(html.deserialize(htmlValue))).toEqual(
`<p><br/><em>Bitte beachten Sie: Die Lieferung erfolgt nach Verfügbarkeit - Jahrgangs- und Designwünschen können wir leider nicht nachkommen. Wir danken Ihnen für Ihr Verständnis.</em><br/></p><p><br/><br/></p><h3>Informationen zum Gold Känguru 1/4</h3><p><br/></p><p><br/>Der <strong>Australian Nugget (Gold Känguru)</strong> ist eine australische Goldmünze, die erstmals 1986 von der Perth Mint geprägt wurde und sich vor allem dank der jährlich wechselnden Motive auf der Vorderseite weltweiter Beliebtheit erfreut. Die Münze hat einen Feingehalt von 999,9 und gilt als offizielles Zahlungsmittel.<br/></p><p><br/><br/></p><p><br/>Seit 1987 wird der <strong>Australian Nugget</strong> in den Stückelungen 1/20 Unze, 1/10 Unze, 1/4 Unze, 1/2 Unze und 1 Unze geprägt, der Nennwert liegt zwischen 5 und 100 australischen Dollar. Mittlerweile existieren aber auch größere Stückelungen mit einem Gewicht von 2 Unzen, 10 Unzen sowie 1 Kilogramm. Im Jahr 2010 wurde eine 2-Dollar-Version der Münze mit einem Gewicht von 0,5 Gramm aufgelegt. 2011 stellte die Perth Mint zudem eine Ausgabe der Münze mit einem Gewicht von 1 Tonne vor – damit ist der <strong>Gold Känguru</strong> zugleich die kleinste und grösste Anlagemünze der Welt.<br/></p><p><br/><br/></p><p><br/>Bis 1989 wurden auf der Vorderseite der Münze außergewöhnliche, in Australien gefundene Goldnuggets abgebildet – daher auch der Name <strong>Australian Nugget</strong>. Seit 1990 zeigt die Goldmünze jährlich wechselnde Motive von Kängurus, der offizielle Name lautet seitdem „Australian Kangaroo“. Größere Einheiten des <strong>Gold Känguru</strong>, wie die 2-Unzen-, 10-Unzen- und 1-Kilogramm-Version tragen jährlich das gleiche Motiv des „Red Kangaroo“. Auf der Rückseite der Goldmünze findet sich - wie bei allen Commonwealth-Münzen - das Porträt von Königin Elizabeth II.<br/></p><p><br/></p><p><br/></p><p><br/></p><h3><br/>Gold Känguru 1/4 kaufen bei philoro kaufen<br/></h3><p><br/></p><p><br/>\tNeben der <strong>Gold Känguru 1/4/strong&gt; finden Sie bei uns eine breite Auswahl renommierter </strong><strong>Goldmünzen</strong><strong> aus aller Welt. Gerne bieten wir Ihnen zum Thema Anlagemünzen unsere umfassende Beratung an. Wir garantieren für die von uns vertriebenen Produkte höchste Qualität. Deshalb arbeiten wir ausschliesslich mit international anerkannten und etablierten Herstellern zusammen.<br/></strong></p><strong><br/><br/></strong><strong>Invalid markup</strong><strong><br/></strong>`
);
});
});

describe('handles (soft-)line-breaks', () => {
it('does remove line-break (\\n) characters from html-input', () => {
const htmlValue = `<p>How much\n is the fish?</p>`;

expect(html.deserialize(htmlValue)).toEqual([
{ children: [{ text: 'How much is the fish?' }], type: 'paragraph' },
]);
});
it('does transform <br/> tags into slate-nodes', () => {
const htmlValue = `<p>How much<br/> is the fish?</p><br/>`;

expect(html.deserialize(htmlValue)).toEqual([
{
children: [
{
text: 'How much',
},
{
text: '\n',
},
{
text: ' is the fish?',
},
],
type: 'paragraph',
},
{
text: '\n',
},
]);
});

it('does transform line-break characters (\\n) into <br/> tags', () => {
const node = {
children: [
{
text: 'How much',
},
{
text: '\n',
},
{
text: ' is the fish?',
},
],
type: 'paragraph',
};

expect(html.serialize(node)).toEqual(
`<p>How much<br/> is the fish?</p>`
);
});

it('does preserve <br/> tags (line-breaks) from html-input to html-output', () => {
const htmlValue = `<p>How much<br/> is the fish?</p>`;
expect(html.serialize(html.deserialize(htmlValue))).toEqual(
`<p>
<em>Bitte beachten Sie: Die Lieferung erfolgt nach Verfügbarkeit - Jahrgangs- und Designwünschen können wir leider nicht nachkommen. Wir danken Ihnen für Ihr Verständnis.</em>
</p><p>
</p><h3>Informationen zum Gold Känguru 1/4</h3><p>
</p><p>
Der <strong>Australian Nugget (Gold Känguru)</strong> ist eine australische Goldmünze, die erstmals 1986 von der Perth Mint geprägt wurde und sich vor allem dank der jährlich wechselnden Motive auf der Vorderseite weltweiter Beliebtheit erfreut. Die Münze hat einen Feingehalt von 999,9 und gilt als offizielles Zahlungsmittel.
</p><p>
</p><p>
Seit 1987 wird der <strong>Australian Nugget</strong> in den Stückelungen 1/20 Unze, 1/10 Unze, 1/4 Unze, 1/2 Unze und 1 Unze geprägt, der Nennwert liegt zwischen 5 und 100 australischen Dollar. Mittlerweile existieren aber auch größere Stückelungen mit einem Gewicht von 2 Unzen, 10 Unzen sowie 1 Kilogramm. Im Jahr 2010 wurde eine 2-Dollar-Version der Münze mit einem Gewicht von 0,5 Gramm aufgelegt. 2011 stellte die Perth Mint zudem eine Ausgabe der Münze mit einem Gewicht von 1 Tonne vor – damit ist der <strong>Gold Känguru</strong> zugleich die kleinste und grösste Anlagemünze der Welt.
</p><p>
</p><p>
Bis 1989 wurden auf der Vorderseite der Münze außergewöhnliche, in Australien gefundene Goldnuggets abgebildet – daher auch der Name <strong>Australian Nugget</strong>. Seit 1990 zeigt die Goldmünze jährlich wechselnde Motive von Kängurus, der offizielle Name lautet seitdem „Australian Kangaroo“. Größere Einheiten des <strong>Gold Känguru</strong>, wie die 2-Unzen-, 10-Unzen- und 1-Kilogramm-Version tragen jährlich das gleiche Motiv des „Red Kangaroo“. Auf der Rückseite der Goldmünze findet sich - wie bei allen Commonwealth-Münzen - das Porträt von Königin Elizabeth II.
</p><p>
</p><p>
</p><p>
</p><h3>
Gold Känguru 1/4 kaufen bei philoro kaufen
</h3><p>
</p><p>
\tNeben der <strong>Gold Känguru 1/4/strong&gt; finden Sie bei uns eine breite Auswahl renommierter </strong><strong>Goldmünzen</strong><strong> aus aller Welt. Gerne bieten wir Ihnen zum Thema Anlagemünzen unsere umfassende Beratung an. Wir garantieren für die von uns vertriebenen Produkte höchste Qualität. Deshalb arbeiten wir ausschliesslich mit international anerkannten und etablierten Herstellern zusammen.
</strong></p><strong>
</strong><strong>Invalid markup</strong><strong>
</strong>`
'<p>How much<br/> is the fish?</p>'
);
});
});
Expand Down
11 changes: 10 additions & 1 deletion packages/components/inputs/rich-text-utils/src/html/html.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import parse from 'style-to-object';
import isEmpty from 'lodash/isEmpty';
import type { HistoryEditor } from 'slate-history';
import { BLOCK_TAGS, MARK_TAGS } from '../tags';
import { Softbreaker } from '../slate-helpers';

type Html = string;

Expand Down Expand Up @@ -70,6 +71,10 @@ const serializeNode = (node: TNode): Html => {
if (node.strikethrough) {
string = `<del>${string}</del>`;
}

// Replace all Linebreaks (that are caused by the editor) with a br-tag
string = Softbreaker.serialize(string);

return string;
}

Expand Down Expand Up @@ -204,6 +209,10 @@ const deserializeElement = (
const { nodeName } = el;
let parent = el;

if (nodeName === 'BR') {
return Softbreaker.getSlatePlaceholder();
}

if (
nodeName === 'PRE' &&
el.childNodes[0] &&
Expand Down Expand Up @@ -287,7 +296,7 @@ const deserializeElement = (
};
const deserialize = (html: Html) => {
const document = new DOMParser().parseFromString(
html || '<p></p>',
Softbreaker.cleanHtml(html) || '<p></p>',
'text/html'
);
return deserializeElement(document.body);
Expand Down
1 change: 1 addition & 0 deletions packages/components/inputs/rich-text-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export { default as version } from './version';
export {
Element,
Leaf,
Softbreaker,
isMarkActive,
isBlockActive,
toggleMark,
Expand Down
47 changes: 41 additions & 6 deletions packages/components/inputs/rich-text-utils/src/slate-helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import html, {

const LIST_TYPES = [BLOCK_TAGS.ol, BLOCK_TAGS.ul];

/*
/*
From Slate's own implementation of rich text editor
https://github.com/ianstormtaylor/slate/blob/main/site/examples/richtext.tsx#L133:L179
*/
Expand Down Expand Up @@ -92,7 +92,7 @@ const Element = ({ attributes, children, element }: RenderElementProps) => {
}
};

/*
/*
From Slate's own implementation of rich text editor
https://github.com/ianstormtaylor/slate/blob/main/site/examples/richtext.tsx#L181:L199
*/
Expand Down Expand Up @@ -122,7 +122,7 @@ const Leaf = ({ attributes, children, leaf }: RenderLeafProps) => {
return <span {...attributes}>{children}</span>;
};

/*
/*
From Slate's own implementation of rich text editor
https://github.com/ianstormtaylor/slate/blob/main/site/examples/richtext.tsx#L128:L131
*/
Expand All @@ -131,7 +131,7 @@ const isMarkActive = (editor: TEditor, format: Format) => {
return marks ? marks[format as keyof typeof marks] === true : false;
};

/*
/*
From Slate's own implementation of rich text editor
https://github.com/ianstormtaylor/slate/blob/main/site/examples/richtext.tsx#L101:L09
*/
Expand All @@ -145,7 +145,7 @@ const toggleMark = (editor: Editor, format: Format) => {
}
};

/*
/*
From Slate's own implementation of rich text editor
https://github.com/ianstormtaylor/slate/blob/main/site/examples/richtext.tsx#L111:L126
*/
Expand All @@ -164,7 +164,7 @@ const isBlockActive = (editor: TEditor, format: Format) => {
return Boolean(match);
};

/*
/*
From slate's own implementation of rich text editor
https://github.com/ianstormtaylor/slate/blob/main/site/examples/richtext.tsx#L67:L99
*/
Expand Down Expand Up @@ -240,9 +240,44 @@ const focusEditor = (editor: Editor) => {
Transforms.select(editor, Editor.end(editor, []));
};

/**
* Softbreaker is a helper that is used to ensure
* that slate editor can handle html-linebreaks correctly.
*/
const Softbreaker = {
/**
* The character that slate-editor considers a soft-linebreak
*/
placeholderCharacter: '\n',
/**
* Returns a formatted version of the soft-linebreak character
* which can be used by internal slate editor functions
*/
getSlatePlaceholder() {
return {
text: this.placeholderCharacter,
};
},
/**
* replaces the slate-placeholder character with the html
* equivalent = <br/>
*/
serialize(str: string) {
return str.split(this.placeholderCharacter).join('<br/>');
},
/**
* Initial HMTL needs to be cleaned from soft-linebreak characters,
* otherwise they are being transformed into <br/> tags
*/
cleanHtml(html: string) {
return html.split(this.placeholderCharacter).join('');
},
};

export {
Element,
Leaf,
Softbreaker,
isMarkActive,
isBlockActive,
toggleMark,
Expand Down

0 comments on commit 2e60131

Please sign in to comment.