All Downloads are FREE. Search and download functionalities are using the official Maven repository.

META-INF.resources.screen.HtmlScreen.js Maven / Gradle / Ivy

There is a newer version: 5.0.55
Show newest version
/**
 * SPDX-FileCopyrightText: (c) 2000 Liferay, Inc. https://liferay.com
 * SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06
 */

import {buildFragment, fetch, runScriptsInElement} from 'frontend-js-web';

import Surface from '../surface/Surface';
import {
	clearNodeAttributes,
	copyNodeAttributes,
	getUid,
	runStylesInElement,
} from '../util/utils';
import RequestScreen from './RequestScreen';

class HtmlScreen extends RequestScreen {

	/**
	 * Screen class that perform a request and extracts surface contents from
	 * the response content.
	 */
	constructor() {
		super();

		/**
		 * Holds the meta selector. Relevant to extract meta tags
		 * elements from request fragments to use as the screen.
		 * @type {!string}
		 * @default meta
		 * @protected
		 */
		this.metaTagsSelector = 'meta';

		/**
		 * Holds the title selector. Relevant to extract the </code>
		 * element from request fragments to use as the screen title.
		 * @type {!string}
		 * @default title
		 * @protected
		 */
		this.titleSelector = 'title';
	}

	/**
	 * @inheritDoc
	 */
	activate() {
		super.activate();
		this.releaseVirtualDocument();
		this.pendingStyles = null;
	}

	/**
	 * Allocates virtual document for content. After allocated virtual document
	 * can be accessed by <code>this.virtualDocument</code>.
	 * @param {!string} htmlString
	 */
	allocateVirtualDocumentForContent(htmlString) {
		if (!this.virtualDocument) {
			this.virtualDocument = document.createElement('html');
		}

		this.copyNodeAttributesFromContent_(htmlString, this.virtualDocument);

		this.virtualDocument.innerHTML = htmlString;
	}

	/**
	 * Customizes logic to append styles into document. Relevant to when
	 * tracking a style by id make sure to re-positions the new style in the
	 * same dom order.
	 * @param {Element} newStyle
	 */
	appendStyleIntoDocument_(newStyle) {
		const isTemporaryStyle = newStyle.matches(
			HtmlScreen.selectors.stylesTemporary
		);
		if (isTemporaryStyle) {
			this.pendingStyles.push(newStyle);
		}
		if (newStyle.id) {
			const styleInDoc = document.getElementById(newStyle.id);
			if (styleInDoc) {
				styleInDoc.parentNode.insertBefore(
					newStyle,
					styleInDoc.nextSibling
				);

				return;
			}
		}
		document.head.appendChild(newStyle);
	}

	/**
	 * If body is used as surface forces the requested documents to have same id
	 * of the initial page.
	 */
	assertSameBodyIdInVirtualDocument() {
		const bodySurface = this.virtualDocument.querySelector('body');
		if (!document.body.id) {
			document.body.id = 'senna_surface_' + getUid();
		}
		if (bodySurface) {
			bodySurface.id = document.body.id;
		}
	}

	/**
	 * Copies attributes from the <html> tag of content to the given node.
	 */
	copyNodeAttributesFromContent_(content, node) {
		content = content.replace(/[<]\s*html/gi, '<senna');
		content = content.replace(/\/html\s*>/gi, '/senna>');

		node.innerHTML = content;

		const placeholder = node.querySelector('senna');

		if (placeholder) {
			clearNodeAttributes(node);
			copyNodeAttributes(placeholder, node);
		}
	}

	/**
	 * @Override
	 */
	disposeInternal() {
		this.disposePendingStyles();
		super.disposeInternal();
	}

	/**
	 * Disposes pending styles if screen get disposed prior to its loading.
	 */
	disposePendingStyles() {
		if (this.pendingStyles) {
			this.pendingStyles.forEach((element) => element.remove());
		}
	}

	/**
	 * @Override
	 */
	evaluateScripts(surfaces) {
		const evaluateTrackedScripts = this.evaluateTrackedResources_(
			runScriptsInElement,
			HtmlScreen.selectors.scripts,
			HtmlScreen.selectors.scriptsTemporary,
			HtmlScreen.selectors.scriptsPermanent
		);

		return evaluateTrackedScripts.then(() =>
			super.evaluateScripts(surfaces)
		);
	}

