831 lines
24 KiB
JavaScript
831 lines
24 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
import type {Instance} from './ReactDOMHostConfig';
|
|
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals.js';
|
|
const {Dispatcher} = ReactDOMSharedInternals;
|
|
import {
|
|
validateUnmatchedLinkResourceProps,
|
|
validatePreloadResourceDifference,
|
|
validateHrefKeyedUpdatedProps,
|
|
validateStyleResourceDifference,
|
|
validateLinkPropsForStyleResource,
|
|
validateLinkPropsForPreloadResource,
|
|
validatePreloadArguments,
|
|
validatePreinitArguments,
|
|
} from '../shared/ReactDOMResourceValidation';
|
|
import {createElement, setInitialProperties} from './ReactDOMComponent';
|
|
import {HTML_NAMESPACE} from '../shared/DOMNamespaces';
|
|
|
|
// The resource types we support. currently they match the form for the as argument.
|
|
// In the future this may need to change, especially when modules / scripts are supported
|
|
type ResourceType = 'style' | 'font';
|
|
|
|
type PreloadProps = {
|
|
rel: 'preload',
|
|
as: ResourceType,
|
|
href: string,
|
|
[string]: mixed,
|
|
};
|
|
type PreloadResource = {
|
|
type: 'preload',
|
|
href: string,
|
|
ownerDocument: Document,
|
|
props: PreloadProps,
|
|
instance: Element,
|
|
};
|
|
|
|
type StyleProps = {
|
|
rel: 'stylesheet',
|
|
href: string,
|
|
'data-rprec': string,
|
|
[string]: mixed,
|
|
};
|
|
type StyleResource = {
|
|
type: 'style',
|
|
|
|
// Ref count for resource
|
|
count: number,
|
|
|
|
// Resource Descriptors
|
|
href: string,
|
|
precedence: string,
|
|
props: StyleProps,
|
|
|
|
// Related Resources
|
|
hint: ?PreloadResource,
|
|
|
|
// Insertion
|
|
preloaded: boolean,
|
|
loaded: boolean,
|
|
error: mixed,
|
|
instance: ?Element,
|
|
ownerDocument: Document,
|
|
};
|
|
|
|
type Props = {[string]: mixed};
|
|
|
|
type Resource = StyleResource | PreloadResource;
|
|
|
|
// Brief on purpose due to insertion by script when streaming late boundaries
|
|
// s = Status
|
|
// l = loaded
|
|
// e = errored
|
|
type StyleResourceLoadingState = Promise<mixed> & {s?: 'l' | 'e'};
|
|
|
|
// When rendering we set the currentDocument if one exists. we use this for Resources
|
|
// we encounter during render. If this is null and we are dispatching preloads and
|
|
// other calls on the ReactDOM module we look for the window global and get the document from there
|
|
let currentDocument: ?Document = null;
|
|
|
|
// It is valid to preload even when we aren't actively rendering. For cases where Float functions are
|
|
// called when there is no rendering we track the last used document. It is not safe to insert
|
|
// arbitrary resources into the lastCurrentDocument b/c it may not actually be the document
|
|
// that the resource is meant to apply too (for example stylesheets or scripts). This is only
|
|
// appropriate for resources that don't really have a strict tie to the document itself for example
|
|
// preloads
|
|
let lastCurrentDocument: ?Document = null;
|
|
|
|
let previousDispatcher = null;
|
|
export function prepareToRenderResources(ownerDocument: Document) {
|
|
currentDocument = lastCurrentDocument = ownerDocument;
|
|
previousDispatcher = Dispatcher.current;
|
|
Dispatcher.current = ReactDOMClientDispatcher;
|
|
}
|
|
|
|
export function cleanupAfterRenderResources() {
|
|
currentDocument = null;
|
|
Dispatcher.current = previousDispatcher;
|
|
previousDispatcher = null;
|
|
}
|
|
|
|
// We want this to be the default dispatcher on ReactDOMSharedInternals but we don't want to mutate
|
|
// internals in Module scope. Instead we export it and Internals will import it. There is already a cycle
|
|
// from Internals -> ReactDOM -> FloatClient -> Internals so this doesn't introduce a new one.
|
|
export const ReactDOMClientDispatcher = {preload, preinit};
|
|
|
|
// global maps of Resources
|
|
const preloadResources: Map<string, PreloadResource> = new Map();
|
|
const styleResources: Map<string, StyleResource> = new Map();
|
|
|
|
// Preloads are somewhat special. Even if we don't have the Document
|
|
// used by the root that is rendering a component trying to insert a preload
|
|
// we can still seed the file cache by doing the preload on any document we have
|
|
// access to. We prefer the currentDocument if it exists, we also prefer the
|
|
// lastCurrentDocument if that exists. As a fallback we will use the window.document
|
|
// if available.
|
|
function getDocumentForPreloads(): ?Document {
|
|
try {
|
|
return currentDocument || lastCurrentDocument || window.document;
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// --------------------------------------
|
|
// ReactDOM.Preload
|
|
// --------------------------------------
|
|
type PreloadAs = ResourceType;
|
|
type PreloadOptions = {as: PreloadAs, crossOrigin?: string};
|
|
function preload(href: string, options: PreloadOptions) {
|
|
if (__DEV__) {
|
|
validatePreloadArguments(href, options);
|
|
}
|
|
const ownerDocument = getDocumentForPreloads();
|
|
if (
|
|
typeof href === 'string' &&
|
|
href &&
|
|
typeof options === 'object' &&
|
|
options !== null &&
|
|
ownerDocument
|
|
) {
|
|
const as = options.as;
|
|
const resource = preloadResources.get(href);
|
|
if (resource) {
|
|
if (__DEV__) {
|
|
const originallyImplicit =
|
|
(resource: any)._dev_implicit_construction === true;
|
|
const latestProps = preloadPropsFromPreloadOptions(href, as, options);
|
|
validatePreloadResourceDifference(
|
|
resource.props,
|
|
originallyImplicit,
|
|
latestProps,
|
|
false,
|
|
);
|
|
}
|
|
} else {
|
|
const resourceProps = preloadPropsFromPreloadOptions(href, as, options);
|
|
createPreloadResource(ownerDocument, href, resourceProps);
|
|
}
|
|
}
|
|
}
|
|
|
|
function preloadPropsFromPreloadOptions(
|
|
href: string,
|
|
as: ResourceType,
|
|
options: PreloadOptions,
|
|
): PreloadProps {
|
|
return {
|
|
href,
|
|
rel: 'preload',
|
|
as,
|
|
crossOrigin: as === 'font' ? '' : options.crossOrigin,
|
|
};
|
|
}
|
|
|
|
// --------------------------------------
|
|
// ReactDOM.preinit
|
|
// --------------------------------------
|
|
|
|
type PreinitAs = 'style';
|
|
type PreinitOptions = {
|
|
as: PreinitAs,
|
|
crossOrigin?: string,
|
|
precedence?: string,
|
|
};
|
|
function preinit(href: string, options: PreinitOptions) {
|
|
if (__DEV__) {
|
|
validatePreinitArguments(href, options);
|
|
}
|
|
|
|
if (
|
|
typeof href === 'string' &&
|
|
href &&
|
|
typeof options === 'object' &&
|
|
options !== null
|
|
) {
|
|
const as = options.as;
|
|
if (!currentDocument) {
|
|
// We are going to emit a preload as a best effort fallback since this preinit
|
|
// was called outside of a render. Given the passive nature of this fallback
|
|
// we do not warn in dev when props disagree if there happens to already be a
|
|
// matching preload with this href
|
|
const preloadDocument = getDocumentForPreloads();
|
|
if (preloadDocument) {
|
|
const preloadResource = preloadResources.get(href);
|
|
if (!preloadResource) {
|
|
const preloadProps = preloadPropsFromPreinitOptions(
|
|
href,
|
|
as,
|
|
options,
|
|
);
|
|
createPreloadResource(preloadDocument, href, preloadProps);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
switch (as) {
|
|
case 'style': {
|
|
const precedence = options.precedence || 'default';
|
|
let resource = styleResources.get(href);
|
|
if (resource) {
|
|
if (__DEV__) {
|
|
const latestProps = stylePropsFromPreinitOptions(
|
|
href,
|
|
precedence,
|
|
options,
|
|
);
|
|
validateStyleResourceDifference(resource.props, latestProps);
|
|
}
|
|
} else {
|
|
const resourceProps = stylePropsFromPreinitOptions(
|
|
href,
|
|
precedence,
|
|
options,
|
|
);
|
|
resource = createStyleResource(
|
|
// $FlowFixMe[incompatible-call] found when upgrading Flow
|
|
currentDocument,
|
|
href,
|
|
precedence,
|
|
resourceProps,
|
|
);
|
|
}
|
|
acquireResource(resource);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function preloadPropsFromPreinitOptions(
|
|
href: string,
|
|
as: ResourceType,
|
|
options: PreinitOptions,
|
|
): PreloadProps {
|
|
return {
|
|
href,
|
|
rel: 'preload',
|
|
as,
|
|
crossOrigin: as === 'font' ? '' : options.crossOrigin,
|
|
};
|
|
}
|
|
|
|
function stylePropsFromPreinitOptions(
|
|
href: string,
|
|
precedence: string,
|
|
options: PreinitOptions,
|
|
): StyleProps {
|
|
return {
|
|
rel: 'stylesheet',
|
|
href,
|
|
'data-rprec': precedence,
|
|
crossOrigin: options.crossOrigin,
|
|
};
|
|
}
|
|
|
|
// --------------------------------------
|
|
// Resources from render
|
|
// --------------------------------------
|
|
|
|
type StyleQualifyingProps = {
|
|
rel: 'stylesheet',
|
|
href: string,
|
|
precedence: string,
|
|
[string]: mixed,
|
|
};
|
|
type PreloadQualifyingProps = {
|
|
rel: 'preload',
|
|
href: string,
|
|
as: ResourceType,
|
|
[string]: mixed,
|
|
};
|
|
|
|
// This function is called in begin work and we should always have a currentDocument set
|
|
export function getResource(
|
|
type: string,
|
|
pendingProps: Props,
|
|
currentProps: null | Props,
|
|
): null | Resource {
|
|
if (!currentDocument) {
|
|
throw new Error(
|
|
'"currentDocument" was expected to exist. This is a bug in React.',
|
|
);
|
|
}
|
|
switch (type) {
|
|
case 'link': {
|
|
const {rel} = pendingProps;
|
|
switch (rel) {
|
|
case 'stylesheet': {
|
|
let didWarn;
|
|
if (__DEV__) {
|
|
if (currentProps) {
|
|
didWarn = validateHrefKeyedUpdatedProps(
|
|
pendingProps,
|
|
currentProps,
|
|
);
|
|
}
|
|
if (!didWarn) {
|
|
didWarn = validateLinkPropsForStyleResource(pendingProps);
|
|
}
|
|
}
|
|
const {precedence, href} = pendingProps;
|
|
if (typeof href === 'string' && typeof precedence === 'string') {
|
|
// We've asserted all the specific types for StyleQualifyingProps
|
|
const styleRawProps: StyleQualifyingProps = (pendingProps: any);
|
|
|
|
// We construct or get an existing resource for the style itself and return it
|
|
let resource = styleResources.get(href);
|
|
if (resource) {
|
|
if (__DEV__) {
|
|
if (!didWarn) {
|
|
const latestProps = stylePropsFromRawProps(styleRawProps);
|
|
if ((resource: any)._dev_preload_props) {
|
|
adoptPreloadProps(
|
|
latestProps,
|
|
(resource: any)._dev_preload_props,
|
|
);
|
|
}
|
|
validateStyleResourceDifference(resource.props, latestProps);
|
|
}
|
|
}
|
|
} else {
|
|
const resourceProps = stylePropsFromRawProps(styleRawProps);
|
|
resource = createStyleResource(
|
|
// $FlowFixMe[incompatible-call] found when upgrading Flow
|
|
currentDocument,
|
|
href,
|
|
precedence,
|
|
resourceProps,
|
|
);
|
|
immediatelyPreloadStyleResource(resource);
|
|
}
|
|
return resource;
|
|
}
|
|
return null;
|
|
}
|
|
case 'preload': {
|
|
if (__DEV__) {
|
|
validateLinkPropsForPreloadResource(pendingProps);
|
|
}
|
|
const {href, as} = pendingProps;
|
|
if (typeof href === 'string' && isResourceAsType(as)) {
|
|
// We've asserted all the specific types for PreloadQualifyingProps
|
|
const preloadRawProps: PreloadQualifyingProps = (pendingProps: any);
|
|
let resource = preloadResources.get(href);
|
|
if (resource) {
|
|
if (__DEV__) {
|
|
const originallyImplicit =
|
|
(resource: any)._dev_implicit_construction === true;
|
|
const latestProps = preloadPropsFromRawProps(preloadRawProps);
|
|
validatePreloadResourceDifference(
|
|
resource.props,
|
|
originallyImplicit,
|
|
latestProps,
|
|
false,
|
|
);
|
|
}
|
|
} else {
|
|
const resourceProps = preloadPropsFromRawProps(preloadRawProps);
|
|
resource = createPreloadResource(
|
|
// $FlowFixMe[incompatible-call] found when upgrading Flow
|
|
currentDocument,
|
|
href,
|
|
resourceProps,
|
|
);
|
|
}
|
|
return resource;
|
|
}
|
|
return null;
|
|
}
|
|
default: {
|
|
if (__DEV__) {
|
|
validateUnmatchedLinkResourceProps(pendingProps, currentProps);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
default: {
|
|
throw new Error(
|
|
`getResource encountered a resource type it did not expect: "${type}". this is a bug in React.`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function preloadPropsFromRawProps(
|
|
rawBorrowedProps: PreloadQualifyingProps,
|
|
): PreloadProps {
|
|
return Object.assign({}, rawBorrowedProps);
|
|
}
|
|
|
|
function stylePropsFromRawProps(rawProps: StyleQualifyingProps): StyleProps {
|
|
const props: StyleProps = Object.assign({}, rawProps);
|
|
props['data-rprec'] = rawProps.precedence;
|
|
props.precedence = null;
|
|
|
|
return props;
|
|
}
|
|
|
|
// --------------------------------------
|
|
// Resource Reconciliation
|
|
// --------------------------------------
|
|
|
|
export function acquireResource(resource: Resource): Instance {
|
|
switch (resource.type) {
|
|
case 'style': {
|
|
return acquireStyleResource(resource);
|
|
}
|
|
case 'preload': {
|
|
return resource.instance;
|
|
}
|
|
default: {
|
|
throw new Error(
|
|
`acquireResource encountered a resource type it did not expect: "${resource.type}". this is a bug in React.`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function releaseResource(resource: Resource) {
|
|
switch (resource.type) {
|
|
case 'style': {
|
|
resource.count--;
|
|
}
|
|
}
|
|
}
|
|
|
|
function createResourceInstance(
|
|
type: string,
|
|
props: Object,
|
|
ownerDocument: Document,
|
|
): Instance {
|
|
const element = createElement(type, props, ownerDocument, HTML_NAMESPACE);
|
|
setInitialProperties(element, type, props);
|
|
return element;
|
|
}
|
|
|
|
function createStyleResource(
|
|
ownerDocument: Document,
|
|
href: string,
|
|
precedence: string,
|
|
props: StyleProps,
|
|
): StyleResource {
|
|
if (__DEV__) {
|
|
if (styleResources.has(href)) {
|
|
console.error(
|
|
'createStyleResource was called when a style Resource matching the same href already exists. This is a bug in React.',
|
|
);
|
|
}
|
|
}
|
|
|
|
const limitedEscapedHref = escapeSelectorAttributeValueInsideDoubleQuotes(
|
|
href,
|
|
);
|
|
const existingEl = ownerDocument.querySelector(
|
|
`link[rel="stylesheet"][href="${limitedEscapedHref}"]`,
|
|
);
|
|
const resource = {
|
|
type: 'style',
|
|
count: 0,
|
|
href,
|
|
precedence,
|
|
props,
|
|
hint: null,
|
|
preloaded: false,
|
|
loaded: false,
|
|
error: false,
|
|
ownerDocument,
|
|
instance: null,
|
|
};
|
|
styleResources.set(href, resource);
|
|
|
|
if (existingEl) {
|
|
// If we have an existing element in the DOM we don't need to preload this resource nor can we
|
|
// adopt props from any preload that might exist already for this resource. We do need to try
|
|
// to reify the Resource loading state the best we can.
|
|
const loadingState: ?StyleResourceLoadingState = (existingEl: any)._p;
|
|
if (loadingState) {
|
|
switch (loadingState.s) {
|
|
case 'l': {
|
|
resource.loaded = true;
|
|
break;
|
|
}
|
|
case 'e': {
|
|
resource.error = true;
|
|
break;
|
|
}
|
|
default: {
|
|
attachLoadListeners(existingEl, resource);
|
|
}
|
|
}
|
|
} else {
|
|
// This is unfortunately just an assumption. The rationale here is that stylesheets without
|
|
// a loading state must have been flushed in the shell and would have blocked until loading
|
|
// or error. we can't know afterwards which happened for all types of stylesheets (cross origin)
|
|
// for instance) and the techniques for determining if a sheet has loaded that we do have still
|
|
// fail if the sheet loaded zero rules. At the moment we are going to just opt to assume the
|
|
// sheet is loaded if it was flushed in the shell
|
|
resource.loaded = true;
|
|
}
|
|
} else {
|
|
const hint = preloadResources.get(href);
|
|
if (hint) {
|
|
// $FlowFixMe[incompatible-type]: found when upgrading Flow
|
|
resource.hint = hint;
|
|
// If a preload for this style Resource already exists there are certain props we want to adopt
|
|
// on the style Resource, primarily focussed on making sure the style network pathways utilize
|
|
// the preload pathways. For instance if you have diffreent crossOrigin attributes for a preload
|
|
// and a stylesheet the stylesheet will make a new request even if the preload had already loaded
|
|
const preloadProps = hint.props;
|
|
adoptPreloadProps(resource.props, hint.props);
|
|
if (__DEV__) {
|
|
(resource: any)._dev_preload_props = preloadProps;
|
|
}
|
|
}
|
|
}
|
|
|
|
return resource;
|
|
}
|
|
|
|
function adoptPreloadProps(
|
|
styleProps: StyleProps,
|
|
preloadProps: PreloadProps,
|
|
): void {
|
|
if (styleProps.crossOrigin == null)
|
|
styleProps.crossOrigin = preloadProps.crossOrigin;
|
|
if (styleProps.referrerPolicy == null)
|
|
styleProps.referrerPolicy = preloadProps.referrerPolicy;
|
|
if (styleProps.media == null) styleProps.media = preloadProps.media;
|
|
if (styleProps.title == null) styleProps.title = preloadProps.title;
|
|
}
|
|
|
|
function immediatelyPreloadStyleResource(resource: StyleResource) {
|
|
// This function must be called synchronously after creating a styleResource otherwise it may
|
|
// violate assumptions around the existence of a preload. The reason it is extracted out is we
|
|
// don't always want to preload a style, in particular when we are going to synchronously insert
|
|
// that style. We confirm the style resource has no preload already and then construct it. If
|
|
// we wait and call this later it is possible a preload will already exist for this href
|
|
if (resource.loaded === false && resource.hint === null) {
|
|
const {href, props} = resource;
|
|
const preloadProps = preloadPropsFromStyleProps(props);
|
|
resource.hint = createPreloadResource(
|
|
resource.ownerDocument,
|
|
href,
|
|
preloadProps,
|
|
);
|
|
}
|
|
}
|
|
|
|
function preloadPropsFromStyleProps(props: StyleProps): PreloadProps {
|
|
return {
|
|
rel: 'preload',
|
|
as: 'style',
|
|
href: props.href,
|
|
crossOrigin: props.crossOrigin,
|
|
integrity: props.integrity,
|
|
media: props.media,
|
|
hrefLang: props.hrefLang,
|
|
referrerPolicy: props.referrerPolicy,
|
|
};
|
|
}
|
|
|
|
function createPreloadResource(
|
|
ownerDocument: Document,
|
|
href: string,
|
|
props: PreloadProps,
|
|
): PreloadResource {
|
|
const limitedEscapedHref = escapeSelectorAttributeValueInsideDoubleQuotes(
|
|
href,
|
|
);
|
|
let element = ownerDocument.querySelector(
|
|
`link[rel="preload"][href="${limitedEscapedHref}"]`,
|
|
);
|
|
if (!element) {
|
|
element = createResourceInstance('link', props, ownerDocument);
|
|
insertPreloadInstance(element, ownerDocument);
|
|
}
|
|
return {
|
|
type: 'preload',
|
|
href: href,
|
|
ownerDocument,
|
|
props,
|
|
instance: element,
|
|
};
|
|
}
|
|
|
|
function acquireStyleResource(resource: StyleResource): Instance {
|
|
if (!resource.instance) {
|
|
const {props, ownerDocument, precedence} = resource;
|
|
const limitedEscapedHref = escapeSelectorAttributeValueInsideDoubleQuotes(
|
|
props.href,
|
|
);
|
|
const existingEl = ownerDocument.querySelector(
|
|
`link[rel="stylesheet"][data-rprec][href="${limitedEscapedHref}"]`,
|
|
);
|
|
if (existingEl) {
|
|
resource.instance = existingEl;
|
|
resource.preloaded = true;
|
|
const loadingState: ?StyleResourceLoadingState = (existingEl: any)._p;
|
|
if (loadingState) {
|
|
// if an existingEl is found there should always be a loadingState because if
|
|
// the resource was flushed in the head it should have already been found when
|
|
// the resource was first created. Still defensively we gate this
|
|
switch (loadingState.s) {
|
|
case 'l': {
|
|
resource.loaded = true;
|
|
resource.error = false;
|
|
break;
|
|
}
|
|
case 'e': {
|
|
resource.error = true;
|
|
break;
|
|
}
|
|
default: {
|
|
attachLoadListeners(existingEl, resource);
|
|
}
|
|
}
|
|
} else {
|
|
resource.loaded = true;
|
|
}
|
|
} else {
|
|
const instance = createResourceInstance(
|
|
'link',
|
|
resource.props,
|
|
ownerDocument,
|
|
);
|
|
|
|
attachLoadListeners(instance, resource);
|
|
insertStyleInstance(instance, precedence, ownerDocument);
|
|
resource.instance = instance;
|
|
}
|
|
}
|
|
resource.count++;
|
|
// $FlowFixMe[incompatible-return] found when upgrading Flow
|
|
return resource.instance;
|
|
}
|
|
|
|
function attachLoadListeners(instance: Instance, resource: StyleResource) {
|
|
const listeners = {};
|
|
listeners.load = onResourceLoad.bind(
|
|
null,
|
|
instance,
|
|
resource,
|
|
listeners,
|
|
loadAndErrorEventListenerOptions,
|
|
);
|
|
listeners.error = onResourceError.bind(
|
|
null,
|
|
instance,
|
|
resource,
|
|
listeners,
|
|
loadAndErrorEventListenerOptions,
|
|
);
|
|
|
|
instance.addEventListener(
|
|
'load',
|
|
listeners.load,
|
|
loadAndErrorEventListenerOptions,
|
|
);
|
|
instance.addEventListener(
|
|
'error',
|
|
listeners.error,
|
|
loadAndErrorEventListenerOptions,
|
|
);
|
|
}
|
|
|
|
const loadAndErrorEventListenerOptions = {
|
|
passive: true,
|
|
};
|
|
|
|
function onResourceLoad(
|
|
instance: Instance,
|
|
resource: StyleResource,
|
|
listeners: {[string]: () => mixed},
|
|
listenerOptions: typeof loadAndErrorEventListenerOptions,
|
|
) {
|
|
resource.loaded = true;
|
|
resource.error = false;
|
|
for (const event in listeners) {
|
|
instance.removeEventListener(event, listeners[event], listenerOptions);
|
|
}
|
|
}
|
|
|
|
function onResourceError(
|
|
instance: Instance,
|
|
resource: StyleResource,
|
|
listeners: {[string]: () => mixed},
|
|
listenerOptions: typeof loadAndErrorEventListenerOptions,
|
|
) {
|
|
resource.loaded = false;
|
|
resource.error = true;
|
|
for (const event in listeners) {
|
|
instance.removeEventListener(event, listeners[event], listenerOptions);
|
|
}
|
|
}
|
|
|
|
function insertStyleInstance(
|
|
instance: Instance,
|
|
precedence: string,
|
|
ownerDocument: Document,
|
|
): void {
|
|
const nodes = ownerDocument.querySelectorAll(
|
|
'link[rel="stylesheet"][data-rprec]',
|
|
);
|
|
const last = nodes.length ? nodes[nodes.length - 1] : null;
|
|
let prior = last;
|
|
for (let i = 0; i < nodes.length; i++) {
|
|
const node = nodes[i];
|
|
const nodePrecedence = node.dataset.rprec;
|
|
if (nodePrecedence === precedence) {
|
|
prior = node;
|
|
} else if (prior !== last) {
|
|
break;
|
|
}
|
|
}
|
|
if (prior) {
|
|
// We get the prior from the document so we know it is in the tree.
|
|
// We also know that links can't be the topmost Node so the parentNode
|
|
// must exist.
|
|
((prior.parentNode: any): Node).insertBefore(instance, prior.nextSibling);
|
|
} else {
|
|
// @TODO call getRootNode on root.container. if it is a Document, insert into head
|
|
// if it is a ShadowRoot insert it into the root node.
|
|
const parent = ownerDocument.head;
|
|
if (parent) {
|
|
parent.insertBefore(instance, parent.firstChild);
|
|
} else {
|
|
throw new Error(
|
|
'While attempting to insert a Resource, React expected the Document to contain' +
|
|
' a head element but it was not found.',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function insertPreloadInstance(
|
|
instance: Instance,
|
|
ownerDocument: Document,
|
|
): void {
|
|
if (!ownerDocument.contains(instance)) {
|
|
const parent = ownerDocument.head;
|
|
if (parent) {
|
|
parent.appendChild(instance);
|
|
} else {
|
|
throw new Error(
|
|
'While attempting to insert a Resource, React expected the Document to contain' +
|
|
' a head element but it was not found.',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function isHostResourceType(type: string, props: Props): boolean {
|
|
switch (type) {
|
|
case 'link': {
|
|
switch (props.rel) {
|
|
case 'stylesheet': {
|
|
if (__DEV__) {
|
|
validateLinkPropsForStyleResource(props);
|
|
}
|
|
const {href, precedence, onLoad, onError, disabled} = props;
|
|
return (
|
|
typeof href === 'string' &&
|
|
typeof precedence === 'string' &&
|
|
!onLoad &&
|
|
!onError &&
|
|
disabled == null
|
|
);
|
|
}
|
|
case 'preload': {
|
|
if (__DEV__) {
|
|
validateLinkPropsForStyleResource(props);
|
|
}
|
|
const {href, as, onLoad, onError} = props;
|
|
return (
|
|
!onLoad &&
|
|
!onError &&
|
|
typeof href === 'string' &&
|
|
isResourceAsType(as)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function isResourceAsType(as: mixed): boolean {
|
|
return as === 'style' || as === 'font';
|
|
}
|
|
|
|
// When passing user input into querySelector(All) the embedded string must not alter
|
|
// the semantics of the query. This escape function is safe to use when we know the
|
|
// provided value is going to be wrapped in double quotes as part of an attribute selector
|
|
// Do not use it anywhere else
|
|
// we escape double quotes and backslashes
|
|
const escapeSelectorAttributeValueInsideDoubleQuotesRegex = /[\n\"\\]/g;
|
|
function escapeSelectorAttributeValueInsideDoubleQuotes(value: string): string {
|
|
return value.replace(
|
|
escapeSelectorAttributeValueInsideDoubleQuotesRegex,
|
|
ch => '\\' + ch.charCodeAt(0).toString(16),
|
|
);
|
|
}
|