post messages from iframe

- fix user info opening
- remove iframe height checks by interval
- update height by mutation observer events
- rename events
This commit is contained in:
Pavel Mineev
2021-06-03 00:56:16 -05:00
committed by Umputun
parent c0b392ad4c
commit 3068dfe637
13 changed files with 139 additions and 164 deletions
@@ -1,9 +0,0 @@
import { parseQuery } from 'utils/parseQuery';
import type { UserInfo } from './types';
export const userInfo: UserInfo = parseQuery();
export const id = userInfo.id;
export const name = userInfo.name;
export const picture = userInfo.picture;
@@ -6,7 +6,7 @@ import b from 'bem-react-helper';
import { User, Sorting, Theme, PostInfo } from 'common/types';
import { IS_STORAGE_AVAILABLE, IS_THIRD_PARTY } from 'common/constants';
import { requestDeletion } from 'utils/email';
import { postMessage } from 'utils/postMessage';
import { postMessageToParent } from 'utils/postMessage';
import { getHandleClickProps } from 'common/accessibility';
import { StoreState } from 'store';
import { Dropdown, DropdownItem } from 'components/dropdown';
@@ -75,11 +75,11 @@ class AuthPanelComponent extends Component<Props, State> {
toggleUserInfoVisibility = () => {
const { user } = this.props;
if (user === null) {
if (!user) {
return;
}
postMessage({ isUserInfoShown: true, user });
postMessageToParent({ profile: user });
};
renderAuthorized = (user: User) => {
+14 -21
View File
@@ -1,14 +1,11 @@
import { useEffect, useRef, useState } from 'preact/hooks';
import { parseMessage, postMessageToParent } from 'utils/postMessage';
function handleChangeIframeSize(element: HTMLElement) {
const { top } = element.getBoundingClientRect();
const height = window.scrollY + Math.abs(top) + element.scrollHeight + 20;
const height = Math.max(window.scrollY + Math.abs(top) + element.scrollHeight + 20, document.body.offsetHeight);
if (window.innerHeight > height) {
return;
}
document.body.style.setProperty('min-height', `${height}px`);
postMessageToParent({ height });
}
export function useDropdown(disableClosing?: boolean) {
@@ -19,28 +16,20 @@ export function useDropdown(disableClosing?: boolean) {
};
useEffect(() => {
if (!showDropdown) {
const dropdownElement = rootRef.current;
if (!showDropdown || !dropdownElement) {
return;
}
const dropdownElement = rootRef.current;
handleChangeIframeSize(dropdownElement);
function handleMessageFromParent(evt: MessageEvent) {
if (typeof evt.data !== 'string' || disableClosing) {
const data = parseMessage(evt);
if (disableClosing && data.clickOutside) {
return;
}
try {
const data = JSON.parse(evt.data);
if (!data.clickOutside) {
return;
}
setShowDropdown(false);
} catch (e) {}
setShowDropdown(false);
}
function handleClickOutside(evt: MouseEvent) {
@@ -64,9 +53,13 @@ export function useDropdown(disableClosing?: boolean) {
const dropdownElement = rootRef.current;
if (!dropdownElement || !showDropdown) {
handleChangeIframeSize(document.body);
return;
}
handleChangeIframeSize(dropdownElement);
const observer = new MutationObserver(() => {
handleChangeIframeSize(dropdownElement);
});
+16 -21
View File
@@ -16,7 +16,7 @@ import { Avatar } from 'components/avatar';
import { Button } from 'components/button';
import { Countdown } from 'components/countdown';
import { getPreview, uploadImage } from 'common/api';
import { postMessage } from 'utils/postMessage';
import { postMessageToParent } from 'utils/postMessage';
import { FormattedMessage, IntlShape, defineMessages } from 'react-intl';
import { getVoteMessage, VoteMessagesTypes } from './getVoteMessage';
import { getBlockingDurations } from './getBlockingDurations';
@@ -211,14 +211,7 @@ export class Comment extends Component<CommentProps, State> {
};
toggleUserInfoVisibility = () => {
if (!window.parent) {
return;
}
const { user } = this.props.data;
const data = JSON.stringify({ isUserInfoShown: true, user });
window.parent.postMessage(data, '*');
postMessageToParent({ profile: this.props.data.user });
};
togglePin = () => {
@@ -357,21 +350,23 @@ export class Comment extends Component<CommentProps, State> {
this.props.setReplyEditState!({ id: this.props.data.id, state: CommentMode.None });
};
scrollToParent = (e: Event) => {
const {
data: { pid },
} = this.props;
e.preventDefault();
scrollToParent = (evt: Event) => {
const { pid } = this.props.data;
const parentCommentNode = document.getElementById(`${COMMENT_NODE_CLASSNAME_PREFIX}${pid}`);
if (parentCommentNode) {
const top = parentCommentNode.getBoundingClientRect().top;
if (!postMessage({ scrollTo: top })) {
parentCommentNode.scrollIntoView();
}
evt.preventDefault();
if (!parentCommentNode) {
return;
}
const top = parentCommentNode.getBoundingClientRect().top;
if (postMessageToParent({ scrollTo: top })) {
return;
}
parentCommentNode.scrollIntoView();
};
copyComment = () => {
+15 -19
View File
@@ -134,25 +134,23 @@ export class Dropdown extends Component<Props, State> {
});
}
receiveMessage(event: MessageEvent<{ clickOutside?: boolean }>) {
try {
const data = parseMessage(event);
receiveMessage(evt: MessageEvent) {
const data = parseMessage(evt);
if (!data || !data.clickOutside || !this.state.isActive) {
return;
if (!data.clickOutside || !this.state.isActive) {
return;
}
this.setState(
{
contentTranslateX: 0,
isActive: false,
},
() => {
this.__onClose();
this.props.onClose?.(this.rootNode.current!);
}
this.setState(
{
contentTranslateX: 0,
isActive: false,
},
() => {
this.__onClose();
this.props.onClose && this.props.onClose(this.rootNode.current!);
}
);
} catch (e) {}
);
}
onOutsideClick(e: MouseEvent) {
@@ -171,13 +169,11 @@ export class Dropdown extends Component<Props, State> {
componentDidMount() {
document.addEventListener('click', this.onOutsideClick);
window.addEventListener('message', this.receiveMessage);
}
componentWillUnmount() {
document.removeEventListener('click', this.onOutsideClick);
window.removeEventListener('message', this.receiveMessage);
}
+11 -11
View File
@@ -4,7 +4,7 @@ import b from 'bem-react-helper';
import { IntlShape, useIntl, FormattedMessage, defineMessages } from 'react-intl';
import clsx from 'clsx';
import type { Sorting, Theme } from 'common/types';
import type { Sorting } from 'common/types';
import type { StoreState } from 'store';
import {
COMMENT_NODE_CLASSNAME_PREFIX,
@@ -39,7 +39,7 @@ import { ConnectedComment as Comment } from 'components/comment/connected-commen
import { uploadImage, getPreview } from 'common/api';
import { isUserAnonymous } from 'utils/isUserAnonymous';
import { bindActions } from 'utils/actionBinder';
import { postMessage, parseMessage } from 'utils/postMessage';
import { postMessageToParent, parseMessage } from 'utils/postMessage';
import { useActions } from 'hooks/useAction';
import { setCollapse } from 'store/thread/actions';
import { logout } from 'components/auth/auth.api';
@@ -122,7 +122,7 @@ export class Root extends Component<Props, State> {
const userloading = this.props.fetchUser().finally(() => this.setState({ isUserLoading: false }));
Promise.all([userloading, this.props.fetchComments()]).finally(() => {
postMessage({ remarkIframeHeight: document.body.offsetHeight });
postMessageToParent({ height: document.body.offsetHeight });
setTimeout(this.checkUrlHash);
window.addEventListener('hashchange', this.checkUrlHash);
});
@@ -169,7 +169,7 @@ export class Root extends Component<Props, State> {
toMessage = (hash: string) => {
const comment = document.querySelector(hash);
if (comment) {
postMessage({ scrollTo: comment.getBoundingClientRect().top });
postMessageToParent({ scrollTo: comment.getBoundingClientRect().top });
comment.classList.add('comment_highlighting');
setTimeout(() => {
comment.classList.remove('comment_highlighting');
@@ -177,14 +177,14 @@ export class Root extends Component<Props, State> {
}
};
onMessage(event: MessageEvent<{ theme?: Theme }>) {
try {
const data = parseMessage(event);
onMessage(event: MessageEvent) {
const data = parseMessage(event);
if (data.theme && THEMES.includes(data.theme)) {
this.props.setTheme(data.theme);
}
} catch (e) {}
if (!data.theme || !THEMES.includes(data.theme)) {
return;
}
this.props.setTheme(data.theme);
}
onBlockedUsersShow = async () => {
@@ -6,9 +6,9 @@ import { useIntl, defineMessages, FormattedMessage, IntlShape } from 'react-intl
import { StoreState } from 'store';
import { Comment } from 'common/types';
import { fetchInfo } from 'store/user-info/actions';
import { userInfo } from 'common/user-info-settings';
import { postMessage } from 'utils/postMessage';
import { parseQuery } from 'utils/parseQuery';
import { bindActions } from 'utils/actionBinder';
import { postMessageToParent } from 'utils/postMessage';
import { useActions } from 'hooks/useAction';
import { Avatar } from 'components/avatar';
@@ -23,6 +23,8 @@ const messages = defineMessages({
},
});
const user = parseQuery();
type Props = {
comments: Comment[] | null;
} & typeof boundActions & { intl: IntlShape };
@@ -38,7 +40,7 @@ class UserInfo extends Component<Props, State> {
componentWillMount(): void {
if (!this.props.comments && this.state.isLoading) {
this.props
.fetchInfo()
.fetchInfo(user.id)
.then(() => {
this.setState({ isLoading: false });
})
@@ -55,7 +57,6 @@ class UserInfo extends Component<Props, State> {
}
render(): JSX.Element | null {
const user = userInfo;
const { comments = [] } = this.props;
const { isLoading } = this.state;
@@ -87,12 +88,12 @@ class UserInfo extends Component<Props, State> {
static onKeyDown(e: KeyboardEvent): void {
// ESCAPE key pressed
if (e.keyCode === 27) {
postMessage({ isUserInfoShown: false });
postMessageToParent({ profile: null });
}
}
}
const commentsSelector = (state: StoreState) => state.userComments[userInfo.id];
const commentsSelector = (state: StoreState) => state.userComments[user.id];
export const ConnectedUserInfo: FunctionComponent = () => {
const comments = useSelector(commentsSelector);
+32 -36
View File
@@ -1,6 +1,6 @@
import type { UserInfo, Theme } from 'common/types';
import { BASE_URL, NODE_ID, COMMENT_NODE_CLASSNAME_PREFIX } from 'common/constants.config';
import { parseMessage, ParentMessage } from 'utils/postMessage';
import { parseMessage, postMessageToIframe } from 'utils/postMessage';
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
@@ -264,59 +264,55 @@ function createInstance(config: typeof window.remark_config) {
},
};
function receiveMessages(event: MessageEvent<ParentMessage>): void {
try {
const data = parseMessage(event);
function receiveMessages(event: MessageEvent): void {
const data = parseMessage(event);
if (data.remarkIframeHeight) {
iframe.style.height = `${data.remarkIframeHeight}px`;
}
if (data.height) {
iframe.style.height = `${data.height}px`;
}
if (data.scrollTo) {
window.scrollTo(window.pageXOffset, data.scrollTo + iframe.getBoundingClientRect().top + window.pageYOffset);
}
if (data.scrollTo) {
window.scrollTo(window.pageXOffset, data.scrollTo + iframe.getBoundingClientRect().top + window.pageYOffset);
}
if (Object.prototype.hasOwnProperty.call(data, 'isUserInfoShown')) {
if (data.isUserInfoShown) {
userInfo.init(data.user);
} else {
userInfo.close();
}
if (typeof data.profile === 'object') {
if (data.profile === null) {
userInfo.close();
} else {
userInfo.init(data.profile);
}
}
if (data.inited) {
postHashToIframe();
postTitleToIframe(document.title);
}
} catch (e) {}
if (data.inited) {
postHashToIframe();
postTitleToIframe(document.title);
}
}
function postHashToIframe(e?: Event & { newURL: string }) {
const hash = e ? `#${e.newURL.split('#')[1]}` : window.location.hash;
function postHashToIframe(evt?: Event & { newURL: string }) {
const hash = evt ? `#${evt.newURL.split('#')[1]}` : window.location.hash;
if (hash.indexOf(`#${COMMENT_NODE_CLASSNAME_PREFIX}`) === 0) {
if (e) e.preventDefault();
iframe.contentWindow!.postMessage({ hash }, '*');
if (!hash.startsWith(`#${COMMENT_NODE_CLASSNAME_PREFIX}`)) {
return;
}
evt?.preventDefault();
postMessageToIframe(iframe, { hash });
}
function postTitleToIframe(title: string) {
if (iframe.contentWindow) {
iframe.contentWindow.postMessage({ title }, '*');
}
postMessageToIframe(iframe, { title });
}
function postClickOutsideToIframe(e: MouseEvent) {
if (iframe.contentWindow && !iframe.contains(e.target as Node)) {
iframe.contentWindow.postMessage({ clickOutside: true }, '*');
function postClickOutsideToIframe(evt: MouseEvent) {
if (iframe.contains(evt.target as Node)) {
return;
}
postMessageToIframe(iframe, { clickOutside: true });
}
function changeTheme(theme: Theme) {
if (iframe.contentWindow) {
iframe.contentWindow.postMessage({ theme }, '*');
}
postMessageToIframe(iframe, { theme });
}
function destroy() {
+1 -1
View File
@@ -31,7 +31,7 @@ async function init(): Promise<void> {
throw new Error("Remark42: Can't find root node.");
}
const params = parseQuery<{ page?: string; locale?: string; simple_view?: boolean }>();
const params = parseQuery();
const locale = getLocale(params);
const messages = await loadLocale(locale).catch(() => ({}));
const boundActions = bindActionCreators({ fetchHiddenUsers, restoreCollapsedThreads }, store.dispatch);
+4 -7
View File
@@ -1,19 +1,16 @@
import { getUserComments } from 'common/api';
import { Comment } from 'common/types';
import { userInfo } from 'common/user-info-settings';
import { StoreAction } from '../index';
import { USER_INFO_SET } from './types';
export const fetchInfo = (): StoreAction<Promise<Comment[] | null>> => async (dispatch) => {
if (!userInfo.id) {
return null;
}
export const fetchInfo = (id: string): StoreAction<Promise<Comment[] | null>> => async (dispatch) => {
// TODO: limit
const info = await getUserComments(userInfo.id, 10);
const info = await getUserComments(id, 10);
dispatch({
type: USER_INFO_SET,
id: userInfo.id,
id,
comments: info.comments,
});
return info.comments;
+1 -1
View File
@@ -1,6 +1,6 @@
/** converts window.location.search into object */
export function parseQuery<T extends {}>(search: string = window.location.search): T {
export function parseQuery<T extends Record<string, string>>(search: string = window.location.search): T {
const params: { [key: string]: string } = {};
new URLSearchParams(search).forEach((value: string, key: string) => {
params[key] = value;
+33 -14
View File
@@ -1,42 +1,61 @@
import type { Theme, UserInfo } from 'common/types';
export type ParentMessage = {
type ParentMessage = {
inited?: true;
scrollTo?: number;
remarkIframeHeight?: number;
} & (
| { isUserInfoShown: true; user: UserInfo }
| { isUserInfoShown: false; user?: never }
| { isUserInfoShown?: never; user?: never }
);
height?: number;
profile?: UserInfo | null;
};
export type ChildMessage = {
type ChildMessage = {
clickOutside?: true;
hash?: string;
title?: string;
theme?: Theme;
};
type AllMessages = ParentMessage & ChildMessage;
type AllMessages = ChildMessage & ParentMessage;
/**
* Sends message to parent window
*
* @returns request success of fail
*/
export function postMessage(data: AllMessages): boolean {
if (!window.parent || window.parent === window) return false;
export function postMessageToParent(data: ParentMessage): boolean {
if (!window.parent || window.parent === window) {
return false;
}
window.parent.postMessage(data, '*');
return true;
}
/**
* Sends message to target iframe
*
* @param target iframe to send data
* @param data that will be send to iframe
* @returns request success of fail
*/
export function postMessageToIframe(target: HTMLIFrameElement, data: ChildMessage): boolean {
if (!target?.contentWindow) {
return false;
}
target.contentWindow.postMessage(data, '*');
return true;
}
/**
* Parses data from post message that was received in iframe
*
* @param evt post message event
* @returns
*/
export function parseMessage<T>({ data }: MessageEvent<T>): T {
export function parseMessage({ data }: MessageEvent): AllMessages {
if (typeof data !== 'object' || data === null || Array.isArray(data)) {
return {} as T;
return {} as AllMessages;
}
return data as T;
return data as AllMessages;
}
+2 -15
View File
@@ -127,20 +127,7 @@
</script>
<% } %>
<script>
// TODO: we must stop using `setInterval` for resize iframe
// TODO: we must use more efficient way to change iframe height
// TODO: we must open comments widget and user-info widget on different pages (now iframe.html opens both of widgets)
var lastHeight = 0;
setInterval(function () {
if (document.body.offsetHeight !== lastHeight && document.body.offsetHeight > 22) {
lastHeight = document.body.offsetHeight;
window.parent.postMessage({ remarkIframeHeight: lastHeight }, '*');
}
}, 200);
window.addEventListener('message', receiveMessages);
function receiveMessages(event) {
window.addEventListener('message', function receiveMessages(event) {
try {
const data = event.data;
const isObj = typeof data === 'object' && data !== null && !Array.isArray(data);
@@ -155,7 +142,7 @@
document.title = data.title;
}
} catch (e) {}
}
});
var remark_config =
window.location.search.length < 2