import './style.scss'; import './live-reload-conditions'; import classnames from 'classnames/dedupe'; import iframeResizer from 'iframe-resizer/js/iframeResizer'; import $ from 'jquery'; import { isEqual, uniq } from 'lodash'; import rafSchd from 'raf-schd'; import { debounce, throttle } from 'throttle-debounce'; import { Spinner } from '@wordpress/components'; import { dispatch, withSelect } from '@wordpress/data'; import { Component, createRef, Fragment } from '@wordpress/element'; import { applyFilters } from '@wordpress/hooks'; import getDynamicCSS, { hasDynamicCSS } from '../../utils/controls-dynamic-css'; const { VPAdminGutenbergVariables: variables, VPGutenbergVariables: { controls: registeredControls }, } = window; let uniqueIdCount = 1; function getUpdatedKeys(oldData, newData) { const keys = uniq([...Object.keys(oldData), ...Object.keys(newData)]); const changedKeys = []; keys.forEach((k) => { if (!isEqual(oldData[k], newData[k])) { changedKeys.push(k); } }); return changedKeys; } /** * Component Class */ class IframePreview extends Component { constructor(...args) { super(...args); this.state = { loading: true, uniqueId: `vpf-preview-${uniqueIdCount}`, currentIframeHeight: 0, latestIframeHeight: 0, }; uniqueIdCount += 1; this.frameRef = createRef(); this.formRef = createRef(); this.maybePreviewTypeChanged = this.maybePreviewTypeChanged.bind(this); this.maybeAttributesChanged = this.maybeAttributesChanged.bind(this); this.onFrameLoad = this.onFrameLoad.bind(this); this.maybeReload = this.maybeReload.bind(this); this.maybeReloadDebounce = debounce( 300, rafSchd(this.maybeReload.bind(this)) ); this.maybeResizePreviews = this.maybeResizePreviews.bind(this); this.maybeResizePreviewsThrottle = throttle( 100, rafSchd(this.maybeResizePreviews) ); this.updateIframeHeight = this.updateIframeHeight.bind(this); this.updateIframeHeightThrottle = throttle( 100, rafSchd(this.updateIframeHeight) ); this.printInput = this.printInput.bind(this); } componentDidMount() { const self = this; const { clientId } = self.props; iframeResizer( { interval: 10, warningTimeout: 60000, checkOrigin: false, onMessage({ message }) { // select current block on click message. if (message === 'clicked') { dispatch('core/block-editor').selectBlock(clientId); window.focus(); } }, onResized({ height }) { self.updateIframeHeightThrottle(`${height}px`); }, }, self.frameRef.current ); self.frameRef.current.addEventListener('load', self.onFrameLoad); window.addEventListener('resize', self.maybeResizePreviewsThrottle); self.maybeReload(); } componentDidUpdate(prevProps) { this.maybePreviewTypeChanged(prevProps); this.maybeAttributesChanged(prevProps); } componentWillUnmount() { this.frameRef.current.removeEventListener('load', this.onFrameLoad); window.removeEventListener('resize', this.maybeResizePreviewsThrottle); if (this.frameRef.current.iframeResizer) { this.frameRef.current.iframeResizer.close(); this.frameRef.current.iframeResizer.removeListeners(); } } /** * On frame load event. * * @param {Object} e - event data. */ onFrameLoad(e) { this.frameWindow = e.target.contentWindow; this.frameJQuery = e.target.contentWindow.jQuery; if (this.frameJQuery) { this.$framePortfolio = this.frameJQuery('.vp-portfolio'); this.maybeResizePreviews(); if (this.frameTimeout) { clearTimeout(this.frameTimeout); } // We need this timeout, since we resize iframe size and layouts resized with transitions. this.frameTimeout = setTimeout(() => { this.setState({ loading: false, }); }, 300); } } maybePreviewTypeChanged(prevProps) { if (prevProps.previewDeviceType === this.props.previewDeviceType) { return; } this.maybeResizePreviews(); } maybeAttributesChanged(prevProps) { if (this.busyReload) { return; } this.busyReload = true; const { attributes: newAttributes } = this.props; const { attributes: oldAttributes } = prevProps; const frame = this.frameRef.current; const changedAttributes = {}; const changedAttributeKeys = getUpdatedKeys( oldAttributes, newAttributes ); // check changed attributes. changedAttributeKeys.forEach((name) => { if (typeof newAttributes[name] !== 'undefined') { changedAttributes[name] = newAttributes[name]; } }); if (Object.keys(changedAttributes).length) { let reload = false; Object.keys(changedAttributes).forEach((name) => { // Don't reload if block has dynamic styles. const hasStyles = hasDynamicCSS(name); // Don't reload if reloading disabled in control attributes. const hasReloadAttribute = registeredControls[name] && registeredControls[name].reload_iframe; reload = reload || (!hasStyles && hasReloadAttribute); }); const data = applyFilters('vpf.editor.changed-attributes', { attributes: changedAttributes, reload, $frame: this.frameRef.current, frameWindow: this.frameWindow, frameJQuery: this.frameJQuery, $framePortfolio: this.$framePortfolio, }); if (!data.reload) { // Update AJAX dynamic data. if (data.frameWindow && data.frameWindow.vp_preview_post_data) { data.frameWindow.vp_preview_post_data[data.name] = data.value; } // Insert dynamic CSS. if (frame.iFrameResizer && newAttributes.block_id) { frame.iFrameResizer.sendMessage({ name: 'dynamic-css', blockId: newAttributes.block_id, styles: getDynamicCSS(newAttributes), }); } } if (data.reload) { this.maybeReloadDebounce(); } this.busyReload = false; } else { this.busyReload = false; } } maybeReload() { let latestIframeHeight = 0; if (this.frameRef.current) { latestIframeHeight = this.state.currentIframeHeight; } this.setState({ loading: true, latestIframeHeight, }); this.formRef.current.submit(); } /** * Resize frame to properly work with @media. */ maybeResizePreviews() { const contentWidth = $( '.editor-styles-wrapper, .edit-post-visual-editor__content-area' ) .eq(0) .width(); if (!contentWidth || !this.frameRef.current) { return; } const frame = this.frameRef.current; const $frame = $(frame); const parentWidth = $frame .closest('.visual-portfolio-gutenberg-preview') .width(); $frame.css({ width: contentWidth, }); if (frame.iFrameResizer) { frame.iFrameResizer.sendMessage({ name: 'resize', width: parentWidth, }); frame.iFrameResizer.resize(); } } /** * Update iframe height. * * @param newHeight */ updateIframeHeight(newHeight) { this.setState({ currentIframeHeight: newHeight, }); } /** * Prepare form input for POST variables. * * @param {string} name - option name. * @param {Mixed} val - option value. * * @return {JSX} - form control. */ printInput(name, val) { const params = { type: 'text', name, value: val, readOnly: true, }; if (typeof val === 'number') { params.type = 'number'; } else if (typeof val === 'boolean') { params.type = 'number'; params.value = val ? 1 : 0; } else if (typeof val === 'object' && val !== null) { return ( <> {Object.keys(val).map((i) => ( {this.printInput(`${name}[${i}]`, val[i])} ))} ); } else { params.value = params.value || ''; } return ; } render() { const { attributes, postType, postId } = this.props; const { loading, uniqueId, currentIframeHeight, latestIframeHeight } = this.state; const { id, content_source: contentSource } = attributes; return (
{contentSource === 'saved' ? ( ) : ( <> {Object.keys(attributes).map((k) => { const val = attributes[k]; return ( {this.printInput(`vp_${k}`, val)} ); })} )}