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 &&