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
The 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
* 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;