Skip to content

[Fiber] Support SVG #8475

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 183 additions & 1 deletion src/renderers/dom/fiber/ReactDOMFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import type { Fiber } from 'ReactFiber';
import type { ReactNodeList } from 'ReactTypes';

var DOMNamespaces = require('DOMNamespaces');
var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
var ReactControlledComponent = require('ReactControlledComponent');
var ReactDOMComponentTree = require('ReactDOMComponentTree');
Expand All @@ -37,6 +38,10 @@ var {
updateProperties,
} = ReactDOMFiberComponent;
var { precacheFiberNode } = ReactDOMComponentTree;
var {
svg: SVG_NAMESPACE,
mathml: MATH_NAMESPACE,
} = DOMNamespaces;

const DOCUMENT_NODE = 9;

Expand All @@ -58,8 +63,184 @@ type TextInstance = Text;
let eventsEnabled : ?boolean = null;
let selectionInformation : ?mixed = null;

// The next few variables are mutable state that changes as we perform work.
// We could have replaced them by a single stack of namespaces but we want
// to avoid it since this is a very hot path.
let currentNamespaceURI : null | SVG_NAMESPACE | MATH_NAMESPACE = null;
// How many <foreignObject>s we have entered so far.
// We increment and decrement it when pushing and popping <foreignObject>.
// We use this counter as the current index for accessing the array below.
let foreignObjectDepth : number = 0;
// How many <svg>s have we entered so far.
// We increment or decrement the last array item when pushing and popping <svg>.
// A new counter is appended to the end whenever we enter a <foreignObject>.
let svgDepthByForeignObjectDepth : Array<number> | null = null;
// For example:
// <svg><foreignObject><svg><svg><svg><foreignObject><svg>
// ^^^ ^^^^^^^^^^^^^ ^^^^^^^^^^^^^ ^^^^^^^^^^^^^ ^^^]
// [ 1 , 3 , 1 ]

// The mutable state above becomes irrelevant whenever we push a portal
// because a portal represents a different DOM tree. We store a snapshot
// of this state and restore it when the portal is popped.
type PortalState = {
currentNamespaceURI: string | null,
foreignObjectDepth: number,
svgDepthByForeignObjectDepth: Array<number> | null,
};
let portalState : Array<PortalState> | null = null;
let portalStateIndex : number = -1;

function getIntrinsicNamespaceURI(type : string) {
switch (type) {
case 'svg':
return SVG_NAMESPACE;
case 'math':
return MATH_NAMESPACE;
default:
return null;
}
}