	/**
	 * @Override
	 */
	preloadStyles(surfaces) {
		const tracked = this.virtualQuerySelectorAll_(
			HtmlScreen.selectors.styles
		);

		return Promise.all(
			tracked.map((resource) => {
				const resourceKey = this.getResourceKey_(resource);

				if (
					!HtmlScreen.permanentResourcesInDoc[resourceKey] &&
					resource.href
				) {
					return fetch(resource.href).then((response) =>
						response.ok ? response.text() : Promise.resolve()
					);
				}

				return Promise.resolve();
			})
		).then(() => super.preloadStyles(surfaces));
	}

	/**
	 * @Override
	 */
	evaluateStyles(surfaces) {
		this.pendingStyles = [];
		const evaluateTrackedStyles = this.evaluateTrackedResources_(
			runStylesInElement,
			HtmlScreen.selectors.styles,
			HtmlScreen.selectors.stylesTemporary,
			HtmlScreen.selectors.stylesPermanent,
			this.appendStyleIntoDocument_.bind(this)
		);

		return evaluateTrackedStyles.then(() => super.evaluateStyles(surfaces));
	}

	/**
	 * Allows a screen to evaluate the favicon style before the screen becomes visible.
	 * @return {Promise}
	 */
	evaluateFavicon_() {
		const resourcesInVirtual = this.virtualQuerySelectorAll_(
			HtmlScreen.selectors.favicon
		);
		const resourcesInDocument = this.querySelectorAll_(
			HtmlScreen.selectors.favicon
		);

		return new Promise((resolve) => {
			resourcesInDocument.forEach((element) => element.remove());
			this.runFaviconInElement_(resourcesInVirtual).then(() => resolve());
		});
	}

	/**
	 * Evaluates tracked resources inside incoming fragment and remove existing
	 * temporary resources.
	 * @param {?function()} appendFn Function to append the node into document.
	 * @param {!string} selector Selector used to find resources to track.
	 * @param {!string} selectorTemporary Selector used to find temporary
	 *     resources to track.
	 * @param {!string} selectorPermanent Selector used to find permanent
	 *     resources to track.
	 * @param {!function} opt_appendResourceFn Optional function used to
	 *     evaluate fragment containing resources.
	 * @return {Promise} Deferred that waits resources evaluation to
	 *     complete.
	 * @private
	 */
	evaluateTrackedResources_(
		evaluatorFn,
		selector,
		selectorTemporary,
		selectorPermanent,
		opt_appendResourceFn
	) {
		const tracked = this.virtualQuerySelectorAll_(selector);
		const temporariesInDoc = this.querySelectorAll_(selectorTemporary);
		const permanentsInDoc = this.querySelectorAll_(selectorPermanent);

		// Adds permanent resources in document to cache.

		permanentsInDoc.forEach((resource) => {
			const resourceKey = this.getResourceKey_(resource);
			if (resourceKey) {
				HtmlScreen.permanentResourcesInDoc[resourceKey] = true;
			}
		});

		const frag = buildFragment();
		tracked.forEach((resource) => {
			const resourceKey = this.getResourceKey_(resource);

			// Do not load permanent resources if already in document.

			if (!HtmlScreen.permanentResourcesInDoc[resourceKey]) {
				frag.appendChild(resource);
			}

			// If resource has key and is permanent add to cache.

			if (resourceKey && resource.matches(selectorPermanent)) {
				HtmlScreen.permanentResourcesInDoc[resourceKey] = true;
			}
		});

		return new Promise((resolve) => {
			evaluatorFn(
				frag,
				() => {
					temporariesInDoc.forEach((element) => element.remove());
					resolve();
				},
				opt_appendResourceFn
			);
		});
	}

	/**
	 * @Override
	 */
	flip(surfaces) {
		return super.flip(surfaces).then(() => {
			clearNodeAttributes(document.documentElement);
			copyNodeAttributes(this.virtualDocument, document.documentElement);
			this.evaluateFavicon_();
			this.updateMetaTags_();
		});
	}

	updateMetaTags_() {
		const currentMetaNodes = this.querySelectorAll_('meta');
		const metasFromVirtualDocument = this.metas;
		if (currentMetaNodes) {
			currentMetaNodes.forEach((element) => element.remove());
			if (metasFromVirtualDocument) {
				metasFromVirtualDocument.forEach((meta) =>
					document.head.appendChild(meta)
				);
			}
		}
	}

	/**
	 * Extracts a key to identify the resource based on its attributes.
	 * @param {Element} resource
	 * @return {string} Extracted key based on resource attributes in order of
	 *     preference: id, href, src.
	 */
	getResourceKey_(resource) {
		return resource.id || resource.href || resource.src || '';
	}

