diff --git a/entry_types/scrolled/config/locales/new/info_table.de.yml b/entry_types/scrolled/config/locales/new/info_table.de.yml
new file mode 100644
index 000000000..0a3573212
--- /dev/null
+++ b/entry_types/scrolled/config/locales/new/info_table.de.yml
@@ -0,0 +1,50 @@
+de:
+ pageflow:
+ info_table_content_element:
+ feature_name: Info-Tabelle Element
+ pageflow_scrolled:
+ editor:
+ content_elements:
+ infoTable:
+ description: Zweispaltige Tabelle mit Name/Wert-Paaren.
+ name: Info-Tabelle
+ tabs:
+ general: Info-Tabelle
+ help_texts:
+ shortcuts: |-
+
+ -
+
+ Enter
+
+
+ Füge eine neue Tabellenzeile hinzu.
+
+
+ -
+ Tabellenzeile einfügen
+
+ -
+
+ Shift + Enter
+
+
+ Füge eine neue Zeile innerhalb einer Tabellenzelle ein.
+
+
+ -
+ Zeilenumbruch einfügen
+
+ -
+
+ Alt + Enter
+
+
+ Füge potentielle Zeilenumbruchspunkte in lange Worte ein,
+ um Silbentrennung zu ermöglichen.
+
+
+ -
+ Bedingten Trennstrich einfügen
+
+
diff --git a/entry_types/scrolled/config/locales/new/info_table.en.yml b/entry_types/scrolled/config/locales/new/info_table.en.yml
new file mode 100644
index 000000000..6a7d805c1
--- /dev/null
+++ b/entry_types/scrolled/config/locales/new/info_table.en.yml
@@ -0,0 +1,50 @@
+en:
+ pageflow:
+ info_table_content_element:
+ feature_name: Info table element
+ pageflow_scrolled:
+ editor:
+ content_elements:
+ infoTable:
+ description: Two-column table for label value pairs.
+ name: Info Table
+ tabs:
+ general: Info Table
+ help_texts:
+ shortcuts: |-
+
+ -
+
+ Enter
+
+
+ Add a new row to the table..
+
+
+ -
+ Insert table row
+
+ -
+
+ Shift + Enter
+
+
+ Insert a new line without beginning a new paragraph.
+
+
+ -
+ Insert soft break/new line
+
+ -
+
+ Alt + Enter
+
+
+ Specify potential line break points within long words
+ to control hyphenation.
+
+
+ -
+ Insert soft hyphen
+
+
diff --git a/entry_types/scrolled/doc/creating_themes/custom_typography.md b/entry_types/scrolled/doc/creating_themes/custom_typography.md
index 1b0b6297b..a67a61d13 100644
--- a/entry_types/scrolled/doc/creating_themes/custom_typography.md
+++ b/entry_types/scrolled/doc/creating_themes/custom_typography.md
@@ -88,6 +88,9 @@ The following rule names are supported:
| `hotspot_tooltip_title` | Applies to the title in hotspot tooltips. |
| `hotspot_tooltip_description` | Applies to the description text in hotspot tooltips. |
| `hotspot_tooltip_link` | Applies to link buttons in hotspot tooltips. |
+| `info_table_label` | Applies to first column of info tables. |
+| `info_table_value` | Applies to second column of info tables. |
+
### Responsive Breakpoints
diff --git a/entry_types/scrolled/lib/pageflow_scrolled/plugin.rb b/entry_types/scrolled/lib/pageflow_scrolled/plugin.rb
index 908cd0a20..4f333d2a5 100644
--- a/entry_types/scrolled/lib/pageflow_scrolled/plugin.rb
+++ b/entry_types/scrolled/lib/pageflow_scrolled/plugin.rb
@@ -41,6 +41,7 @@ def configure(config)
c.features.register('scrolled_entry_fragment_caching')
c.features.register('backdrop_content_elements')
c.features.register('external_links_options')
+ c.features.register('info_table_content_element')
c.additional_frontend_seed_data.register(
'frontendVersion',
diff --git a/entry_types/scrolled/package/spec/frontend/EditableTable-spec.js b/entry_types/scrolled/package/spec/frontend/EditableTable-spec.js
new file mode 100644
index 000000000..04f8cc852
--- /dev/null
+++ b/entry_types/scrolled/package/spec/frontend/EditableTable-spec.js
@@ -0,0 +1,149 @@
+import React from 'react';
+
+import {EditableTable} from 'frontend';
+
+import {render, screen} from '@testing-library/react';
+import '@testing-library/jest-dom/extend-expect'
+
+describe('EditableTable', () => {
+ it('renders class name on table', () => {
+ render();
+
+ expect(screen.getByRole('table')).toHaveClass('some-class');
+ });
+
+ it('renders table with label and value typography classes on cells', () => {
+ const value = [
+ {
+ type: 'row',
+ children: [
+ {
+ type: 'label',
+ children: [
+ {text: 'Name'}
+ ]
+ },
+ {
+ type: 'value',
+ children: [
+ {text: 'Jane'}
+ ]
+ }
+ ]
+ }
+ ];
+
+ render();
+
+ expect(
+ screen.getByRole('cell', {name: 'Name'}).querySelector('.typography-infoTableLabel')
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole('cell', {name: 'Jane'}).querySelector('.typography-infoTableValue')
+ ).toBeInTheDocument();
+ });
+
+ it('renders links', () => {
+ const value = [{
+ type: 'row',
+ children: [
+ {
+ type: 'label',
+ children: [
+ {text: ''}
+ ]
+ },
+ {
+ type: 'value',
+ children: [
+ {text: 'Find more '},
+ {
+ type: 'link',
+ href: 'https://example.com',
+ children: [
+ {text: 'here'}
+ ]
+ },
+ {text: '.'}
+ ]
+ }
+ ]
+ }];
+
+ const {getByRole} = render();
+
+ expect(getByRole('link')).toHaveTextContent('here')
+ expect(getByRole('link')).toHaveAttribute('href', 'https://example.com')
+ expect(getByRole('link')).toHaveClass('typography-contentLink')
+ expect(getByRole('link')).not.toHaveAttribute('target')
+ expect(getByRole('link')).not.toHaveAttribute('rel')
+ });
+
+ it('supports rendering links with target blank', () => {
+ const value = [{
+ type: 'row',
+ children: [
+ {
+ type: 'label',
+ children: [
+ {text: ''}
+ ]
+ },
+ {
+ type: 'value',
+ children: [
+ {text: 'Find more '},
+ {
+ type: 'link',
+ href: 'https://example.com',
+ openInNewTab: true,
+ children: [
+ {text: 'here'}
+ ]
+ },
+ {text: '.'}
+ ]
+ }
+ ]
+ }];
+
+ const {getByRole} = render();
+
+ expect(getByRole('link')).toHaveTextContent('here')
+ expect(getByRole('link')).toHaveAttribute('target', '_blank')
+ expect(getByRole('link')).toHaveAttribute('rel', 'noopener noreferrer')
+ });
+
+ it('supports text formatting', () => {
+ const value = [{
+ type: 'row',
+ children: [
+ {
+ type: 'label',
+ children: [
+ {text: ''}
+ ]
+ },
+ {
+ type: 'value',
+ children: [
+ {text: 'x', bold: true},
+ {text: '3', sup: true},
+ {text: ' and '},
+ {text: 'CO'},
+ {text: '2', sub: true}
+ ]
+ }
+ ]
+ }];
+
+ const {container} = render();
+
+ expect(container.querySelector('strong')).toHaveTextContent('x')
+ expect(container.querySelector('sup')).toHaveTextContent('3')
+ expect(container.querySelector('sub')).toHaveTextContent('2')
+ });
+});
diff --git a/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableTable-spec.js b/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableTable-spec.js
new file mode 100644
index 000000000..27ae215c6
--- /dev/null
+++ b/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableTable-spec.js
@@ -0,0 +1,161 @@
+import React from 'react';
+
+import {EditableTable} from 'frontend';
+import {loadInlineEditingComponents} from 'frontend/inlineEditing';
+
+import {render, screen} from '@testing-library/react';
+import '@testing-library/jest-dom/extend-expect'
+
+describe('EditableText', () => {
+ beforeAll(loadInlineEditingComponents);
+
+ it('renders class name on table', () => {
+ render();
+
+ expect(screen.getByRole('table')).toHaveClass('some-class');
+ });
+
+ it('renders table with label and value typography classes on cells', () => {
+ const value = [
+ {
+ type: 'row',
+ children: [
+ {
+ type: 'label',
+ children: [
+ {text: 'Name'}
+ ]
+ },
+ {
+ type: 'value',
+ children: [
+ {text: 'Jane'}
+ ]
+ }
+ ]
+ }
+ ];
+
+ render();
+
+ expect(
+ screen.getByRole('cell', {name: 'Name'}).querySelector('.typography-infoTableLabel')
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole('cell', {name: 'Jane'}).querySelector('.typography-infoTableValue')
+ ).toBeInTheDocument();
+ });
+
+ it('renders placeholder for empty table with single row', () => {
+ const value = [
+ {
+ type: 'row',
+ children: [
+ {
+ type: 'label',
+ children: [
+ {text: ''}
+ ]
+ },
+ {
+ type: 'value',
+ children: [
+ {text: ''}
+ ]
+ }
+ ]
+ }
+ ];
+
+ render();
+
+ expect(
+ screen.getAllByRole('cell')[0].querySelector('[contenteditable=false]')
+ ).toHaveAttribute('data-text', 'Enter Label');
+ expect(
+ screen.getAllByRole('cell')[1].querySelector('[contenteditable=false]')
+ ).toHaveAttribute('data-text', 'Enter Value');
+ });
+
+ it('does not render placeholders for table with multiple rows ', () => {
+ const value = [
+ {
+ type: 'row',
+ children: [
+ {
+ type: 'label',
+ children: [
+ {text: ''}
+ ]
+ },
+ {
+ type: 'value',
+ children: [
+ {text: ''}
+ ]
+ }
+ ]
+ },
+ {
+ type: 'row',
+ children: [
+ {
+ type: 'label',
+ children: [
+ {text: ''}
+ ]
+ },
+ {
+ type: 'value',
+ children: [
+ {text: ''}
+ ]
+ }
+ ]
+ }
+ ];
+
+ render();
+
+ expect(
+ screen.getAllByRole('cell')[0].querySelector('[contenteditable=false]')
+ ).toBeNull();
+ });
+
+ it('does not render placeholder in non-empty cell', () => {
+ const value = [
+ {
+ type: 'row',
+ children: [
+ {
+ type: 'label',
+ children: [
+ {text: 'Some text'}
+ ]
+ },
+ {
+ type: 'value',
+ children: [
+ {text: ''}
+ ]
+ }
+ ]
+ }
+ ];
+
+ render();
+
+ expect(
+ screen.getAllByRole('cell')[0].querySelector('[contenteditable=false]')
+ ).toBeNull();
+ });
+});
diff --git a/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableTable/withFixedColumns-spec.js b/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableTable/withFixedColumns-spec.js
new file mode 100644
index 000000000..24a3b7600
--- /dev/null
+++ b/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableTable/withFixedColumns-spec.js
@@ -0,0 +1,949 @@
+/** @jsx jsx */
+import {
+ handleTableNavigation,
+ withFixedColumns
+} from 'frontend/inlineEditing/EditableTable/withFixedColumns';
+
+import {createHyperscript} from 'slate-hyperscript';
+
+export const h = createHyperscript({
+ elements: {
+ row: {type: 'row'},
+ label: {type: 'label'},
+ value: {type: 'value'},
+ inline: { inline: true }
+ },
+});
+
+// Strip meta tags to make deep equality checks work
+const jsx = (tagName, attributes, ...children) => {
+ delete attributes.__self;
+ delete attributes.__source;
+ return h(tagName, attributes, ...children);
+}
+
+describe('withFixedColumns', () => {
+ describe('insertBreak', () => {
+ it('adds new row when inserting break', () => {
+ const editor = withFixedColumns(
+
+
+
+
+ Jane Doe
+
+
+
+ );
+
+ editor.insertBreak();
+
+ expect(editor.children).toEqual((
+
+
+
+
+ Jane Doe
+
+
+
+
+
+
+
+
+ ).children
+ );
+ expect(editor.selection).toEqual({
+ anchor: {path: [1, 0, 0], offset: 0},
+ focus: {path: [1, 0, 0], offset: 0},
+ });
+ });
+
+ it('splits content of first cell when inserting break inside cell text', () => {
+ const editor = withFixedColumns(
+
+
+
+
+ Jane Doe
+
+
+
+ );
+
+ editor.insertBreak();
+
+ expect(editor.children).toEqual((
+
+
+
+
+
+
+
+
+
+
+ Jane Doe
+
+
+ ).children
+ );
+ expect(editor.selection).toEqual({
+ anchor: {path: [1, 0, 0], offset: 0},
+ focus: {path: [1, 0, 0], offset: 0},
+ });
+ });
+
+ it('splits content of second cell when inserting break inside cell text', () => {
+ const editor = withFixedColumns(
+
+
+
+
+ Jane DoeJoe Shmoe
+
+
+
+ );
+
+ editor.insertBreak();
+
+ expect(editor.children).toEqual((
+
+
+
+
+ Jane Doe
+
+
+
+
+
+ Joe Shmoe
+
+
+ ).children
+ );
+ expect(editor.selection).toEqual({
+ anchor: {path: [1, 1, 0], offset: 0},
+ focus: {path: [1, 1, 0], offset: 0},
+ });
+ });
+ });
+
+ describe('deleteBackwards', () => {
+ it('can delete backward in first column', () => {
+ const editor = withFixedColumns(
+
+
+
+
+ Jane Doe
+
+
+
+ );
+
+ editor.deleteBackward();
+
+ expect(editor.children).toEqual((
+
+
+
+
+ Jane Doe
+
+
+ ).children
+ );
+ });
+
+ it('can delete backward in second column', () => {
+ const editor = withFixedColumns(
+
+
+
+
+ Jane Doe
+
+
+
+ );
+
+ editor.deleteBackward();
+
+ expect(editor.children).toEqual((
+
+
+
+
+ Jan Doe
+
+
+ ).children
+ );
+ });
+
+ it('moves cursor to first column when deleting backward from beginning of second column', () => {
+ const editor = withFixedColumns(
+
+
+
+
+ Jane Doe
+
+
+
+ );
+
+ editor.deleteBackward();
+
+ expect(editor.children).toEqual((
+
+
+
+
+ Jane Doe
+
+
+ ).children
+ );
+ expect(editor.selection).toEqual({
+ anchor: {path: [0, 0, 0], offset: 4},
+ focus: {path: [0, 0, 0], offset: 4},
+ });
+ });
+
+ it('moves cursor to end of previous row when deleting backwards from first cell', () => {
+ const editor = withFixedColumns(
+
+
+
+
+ B
+
+
+
+
+
+ D
+
+
+
+ );
+
+ editor.deleteBackward();
+
+ expect(editor.children).toEqual((
+
+
+
+
+ B
+
+
+
+
+
+ D
+
+
+ ).children
+ );
+ expect(editor.selection).toEqual({
+ anchor: {path: [0, 1, 0], offset: 1},
+ focus: {path: [0, 1, 0], offset: 1},
+ });
+ });
+
+ it('removes empty previous row when deleting backwards from first cell', () => {
+ const editor = withFixedColumns(
+
+
+
+
+
+
+
+
+
+
+ B
+
+
+
+ );
+
+ editor.deleteBackward();
+
+ expect(editor.children).toEqual((
+
+
+
+
+ B
+
+
+ ).children
+ );
+ expect(editor.selection).toEqual({
+ anchor: {path: [0, 0, 0], offset: 0},
+ focus: {path: [0, 0, 0], offset: 0},
+ });
+ });
+
+ it('removes empty row when deleting backward from first cell', () => {
+ const editor = withFixedColumns(
+
+
+
+
+ b
+
+
+
+
+
+
+
+
+
+
+
+ B
+
+
+
+ );
+
+ editor.deleteBackward();
+
+ expect(editor.children).toEqual((
+
+
+
+
+ b
+
+
+
+
+
+ B
+
+
+ ).children
+ );
+ expect(editor.selection).toEqual({
+ anchor: {path: [0, 1, 0], offset: 1},
+ focus: {path: [0, 1, 0], offset: 1},
+ });
+ });
+
+ it('makes deleting backward from first cell no-op', () => {
+ const editor = withFixedColumns(
+
+
+
+
+ B
+
+
+
+ );
+
+ editor.deleteBackward();
+
+ expect(editor.children).toEqual((
+
+
+
+
+ B
+
+
+ ).children
+ );
+ expect(editor.selection).toEqual({
+ anchor: {path: [0, 0, 0], offset: 0},
+ focus: {path: [0, 0, 0], offset: 0},
+ });
+ });
+ });
+
+ describe('deleteForward', () => {
+ it('can delete forward in first column', () => {
+ const editor = withFixedColumns(
+
+
+
+
+ Jane Doe
+
+
+
+ );
+
+ editor.deleteForward();
+
+ expect(editor.children).toEqual((
+
+
+
+
+ Jane Doe
+
+
+ ).children
+ );
+ });
+
+ it('can delete forward in at start of first column', () => {
+ const editor = withFixedColumns(
+
+
+
+
+ Jane Doe
+
+
+
+ );
+
+ editor.deleteForward();
+
+ expect(editor.children).toEqual((
+
+
+
+
+ Jane Doe
+
+
+ ).children
+ );
+ });
+
+ it('can delete forward in second column', () => {
+ const editor = withFixedColumns(
+
+
+
+
+ Jane Doe
+
+
+
+ );
+
+ editor.deleteForward();
+
+ expect(editor.children).toEqual((
+
+
+
+
+ JaneDoe
+
+
+ ).children
+ );
+ });
+
+ it('moves cursor to second column when deleting forward from end of first column', () => {
+ const editor = withFixedColumns(
+
+
+
+
+ Jane Doe
+
+
+
+ );
+
+ editor.deleteForward();
+
+ expect(editor.children).toEqual((
+
+
+
+
+ Jane Doe
+
+
+ ).children
+ );
+ expect(editor.selection).toEqual({
+ anchor: {path: [0, 1, 0], offset: 0},
+ focus: {path: [0, 1, 0], offset: 0},
+ });
+ });
+
+ it('moves cursor to start of next row when deleting forward from second cell', () => {
+ const editor = withFixedColumns(
+
+
+
+
+ B
+
+
+
+
+
+ D
+
+
+
+ );
+
+ editor.deleteForward();
+
+ expect(editor.children).toEqual((
+
+
+
+
+ B
+
+
+
+
+
+ D
+
+
+ ).children
+ );
+ expect(editor.selection).toEqual({
+ anchor: {path: [1, 0, 0], offset: 0},
+ focus: {path: [1, 0, 0], offset: 0},
+ });
+ });
+
+ it('removes empty next row when deleting forward from last cell', () => {
+ const editor = withFixedColumns(
+
+
+
+
+ B
+
+
+
+
+
+
+
+
+
+ );
+
+ editor.deleteForward();
+
+ expect(editor.children).toEqual((
+
+
+
+
+ B
+
+
+ ).children
+ );
+ expect(editor.selection).toEqual({
+ anchor: {path: [0, 1, 0], offset: 1},
+ focus: {path: [0, 1, 0], offset: 1},
+ });
+ });
+
+ it('removes empty row when deleting forward from first cell', () => {
+ const editor = withFixedColumns(
+
+
+
+
+ b
+
+
+
+
+
+
+
+
+
+
+
+ B
+
+
+
+ );
+
+ editor.deleteForward();
+
+ expect(editor.children).toEqual((
+
+
+
+
+ b
+
+
+
+
+
+ B
+
+
+ ).children
+ );
+ expect(editor.selection).toEqual({
+ anchor: {path: [1, 0, 0], offset: 0},
+ focus: {path: [1, 0, 0], offset: 0},
+ });
+ });
+
+ it('removes empty row when deleting forward from first cell of last line', () => {
+ const editor = withFixedColumns(
+
+
+
+
+ b
+
+
+
+
+
+
+
+
+
+ );
+
+ editor.deleteForward();
+
+ expect(editor.children).toEqual((
+
+
+
+
+ b
+
+
+ ).children
+ );
+ expect(editor.selection).toEqual({
+ anchor: {path: [0, 0, 0], offset: 0},
+ focus: {path: [0, 0, 0], offset: 0},
+ });
+ });
+
+ it('makes deleting forward from first cell of last remaining line no-op', () => {
+ const editor = withFixedColumns(
+
+
+
+
+
+
+
+
+ );
+
+ editor.deleteForward();
+
+ expect(editor.children).toEqual((
+
+
+
+
+
+
+
+ ).children
+ );
+ expect(editor.selection).toEqual({
+ anchor: {path: [0, 0, 0], offset: 0},
+ focus: {path: [0, 0, 0], offset: 0},
+ });
+ });
+
+ it('makes deleting forward from last cell no-op', () => {
+ const editor = withFixedColumns(
+
+
+
+
+ B
+
+
+
+ );
+
+ editor.deleteForward();
+
+ expect(editor.children).toEqual((
+
+
+
+
+ B
+
+
+ ).children
+ );
+ expect(editor.selection).toEqual({
+ anchor: {path: [0, 1, 0], offset: 1},
+ focus: {path: [0, 1, 0], offset: 1},
+ });
+ });
+ });
+
+ describe('deleteFragment', () => {
+ it('does not remove cells when deleting selection across cells', () => {
+ const editor = withFixedColumns(
+
+
+
+
+ Jane Doe
+
+
+
+ );
+
+ editor.deleteFragment();
+
+ expect(editor.children).toEqual((
+
+
+
+
+ Doe
+
+
+ ).children
+ );
+ });
+
+ it('does not remove cells when deleting selection across rows', () => {
+ const editor = withFixedColumns(
+
+
+
+
+ Jane Doe
+
+
+
+
+
+ B
+
+
+
+
+
+ Foo Content
+
+
+
+ );
+
+ editor.deleteFragment();
+
+ expect(editor.children).toEqual((
+
+
+
+
+ Content
+
+
+ ).children
+ );
+ });
+ });
+});
+
+ describe('handleTableNavigation', () => {
+ it('moves the cursor to the cell above when pressing ArrowUp', () => {
+ const editor = withFixedColumns(
+
+
+
+ Row 1, Col 2
+
+
+
+
+ Row 2, Col 2
+
+
+
+ );
+
+ const event = new KeyboardEvent('keydown', {key: 'ArrowUp'});
+ handleTableNavigation(editor, event);
+
+ expect(editor.selection).toEqual({
+ anchor: {path: [0, 1, 0], offset: 0},
+ focus: {path: [0, 1, 0], offset: 0}
+ });
+ });
+
+ it('moves the cursor to the cell below when pressing ArrowDown', () => {
+ const editor = withFixedColumns(
+
+
+
+
+ Row 1, Col 2
+
+
+
+
+ Row 2, Col 2
+
+
+ );
+
+ const event = new KeyboardEvent('keydown', {key: 'ArrowDown'});
+ handleTableNavigation(editor, event);
+
+ expect(editor.selection).toEqual({
+ anchor: {path: [1, 1, 0], offset: 0},
+ focus: {path: [1, 1, 0], offset: 0}
+ });
+ });
+});
diff --git a/entry_types/scrolled/package/src/contentElements/editor.js b/entry_types/scrolled/package/src/contentElements/editor.js
index ff58b0431..57fafefef 100644
--- a/entry_types/scrolled/package/src/contentElements/editor.js
+++ b/entry_types/scrolled/package/src/contentElements/editor.js
@@ -17,3 +17,4 @@ import './tikTokEmbed/editor'
import './question/editor'
import './counter/editor'
import './quote/editor'
+import './infoTable/editor'
diff --git a/entry_types/scrolled/package/src/contentElements/frontend.js b/entry_types/scrolled/package/src/contentElements/frontend.js
index 019596e3c..996fea57e 100644
--- a/entry_types/scrolled/package/src/contentElements/frontend.js
+++ b/entry_types/scrolled/package/src/contentElements/frontend.js
@@ -15,3 +15,4 @@ import './question/frontend'
import './counter/frontend'
import './quote/frontend'
import './imageGallery/frontend'
+import './infoTable/frontend'
diff --git a/entry_types/scrolled/package/src/contentElements/infoTable/InfoTable.js b/entry_types/scrolled/package/src/contentElements/infoTable/InfoTable.js
new file mode 100644
index 000000000..46c9cbdab
--- /dev/null
+++ b/entry_types/scrolled/package/src/contentElements/infoTable/InfoTable.js
@@ -0,0 +1,30 @@
+import React from 'react';
+import classNames from 'classnames';
+
+import {
+ EditableTable,
+ useContentElementConfigurationUpdate,
+ useContentElementEditorState,
+ useI18n
+} from 'pageflow-scrolled/frontend';
+
+import styles from './InfoTable.module.css';
+
+export function InfoTable({configuration, sectionProps}) {
+ const {isSelected} = useContentElementEditorState();
+ const updateConfiguration = useContentElementConfigurationUpdate();
+
+ const {t} = useI18n({locale: 'ui'});
+
+ return (
+ updateConfiguration({value})} />
+ );
+}
diff --git a/entry_types/scrolled/package/src/contentElements/infoTable/InfoTable.module.css b/entry_types/scrolled/package/src/contentElements/infoTable/InfoTable.module.css
new file mode 100644
index 000000000..6d7f754c3
--- /dev/null
+++ b/entry_types/scrolled/package/src/contentElements/infoTable/InfoTable.module.css
@@ -0,0 +1,38 @@
+.table {
+ width: 100%;
+ border-collapse: collapse;
+ border-spacing: 0;
+ --table-border-color: transparent;
+}
+
+.table td {
+ min-width: 50px;
+ vertical-align: top;
+}
+
+.table td:first-child {
+ border-right: solid 1px var(--table-border-color);
+ padding: 0.5rem 0.5rem 0.5rem 0;
+ white-space: pre;
+}
+
+.table td:last-child {
+ width: 100%;
+ padding: 0.5rem 0 0.5rem 0.5rem;
+}
+
+.center td {
+ width: 50%;
+}
+
+.center td:first-child {
+ text-align: right;
+}
+
+.table tr:nth-child(n + 2) td {
+ border-top: solid 1px var(--table-border-color);
+}
+
+.selected {
+ --table-border-color: color-mix(in srgb, transparent, currentColor);
+}
diff --git a/entry_types/scrolled/package/src/contentElements/infoTable/editor.js b/entry_types/scrolled/package/src/contentElements/infoTable/editor.js
new file mode 100644
index 000000000..46ad506de
--- /dev/null
+++ b/entry_types/scrolled/package/src/contentElements/infoTable/editor.js
@@ -0,0 +1,27 @@
+import I18n from 'i18n-js';
+import {editor} from 'pageflow-scrolled/editor';
+import {InfoBoxView} from 'pageflow/editor';
+import {SeparatorView} from 'pageflow/ui'
+
+import pictogram from './pictogram.svg';
+
+editor.contentElementTypes.register('infoTable', {
+ featureName: 'info_table_content_element',
+ pictogram,
+ supportedPositions: ['inline', 'sticky', 'standAlone', 'left', 'right'],
+ supportedWidthRange: ['s', 'xl'],
+
+ configurationEditor({entry, contentElement}) {
+ this.tab('general', function() {
+ this.group('ContentElementPosition');
+
+ this.view(SeparatorView);
+
+ this.view(InfoBoxView, {
+ text: I18n.t(
+ 'pageflow_scrolled.editor.content_elements.infoTable.help_texts.shortcuts'
+ ),
+ });
+ });
+ }
+});
diff --git a/entry_types/scrolled/package/src/contentElements/infoTable/frontend.js b/entry_types/scrolled/package/src/contentElements/infoTable/frontend.js
new file mode 100644
index 000000000..1ded35c4a
--- /dev/null
+++ b/entry_types/scrolled/package/src/contentElements/infoTable/frontend.js
@@ -0,0 +1,6 @@
+import {frontend} from 'pageflow-scrolled/frontend';
+import {InfoTable} from './InfoTable';
+
+frontend.contentElementTypes.register('infoTable', {
+ component: InfoTable
+});
diff --git a/entry_types/scrolled/package/src/contentElements/infoTable/pictogram.svg b/entry_types/scrolled/package/src/contentElements/infoTable/pictogram.svg
new file mode 100644
index 000000000..76b242371
--- /dev/null
+++ b/entry_types/scrolled/package/src/contentElements/infoTable/pictogram.svg
@@ -0,0 +1,3 @@
+
diff --git a/entry_types/scrolled/package/src/contentElements/infoTable/stories.js b/entry_types/scrolled/package/src/contentElements/infoTable/stories.js
new file mode 100644
index 000000000..9e12e09dd
--- /dev/null
+++ b/entry_types/scrolled/package/src/contentElements/infoTable/stories.js
@@ -0,0 +1,53 @@
+import '../frontend';
+import {storiesOfContentElement} from 'pageflow-scrolled/spec/support/stories';
+
+storiesOfContentElement(module, {
+ typeName: 'infoTable',
+ baseConfiguration: {
+ value: [
+ {
+ type: 'row',
+ children: [
+ {
+ type: 'label',
+ children: [
+ {text: 'Name'}
+ ]
+ },
+ {
+ type: 'value',
+ children: [
+ {text: 'Jane '},
+ {text: 'Doe', italic: true}
+ ]
+ }
+ ]
+ },
+ {
+ type: 'row',
+ children: [
+ {
+ type: 'label',
+ children: [
+ {text: 'Website'}
+ ]
+ },
+ {
+ type: 'value',
+ children: [
+ {text: 'Find more '},
+ {
+ type: 'link',
+ href: 'https://example.com',
+ openInNewTab: true,
+ children: [
+ {text: 'here'}
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+});
diff --git a/entry_types/scrolled/package/src/frontend/EditableTable.js b/entry_types/scrolled/package/src/frontend/EditableTable.js
new file mode 100644
index 000000000..39d50109b
--- /dev/null
+++ b/entry_types/scrolled/package/src/frontend/EditableTable.js
@@ -0,0 +1,99 @@
+import React from 'react';
+import classNames from 'classnames';
+
+import {withInlineEditingAlternative} from './inlineEditing';
+import {Text} from './Text';
+
+import {
+ renderLink,
+ renderLeaf
+} from './EditableText';
+
+const defaultValue = [{
+ type: 'row',
+ children: [
+ {
+ type: 'label',
+ children: [
+ {text: ''}
+ ]
+ },
+ {
+ type: 'value',
+ children: [
+ {text: ''}
+ ]
+ }
+ ],
+}];
+
+export const EditableTable = withInlineEditingAlternative('EditableTable', function EditableTable({
+ value, className,
+ labelScaleCategory = 'body',
+ valueScaleCategory = 'body'
+}) {
+ return (
+
+
+ {render(value || defaultValue, {
+ labelScaleCategory,
+ valueScaleCategory
+ })}
+
+
+ );
+});
+
+function render(children, options) {
+ return children.map((element, index) => {
+ if (element.type) {
+ return createRenderElement(options)({
+ attributes: {key: index},
+ element,
+ children: render(element.children, options),
+ });
+ }
+ else {
+ return renderLeaf({
+ attributes: {key: index},
+ leaf: element,
+ children: children.length === 1 &&
+ element.text.trim() === '' ? '\uFEFF' : element.text
+ });
+ }
+ });
+}
+
+export function createRenderElement({labelScaleCategory, valueScaleCategory}) {
+ return function renderElement({
+ attributes, children, element
+ }) {
+ switch (element.type) {
+ case 'row':
+ return (
+
+ {children}
+
+ );
+ case 'link':
+ return renderLink({attributes, children, element});
+ case 'label':
+ return (
+
+
+ {children}
+
+ |
+ );
+ default:
+ return (
+
+
+ {children}
+
+
+ |
+ );
+ }
+ }
+}
diff --git a/entry_types/scrolled/package/src/frontend/EditableText.js b/entry_types/scrolled/package/src/frontend/EditableText.js
index 683f1cb1f..ad765b91a 100644
--- a/entry_types/scrolled/package/src/frontend/EditableText.js
+++ b/entry_types/scrolled/package/src/frontend/EditableText.js
@@ -120,7 +120,7 @@ function Heading({attributes, variantClassName, styles: inlineStyles, children})
);
}
-function renderLink({attributes, children, element}) {
+export function renderLink({attributes, children, element}) {
const {key, ...otherAttributes} = attributes;
return (
diff --git a/entry_types/scrolled/package/src/frontend/Text.js b/entry_types/scrolled/package/src/frontend/Text.js
index 1b6b127f0..ecaf45734 100644
--- a/entry_types/scrolled/package/src/frontend/Text.js
+++ b/entry_types/scrolled/package/src/frontend/Text.js
@@ -15,6 +15,7 @@ import styles from './Text.module.css';
* `'quoteText-lg'`, `'quoteText-md'`, `'quoteText-sm'`, `'quoteText-xs'`, `'quoteAttribution'`,
* `'counterNumber-lg'`, `'counterNumber-md'`, `'counterNumber-sm'`,
* `'counterNumber-xs'`, `'counterDescription`'.
+ * `'infoTableLabel'`, `'infoTableValue`'.
* `'hotspotsTooltipTitle'`, `'hotspotsTooltipDescription`', `'hotspotsTooltipLink`'.
* @param {string} [props.inline] - Render a span instread of a div.
* @param {string} props.children - Nodes to render with specified typography.
@@ -37,6 +38,7 @@ Text.propTypes = {
'hotspotsTooltipTitle', 'hotspotsTooltipDescription', 'hotspotsTooltipLink',
'counterNumber-lg', 'counterNumber-md', 'counterNumber-sm', 'counterNumber-xs',
'counterDescription',
+ 'infoTableLabel', 'infoTableValue',
'body', 'caption', 'question'
]),
}
diff --git a/entry_types/scrolled/package/src/frontend/Text.module.css b/entry_types/scrolled/package/src/frontend/Text.module.css
index 7a1c15ab2..746cdca2e 100644
--- a/entry_types/scrolled/package/src/frontend/Text.module.css
+++ b/entry_types/scrolled/package/src/frontend/Text.module.css
@@ -207,6 +207,19 @@
font-weight: bold;
}
+.infoTableLabel {
+ composes: typography-infoTableLabel from global;
+ font-size: text-s;
+ line-height: 1.4;
+ font-weight: 600;
+}
+
+.infoTableValue {
+ composes: typography-infoTableValue from global;
+ font-size: text-s;
+ line-height: 1.4;
+}
+
@media (max-width: 600px) {
.heading-lg {
font-size: text-xl;
diff --git a/entry_types/scrolled/package/src/frontend/index.js b/entry_types/scrolled/package/src/frontend/index.js
index 01ff97a78..6ce57e8f8 100644
--- a/entry_types/scrolled/package/src/frontend/index.js
+++ b/entry_types/scrolled/package/src/frontend/index.js
@@ -99,6 +99,7 @@ export {useScrollPosition} from './useScrollPosition';
export {usePhonePlatform} from './usePhonePlatform';
export {useIsomorphicLayoutEffect} from './useIsomorphicLayoutEffect';
+export {EditableTable} from './EditableTable';
export {EditableText} from './EditableText';
export {EditableInlineText} from './EditableInlineText';
export {EditableLink} from './EditableLink';
diff --git a/entry_types/scrolled/package/src/frontend/inlineEditing/EditableTable/index.js b/entry_types/scrolled/package/src/frontend/inlineEditing/EditableTable/index.js
new file mode 100644
index 000000000..8bc1d921d
--- /dev/null
+++ b/entry_types/scrolled/package/src/frontend/inlineEditing/EditableTable/index.js
@@ -0,0 +1,120 @@
+import React, {useCallback, useMemo} from 'react';
+import classNames from 'classnames';
+import {createEditor, Range} from 'slate';
+import {Slate, Editable, withReact} from 'slate-react';
+import {withHistory} from 'slate-history';
+
+import {useCachedValue} from '../useCachedValue';
+import {useContentElementEditorState} from '../../useContentElementEditorState';
+
+import {
+ decorateLineBreaks,
+ renderLeafWithLineBreakDecoration,
+ selectedClassName,
+ useLineBreakHandler,
+ useShortcutHandler,
+ withLineBreakNormalization,
+ withLinks,
+ wrapRenderElementWithLinkPreview,
+ HoveringToolbar
+} from '../EditableText';
+
+import {
+ handleTableNavigation,
+ withFixedColumns
+} from './withFixedColumns';
+
+import {
+ createRenderElementWithPlaceholder
+} from './placeholders';
+
+import {LinkTooltipProvider} from '../LinkTooltip';
+
+export const EditableTable = React.memo(function EditableTable({
+ value, onChange, className,
+ labelScaleCategory = 'body',
+ valueScaleCategory = 'body',
+ labelPlaceholder, valuePlaceholder,
+ floatingControlsPosition = 'below'
+}) {
+ const editor = useMemo(
+ () => withFixedColumns(
+ withLinks(
+ withLineBreakNormalization(
+ withReact(
+ withHistory(
+ createEditor()
+ )
+ )
+ )
+ )
+ ),
+ []
+ );
+
+ const {isSelected} = useContentElementEditorState();
+
+ const handleLineBreaks = useLineBreakHandler(editor);
+ const handleShortcuts = useShortcutHandler(editor);
+
+ const handleKeyDown = useCallback(event => {
+ handleLineBreaks(event);
+ handleShortcuts(event);
+ handleTableNavigation(editor, event);
+ }, [editor, handleLineBreaks, handleShortcuts]);
+
+ const [cachedValue, setCachedValue] = useCachedValue(value, {
+ defaultValue: [{
+ type: 'row',
+ children: [
+ {
+ type: 'label',
+ children: [
+ {text: ''}
+ ]
+ },
+ {
+ type: 'value',
+ children: [
+ {text: ''}
+ ]
+ }
+ ],
+ }],
+ onDebouncedChange: onChange
+ });
+
+ const showPlaceholders = cachedValue.length === 1;
+
+ const renderElement = useMemo(
+ () => wrapRenderElementWithLinkPreview(
+ createRenderElementWithPlaceholder({
+ labelScaleCategory, valueScaleCategory,
+ labelPlaceholder, valuePlaceholder,
+ showPlaceholders
+ })
+ ),
+ [
+ labelScaleCategory, valueScaleCategory,
+ labelPlaceholder, valuePlaceholder,
+ showPlaceholders
+ ]
+ );
+
+ return (
+
+
+
+
+
+
+ );
+});
diff --git a/entry_types/scrolled/package/src/frontend/inlineEditing/EditableTable/placeholders.js b/entry_types/scrolled/package/src/frontend/inlineEditing/EditableTable/placeholders.js
new file mode 100644
index 000000000..b3610b1c8
--- /dev/null
+++ b/entry_types/scrolled/package/src/frontend/inlineEditing/EditableTable/placeholders.js
@@ -0,0 +1,30 @@
+import React from 'react';
+import { Node} from 'slate';
+import {createRenderElement} from '../../EditableTable';
+
+import styles from './placeholders.module.css';
+
+export function createRenderElementWithPlaceholder(options) {
+ const renderElement = createRenderElement(options);
+
+ return function({attributes, children, element}) {
+ if ((element.type === 'label' || element.type === 'value') &&
+ options.showPlaceholders &&
+ Node.string(element) === '') {
+ children = <>
+ {children}
+
+ >;
+ }
+
+ return renderElement({
+ attributes,
+ children,
+ element
+ });
+ };
+}
diff --git a/entry_types/scrolled/package/src/frontend/inlineEditing/EditableTable/placeholders.module.css b/entry_types/scrolled/package/src/frontend/inlineEditing/EditableTable/placeholders.module.css
new file mode 100644
index 000000000..a5490f7e1
--- /dev/null
+++ b/entry_types/scrolled/package/src/frontend/inlineEditing/EditableTable/placeholders.module.css
@@ -0,0 +1,13 @@
+.placeholder {
+ pointer-events: none;
+ opacity: 0.5;
+ font-weight: normal;
+}
+
+.placeholder::before {
+ content: attr(data-text);
+ display: block;
+ height: 0;
+ position: relative;
+ top: -1lh;
+}
diff --git a/entry_types/scrolled/package/src/frontend/inlineEditing/EditableTable/withFixedColumns.js b/entry_types/scrolled/package/src/frontend/inlineEditing/EditableTable/withFixedColumns.js
new file mode 100644
index 000000000..ff785a620
--- /dev/null
+++ b/entry_types/scrolled/package/src/frontend/inlineEditing/EditableTable/withFixedColumns.js
@@ -0,0 +1,302 @@
+import {Editor, Node, Path, Point, Range, Transforms} from 'slate';
+
+export function withFixedColumns(editor) {
+ const {insertBreak, deleteBackward, deleteForward, deleteFragment} = editor;
+
+ editor.insertBreak = () => {
+ const cellMatch = matchCurrentCell(editor);
+
+ if (cellMatch) {
+ const [cellNode, cellPath] = cellMatch;
+
+ const [rowMatch] = Editor.nodes(editor, {
+ match: (n) => n.type === 'row',
+ });
+
+ if (rowMatch) {
+ const [, rowPath] = rowMatch;
+
+ const columnIndex = cellPath[cellPath.length - 1];
+
+ const cursorOffset = editor.selection.anchor.offset;
+ const text = Node.string(cellNode);
+ const beforeText = text.slice(0, cursorOffset);
+ const afterText = text.slice(cursorOffset);
+
+ const newRowPath = Path.next(rowPath);
+
+ if (columnIndex === 0) {
+ Transforms.insertText(editor, afterText, {at: cellPath });
+
+ const newRow = {
+ type: 'row',
+ children: [
+ { type: 'label', children: [{text: beforeText}] },
+ { type: 'value', children: [{text: ''}]}
+ ]
+ };
+
+ Transforms.insertNodes(editor, newRow, { at: rowPath});
+ }
+ else {
+ Transforms.insertText(editor, beforeText, { at: cellPath });
+
+ const newRow = {
+ type: 'row',
+ children: [
+ { type: 'label', children: [{text: ''}] },
+ { type: 'value', children: [{text: afterText}]}
+ ]
+ };
+
+ Transforms.insertNodes(editor, newRow, { at: newRowPath });
+ }
+
+ const cursor = {
+ path: [...newRowPath, afterText.length ? columnIndex : 0, 0],
+ offset: 0
+ };
+
+ Transforms.select(editor, {
+ anchor: cursor,
+ focus: cursor,
+ });
+
+ return;
+ }
+ };
+
+ insertBreak();
+ };
+
+ editor.deleteBackward = function() {
+ const {selection} = editor;
+
+ if (selection && Range.isCollapsed(selection)) {
+ const cellMatch = matchCurrentCell(editor);
+
+ if (cellMatch) {
+ const [, cellPath] = cellMatch;
+ const start = Editor.start(editor, cellPath);
+
+ if (Point.equals(selection.anchor, start)) {
+ const columnIndex = cellPath[cellPath.length - 1];
+
+ if (columnIndex === 0) {
+ const rowMatch = matchCurrentRow(editor);
+ const previousRowMatch = matchPreviousRow(editor);
+
+ if (previousRowMatch) {
+ const [row, rowPath] = rowMatch;
+ const [previousRow, previousRowPath] = previousRowMatch;
+
+ if (Node.string(previousRow) === '') {
+ Transforms.delete(editor, {at: previousRowPath});
+ }
+ else if (Node.string(row) === '') {
+ Transforms.delete(editor, {at: rowPath});
+ }
+ else {
+ Transforms.select(editor, Editor.end(editor, previousRowPath));
+ }
+ }
+ }
+ else {
+ Transforms.select(editor, Editor.end(editor, Path.previous(cellPath)));
+ }
+
+ return;
+ }
+ }
+ }
+
+ deleteBackward.apply(this, arguments);
+ };
+
+ editor.deleteForward = () => {
+ const {selection} = editor;
+
+ if (selection && Range.isCollapsed(selection)) {
+ const cellMatch = matchCurrentCell(editor);
+
+ if (cellMatch) {
+ const [, cellPath] = cellMatch;
+ const columnIndex = cellPath[cellPath.length - 1];
+
+ if (Point.equals(selection.anchor, Editor.end(editor, cellPath))) {
+ if (columnIndex === 0) {
+ const rowMatch = matchCurrentRow(editor);
+
+ if (rowMatch) {
+ const [row, rowPath] = rowMatch;
+
+ if (Node.string(row) === '') {
+ const previousRowMatch = matchPreviousRow(editor);
+ const nextRowMatch = matchNextRow(editor);
+
+ if (previousRowMatch || nextRowMatch) {
+ Transforms.delete(editor, {at: rowPath});
+
+ if (Node.has(editor, rowPath)) {
+ Transforms.select(editor, Editor.start(editor, rowPath));
+ }
+ else {
+ const [, previousRowPath] = previousRowMatch;
+ Transforms.select(editor, Editor.start(editor, previousRowPath));
+ }
+ }
+ }
+ else {
+ Transforms.select(editor, Editor.start(editor, Path.next(cellPath)));
+ }
+
+ return;
+ }
+ }
+
+ if (columnIndex === 1) {
+ const nextRowMatch = matchNextRow(editor);
+
+ if (nextRowMatch) {
+ const [nextRow, nextRowPath] = nextRowMatch;
+
+ if (Node.string(nextRow) === '') {
+ Transforms.delete(editor, {at: nextRowPath});
+ }
+ else {
+ Transforms.select(editor, Editor.start(editor, nextRowPath));
+ }
+ }
+ }
+
+ return;
+ }
+ }
+ }
+
+ deleteForward();
+ };
+
+ editor.deleteFragment = () => {
+ const { selection } = editor;
+
+ if (selection && Range.isExpanded(selection)) {
+ const [startCellMatch] = Editor.nodes(editor, {
+ match: (n) => n.type === 'label' || n.type === 'value',
+ at: selection.anchor.path,
+ });
+
+ const [endCellMatch] = Editor.nodes(editor, {
+ match: (n) => n.type === 'label' || n.type === 'value',
+ at: selection.focus.path,
+ });
+
+ if (startCellMatch && endCellMatch) {
+ const [, startCellPath] = startCellMatch;
+ const [, startRowSecondCellPath] = Editor.next(editor, {at: startCellPath});
+ const [, endCellPath] = endCellMatch;
+
+ // Collect all rows in the selection range
+ const rows = Array.from(Editor.nodes(editor, {
+ match: (n) => n.type === 'row',
+ at: { anchor: selection.anchor, focus: selection.focus },
+ }));
+
+ // Delete text in the end cell from the start of the cell to the selection focus
+ const endCellText = Editor.string(editor, {
+ focus: selection.focus,
+ anchor: Editor.end(editor, endCellPath),
+ });
+ Transforms.insertText(editor, endCellText, {at: startRowSecondCellPath})
+
+ // Delete text in the start cell from the selection anchor to the end of the cell
+ Transforms.delete(editor, {
+ at: {
+ anchor: selection.anchor,
+ focus: Editor.end(editor, startCellPath),
+ },
+ });
+
+ // Remove all middle rows between start and end rows
+ const middleRows = rows.slice(1); // Exclude the first and last rows
+ middleRows.reverse().forEach(([_, rowPath]) => {
+ Transforms.removeNodes(editor, { at: rowPath });
+ });
+
+ return;
+ }
+ }
+
+ // Default delete behavior for non-table cases
+ deleteFragment();
+ };
+
+ return editor;
+}
+
+export function handleTableNavigation(editor, event) {
+ const {selection} = editor;
+
+ if (selection && Range.isCollapsed(selection)) {
+ const cellMatch = matchCurrentCell(editor);
+
+ if (cellMatch) {
+ const [, cellPath] = cellMatch;
+ const rowPath = cellPath.slice(0, -1);
+
+ if (event.key === 'ArrowUp') {
+ event.preventDefault();
+
+ if (rowPath[rowPath.length - 1] > 0) {
+ const previousRowPath = Path.previous(rowPath);
+ const targetPath = [...previousRowPath, cellPath[cellPath.length - 1]];
+
+ Transforms.select(editor, Editor.start(editor, targetPath));
+ }
+ } else if (event.key === 'ArrowDown') {
+ event.preventDefault();
+
+ const nextRowPath = Path.next(rowPath);
+ const targetPath = [...nextRowPath, cellPath[cellPath.length - 1]];
+
+ if (Node.has(editor, targetPath)) {
+ Transforms.select(editor, Editor.start(editor, targetPath));
+ }
+ }
+ }
+ }
+}
+
+function matchCurrentCell(editor) {
+ const [cellMatch] = Editor.nodes(editor, {
+ match: n => n.type === 'label' || n.type === 'value'
+ });
+
+ return cellMatch;
+}
+
+function matchCurrentRow(editor) {
+ const [rowMatch] = Editor.nodes(editor, {
+ match: (n) => n.type === 'row',
+ });
+
+ return rowMatch;
+}
+
+function matchPreviousRow(editor) {
+ const rowMatch = matchCurrentRow(editor);
+
+ if (rowMatch) {
+ const [, rowPath] = rowMatch;
+ return Editor.previous(editor, {at: rowPath});
+ }
+}
+
+function matchNextRow(editor) {
+ const rowMatch = matchCurrentRow(editor);
+
+ if (rowMatch) {
+ const [, rowPath] = rowMatch;
+ return Editor.next(editor, {at: rowPath});
+ }
+}
diff --git a/entry_types/scrolled/package/src/frontend/inlineEditing/EditableText/index.js b/entry_types/scrolled/package/src/frontend/inlineEditing/EditableText/index.js
index d8f6d6170..000e56588 100644
--- a/entry_types/scrolled/package/src/frontend/inlineEditing/EditableText/index.js
+++ b/entry_types/scrolled/package/src/frontend/inlineEditing/EditableText/index.js
@@ -38,6 +38,23 @@ import {useShortcutHandler} from './shortcuts';
import styles from './index.module.css';
+export {
+ wrapRenderElementWithLinkPreview
+} from './withLinks';
+
+export {
+ decorateLineBreaks,
+ useLineBreakHandler,
+ useShortcutHandler,
+ renderLeafWithLineBreakDecoration,
+ withLinks,
+ withLineBreakNormalization,
+ LinkTooltipProvider,
+ HoveringToolbar
+};
+
+export const selectedClassName = styles.selected;
+
export const EditableText = React.memo(function EditableText({
value, contentElementId, placeholder, onChange, selectionRect, className,
placeholderClassName, scaleCategory = 'body', autoFocus,
diff --git a/entry_types/scrolled/package/src/frontend/inlineEditing/EditableText/withLinks.js b/entry_types/scrolled/package/src/frontend/inlineEditing/EditableText/withLinks.js
index 9f7d56adf..bd815d3e1 100644
--- a/entry_types/scrolled/package/src/frontend/inlineEditing/EditableText/withLinks.js
+++ b/entry_types/scrolled/package/src/frontend/inlineEditing/EditableText/withLinks.js
@@ -13,17 +13,21 @@ export function withLinks(editor) {
return editor
}
-export function renderElementWithLinkPreview(options) {
- if (options.element.type === 'link') {
- return (
+export const renderElementWithLinkPreview =
+ wrapRenderElementWithLinkPreview(renderElement);
-
- {renderElement(options)}
-
- )
- }
- else {
- return renderElement(options);
+export function wrapRenderElementWithLinkPreview(renderElement) {
+ return function(options) {
+ if (options.element.type === 'link') {
+ return (
+
+ {renderElement(options)}
+
+ )
+ }
+ else {
+ return renderElement(options);
+ }
}
}
diff --git a/entry_types/scrolled/package/src/frontend/inlineEditing/components.js b/entry_types/scrolled/package/src/frontend/inlineEditing/components.js
index 89ba8be5b..ba09d4908 100644
--- a/entry_types/scrolled/package/src/frontend/inlineEditing/components.js
+++ b/entry_types/scrolled/package/src/frontend/inlineEditing/components.js
@@ -6,6 +6,7 @@ export {LayoutWithPlaceholder} from './LayoutWithPlaceholder';
export {EditableText} from './EditableText';
export {EditableInlineText} from './EditableInlineText';
+export {EditableTable} from './EditableTable';
export {EditableLink} from './EditableLink';
export {ActionButton} from './ActionButton';