/** * External dependencies */ import debounce from 'lodash/debounce'; import throttle from 'lodash/throttle'; import type { CssWorkingContentValue } from '@nab/types'; /** * Internal dependencies */ import './style.scss'; import { initContentValues } from '../utils/init-content-values'; import { initCssSelectorFinder } from '../utils/init-css-selector-finder'; import { markTestPreviewAsActive } from '../utils/mark-test-preview-as-active'; import { onCssChanged } from '../utils/on-css-changed'; import { onTogglePreview } from '../utils/on-toggle-preview'; import { onContentChanged } from '../utils/on-content-changed'; import { syncStatesEffect } from '../utils/sync-states-effect'; import { domReady } from '../../../../../../../assets/src/public/utils/helpers/dom-ready'; export function initCssPreviewer( enabled: boolean ): void { markTestPreviewAsActive( enabled ); domReady( () => { initContentValues(); syncStatesEffect(); initCssSelectorFinder(); if ( ! enabled ) { return; } addPageUpdaters(); } ); } // ======= // HELPERS // ======= function addPageUpdaters() { onTogglePreview( togglePreviewChanges ); onCssChanged( ( { mode, css } ) => 'css-editor' === mode ? previewCssDebounced( css ) : previewCss( css ) ); onContentChanged( throttle( ( changes: ReadonlyArray< CssWorkingContentValue >, prevChanges: ReadonlyArray< CssWorkingContentValue > ) => { prevChanges.forEach( restoreOriginalContent ); changes.forEach( applyContentChange ); requestHighlightersUpdate(); }, 500 ) ); } const previewCss = ( css: string ) => { const style = document.getElementById( 'nab-css-style' ); if ( style ) { style.innerHTML = css; } requestHighlightersUpdate(); }; const previewCssDebounced = debounce( previewCss, 500 ); const togglePreviewChanges = ( isActive: boolean, css: string, changes: ReadonlyArray< CssWorkingContentValue > ) => { const style = document.getElementById( 'nab-css-style' ); if ( style ) { style.innerHTML = isActive ? css : ''; } changes.forEach( isActive ? applyContentChange : restoreOriginalContent ); }; function applyContentChange( change: CssWorkingContentValue ) { if ( 'active' !== change.status ) { return; } try { const el = document.querySelector( change.selector ); if ( ! el ) { return; } switch ( change.type ) { case 'element': { el.innerHTML = hasContent( change.html ) ? `${ change.html }.` : change.originalHtml; break; } case 'image': { if ( ! isImage( el ) ) { return; } el.src = change.src || change.originalSrc; el.alt = change.alt || change.originalAlt; el.sizes = ''; el.srcset = ''; checkImage( change.src, requestHighlightersUpdateThrottled ); break; } } } catch ( _ ) {} } function restoreOriginalContent( change: CssWorkingContentValue ) { try { const el = document.querySelector( change.selector ); if ( ! el ) { return; } switch ( change.type ) { case 'element': { el.innerHTML = change.originalHtml; break; } case 'image': { if ( ! isImage( el ) ) { return; } el.src = change.originalSrc; el.alt = change.originalAlt; break; } } } catch ( _ ) {} } function requestHighlightersUpdate() { document.dispatchEvent( new CustomEvent( 'nab/css-selector-finder/refresh-highlighters' ) ); } const requestHighlightersUpdateThrottled = throttle( requestHighlightersUpdate, 250 ); const checkedImageUrls = [] as string[]; function checkImage( url: string, callback: () => void ) { if ( checkedImageUrls.includes( url ) ) { callback(); return; } let img: HTMLImageElement | null = new Image(); img.onload = function () { callback(); cleanup(); }; img.onerror = function () { callback(); cleanup(); }; img.src = url; function cleanup() { if ( ! img ) { return; } checkedImageUrls.push( url ); img.onload = null; img.onerror = null; img = null; } } //checkImage() const isImage = ( el: Element ): el is HTMLImageElement => 'IMG' === el.nodeName; const AUX = document.createElement( 'div' ); const hasContent = ( html: string ): boolean => { AUX.innerHTML = html; return !! AUX.textContent?.trim(); };