Skip to content
This repository was archived by the owner on Jun 26, 2020. It is now read-only.
Merged
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
97 changes: 95 additions & 2 deletions frontend/Highlighter/Overlay.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
'use strict';

var assign = require('object-assign');
import type {DOMNode} from '../types';
import type {DOMNode, DOMRect, Window} from '../types';

class Overlay {
win: Object;
Expand Down Expand Up @@ -89,7 +89,7 @@ class Overlay {
if (node.nodeType !== Node.ELEMENT_NODE) {
return;
}
var box = node.getBoundingClientRect();
var box = getNestedBoundingClientRect(node, this.win);
var dims = getElementDimensions(node);

boxWrap(dims, 'margin', this.node);
Expand Down Expand Up @@ -169,6 +169,99 @@ function getElementDimensions(element) {
};
}

// Get the window object for the document that a node belongs to,
// or return null if it cannot be found (node not attached to DOM,
// etc).
function getOwnerWindow(node: DOMNode): Window | null {
if (!node.ownerDocument) {
return null;
}
return node.ownerDocument.defaultView;
}

// Get the iframe containing a node, or return null if it cannot
// be found (node not within iframe, etc).
function getOwnerIframe(node: DOMNode): DOMNode | null {
var nodeWindow = getOwnerWindow(node);
if (nodeWindow) {
return nodeWindow.frameElement;
}
return null;
}

// Get a bounding client rect for a node, with an
// offset added to compensate for its border.
function getBoundingClientRectWithBorderOffset(node: DOMNode) {
var dimensions = getElementDimensions(node);

return mergeRectOffsets([
node.getBoundingClientRect(),
{
top: dimensions.borderTop,
left: dimensions.borderLeft,
bottom: dimensions.borderBottom,
right: dimensions.borderRight,
// This width and height won't get used by mergeRectOffsets (since this
// is not the first rect in the array), but we set them so that this
// object typechecks as a DOMRect.
width: 0,
height: 0,
},
]);
}

// Add together the top, left, bottom, and right properties of
// each DOMRect, but keep the width and height of the first one.
function mergeRectOffsets(rects: Array<DOMRect>): DOMRect {
return rects.reduce((previousRect, rect) => {
if (previousRect == null) {
return rect;
}

return {
top: previousRect.top + rect.top,
left: previousRect.left + rect.left,
width: previousRect.width,
height: previousRect.height,
bottom: previousRect.bottom + rect.bottom,
right: previousRect.right + rect.right,
};
});
}

// Calculate a boundingClientRect for a node relative to boundaryWindow,
// taking into account any offsets caused by intermediate iframes.
function getNestedBoundingClientRect(node: DOMNode, boundaryWindow: Window): DOMRect {
var ownerIframe = getOwnerIframe(node);
if (
ownerIframe &&
ownerIframe !== boundaryWindow
) {
var rects = [node.getBoundingClientRect()];
var currentIframe = ownerIframe;
var onlyOneMore = false;
while (currentIframe) {
var rect = getBoundingClientRectWithBorderOffset(currentIframe);
rects.push(rect);
currentIframe = getOwnerIframe(currentIframe);

if (onlyOneMore) {
break;
}
// We don't want to calculate iframe offsets upwards beyond
// the iframe containing the boundaryWindow, but we
// need to calculate the offset relative to the boundaryWindow.
if (currentIframe && getOwnerWindow(currentIframe) === boundaryWindow) {
onlyOneMore = true;
}
}

return mergeRectOffsets(rects);
} else {
return node.getBoundingClientRect();
}
}

function boxWrap(dims, what, node) {
assign(node.style, {
borderTopWidth: dims[what + 'Top'] + 'px',
Expand Down
27 changes: 19 additions & 8 deletions frontend/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,18 @@ export type Dest = 'firstChild' | 'lastChild' | 'prevSibling' | 'nextSibling' |

export type ElementID = string;

export type Window = {
frameElement: DOMNode | null,
};

export type Document = {
defaultView: Window | null,
};

export type DOMNode = {
appendChild: (child: DOMNode) => void,
childNodes: Array<DOMNode>,
getBoundingClientRect: () => {
top: number,
left: number,
width: number,
height: number,
bottom: number,
right: number,
},
getBoundingClientRect: () => DOMRect,
innerHTML: string,
innerText: string,
nodeName: string,
Expand All @@ -48,6 +49,7 @@ export type DOMNode = {
style: Object,
textContent: string,
value: string,
ownerDocument: Document | null,
};

export type DOMEvent = {
Expand All @@ -61,6 +63,15 @@ export type DOMEvent = {
target: DOMNode,
};

export type DOMRect = {
top: number,
left: number,
width: number,
height: number,
bottom: number,
right: number,
};

export type ControlState = {
enabled: boolean,
} & Record;
57 changes: 57 additions & 0 deletions test/example/sink.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,61 @@ class LotsOfMounts extends React.Component {
}
}

class IframeWrapper extends React.Component {
componentDidMount() {
this.root = document.createElement('div');
this.frame.contentDocument.body.appendChild(this.root);
ReactDOM.render(this.props.children, this.root);
}

componentWillUnmount() {
ReactDOM.unmountComponentAtNode(this.root);
}

render() {
var { children, ...props } = this.props; // eslint-disable-line no-unused-vars

return (
<div>
<div>Iframe below</div>
<iframe ref={frame => this.frame = frame} {...props} />
</div>
);
}
}

class InnerContent extends React.Component {
render() {
return (
<div>
Inner content (highlight should overlap properly)
</div>
);
}
}

class IframeWithMountedChild extends React.Component {
render() {
return (
<IframeWrapper>
<InnerContent />
</IframeWrapper>
);
}
}

class NestedMountedIframesWithVaryingBorder extends React.Component {
render() {
return (
<IframeWrapper>
<IframeWrapper frameBorder="0">
<InnerContent />
</IframeWrapper>
</IframeWrapper>
);
}
}

// Render the list of tests

class Sink extends React.Component {
Expand All @@ -147,6 +202,8 @@ class Sink extends React.Component {
BadUnmount,
Nester,
LotsOfMounts,
IframeWithMountedChild,
NestedMountedIframesWithVaryingBorder,
};

var view = Comp => run(View, {Comp});
Expand Down