	/**
	 * @inheritDoc
	 */
	getSurfaceContent(surfaceId) {
		const surface = this.virtualDocument.querySelector('#' + surfaceId);
		if (surface) {
			const defaultChild = surface.querySelector(
				'#' + surfaceId + '-' + Surface.DEFAULT
			);
			if (defaultChild) {
				return defaultChild.innerHTML;
			}

			return surface.innerHTML; // If default content not found, use surface content
		}
	}

	/**
	 * Gets the title selector.
	 * @return {!string}
	 */
	getTitleSelector() {
		return this.titleSelector;
	}

	/**
	 * @inheritDoc
	 */
	load(path) {
		return super.load(path).then((content) => {
			this.allocateVirtualDocumentForContent(content);
			this.resolveTitleFromVirtualDocument();
			this.resolveMetaTagsFromVirtualDocument();
			this.assertSameBodyIdInVirtualDocument();

			return content;
		});
	}

	/**
	 * Adds the favicon elements to the document.
	 * @param {!Array<Element>} elements
	 * @private
	 * @return {Promise}
	 */
	runFaviconInElement_(elements) {
		return new Promise((resolve) => {
			elements.forEach((element) => {
				document.head.appendChild(element);
			});
			resolve();
		});
	}

	/**
	 * Queries elements from virtual document and returns an array of elements.
	 * @param {!string} selector
	 * @return {array.<Element>}
	 */
	virtualQuerySelectorAll_(selector) {
		return Array.prototype.slice.call(
			this.virtualDocument.querySelectorAll(selector)
		);
	}

	/**
	 * Queries elements from document and returns an array of elements.
	 * @param {!string} selector
	 * @return {array.<Element>}
	 */
	querySelectorAll_(selector) {
		return Array.prototype.slice.call(document.querySelectorAll(selector));
	}

	/**
	 * Releases virtual document allocated for content.
	 */
	releaseVirtualDocument() {
		this.virtualDocument = null;
	}

	/**
	 * Resolves title from allocated virtual document.
	 */
	resolveTitleFromVirtualDocument() {
		const title = this.virtualDocument.querySelector(this.titleSelector);
		if (title) {
			this.setTitle(title.textContent.trim());
		}
	}

	resolveMetaTagsFromVirtualDocument() {
		const metas = this.virtualQuerySelectorAll_(this.metaTagsSelector);
		if (metas) {
			this.setMetas(metas);
		}
	}

	/**
	 * Sets the title selector.
	 * @param {!string} titleSelector
	 */
	setTitleSelector(titleSelector) {
		this.titleSelector = titleSelector;
	}
}

/**
 * Helper selector for ignore favicon when exist data-senna-track.
 */
const ignoreFavicon =
	':not([rel="Shortcut Icon"]):not([rel="shortcut icon"]):not([rel="icon"]):not([href$="favicon.icon"])';

/**
 * Helper selectors for tracking resources.
 * @type {object}
 * @protected
 * @static
 */
HtmlScreen.selectors = {
	favicon:
		'link[rel="Shortcut Icon"],link[rel="shortcut icon"],link[rel="icon"],link[href$="favicon.icon"]',
	scripts: 'script[data-senna-track]',
	scriptsPermanent: 'script[data-senna-track="permanent"]',
	scriptsTemporary: 'script[data-senna-track="temporary"]',
	styles: `style[data-senna-track],link[data-senna-track]${ignoreFavicon}`,
	stylesPermanent: `style[data-senna-track="permanent"],link[data-senna-track="permanent"]${ignoreFavicon}`,
	stylesTemporary: `style[data-senna-track="temporary"],link[data-senna-track="temporary"]${ignoreFavicon}`,
};

/**
 * Caches permanent resource keys.
 * @type {object}
 * @protected
 * @static
 */
HtmlScreen.permanentResourcesInDoc = {};

export default HtmlScreen;
</code></pre>    <br/>
    <br/>
    <!--<div id="right-banner">-->
            <!--</div>-->
    <!--<div id="left-banner">-->
            <!--</div>-->
<div class='clear'></div>
</main>
</div>
<br/><br/>
    <div class="align-center">© 2015 - 2024 <a href="/legal-notice.php">Weber Informatics LLC</a> | <a href="/data-protection.php">Privacy Policy</a></div>
<br/><br/><br/><br/><br/><br/>
</body>
</html>