Skip to content

Commit ba28621

Browse files
committed
Prevent duplicate listeners in editor preview controller
Move READY message handling from ContentDecorator up to EntryDecorator so that it sits outside the content tree that gets re-mounted in the editor when WidgetPresenceWrapper receives widget data. Guard the READY handler in PreviewMessageController so that a second READY message from the iframe does not register duplicate collection watchers and event listeners. REDMINE-21246
1 parent 2dd2256 commit ba28621

File tree

4 files changed

+172
-143
lines changed

4 files changed

+172
-143
lines changed

entry_types/scrolled/package/spec/editor/controllers/PreviewMessageController-spec.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,32 @@ describe('PreviewMessageController', () => {
628628
}).not.toThrowError();
629629
});
630630

631+
it('ignores second READY message', async () => {
632+
const entry = factories.entry(ScrolledEntry, {}, {
633+
entryTypeSeed: normalizeSeed({
634+
contentElements: [{id: 1}]
635+
})
636+
});
637+
const iframeWindow = createIframeWindow();
638+
controller = new PreviewMessageController({entry, iframeWindow});
639+
640+
await postReadyMessageAndWaitForAcknowledgement(iframeWindow);
641+
await postReadyMessageAndWaitForAcknowledgement(iframeWindow);
642+
643+
const actions = [];
644+
iframeWindow.addEventListener('message', event => {
645+
if (event.data.type === 'ACTION') {
646+
actions.push(event.data.payload);
647+
}
648+
});
649+
650+
entry.contentElements.first().configuration.set({title: 'update'});
651+
652+
return expect(new Promise(resolve => {
653+
setTimeout(() => resolve(actions.length), 100);
654+
})).resolves.toEqual(1);
655+
});
656+
631657
it('sends CHANGE_EMULATION_MODE message to iframe on change:emulation_mode event on model', async () => {
632658
const entry = factories.entry(ScrolledEntry, {}, {
633659
entryTypeSeed: normalizeSeed({

entry_types/scrolled/package/src/editor/controllers/PreviewMessageController.js

Lines changed: 100 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -24,106 +24,110 @@ export const PreviewMessageController = Object.extend({
2424

2525
if (window.location.href.indexOf(message.origin) === 0) {
2626
if (message.data.type === 'READY') {
27-
watchCollections(this.entry, {
28-
dispatch: action => {
29-
postMessage({type: 'ACTION', payload: action})
30-
}
31-
});
32-
33-
this.listenTo(this.entry, 'scrollToSection', (section, options) =>
34-
postMessage({
35-
type: 'SCROLL_TO_SECTION',
36-
payload: {
37-
id: section.id,
38-
...options
39-
}
40-
})
41-
);
27+
if (!this.ready) {
28+
this.ready = true;
4229

43-
this.listenTo(this.entry.contentElements, 'postCommand', (contentElementId, command) =>
44-
postMessage({
45-
type: 'CONTENT_ELEMENT_EDITOR_COMMAND',
46-
payload: {
47-
contentElementId,
48-
command
30+
watchCollections(this.entry, {
31+
dispatch: action => {
32+
postMessage({type: 'ACTION', payload: action})
4933
}
50-
})
51-
);
34+
});
5235

53-
this.listenTo(this.entry, 'selectSection', section =>
54-
postMessage({
55-
type: 'SELECT',
56-
payload: {
57-
id: section.id,
58-
type: 'section'
59-
}
60-
})
61-
);
62-
63-
this.listenTo(this.entry, 'selectSectionSettings', section =>
64-
postMessage({
65-
type: 'SELECT',
66-
payload: {
67-
id: section.id,
68-
type: 'sectionSettings'
69-
}
70-
})
71-
);
72-
73-
this.listenTo(this.entry, 'selectSectionTransition', section =>
74-
postMessage({
75-
type: 'SELECT',
76-
payload: {
77-
id: section.id,
78-
type: 'sectionTransition'
79-
}
80-
})
81-
);
82-
83-
this.listenTo(this.entry, 'selectSectionPaddings', section =>
84-
postMessage({
85-
type: 'SELECT',
86-
payload: {
87-
id: section.id,
88-
type: 'sectionPaddings'
89-
}
90-
})
91-
);
92-
93-
this.listenTo(this.entry, 'selectContentElement', (contentElement, options) => {
94-
postMessage({
95-
type: 'SELECT',
96-
payload: {
97-
id: contentElement.id,
98-
range: options?.range,
99-
type: 'contentElement'
100-
}
101-
})
102-
});
103-
104-
this.listenTo(this.entry, 'selectWidget', widget => {
105-
postMessage({
106-
type: 'SELECT',
107-
payload: {
108-
id: widget.get('role'),
109-
type: 'widget'
110-
}
111-
})
112-
});
113-
114-
this.listenTo(this.entry, 'resetSelection', contentElement =>
115-
postMessage({
116-
type: 'SELECT',
117-
payload: null
118-
})
119-
);
36+
this.listenTo(this.entry, 'scrollToSection', (section, options) =>
37+
postMessage({
38+
type: 'SCROLL_TO_SECTION',
39+
payload: {
40+
id: section.id,
41+
...options
42+
}
43+
})
44+
);
45+
46+
this.listenTo(this.entry.contentElements, 'postCommand', (contentElementId, command) =>
47+
postMessage({
48+
type: 'CONTENT_ELEMENT_EDITOR_COMMAND',
49+
payload: {
50+
contentElementId,
51+
command
52+
}
53+
})
54+
);
55+
56+
this.listenTo(this.entry, 'selectSection', section =>
57+
postMessage({
58+
type: 'SELECT',
59+
payload: {
60+
id: section.id,
61+
type: 'section'
62+
}
63+
})
64+
);
65+
66+
this.listenTo(this.entry, 'selectSectionSettings', section =>
67+
postMessage({
68+
type: 'SELECT',
69+
payload: {
70+
id: section.id,
71+
type: 'sectionSettings'
72+
}
73+
})
74+
);
75+
76+
this.listenTo(this.entry, 'selectSectionTransition', section =>
77+
postMessage({
78+
type: 'SELECT',
79+
payload: {
80+
id: section.id,
81+
type: 'sectionTransition'
82+
}
83+
})
84+
);
85+
86+
this.listenTo(this.entry, 'selectSectionPaddings', section =>
87+
postMessage({
88+
type: 'SELECT',
89+
payload: {
90+
id: section.id,
91+
type: 'sectionPaddings'
92+
}
93+
})
94+
);
95+
96+
this.listenTo(this.entry, 'selectContentElement', (contentElement, options) => {
97+
postMessage({
98+
type: 'SELECT',
99+
payload: {
100+
id: contentElement.id,
101+
range: options?.range,
102+
type: 'contentElement'
103+
}
104+
})
105+
});
106+
107+
this.listenTo(this.entry, 'selectWidget', widget => {
108+
postMessage({
109+
type: 'SELECT',
110+
payload: {
111+
id: widget.get('role'),
112+
type: 'widget'
113+
}
114+
})
115+
});
116+
117+
this.listenTo(this.entry, 'resetSelection', contentElement =>
118+
postMessage({
119+
type: 'SELECT',
120+
payload: null
121+
})
122+
);
120123

121-
this.listenTo(this.entry, 'change:emulation_mode', entry =>
122-
postMessage({
123-
type: 'CHANGE_EMULATION_MODE',
124-
payload: this.entry.get('emulation_mode')
125-
})
126-
);
124+
this.listenTo(this.entry, 'change:emulation_mode', entry =>
125+
postMessage({
126+
type: 'CHANGE_EMULATION_MODE',
127+
payload: this.entry.get('emulation_mode')
128+
})
129+
);
130+
}
127131

128132
postMessage({type: 'ACK'})
129133
}
Lines changed: 4 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,16 @@
1-
import React, {useEffect, useCallback} from 'react';
1+
import React from 'react';
22
import {DndProvider} from 'react-dnd';
33
import {HTML5Backend} from 'react-dnd-html5-backend';
44

5-
import {useEntryStateDispatch} from '../../entryState';
6-
import {usePostMessageListener} from '../usePostMessageListener';
7-
import {useEditorSelection} from './EditorState';
8-
import {
9-
useContentElementEditorCommandEmitter,
10-
ContentElementEditorCommandSubscriptionProvider
11-
} from './ContentElementEditorCommandSubscriptionProvider';
125
import {ScrollPointMessageHandler} from './scrollPoints';
136

147
export function ContentDecorator(props) {
15-
const contentElementEditorCommandEmitter = useContentElementEditorCommandEmitter();
16-
178
return (
189
<>
19-
<MessageHandler contentElementEditorCommandEmitter={contentElementEditorCommandEmitter} />
2010
<ScrollPointMessageHandler />
21-
<ContentElementEditorCommandSubscriptionProvider emitter={contentElementEditorCommandEmitter}>
22-
<DndProvider backend={HTML5Backend}>
23-
{props.children}
24-
</DndProvider>
25-
</ContentElementEditorCommandSubscriptionProvider>
11+
<DndProvider backend={HTML5Backend}>
12+
{props.children}
13+
</DndProvider>
2614
</>
2715
);
2816
}
29-
30-
function MessageHandler({contentElementEditorCommandEmitter}) {
31-
const {select} = useEditorSelection()
32-
const dispatch = useEntryStateDispatch();
33-
34-
const receiveMessage = useCallback(data => {
35-
if (data.type === 'ACTION') {
36-
dispatch(data.payload);
37-
}
38-
else if (data.type === 'SELECT') {
39-
select(data.payload);
40-
}
41-
else if (data.type === 'CONTENT_ELEMENT_EDITOR_COMMAND') {
42-
contentElementEditorCommandEmitter.trigger(`command:${data.payload.contentElementId}`,
43-
data.payload.command);
44-
}
45-
}, [dispatch, select, contentElementEditorCommandEmitter]);
46-
47-
usePostMessageListener(receiveMessage);
48-
49-
useEffect(() => {
50-
if (window.parent !== window) {
51-
window.parent.postMessage({type: 'READY'}, window.location.origin);
52-
}
53-
}, []);
54-
55-
return null;
56-
}
Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,50 @@
1-
import React from 'react';
1+
import React, {useEffect, useCallback} from 'react';
22

3-
import {EditorStateProvider} from './EditorState';
3+
import {useEntryStateDispatch} from '../../entryState';
4+
import {usePostMessageListener} from '../usePostMessageListener';
5+
import {EditorStateProvider, useEditorSelection} from './EditorState';
6+
import {
7+
useContentElementEditorCommandEmitter,
8+
ContentElementEditorCommandSubscriptionProvider
9+
} from './ContentElementEditorCommandSubscriptionProvider';
410

511
export function EntryDecorator({children}) {
12+
const contentElementEditorCommandEmitter = useContentElementEditorCommandEmitter();
13+
614
return (
715
<EditorStateProvider>
8-
{children}
16+
<MessageHandler contentElementEditorCommandEmitter={contentElementEditorCommandEmitter} />
17+
<ContentElementEditorCommandSubscriptionProvider emitter={contentElementEditorCommandEmitter}>
18+
{children}
19+
</ContentElementEditorCommandSubscriptionProvider>
920
</EditorStateProvider>
1021
);
1122
}
23+
24+
function MessageHandler({contentElementEditorCommandEmitter}) {
25+
const {select} = useEditorSelection()
26+
const dispatch = useEntryStateDispatch();
27+
28+
const receiveMessage = useCallback(data => {
29+
if (data.type === 'ACTION') {
30+
dispatch(data.payload);
31+
}
32+
else if (data.type === 'SELECT') {
33+
select(data.payload);
34+
}
35+
else if (data.type === 'CONTENT_ELEMENT_EDITOR_COMMAND') {
36+
contentElementEditorCommandEmitter.trigger(`command:${data.payload.contentElementId}`,
37+
data.payload.command);
38+
}
39+
}, [dispatch, select, contentElementEditorCommandEmitter]);
40+
41+
usePostMessageListener(receiveMessage);
42+
43+
useEffect(() => {
44+
if (window.parent !== window) {
45+
window.parent.postMessage({type: 'READY'}, window.location.origin);
46+
}
47+
}, []);
48+
49+
return null;
50+
}

0 commit comments

Comments
 (0)