META-INF.resources.screen.HtmlScreen.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of com.liferay.frontend.js.spa.web
Show all versions of com.liferay.frontend.js.spa.web
Liferay Frontend JS SPA Web
/**
* 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
* 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 this.virtualDocument
.
* @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 tag of content to the given node.
*/
copyNodeAttributesFromContent_(content, node) {
content = content.replace(/[<]\s*html/gi, '/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} 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.}
*/
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.}
*/
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;