var DOMRenderer = ReactFiberReconciler({

pushHostContext(type : string) {
switch (type) {
case 'svg':
// Lazily initialize the array for the first time.
if (!svgDepthByForeignObjectDepth) {
svgDepthByForeignObjectDepth = [0];
}
if (currentNamespaceURI == null) {
// We are entering an <svg> for the first time.
currentNamespaceURI = SVG_NAMESPACE;
svgDepthByForeignObjectDepth[foreignObjectDepth] = 1;
} else if (currentNamespaceURI === SVG_NAMESPACE) {
// We are entering an <svg> inside <svg>.
// We record this fact so that when we pop this <svg>, we stay in the
// SVG mode instead of switching to HTML mode.
svgDepthByForeignObjectDepth[foreignObjectDepth]++;
}
break;
case 'math':
if (currentNamespaceURI == null) {
currentNamespaceURI = MATH_NAMESPACE;
}
break;
case 'foreignObject':
if (currentNamespaceURI === SVG_NAMESPACE) {
currentNamespaceURI = null;
// We are in HTML mode again, so current <svg> nesting counter needs
// to be reset. However we still need to remember its value when we
// pop this <foreignObject>. So instead of resetting the counter, we
// advance the pointer, and start a new independent <svg> depth
// counter at the next array index.
foreignObjectDepth++;
if (!svgDepthByForeignObjectDepth) {
throw new Error('Expected to already be in SVG mode.');
}
svgDepthByForeignObjectDepth[foreignObjectDepth] = 0;
}
break;
}
},

popHostContext(type : string) {
switch (type) {
case 'svg':
if (currentNamespaceURI === SVG_NAMESPACE) {
if (!svgDepthByForeignObjectDepth) {
throw new Error('Expected to already be in SVG mode.');
}
if (svgDepthByForeignObjectDepth[foreignObjectDepth] === 1) {
// We exited all nested <svg> nodes.
// We can switch to HTML mode.
currentNamespaceURI = null;
} else {
// There is still an <svg> above so we stay in SVG mode.
// We decrease the counter so that next time we leave <svg>
// we will be able to switch to HTML mode.
svgDepthByForeignObjectDepth[foreignObjectDepth]--;
}
}
break;
case 'math':
if (currentNamespaceURI === MATH_NAMESPACE) {
currentNamespaceURI = null;
}
break;
case 'foreignObject':
if (currentNamespaceURI == null) {
// We are exiting <foreignObject> and nested <svg>s may exist above.
// Switch to the previous <svg> depth counter by decreasing the index.
foreignObjectDepth--;
currentNamespaceURI = SVG_NAMESPACE;
}
break;
}
},

pushHostPortal() : void {
// We optimize for the simple case: portals usually exist outside of SVG.
const canBailOutOfTrackingPortalState = (
// If we're in HTML mode, we don't need to save this.
currentNamespaceURI == null &&
// If state was ever saved before, we can't bail out because we wouldn't
// be able to tell whether to restore it or not next time we pop a portal.
portalStateIndex === -1
);
if (canBailOutOfTrackingPortalState) {
return;
}
// We are going to save the state before entering the portal.
portalStateIndex++;
// We are inside <svg> (or deeper) and need to store that before
// jumping into a portal elsewhere in the tree.
if (!portalState) {
portalState = [];
}
if (!portalState[portalStateIndex]) {
// Lazily allocate a single object for every portal nesting level.
portalState[portalStateIndex] = {
currentNamespaceURI,
foreignObjectDepth,
svgDepthByForeignObjectDepth,
};
} else {
// If we already have state on the stack, just mutate it.
const mutableState = portalState[portalStateIndex];
mutableState.currentNamespaceURI = currentNamespaceURI;
mutableState.foreignObjectDepth = foreignObjectDepth;
mutableState.svgDepthByForeignObjectDepth = svgDepthByForeignObjectDepth;
}
// Reset the host context we're working with.
// TODO: what if the portal is inside <svg> element itself?
// We currently don't handle this case.
currentNamespaceURI = null;
foreignObjectDepth = 0;
svgDepthByForeignObjectDepth = null;
},

popHostPortal() {
if (portalStateIndex === -1 || portalState == null) {
// There is nothing interesting to restore.
return;
}
// Restore to the state before we entered that portal.
const savedState = portalState[portalStateIndex];
currentNamespaceURI = savedState.currentNamespaceURI;
foreignObjectDepth = savedState.foreignObjectDepth;
svgDepthByForeignObjectDepth = savedState.svgDepthByForeignObjectDepth;
// We have restored the state.
portalStateIndex--;
},

resetHostContext() : void {
currentNamespaceURI = null;
foreignObjectDepth = 0;
portalStateIndex = -1;
},

prepareForCommit() : void {
eventsEnabled = ReactBrowserEventEmitter.isEnabled();
ReactBrowserEventEmitter.setEnabled(false);
Expand All @@ -80,7 +261,8 @@ var DOMRenderer = ReactFiberReconciler({
) : Instance {
const root = document.documentElement; // HACK

const domElement : Instance = createElement(type, props, root);
const namespaceURI = currentNamespaceURI || getIntrinsicNamespaceURI(type);
const domElement : Instance = createElement(type, props, namespaceURI, root);
precacheFiberNode(internalInstanceHandle, domElement);
return domElement;
},
Expand Down
29 changes: 2 additions & 27 deletions src/renderers/dom/fiber/ReactDOMFiberComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
'use strict';

var CSSPropertyOperations = require('CSSPropertyOperations');
var DOMNamespaces = require('DOMNamespaces');
var DOMProperty = require('DOMProperty');
var DOMPropertyOperations = require('DOMPropertyOperations');
var EventPluginRegistry = require('EventPluginRegistry');
Expand Down Expand Up @@ -453,43 +452,19 @@ function updateDOMProperties(

var ReactDOMFiberComponent = {

// TODO: Use this to keep track of changes to the host context and use this
// to determine whether we switch to svg and back.
// TODO: Does this need to check the current namespace? In case these tags
// happen to be valid in some other namespace.
isNewHostContainer(tag : string) {
return tag === 'svg' || tag === 'foreignobject';
},

createElement(
tag : string,
props : Object,
namespaceURI : string | null,
rootContainerElement : Element
) : Element {
validateDangerousTag(tag);
// TODO:
// tag.toLowerCase(); Do we need to apply lower case only on non-custom elements?

// We create tags in the namespace of their parent container, except HTML
// tags get no namespace.
var namespaceURI = rootContainerElement.namespaceURI;
if (namespaceURI == null ||
namespaceURI === DOMNamespaces.svg &&
rootContainerElement.tagName === 'foreignObject') {
namespaceURI = DOMNamespaces.html;
}
if (namespaceURI === DOMNamespaces.html) {
if (tag === 'svg') {
namespaceURI = DOMNamespaces.svg;
} else if (tag === 'math') {
namespaceURI = DOMNamespaces.mathml;
}
// TODO: Make this a new root container element.
}

var ownerDocument = rootContainerElement.ownerDocument;
var domElement : Element;
if (namespaceURI === DOMNamespaces.html) {
if (namespaceURI == null) {
if (tag === 'script') {
// Create the script via .innerHTML so its "parser-inserted" flag is
// set to true and it does not execute
Expand Down
Loading