diff --git a/entry_types/scrolled/package/spec/frontend/extensions-spec.js b/entry_types/scrolled/package/spec/frontend/extensions-spec.js
new file mode 100644
index 0000000000..fe5b2d253d
--- /dev/null
+++ b/entry_types/scrolled/package/spec/frontend/extensions-spec.js
@@ -0,0 +1,188 @@
+import React from 'react';
+
+import '@testing-library/jest-dom/extend-expect'
+import {render, act} from '@testing-library/react'
+import {
+ extensible,
+ provideExtensions,
+ clearExtensions,
+ ExtensionsProvider
+} from 'frontend/extensions';
+import {StaticPreview} from 'frontend/useScrollPositionLifecycle';
+
+describe('extensions', () => {
+ afterEach(() => {
+ act(() => clearExtensions());
+ });
+
+ describe('extensible with decorator', () => {
+ it('wraps component with decorator receiving same props', () => {
+ const TestComponent = extensible('TestComponent', function TestComponent({text}) {
+ return {text} Component;
+ });
+
+ provideExtensions({
+ decorators: {
+ TestComponent({text, children}) {
+ return
{text} Decorator{children}
;
+ }
+ }
+ });
+
+ const {container} = render();
+
+ expect(container).toHaveTextContent('Hello Decorator');
+ expect(container).toHaveTextContent('Hello Component');
+ });
+
+ it('renders original component when no extensions provided', () => {
+ const TestComponent = extensible('TestComponent', function TestComponent() {
+ return Component;
+ });
+
+ const {container} = render();
+
+ expect(container).toHaveTextContent('Component');
+ });
+
+ it('renders original component in static preview', () => {
+ const TestComponent = extensible('TestComponent', function TestComponent() {
+ return Component;
+ });
+
+ provideExtensions({
+ decorators: {
+ TestComponent({children}) {
+ return Decorator{children}
;
+ }
+ }
+ });
+
+ const {container} = render(
+
+
+
+ );
+
+ expect(container).toHaveTextContent('Component');
+ expect(container).not.toHaveTextContent('Decorator');
+ });
+ });
+
+ describe('extensible with alternative', () => {
+ it('renders alternative instead of original', () => {
+ const TestComponent = extensible('TestComponent', function TestComponent() {
+ return Original;
+ });
+
+ provideExtensions({
+ alternatives: {
+ TestComponent() {
+ return Alternative;
+ }
+ }
+ });
+
+ const {container} = render();
+
+ expect(container).toHaveTextContent('Alternative');
+ expect(container).not.toHaveTextContent('Original');
+ });
+
+ it('renders original component in static preview', () => {
+ const TestComponent = extensible('TestComponent', function TestComponent() {
+ return Original;
+ });
+
+ provideExtensions({
+ alternatives: {
+ TestComponent() {
+ return Alternative;
+ }
+ }
+ });
+
+ const {container} = render(
+
+
+
+ );
+
+ expect(container).toHaveTextContent('Original');
+ expect(container).not.toHaveTextContent('Alternative');
+ });
+ });
+
+ describe('provideExtensions', () => {
+ it('re-renders decorator after mount', () => {
+ const TestComponent = extensible('TestComponent', function TestComponent() {
+ return Component;
+ });
+
+ const {container} = render();
+ expect(container).not.toHaveTextContent('Decorator');
+
+ act(() => {
+ provideExtensions({
+ decorators: {
+ TestComponent({children}) {
+ return Decorator{children}
;
+ }
+ }
+ });
+ });
+
+ expect(container).toHaveTextContent('DecoratorComponent');
+ });
+
+ it('re-renders alternative after mount', () => {
+ const TestComponent = extensible('TestComponent', function TestComponent() {
+ return Original;
+ });
+
+ const {container} = render();
+ expect(container).toHaveTextContent('Original');
+
+ act(() => {
+ provideExtensions({
+ alternatives: {
+ TestComponent() {
+ return Alternative;
+ }
+ }
+ });
+ });
+
+ expect(container).toHaveTextContent('Alternative');
+ expect(container).not.toHaveTextContent('Original');
+ });
+
+
+ it('replaces previous extensions', () => {
+ const TestComponent = extensible('TestComponent', function TestComponent() {
+ return Original;
+ });
+
+ provideExtensions({
+ alternatives: {
+ TestComponent() {
+ return First;
+ }
+ }
+ });
+
+ provideExtensions({
+ alternatives: {
+ TestComponent() {
+ return Second;
+ }
+ }
+ });
+
+ const {container} = render();
+
+ expect(container).toHaveTextContent('Second');
+ expect(container).not.toHaveTextContent('First');
+ });
+ });
+});
diff --git a/entry_types/scrolled/package/spec/frontend/inlineEditing-spec.js b/entry_types/scrolled/package/spec/frontend/inlineEditing-spec.js
deleted file mode 100644
index 03c302c690..0000000000
--- a/entry_types/scrolled/package/spec/frontend/inlineEditing-spec.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import React from 'react';
-
-import '@testing-library/jest-dom/extend-expect'
-import {render} from '@testing-library/react'
-import {
- withInlineEditingDecorator,
- withInlineEditingAlternative
-} from 'frontend/inlineEditing';
-
-jest.mock('frontend/inlineEditing/components', () => ({
- TestDecorator({text, children}) {
- return {text} Decorator
{children}
;
- },
-
- TestAlternative({text, children}) {
- return {text} Alternative
;
- }
-}));
-
-describe('inlineEditing', () => {
- // see inlineEditingWithLoadedComponents-spec for cases where inline
- // editing components are loaded.
-
- describe('when inline editing components are not loaded', () => {
- describe('withInlineEditingDecorator', () => {
- it('only renders component', () => {
- const TestComponent = withInlineEditingDecorator('TestDecorator', function TestComponent({text}) {
- return {text} Test
;
- });
-
- const {container} = render();
-
- expect(container).not.toHaveTextContent('Decorator');
- expect(container).toHaveTextContent('Hello Test');
- })
- });
-
- describe('withInlineEditingAlternative', () => {
- it('only renders component', () => {
- const TestComponent = withInlineEditingAlternative('TestAlternative', function TestComponent({text}) {
- return {text} Test
;
- });
-
- const {container} = render();
-
- expect(container).not.toHaveTextContent('Alternative');
- expect(container).toHaveTextContent('Hello Test');
- })
- });
- });
-});
diff --git a/entry_types/scrolled/package/spec/frontend/inlineEditingWithLoadedComponents-spec.js b/entry_types/scrolled/package/spec/frontend/inlineEditingWithLoadedComponents-spec.js
deleted file mode 100644
index 658ce0ca66..0000000000
--- a/entry_types/scrolled/package/spec/frontend/inlineEditingWithLoadedComponents-spec.js
+++ /dev/null
@@ -1,87 +0,0 @@
-import React from 'react';
-
-import '@testing-library/jest-dom/extend-expect'
-import {render} from '@testing-library/react'
-import {
- withInlineEditingDecorator,
- withInlineEditingAlternative,
- loadInlineEditingComponents
-} from 'frontend/inlineEditing';
-import {StaticPreview} from 'frontend/useScrollPositionLifecycle';
-
-jest.mock('frontend/inlineEditing/components', () => ({
- TestDecorator({text, children}) {
- return {text} Decorator
{children}
;
- },
-
- TestAlternative({text, children}) {
- return {text} Alternative
;
- }
-}));
-
-describe('inlineEditing', () => {
- // see inlineEditing-spec for cases where inline editing components
- // are not loaded.
-
- describe('when inline editing components are loaded', () => {
- describe('withInlineEditingDecorator', () => {
- it('wraps component with decorator receiving same props', async () => {
- const TestComponent = withInlineEditingDecorator('TestDecorator', function TestComponent({text}) {
- return {text} Test
;
- });
-
- await loadInlineEditingComponents();
- const {container} = render();
-
- expect(container).toHaveTextContent('Hello Decorator');
- expect(container).toHaveTextContent('Hello Test');
- });
-
- it('renders component without decorator in static preview', async () => {
- const TestComponent = withInlineEditingDecorator('TestDecorator', function TestComponent({text}) {
- return {text} Test
;
- });
-
- await loadInlineEditingComponents();
- const {container} = render(
-
-
-
- );
-
- expect(container).not.toHaveTextContent('Decorator');
- expect(container).toHaveTextContent('Hello Test');
- });
- });
-
- describe('withInlineEditingAlternative', () => {
- it('renders alternative component instead', async () => {
- const TestComponent = withInlineEditingAlternative('TestAlternative', function TestComponent({text}) {
- return {text} Test
;
- });
-
- await loadInlineEditingComponents();
- const {container} = render();
-
- expect(container).toHaveTextContent('Hello Alternative');
- expect(container).not.toHaveTextContent('Test');
- });
-
- it('renders original component in static preview', async () => {
- const TestComponent = withInlineEditingAlternative('TestAlternative', function TestComponent({text}) {
- return {text} Test
;
- });
-
- await loadInlineEditingComponents();
- const {container} = render(
-
-
-
- );
-
- expect(container).not.toHaveTextContent('Alternative');
- expect(container).toHaveTextContent('Hello Test');
- });
- });
- });
-});
diff --git a/entry_types/scrolled/package/src/frontend/ActionButton.js b/entry_types/scrolled/package/src/frontend/ActionButton.js
index 2555f09e11..9cc5ead6dc 100644
--- a/entry_types/scrolled/package/src/frontend/ActionButton.js
+++ b/entry_types/scrolled/package/src/frontend/ActionButton.js
@@ -1,5 +1,5 @@
-import {withInlineEditingAlternative} from './inlineEditing';
+import {extensible} from './extensions';
-export const ActionButton = withInlineEditingAlternative('ActionButton', function ActionButton() {
+export const ActionButton = extensible('ActionButton', function ActionButton() {
return null;
});
diff --git a/entry_types/scrolled/package/src/frontend/ActionButtons.js b/entry_types/scrolled/package/src/frontend/ActionButtons.js
index f86ebdc3f3..05ee0e883a 100644
--- a/entry_types/scrolled/package/src/frontend/ActionButtons.js
+++ b/entry_types/scrolled/package/src/frontend/ActionButtons.js
@@ -1,5 +1,5 @@
-import {withInlineEditingAlternative} from './inlineEditing';
+import {extensible} from './extensions';
-export const ActionButtons = withInlineEditingAlternative('ActionButtons', function ActionButtons() {
+export const ActionButtons = extensible('ActionButtons', function ActionButtons() {
return null;
});
diff --git a/entry_types/scrolled/package/src/frontend/Backdrop/BackgroundContentElement.js b/entry_types/scrolled/package/src/frontend/Backdrop/BackgroundContentElement.js
index 88d038d81e..7579a624b1 100644
--- a/entry_types/scrolled/package/src/frontend/Backdrop/BackgroundContentElement.js
+++ b/entry_types/scrolled/package/src/frontend/Backdrop/BackgroundContentElement.js
@@ -3,10 +3,10 @@ import React, {useMemo} from 'react';
import {ContentElement} from '../ContentElement';
import {useSectionLifecycle} from '../useSectionLifecycle';
-import {withInlineEditingDecorator} from '../inlineEditing';
+import {extensible} from '../extensions';
-export const BackgroundContentElement = withInlineEditingDecorator(
- 'BackgroundContentElementDecorator',
+export const BackgroundContentElement = extensible(
+ 'BackgroundContentElement',
function BackgroundContentElement({
contentElement, isIntersecting, onMotifAreaUpdate, containerDimension
}) {
diff --git a/entry_types/scrolled/package/src/frontend/Backdrop/index.js b/entry_types/scrolled/package/src/frontend/Backdrop/index.js
index 4ad4034415..29361d3b63 100644
--- a/entry_types/scrolled/package/src/frontend/Backdrop/index.js
+++ b/entry_types/scrolled/package/src/frontend/Backdrop/index.js
@@ -1,7 +1,7 @@
import React from 'react';
import classNames from 'classnames';
-import {withInlineEditingDecorator} from '../inlineEditing';
+import {extensible} from '../extensions';
import useDimension from '../useDimension';
import {useSectionLifecycle} from '../useSectionLifecycle';
@@ -10,7 +10,7 @@ import {BackgroundAsset} from './BackgroundAsset';
import styles from '../Backdrop.module.css';
import sharedTransitionStyles from '../transitions/shared.module.css';
-export const Backdrop = withInlineEditingDecorator('BackdropDecorator', function Backdrop(props) {
+export const Backdrop = extensible('Backdrop', function Backdrop(props) {
const [containerDimension, setContainerRef] = useDimension();
const {shouldLoad} = useSectionLifecycle();
diff --git a/entry_types/scrolled/package/src/frontend/Content.js b/entry_types/scrolled/package/src/frontend/Content.js
index bf909e307b..f3e47c6da1 100644
--- a/entry_types/scrolled/package/src/frontend/Content.js
+++ b/entry_types/scrolled/package/src/frontend/Content.js
@@ -5,7 +5,7 @@ import {VhFix} from './VhFix';
import {useActiveExcursion} from './useActiveExcursion';
import {useCurrentSectionIndexState} from './useCurrentChapter';
import {useEntryStructure} from '../entryState';
-import {withInlineEditingDecorator} from './inlineEditing';
+import {extensible} from './extensions';
import {usePostMessageListener} from './usePostMessageListener';
import {useSectionChangeEvents} from './useSectionChangeEvents';
import {sectionChangeMessagePoster} from './sectionChangeMessagePoster';
@@ -22,7 +22,7 @@ import {
import styles from './Content.module.css';
-export const Content = withInlineEditingDecorator('ContentDecorator', function Content(props) {
+export const Content = extensible('Content', function Content(props) {
const entryStructure = useEntryStructure();
const scrollToTarget = useScrollToTarget();
diff --git a/entry_types/scrolled/package/src/frontend/ContentElement.js b/entry_types/scrolled/package/src/frontend/ContentElement.js
index 58b16232a1..6357edd83d 100644
--- a/entry_types/scrolled/package/src/frontend/ContentElement.js
+++ b/entry_types/scrolled/package/src/frontend/ContentElement.js
@@ -1,7 +1,7 @@
import React from 'react';
import {api} from './api';
-import {withInlineEditingDecorator} from './inlineEditing';
+import {extensible} from './extensions';
import {ContentElementAttributesProvider} from './useContentElementAttributes';
import {ContentElementLifecycleProvider} from './useContentElementLifecycle';
import {ContentElementMargin} from './ContentElementMargin';
@@ -9,8 +9,8 @@ import {ContentElementErrorBoundary} from './ContentElementErrorBoundary';
import styles from './ContentElement.module.css';
-export const ContentElement = React.memo(withInlineEditingDecorator(
- 'ContentElementDecorator',
+export const ContentElement = React.memo(extensible(
+ 'ContentElement',
function ContentElement(props) {
const Component = api.contentElementTypes.getComponent(props.type);
const {defaultMarginTop} = api.contentElementTypes.getOptions(props.type) || {};
diff --git a/entry_types/scrolled/package/src/frontend/EditableInlineText.js b/entry_types/scrolled/package/src/frontend/EditableInlineText.js
index efcdf2e536..71f4cd66dc 100644
--- a/entry_types/scrolled/package/src/frontend/EditableInlineText.js
+++ b/entry_types/scrolled/package/src/frontend/EditableInlineText.js
@@ -1,11 +1,11 @@
import React from 'react';
import classNames from 'classnames';
-import {withInlineEditingAlternative} from './inlineEditing';
+import {extensible} from './extensions';
import styles from './EditableInlineText.module.css';
-export const EditableInlineText = withInlineEditingAlternative(
+export const EditableInlineText = extensible(
'EditableInlineText',
function EditableInlineText({value, hyphens, defaultValue = ''}) {
const text = value ? value[0]?.children[0]?.text : defaultValue;
diff --git a/entry_types/scrolled/package/src/frontend/EditableLink.js b/entry_types/scrolled/package/src/frontend/EditableLink.js
index d6a56ba298..6633392e8e 100644
--- a/entry_types/scrolled/package/src/frontend/EditableLink.js
+++ b/entry_types/scrolled/package/src/frontend/EditableLink.js
@@ -1,9 +1,9 @@
import React from 'react';
-import {withInlineEditingAlternative} from './inlineEditing';
+import {extensible} from './extensions';
import {Link} from './Link';
-export const EditableLink = withInlineEditingAlternative(
+export const EditableLink = extensible(
'EditableLink',
function EditableLink({className, href, openInNewTab, onClick, children}) {
return (
diff --git a/entry_types/scrolled/package/src/frontend/EditableTable.js b/entry_types/scrolled/package/src/frontend/EditableTable.js
index 305ffe3e83..277c36e841 100644
--- a/entry_types/scrolled/package/src/frontend/EditableTable.js
+++ b/entry_types/scrolled/package/src/frontend/EditableTable.js
@@ -1,7 +1,7 @@
import React from 'react';
import classNames from 'classnames';
-import {withInlineEditingAlternative} from './inlineEditing';
+import {extensible} from './extensions';
import {Text} from './Text';
import {utils} from './utils';
@@ -31,7 +31,7 @@ const defaultValue = [{
],
}];
-export const EditableTable = withInlineEditingAlternative('EditableTable', function EditableTable({
+export const EditableTable = extensible('EditableTable', function EditableTable({
value, className,
labelScaleCategory = 'body',
valueScaleCategory = 'body',
diff --git a/entry_types/scrolled/package/src/frontend/EditableText.js b/entry_types/scrolled/package/src/frontend/EditableText.js
index e4ef902853..93523b550a 100644
--- a/entry_types/scrolled/package/src/frontend/EditableText.js
+++ b/entry_types/scrolled/package/src/frontend/EditableText.js
@@ -3,7 +3,7 @@ import classNames from 'classnames';
import {camelize} from './utils/camelize';
import {paletteColor} from './paletteColor';
-import {withInlineEditingAlternative} from './inlineEditing';
+import {extensible} from './extensions';
import {useDarkBackground} from './backgroundColor';
import {Text} from './Text';
import {Link} from './Link';
@@ -16,7 +16,7 @@ const defaultValue = [{
children: [{ text: '' }],
}];
-export const EditableText = withInlineEditingAlternative('EditableText', function EditableText({
+export const EditableText = extensible('EditableText', function EditableText({
value, className, scaleCategory = 'body', typographyVariant, typographySize
}) {
return (
diff --git a/entry_types/scrolled/package/src/frontend/Entry.js b/entry_types/scrolled/package/src/frontend/Entry.js
index 5167bb69a3..796bc86be0 100644
--- a/entry_types/scrolled/package/src/frontend/Entry.js
+++ b/entry_types/scrolled/package/src/frontend/Entry.js
@@ -5,9 +5,9 @@ import {Widget} from './Widget';
import {SelectableWidget} from './SelectableWidget';
import {WidgetPresenceWrapper} from './WidgetPresenceWrapper';
-import {withInlineEditingDecorator} from './inlineEditing';
+import {extensible} from './extensions';
-export const Entry = withInlineEditingDecorator('EntryDecorator', function Entry() {
+export const Entry = extensible('Entry', function Entry() {
return (
diff --git a/entry_types/scrolled/package/src/frontend/Foreground.js b/entry_types/scrolled/package/src/frontend/Foreground.js
index 135b1b1fae..2047659b88 100644
--- a/entry_types/scrolled/package/src/frontend/Foreground.js
+++ b/entry_types/scrolled/package/src/frontend/Foreground.js
@@ -1,13 +1,13 @@
import React, {createContext, useContext} from 'react';
import classNames from 'classnames';
-import {withInlineEditingDecorator} from './inlineEditing';
+import {extensible} from './extensions';
import styles from './Foreground.module.css';
export const ForcePaddingContext = createContext(false);
-export const Foreground = withInlineEditingDecorator('ForegroundDecorator', function Foreground(props) {
+export const Foreground = extensible('Foreground', function Foreground(props) {
const forcePadding = useContext(ForcePaddingContext);
return (
diff --git a/entry_types/scrolled/package/src/frontend/LinkTooltipProvider.js b/entry_types/scrolled/package/src/frontend/LinkTooltipProvider.js
index 54f68e836e..14d06e1816 100644
--- a/entry_types/scrolled/package/src/frontend/LinkTooltipProvider.js
+++ b/entry_types/scrolled/package/src/frontend/LinkTooltipProvider.js
@@ -1,6 +1,6 @@
-import {withInlineEditingAlternative} from './inlineEditing';
+import {extensible} from './extensions';
-export const LinkTooltipProvider = withInlineEditingAlternative(
+export const LinkTooltipProvider = extensible(
'LinkTooltipProvider',
function LinkTooltipProvider({children}) {
return children;
diff --git a/entry_types/scrolled/package/src/frontend/PhonePlatformProvider.js b/entry_types/scrolled/package/src/frontend/PhonePlatformProvider.js
index 4909bdeaa4..09fd90f6ab 100644
--- a/entry_types/scrolled/package/src/frontend/PhonePlatformProvider.js
+++ b/entry_types/scrolled/package/src/frontend/PhonePlatformProvider.js
@@ -3,9 +3,9 @@ import React from 'react';
import {PhonePlatformContext} from './PhonePlatformContext';
import {useBrowserFeature} from './useBrowserFeature';
-import {withInlineEditingAlternative} from './inlineEditing';
+import {extensible} from './extensions';
-export const PhonePlatformProvider = withInlineEditingAlternative('PhonePlatformProvider', function PhonePlatformProvider({children}) {
+export const PhonePlatformProvider = extensible('PhonePlatformProvider', function PhonePlatformProvider({children}) {
const isPhonePlatform = useBrowserFeature('phone platform')
return (
diff --git a/entry_types/scrolled/package/src/frontend/Placeholder.js b/entry_types/scrolled/package/src/frontend/Placeholder.js
index d2dad8c60e..826471d073 100644
--- a/entry_types/scrolled/package/src/frontend/Placeholder.js
+++ b/entry_types/scrolled/package/src/frontend/Placeholder.js
@@ -1,7 +1,7 @@
import React from 'react';
-import {withInlineEditingAlternative} from './inlineEditing';
+import {extensible} from './extensions';
import styles from './Placeholder.module.css';
-export const Placeholder = withInlineEditingAlternative('Placeholder', function Placeholder() {
+export const Placeholder = extensible('Placeholder', function Placeholder() {
return ;
});
diff --git a/entry_types/scrolled/package/src/frontend/RootProviders.js b/entry_types/scrolled/package/src/frontend/RootProviders.js
index eac31d9a0c..330e05c70c 100644
--- a/entry_types/scrolled/package/src/frontend/RootProviders.js
+++ b/entry_types/scrolled/package/src/frontend/RootProviders.js
@@ -12,31 +12,34 @@ import {MediaMutedProvider} from './useMediaMuted';
import {AudioFocusProvider} from './useAudioFocus';
import {ConsentProvider} from './thirdPartyConsent';
import {CurrentSectionProvider} from './useCurrentChapter';
+import {ExtensionsProvider} from './extensions';
import {ScrollTargetEmitterProvider} from './useScrollTarget';
export function RootProviders({seed, consent = consentApi, children}) {
return (
-
-
-
-
-
-
-
-
-
- {children}
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+
+
+
);
diff --git a/entry_types/scrolled/package/src/frontend/Section.js b/entry_types/scrolled/package/src/frontend/Section.js
index 25ae3d9c96..75db6c2aa3 100644
--- a/entry_types/scrolled/package/src/frontend/Section.js
+++ b/entry_types/scrolled/package/src/frontend/Section.js
@@ -16,7 +16,7 @@ import {useScrollTarget} from './useScrollTarget';
import {usePhoneLayout} from './usePhoneLayout';
import {SectionLifecycleProvider, useSectionLifecycle} from './useSectionLifecycle'
import {SectionViewTimelineProvider} from './SectionViewTimelineProvider';
-import {withInlineEditingDecorator} from './inlineEditing';
+import {extensible} from './extensions';
import {BackgroundColorProvider} from './backgroundColor';
import {SelectableWidget} from './SelectableWidget';
import {useSectionPadding} from './useSectionPaddingCustomProperties';
@@ -30,7 +30,7 @@ import {useBackdrop} from './useBackdrop';
import styles from './Section.module.css';
import {getTransitionStyles, getEnterAndExitTransitions} from './transitions'
-const Section = withInlineEditingDecorator('SectionDecorator', function Section({
+const Section = extensible('Section', function Section({
section, transitions, backdrop, contentElements, state, onActivate, domIdPrefix
}) {
const ref = useScrollTarget(section.id);
diff --git a/entry_types/scrolled/package/src/frontend/SelectableWidget.js b/entry_types/scrolled/package/src/frontend/SelectableWidget.js
index b7a1989828..f67543da9c 100644
--- a/entry_types/scrolled/package/src/frontend/SelectableWidget.js
+++ b/entry_types/scrolled/package/src/frontend/SelectableWidget.js
@@ -1,8 +1,8 @@
-import {withInlineEditingDecorator} from './inlineEditing';
+import {extensible} from './extensions';
import {Widget} from './Widget'
-export const SelectableWidget = withInlineEditingDecorator(
- 'SelectableWidgetDecorator',
+export const SelectableWidget = extensible(
+ 'SelectableWidget',
Widget
);
diff --git a/entry_types/scrolled/package/src/frontend/Widget.js b/entry_types/scrolled/package/src/frontend/Widget.js
index bb305effc3..6a08154e68 100644
--- a/entry_types/scrolled/package/src/frontend/Widget.js
+++ b/entry_types/scrolled/package/src/frontend/Widget.js
@@ -2,9 +2,9 @@ import React from 'react';
import {api} from './api';
import {useWidget} from '../entryState';
-import {withInlineEditingDecorator} from './inlineEditing';
+import {extensible} from './extensions';
-export const Widget = withInlineEditingDecorator('WidgetDecorator', function Widget({role, props, children, renderFallback}) {
+export const Widget = extensible('Widget', function Widget({role, props, children, renderFallback}) {
const widget = useWidget({role});
if (!widget) {
diff --git a/entry_types/scrolled/package/src/frontend/WidgetSelectionRect.js b/entry_types/scrolled/package/src/frontend/WidgetSelectionRect.js
index 9f29bc0cac..ac9d9aeb4e 100644
--- a/entry_types/scrolled/package/src/frontend/WidgetSelectionRect.js
+++ b/entry_types/scrolled/package/src/frontend/WidgetSelectionRect.js
@@ -1,6 +1,6 @@
-import {withInlineEditingAlternative} from './inlineEditing';
+import {extensible} from './extensions';
-export const WidgetSelectionRect = withInlineEditingAlternative(
+export const WidgetSelectionRect = extensible(
'WidgetSelectionRect',
function WidgetSelectionRect({children}) {
return children;
diff --git a/entry_types/scrolled/package/src/frontend/extensions.js b/entry_types/scrolled/package/src/frontend/extensions.js
new file mode 100644
index 0000000000..8dd6c5e4f1
--- /dev/null
+++ b/entry_types/scrolled/package/src/frontend/extensions.js
@@ -0,0 +1,71 @@
+import React, {createContext, useContext, useEffect, useState} from 'react';
+
+import {useIsStaticPreview} from './useScrollPositionLifecycle';
+
+export function extensible(name, Component) {
+ return function ExtensibleComponent(props) {
+ const isStaticPreview = useIsStaticPreview();
+ const extensions = useExtensions();
+
+ if (isStaticPreview) {
+ return ;
+ }
+
+ const Alternative = extensions.alternatives[name];
+
+ if (Alternative) {
+ return ;
+ }
+
+ const Decorator = extensions.decorators[name];
+
+ if (Decorator) {
+ return ;
+ }
+
+ return ;
+ };
+}
+
+export function provideExtensions({decorators: d, alternatives: a} = {}) {
+ decorators = d || {};
+ alternatives = a || {};
+ notifyListeners();
+}
+
+export function clearExtensions() {
+ decorators = {};
+ alternatives = {};
+ notifyListeners();
+}
+
+export function ExtensionsProvider({children}) {
+ const [version, setVersion] = useState(0);
+
+ useEffect(() => subscribe(() => setVersion(v => v + 1)), []);
+
+ return (
+
+ {children}
+
+ );
+}
+
+let decorators = {};
+let alternatives = {};
+let listeners = [];
+const ExtensionsContext = createContext(0);
+
+function useExtensions() {
+ useContext(ExtensionsContext);
+ return {decorators, alternatives};
+}
+
+function subscribe(listener) {
+ listeners = [...listeners, listener];
+ return () => { listeners = listeners.filter(l => l !== listener); };
+}
+
+function notifyListeners() {
+ listeners.forEach(l => l());
+}
diff --git a/entry_types/scrolled/package/src/frontend/inlineEditing/components.js b/entry_types/scrolled/package/src/frontend/inlineEditing/components.js
index 610163a1e1..51c8bbe926 100644
--- a/entry_types/scrolled/package/src/frontend/inlineEditing/components.js
+++ b/entry_types/scrolled/package/src/frontend/inlineEditing/components.js
@@ -1,29 +1,48 @@
-export {EntryDecorator} from './EntryDecorator';
-export {ContentDecorator} from './ContentDecorator';
-export {SectionDecorator} from './SectionDecorator';
-export {ContentElementDecorator} from './ContentElementDecorator';
-
-export {LayoutWithPlaceholder} from './LayoutWithPlaceholder';
-
-export {EditableText} from './EditableText';
-export {EditableInlineText} from './EditableInlineText';
-export {EditableTable} from './EditableTable';
-export {EditableLink} from './EditableLink';
-
-export {LinkTooltipProvider} from './LinkTooltip';
-
-export {SelectableWidgetDecorator} from './SelectableWidgetDecorator';
-export {WidgetDecorator} from './WidgetDecorator';
-export {WidgetSelectionRect} from './WidgetSelectionRect';
-
-export {ActionButton} from './ActionButton';
-export {ActionButtons} from './ActionButtons';
-
-export {PhonePlatformProvider} from './PhonePlatformProvider';
-
-export {BackdropDecorator} from './BackdropDecorator';
-export {BackgroundContentElementDecorator} from './BackgroundContentElementDecorator';
-
-export {ForegroundDecorator} from './ForegroundDecorator';
-
-export {Placeholder} from './Placeholder';
+import {EntryDecorator} from './EntryDecorator';
+import {ContentDecorator} from './ContentDecorator';
+import {SectionDecorator} from './SectionDecorator';
+import {ContentElementDecorator} from './ContentElementDecorator';
+import {SelectableWidgetDecorator} from './SelectableWidgetDecorator';
+import {WidgetDecorator} from './WidgetDecorator';
+import {BackdropDecorator} from './BackdropDecorator';
+import {BackgroundContentElementDecorator} from './BackgroundContentElementDecorator';
+import {ForegroundDecorator} from './ForegroundDecorator';
+
+import {LayoutWithPlaceholder} from './LayoutWithPlaceholder';
+import {EditableText} from './EditableText';
+import {EditableInlineText} from './EditableInlineText';
+import {EditableTable} from './EditableTable';
+import {EditableLink} from './EditableLink';
+import {LinkTooltipProvider} from './LinkTooltip';
+import {WidgetSelectionRect} from './WidgetSelectionRect';
+import {ActionButton} from './ActionButton';
+import {ActionButtons} from './ActionButtons';
+import {PhonePlatformProvider} from './PhonePlatformProvider';
+import {Placeholder} from './Placeholder';
+
+export const extensions = {
+ decorators: {
+ Entry: EntryDecorator,
+ Content: ContentDecorator,
+ Section: SectionDecorator,
+ ContentElement: ContentElementDecorator,
+ SelectableWidget: SelectableWidgetDecorator,
+ Widget: WidgetDecorator,
+ Backdrop: BackdropDecorator,
+ BackgroundContentElement: BackgroundContentElementDecorator,
+ Foreground: ForegroundDecorator
+ },
+ alternatives: {
+ LayoutWithPlaceholder,
+ EditableText,
+ EditableInlineText,
+ EditableTable,
+ EditableLink,
+ LinkTooltipProvider,
+ WidgetSelectionRect,
+ ActionButton,
+ ActionButtons,
+ PhonePlatformProvider,
+ Placeholder
+ }
+};
diff --git a/entry_types/scrolled/package/src/frontend/inlineEditing/index.js b/entry_types/scrolled/package/src/frontend/inlineEditing/index.js
index 32b45de16f..b22f0d2ccc 100644
--- a/entry_types/scrolled/package/src/frontend/inlineEditing/index.js
+++ b/entry_types/scrolled/package/src/frontend/inlineEditing/index.js
@@ -1,44 +1,8 @@
-import React from 'react';
-
import {importComponents} from './importComponents';
-import {useIsStaticPreview} from '../useScrollPositionLifecycle';
-
-let components = {};
+import {provideExtensions} from '../extensions';
export function loadInlineEditingComponents() {
- return importComponents().then(importedComponents => {
- components = importedComponents;
+ return importComponents().then(({extensions}) => {
+ provideExtensions(extensions);
});
}
-
-export function withInlineEditingDecorator(name, Component) {
- return function InlineEditingDecorator(props) {
- const Decorator = components[name];
- const isStaticPreview = useIsStaticPreview();
-
- if (Decorator && !isStaticPreview) {
- return (
-
-
-
- );
- }
- else {
- return ;
- }
- }
-}
-
-export function withInlineEditingAlternative(name, Component) {
- return function InlineEditingDecorator(props) {
- const Alternative = components[name];
- const isStaticPreview = useIsStaticPreview();
-
- if (Alternative && !isStaticPreview) {
- return ;
- }
- else {
- return ;
- }
- }
-}
diff --git a/entry_types/scrolled/package/src/frontend/layouts/index.js b/entry_types/scrolled/package/src/frontend/layouts/index.js
index bedae7a117..1d646520d3 100644
--- a/entry_types/scrolled/package/src/frontend/layouts/index.js
+++ b/entry_types/scrolled/package/src/frontend/layouts/index.js
@@ -3,12 +3,12 @@ import React from 'react';
import {TwoColumn} from './TwoColumn';
import {Center} from './Center';
-import {withInlineEditingAlternative} from '../inlineEditing';
+import {extensible} from '../extensions';
export {widths, widthName} from './widths';
export const Layout = React.memo(
- withInlineEditingAlternative('LayoutWithPlaceholder', LayoutWithoutInlineEditing),
+ extensible('LayoutWithPlaceholder', LayoutWithoutInlineEditing),
(prevProps, nextProps) => (
prevProps.sectionId === nextProps.sectionId &&
prevProps.items === nextProps.items &&