Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Avoid using global document, global window #193189

Merged
merged 3 commits into from
Sep 15, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
91 changes: 67 additions & 24 deletions src/vs/base/browser/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,29 @@ import { FileAccess, RemoteAuthorities, Schemas } from 'vs/base/common/network';
import * as platform from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri';

export const { registerWindow, getWindows, onDidCreateWindow } = (function () {
const windows: Window[] = [];
const onDidCreateWindow = new event.Emitter<{ window: Window; disposableStore: DisposableStore }>();
return {
onDidCreateWindow: onDidCreateWindow.event,
registerWindow(window: Window): IDisposable {
windows.push(window);
const disposableStore = new DisposableStore();
disposableStore.add(toDisposable(() => {
const index = windows.indexOf(window);
if (index !== -1) {
windows.splice(index, 1);
}
}));
onDidCreateWindow.fire({ window, disposableStore });
return disposableStore;
},
getWindows(): Window[] {
return windows;
}
};
})();

export function clearNode(node: HTMLElement): void {
while (node.firstChild) {
node.firstChild.remove();
Expand Down Expand Up @@ -282,34 +305,37 @@ export function addDisposableThrottledListener<R, E extends Event = Event>(node:
}

export function getComputedStyle(el: HTMLElement): CSSStyleDeclaration {
return document.defaultView!.getComputedStyle(el, null);
return el.ownerDocument.defaultView!.getComputedStyle(el, null);
}

export function getClientArea(element: HTMLElement): Dimension {

const elDocument = element.ownerDocument;
const elWindow = elDocument.defaultView?.window;

// Try with DOM clientWidth / clientHeight
if (element !== document.body) {
if (element !== elDocument.body) {
return new Dimension(element.clientWidth, element.clientHeight);
}

// If visual view port exits and it's on mobile, it should be used instead of window innerWidth / innerHeight, or document.body.clientWidth / document.body.clientHeight
if (platform.isIOS && window.visualViewport) {
return new Dimension(window.visualViewport.width, window.visualViewport.height);
if (platform.isIOS && elWindow?.visualViewport) {
return new Dimension(elWindow.visualViewport.width, elWindow.visualViewport.height);
}

// Try innerWidth / innerHeight
if (window.innerWidth && window.innerHeight) {
return new Dimension(window.innerWidth, window.innerHeight);
if (elWindow?.innerWidth && elWindow.innerHeight) {
return new Dimension(elWindow.innerWidth, elWindow.innerHeight);
}

// Try with document.body.clientWidth / document.body.clientHeight
if (document.body && document.body.clientWidth && document.body.clientHeight) {
return new Dimension(document.body.clientWidth, document.body.clientHeight);
if (elDocument.body && elDocument.body.clientWidth && elDocument.body.clientHeight) {
return new Dimension(elDocument.body.clientWidth, elDocument.body.clientHeight);
}

// Try with document.documentElement.clientWidth / document.documentElement.clientHeight
if (document.documentElement && document.documentElement.clientWidth && document.documentElement.clientHeight) {
return new Dimension(document.documentElement.clientWidth, document.documentElement.clientHeight);
if (elDocument.documentElement && elDocument.documentElement.clientWidth && elDocument.documentElement.clientHeight) {
return new Dimension(elDocument.documentElement.clientWidth, elDocument.documentElement.clientHeight);
}

throw new Error('Unable to figure out browser width and height');
Expand Down Expand Up @@ -431,8 +457,8 @@ export function getTopLeftOffset(element: HTMLElement): IDomPosition {

while (
(element = <HTMLElement>element.parentNode) !== null
&& element !== document.body
&& element !== document.documentElement
&& element !== element.ownerDocument.body
&& element !== element.ownerDocument.documentElement
) {
top -= element.scrollTop;
const c = isShadowRoot(element) ? null : getComputedStyle(element);
Expand Down Expand Up @@ -498,8 +524,8 @@ export function position(element: HTMLElement, top: number, right?: number, bott
export function getDomNodePagePosition(domNode: HTMLElement): IDomNodePagePosition {
const bb = domNode.getBoundingClientRect();
return {
left: bb.left + window.scrollX,
top: bb.top + window.scrollY,
left: bb.left + (domNode.ownerDocument.defaultView?.scrollX ?? 0),
top: bb.top + (domNode.ownerDocument.defaultView?.scrollY ?? 0),
width: bb.width,
height: bb.height
};
Expand All @@ -518,7 +544,7 @@ export function getDomNodeZoomLevel(domNode: HTMLElement): number {
}

testElement = testElement.parentElement;
} while (testElement !== null && testElement !== document.documentElement);
} while (testElement !== null && testElement !== testElement.ownerDocument.documentElement);

return zoom;
}
Expand Down Expand Up @@ -602,7 +628,7 @@ export function setParentFlowTo(fromChildElement: HTMLElement, toParentElement:
function getParentFlowToElement(node: HTMLElement): HTMLElement | null {
const flowToParentId = node.dataset[parentFlowToDataKey];
if (typeof flowToParentId === 'string') {
return document.getElementById(flowToParentId);
return node.ownerDocument.getElementById(flowToParentId);
}
return null;
}
Expand Down Expand Up @@ -671,7 +697,7 @@ export function isInShadowDOM(domNode: Node): boolean {

export function getShadowRoot(domNode: Node): ShadowRoot | null {
while (domNode.parentNode) {
if (domNode === document.body) {
if (domNode === domNode.ownerDocument?.body) {
// reached the body
return null;
}
Expand All @@ -680,8 +706,12 @@ export function getShadowRoot(domNode: Node): ShadowRoot | null {
return isShadowRoot(domNode) ? domNode : null;
}

/**
* Returns the active element across all child windows.
* Use this instead of `document.activeElement` to handle multiple windows.
*/
export function getActiveElement(): Element | null {
let result = document.activeElement;
let result = getActiveDocument().activeElement;

while (result?.shadowRoot) {
result = result.shadowRoot.activeElement;
Expand All @@ -690,6 +720,15 @@ export function getActiveElement(): Element | null {
return result;
}

/**
* Returns the active document across all child windows.
* Use this instead of `document` when reacting to dom events to handle multiple windows.
*/
export function getActiveDocument(): Document {
const documents = [document, ...getWindows().map(w => w.document)];
return documents.find(doc => doc.hasFocus()) ?? document;
}

export function createStyleSheet(container: HTMLElement = document.getElementsByTagName('head')[0], beforeAppend?: (style: HTMLStyleElement) => void): HTMLStyleElement {
const style = document.createElement('style');
style.type = 'text/css';
Expand Down Expand Up @@ -875,15 +914,19 @@ class FocusTracker extends Disposable implements IFocusTracker {

private _refreshStateHandler: () => void;

private static hasFocusWithin(element: HTMLElement): boolean {
const shadowRoot = getShadowRoot(element);
const activeElement = (shadowRoot ? shadowRoot.activeElement : document.activeElement);
return isAncestor(activeElement, element);
private static hasFocusWithin(element: HTMLElement | Window): boolean {
if (isHTMLElement(element)) {
const shadowRoot = getShadowRoot(element);
const activeElement = (shadowRoot ? shadowRoot.activeElement : element.ownerDocument.activeElement);
return isAncestor(activeElement, element);
} else {
return isAncestor(window.document.activeElement, window.document);
}
}

constructor(element: HTMLElement | Window) {
super();
let hasFocus = FocusTracker.hasFocusWithin(<HTMLElement>element);
let hasFocus = FocusTracker.hasFocusWithin(element);
let loosingFocus = false;

const onFocus = () => {
Expand Down Expand Up @@ -1092,7 +1135,7 @@ export function removeTabIndexAndUpdateFocus(node: HTMLElement): void {
// standard DOM behavior is to move focus to the <body> element. We
// typically never want that, rather put focus to the closest element
// in the hierarchy of the parent DOM nodes.
if (document.activeElement === node) {
if (node.ownerDocument.activeElement === node) {
const parentFocusable = findParentWithAttribute(node.parentElement, 'tabIndex');
parentFocusable?.focus();
}
Expand Down
4 changes: 2 additions & 2 deletions src/vs/base/browser/mouseEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ export class StandardMouseEvent implements IMouseEvent {
this.posy = e.pageY;
} else {
// Probably hit by MSGestureEvent
this.posx = e.clientX + document.body.scrollLeft + document.documentElement!.scrollLeft;
this.posy = e.clientY + document.body.scrollTop + document.documentElement!.scrollTop;
this.posx = e.clientX + this.target.ownerDocument.body.scrollLeft + this.target.ownerDocument.documentElement.scrollLeft;
this.posy = e.clientY + this.target.ownerDocument.body.scrollTop + this.target.ownerDocument.documentElement.scrollTop;
}

// Find the position of the iframe this code is executing in relative to the iframe where the event was captured.
Expand Down
2 changes: 1 addition & 1 deletion src/vs/editor/browser/controller/mouseHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export class MouseHandler extends ViewEventHandler {
// remove this listener

if (!this._mouseLeaveMonitor) {
this._mouseLeaveMonitor = dom.addDisposableListener(document, 'mousemove', (e) => {
this._mouseLeaveMonitor = dom.addDisposableListener(this.viewHelper.viewDomNode.ownerDocument, 'mousemove', (e) => {
if (!this.viewHelper.viewDomNode.contains(e.target as Node | null)) {
// went outside the editor!
this._onMouseLeave(new EditorMouseEvent(e, false, this.viewHelper.viewDomNode));
Expand Down
10 changes: 5 additions & 5 deletions src/vs/editor/browser/controller/mouseTarget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ export class HitTestContext {
}

private static _findAttribute(element: Element, attr: string, stopAt: Element): string | null {
while (element && element !== document.body) {
while (element && element !== element.ownerDocument.body) {
if (element.hasAttribute && element.hasAttribute(attr)) {
return element.getAttribute(attr);
}
Expand Down Expand Up @@ -917,7 +917,7 @@ export class MouseTargetFactory {
range = (<any>shadowRoot).caretRangeFromPoint(coords.clientX, coords.clientY);
}
} else {
range = (<any>document).caretRangeFromPoint(coords.clientX, coords.clientY);
range = (<any>ctx.viewDomNode.ownerDocument).caretRangeFromPoint(coords.clientX, coords.clientY);
}

if (!range || !range.startContainer) {
Expand Down Expand Up @@ -959,7 +959,7 @@ export class MouseTargetFactory {
* Most probably Gecko
*/
private static _doHitTestWithCaretPositionFromPoint(ctx: HitTestContext, coords: ClientCoordinates): HitTestResult {
const hitResult: { offsetNode: Node; offset: number } = (<any>document).caretPositionFromPoint(coords.clientX, coords.clientY);
const hitResult: { offsetNode: Node; offset: number } = (<any>ctx.viewDomNode.ownerDocument).caretPositionFromPoint(coords.clientX, coords.clientY);

if (hitResult.offsetNode.nodeType === hitResult.offsetNode.TEXT_NODE) {
// offsetNode is expected to be the token text
Expand Down Expand Up @@ -1011,9 +1011,9 @@ export class MouseTargetFactory {
private static _doHitTest(ctx: HitTestContext, request: BareHitTestRequest): HitTestResult {

let result: HitTestResult = new UnknownHitTestResult();
if (typeof (<any>document).caretRangeFromPoint === 'function') {
if (typeof (<any>ctx.viewDomNode.ownerDocument).caretRangeFromPoint === 'function') {
result = this._doHitTestWithCaretRangeFromPoint(ctx, request);
} else if ((<any>document).caretPositionFromPoint) {
} else if ((<any>ctx.viewDomNode.ownerDocument).caretPositionFromPoint) {
result = this._doHitTestWithCaretPositionFromPoint(ctx, request.pos.toClientCoordinates());
}
if (result.type === HitTestResultType.Content) {
Expand Down
4 changes: 2 additions & 2 deletions src/vs/editor/browser/controller/textAreaHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ export class TextAreaHandler extends ViewPart {
const distanceToModelLineStart = startModelPosition.column - 1 - visibleBeforeCharCount;
const hiddenLineTextBefore = lineTextBeforeSelection.substring(0, lineTextBeforeSelection.length - visibleBeforeCharCount);
const { tabSize } = this._context.viewModel.model.getOptions();
const widthOfHiddenTextBefore = measureText(hiddenLineTextBefore, this._fontInfo, tabSize);
const widthOfHiddenTextBefore = measureText(this.textArea.domNode.ownerDocument, hiddenLineTextBefore, this._fontInfo, tabSize);

return { distanceToModelLineStart, widthOfHiddenTextBefore };
})();
Expand Down Expand Up @@ -927,7 +927,7 @@ interface IRenderData {
strikethrough?: boolean;
}

function measureText(text: string, fontInfo: FontInfo, tabSize: number): number {
function measureText(document: Document, text: string, fontInfo: FontInfo, tabSize: number): number {
if (text.length === 0) {
return 0;
}
Expand Down
12 changes: 9 additions & 3 deletions src/vs/editor/browser/controller/textAreaInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ export interface ICompleteTextAreaWrapper extends ITextAreaWrapper {
readonly onBlur: Event<FocusEvent>;
readonly onSyntheticTap: Event<void>;

readonly ownerDocument: Document;

setIgnoreSelectionChangeTime(reason: string): void;
getIgnoreSelectionChangeTime(): number;
resetSelectionChangeTime(): void;
Expand Down Expand Up @@ -494,7 +496,7 @@ export class TextAreaInput extends Disposable {
// `selectionchange` events often come multiple times for a single logical change
// so throttle multiple `selectionchange` events that burst in a short period of time.
let previousSelectionChangeEventTime = 0;
return dom.addDisposableListener(document, 'selectionchange', (e) => {
return dom.addDisposableListener(this._textArea.ownerDocument, 'selectionchange', (e) => {//todo
inputLatency.onSelectionChange();

if (!this._hasFocus) {
Expand Down Expand Up @@ -701,6 +703,10 @@ export class TextAreaWrapper extends Disposable implements ICompleteTextAreaWrap
public readonly onFocus = this._register(new DomEmitter(this._actual, 'focus')).event;
public readonly onBlur = this._register(new DomEmitter(this._actual, 'blur')).event;

public get ownerDocument(): Document {
return this._actual.ownerDocument;
}

private _onSyntheticTap = this._register(new Emitter<void>());
public readonly onSyntheticTap: Event<void> = this._onSyntheticTap.event;

Expand All @@ -725,7 +731,7 @@ export class TextAreaWrapper extends Disposable implements ICompleteTextAreaWrap
if (shadowRoot) {
return shadowRoot.activeElement === this._actual;
} else if (dom.isInDOM(this._actual)) {
return document.activeElement === this._actual;
return this._actual.ownerDocument.activeElement === this._actual;
} else {
return false;
}
Expand Down Expand Up @@ -775,7 +781,7 @@ export class TextAreaWrapper extends Disposable implements ICompleteTextAreaWrap
if (shadowRoot) {
activeElement = shadowRoot.activeElement;
} else {
activeElement = document.activeElement;
activeElement = textArea.ownerDocument.activeElement;
}

const currentIsFocused = (activeElement === textArea);
Expand Down
23 changes: 12 additions & 11 deletions src/vs/editor/browser/coreCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { KeybindingWeight, KeybindingsRegistry } from 'vs/platform/keybinding/co
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { IViewModel } from 'vs/editor/common/viewModel';
import { ISelection } from 'vs/editor/common/core/selection';
import { getActiveElement } from 'vs/base/browser/dom';

const CORE_WEIGHT = KeybindingWeight.EditorCore;

Expand Down Expand Up @@ -315,9 +316,9 @@ abstract class EditorOrNativeTextInputCommand {
// 2. handle case when focus is in some other `input` / `textarea`.
target.addImplementation(1000, 'generic-dom-input-textarea', (accessor: ServicesAccessor, args: unknown) => {
// Only if focused on an element that allows for entering text
const activeElement = <HTMLElement>document.activeElement;
const activeElement = getActiveElement();
if (activeElement && ['input', 'textarea'].indexOf(activeElement.tagName.toLowerCase()) >= 0) {
this.runDOMCommand();
this.runDOMCommand(activeElement);
return true;
}
return false;
Expand All @@ -343,7 +344,7 @@ abstract class EditorOrNativeTextInputCommand {
return true;
}

public abstract runDOMCommand(): void;
public abstract runDOMCommand(activeElement: Element): void;
public abstract runEditorCommand(accessor: ServicesAccessor | null, editor: ICodeEditor, args: unknown): void | Promise<void>;
}

Expand Down Expand Up @@ -1875,13 +1876,13 @@ export namespace CoreNavigationCommands {
constructor() {
super(SelectAllCommand);
}
public runDOMCommand(): void {
public runDOMCommand(activeElement: Element): void {
if (isFirefox) {
(<HTMLInputElement>document.activeElement).focus();
(<HTMLInputElement>document.activeElement).select();
(<HTMLInputElement>activeElement).focus();
(<HTMLInputElement>activeElement).select();
}

document.execCommand('selectAll');
activeElement.ownerDocument.execCommand('selectAll');
}
public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: unknown): void {
const viewModel = editor._getViewModel();
Expand Down Expand Up @@ -2090,8 +2091,8 @@ export namespace CoreEditingCommands {
constructor() {
super(UndoCommand);
}
public runDOMCommand(): void {
document.execCommand('undo');
public runDOMCommand(activeElement: Element): void {
activeElement.ownerDocument.execCommand('undo');
}
public runEditorCommand(accessor: ServicesAccessor | null, editor: ICodeEditor, args: unknown): void | Promise<void> {
if (!editor.hasModel() || editor.getOption(EditorOption.readOnly) === true) {
Expand All @@ -2105,8 +2106,8 @@ export namespace CoreEditingCommands {
constructor() {
super(RedoCommand);
}
public runDOMCommand(): void {
document.execCommand('redo');
public runDOMCommand(activeElement: Element): void {
activeElement.ownerDocument.execCommand('redo');
}
public runEditorCommand(accessor: ServicesAccessor | null, editor: ICodeEditor, args: unknown): void | Promise<void> {
if (!editor.hasModel() || editor.getOption(EditorOption.readOnly) === true) {
Expand Down
2 changes: 1 addition & 1 deletion src/vs/editor/browser/editorDom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ export class GlobalEditorPointerMoveMonitor extends Disposable {

// Add a <<capture>> keydown event listener that will cancel the monitoring
// if something other than a modifier key is pressed
this._keydownListener = dom.addStandardDisposableListener(<any>document, 'keydown', (e) => {
this._keydownListener = dom.addStandardDisposableListener(<any>initialElement.ownerDocument, 'keydown', (e) => {
const chord = e.toKeyCodeChord();
if (chord.isModifierKey()) {
// Allow modifier keys
Expand Down