2019-01-19 04:46:40 +07:00
|
|
|
/// <reference path="../../../../lib/vscode/src/typings/electron.d.ts" />
|
|
|
|
import { EventEmitter } from "events";
|
|
|
|
import * as fs from "fs";
|
|
|
|
import { logger, field } from "@coder/logger";
|
|
|
|
import { IKey, Dialog as DialogBox } from "./dialog";
|
|
|
|
import { clipboard } from "./clipboard";
|
2019-02-08 00:05:17 +07:00
|
|
|
import { client } from "./client";
|
|
|
|
|
2019-02-19 23:17:03 +07:00
|
|
|
declare var __non_webpack_require__: typeof require;
|
2019-01-19 04:46:40 +07:00
|
|
|
|
|
|
|
// tslint:disable-next-line no-any
|
|
|
|
(global as any).getOpenUrls = (): string[] => {
|
|
|
|
return [];
|
|
|
|
};
|
|
|
|
|
2019-02-07 05:45:11 +07:00
|
|
|
// This is required to make the fill load in Node without erroring.
|
2019-01-19 04:46:40 +07:00
|
|
|
if (typeof document === "undefined") {
|
2019-02-07 05:45:11 +07:00
|
|
|
// tslint:disable-next-line no-any
|
|
|
|
(global as any).document = {} as any;
|
2019-01-19 04:46:40 +07:00
|
|
|
}
|
|
|
|
|
2019-02-22 00:55:42 +07:00
|
|
|
const oldCreateElement = document.createElement;
|
2019-01-19 04:46:40 +07:00
|
|
|
const newCreateElement = <K extends keyof HTMLElementTagNameMap>(tagName: K): HTMLElementTagNameMap[K] => {
|
|
|
|
const createElement = <K extends keyof HTMLElementTagNameMap>(tagName: K): HTMLElementTagNameMap[K] => {
|
2019-02-22 00:55:42 +07:00
|
|
|
// tslint:disable-next-line:no-any
|
|
|
|
return oldCreateElement.call(document, tagName as any);
|
2019-01-19 04:46:40 +07:00
|
|
|
};
|
|
|
|
|
2019-02-22 00:55:42 +07:00
|
|
|
if (tagName === "style") {
|
|
|
|
const style = createElement("style");
|
|
|
|
// tslint:disable-next-line:no-any
|
|
|
|
const getPropertyDescriptor = (object: any, id: string): PropertyDescriptor | undefined => {
|
|
|
|
let op = Object.getPrototypeOf(object);
|
|
|
|
while (!Object.getOwnPropertyDescriptor(op, id)) {
|
|
|
|
op = Object.getPrototypeOf(op);
|
|
|
|
}
|
|
|
|
|
|
|
|
return Object.getOwnPropertyDescriptor(op, id);
|
|
|
|
};
|
|
|
|
const oldInnerHtml = getPropertyDescriptor(style, "innerHTML");
|
|
|
|
if (!oldInnerHtml) {
|
|
|
|
throw new Error("Failed to find innerHTML property");
|
|
|
|
}
|
|
|
|
Object.defineProperty(style, "innerHTML", {
|
|
|
|
get: (): string => {
|
|
|
|
return oldInnerHtml!.get!.call(style);
|
|
|
|
},
|
|
|
|
set: (value: string): void => {
|
|
|
|
value = value.replace(/file:\/\//g, "/resource");
|
|
|
|
oldInnerHtml!.set!.call(style, value);
|
|
|
|
},
|
|
|
|
});
|
|
|
|
let overridden = false;
|
|
|
|
const oldSheet = getPropertyDescriptor(style, "sheet");
|
|
|
|
Object.defineProperty(style, "sheet", {
|
|
|
|
// tslint:disable-next-line:no-any
|
|
|
|
get: (): any => {
|
|
|
|
const sheet = oldSheet!.get!.call(style);
|
|
|
|
if (sheet && !overridden) {
|
|
|
|
const oldInsertRule = sheet.insertRule;
|
|
|
|
sheet.insertRule = (rule: string, index?: number): void => {
|
|
|
|
rule = rule.replace(/file:\/\//g, "/resource");
|
|
|
|
oldInsertRule.call(sheet, rule, index);
|
|
|
|
};
|
|
|
|
overridden = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return sheet;
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
return style;
|
|
|
|
}
|
|
|
|
|
2019-01-19 04:46:40 +07:00
|
|
|
if (tagName === "webview") {
|
|
|
|
const view = createElement("iframe") as HTMLIFrameElement;
|
|
|
|
view.style.border = "0px";
|
|
|
|
const frameID = Math.random().toString();
|
|
|
|
view.addEventListener("error", (event) => {
|
|
|
|
logger.error("iframe error", field("event", event));
|
|
|
|
});
|
|
|
|
window.addEventListener("message", (event) => {
|
|
|
|
if (!event.data || !event.data.id) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (event.data.id !== frameID) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const e = new CustomEvent("ipc-message");
|
|
|
|
(e as any).channel = event.data.channel; // tslint:disable-line no-any
|
|
|
|
(e as any).args = event.data.data; // tslint:disable-line no-any
|
|
|
|
view.dispatchEvent(e);
|
|
|
|
});
|
|
|
|
view.sandbox.add("allow-same-origin", "allow-scripts", "allow-popups", "allow-forms");
|
|
|
|
Object.defineProperty(view, "preload", {
|
|
|
|
set: (url: string): void => {
|
|
|
|
view.onload = (): void => {
|
|
|
|
if (view.contentDocument) {
|
|
|
|
view.contentDocument.body.id = frameID;
|
|
|
|
view.contentDocument.body.parentElement!.style.overflow = "hidden";
|
2019-02-07 05:45:11 +07:00
|
|
|
const script = createElement("script");
|
2019-01-19 04:46:40 +07:00
|
|
|
script.src = url;
|
2019-02-22 00:55:42 +07:00
|
|
|
script.addEventListener("load", () => {
|
|
|
|
view.contentDocument!.dispatchEvent(new Event("DOMContentLoaded", {
|
|
|
|
bubbles: true,
|
|
|
|
cancelable: true,
|
|
|
|
}));
|
|
|
|
// const e = new CustomEvent("ipc-message");
|
|
|
|
// (e as any).channel = "webview-ready"; // tslint:disable-line no-any
|
|
|
|
// (e as any).args = [frameID]; // tslint:disable-line no-any
|
|
|
|
// view.dispatchEvent(e);
|
|
|
|
});
|
2019-01-19 04:46:40 +07:00
|
|
|
view.contentDocument.head.appendChild(script);
|
|
|
|
}
|
2019-02-22 00:55:42 +07:00
|
|
|
|
2019-01-19 04:46:40 +07:00
|
|
|
};
|
|
|
|
},
|
|
|
|
});
|
|
|
|
(view as any).getWebContents = (): void => undefined; // tslint:disable-line no-any
|
|
|
|
(view as any).send = (channel: string, ...args: any[]): void => { // tslint:disable-line no-any
|
|
|
|
if (args[0] && typeof args[0] === "object" && args[0].contents) {
|
|
|
|
// TODO
|
2019-02-22 00:55:42 +07:00
|
|
|
args[0].contents = (args[0].contents as string).replace(/"(file:\/\/[^"]*)"/g, (m1) => `"/resource${m1}"`);
|
|
|
|
args[0].contents = (args[0].contents as string).replace(/"vscode-resource:([^"]*)"/g, (m, m1) => `"/resource${m1}"`);
|
2019-01-19 04:46:40 +07:00
|
|
|
}
|
|
|
|
if (view.contentWindow) {
|
|
|
|
view.contentWindow.postMessage({
|
|
|
|
channel,
|
|
|
|
data: args,
|
|
|
|
id: frameID,
|
|
|
|
}, "*");
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
return view;
|
|
|
|
}
|
|
|
|
|
|
|
|
return createElement(tagName);
|
|
|
|
};
|
|
|
|
|
|
|
|
document.createElement = newCreateElement;
|
|
|
|
|
|
|
|
class Clipboard {
|
|
|
|
public has(): boolean {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
public writeText(value: string): Promise<void> {
|
|
|
|
return clipboard.writeText(value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class Shell {
|
|
|
|
public async moveItemToTrash(path: string): Promise<void> {
|
2019-02-23 04:56:29 +07:00
|
|
|
await client.evaluate((_helper, path) => {
|
2019-02-19 23:17:03 +07:00
|
|
|
const trash = __non_webpack_require__("trash") as typeof import("trash");
|
2019-02-08 00:05:17 +07:00
|
|
|
|
|
|
|
return trash(path);
|
|
|
|
}, path);
|
2019-01-19 04:46:40 +07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class App extends EventEmitter {
|
|
|
|
public isAccessibilitySupportEnabled(): boolean {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
public setAsDefaultProtocolClient(): void {
|
|
|
|
throw new Error("not implemented");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class Dialog {
|
|
|
|
public showSaveDialog(_: void, options: Electron.SaveDialogOptions, callback: (filename: string | undefined) => void): void {
|
|
|
|
const defaultPath = options.defaultPath || "/untitled";
|
|
|
|
const fileIndex = defaultPath.lastIndexOf("/");
|
|
|
|
const extensionIndex = defaultPath.lastIndexOf(".");
|
|
|
|
const saveDialogOptions = {
|
|
|
|
buttons: ["Cancel", "Save"],
|
|
|
|
detail: "Enter a path for this file",
|
|
|
|
input: {
|
|
|
|
value: defaultPath,
|
|
|
|
selection: {
|
|
|
|
start: fileIndex === -1 ? 0 : fileIndex + 1,
|
|
|
|
end: extensionIndex === -1 ? defaultPath.length : extensionIndex,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
message: "Save file",
|
|
|
|
};
|
|
|
|
|
|
|
|
const dialog = new DialogBox(saveDialogOptions);
|
|
|
|
dialog.onAction((action) => {
|
|
|
|
if (action.key !== IKey.Enter && action.buttonIndex !== 1) {
|
|
|
|
dialog.hide();
|
|
|
|
|
|
|
|
return callback(undefined);
|
|
|
|
}
|
|
|
|
|
|
|
|
const inputValue = dialog.inputValue || "";
|
|
|
|
const filePath = inputValue.replace(/\/+$/, "");
|
|
|
|
const split = filePath.split("/");
|
|
|
|
const fileName = split.pop();
|
|
|
|
const parentName = split.pop() || "/";
|
|
|
|
if (fileName === "") {
|
|
|
|
dialog.error = "You must enter a file name.";
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
fs.stat(filePath, (error, stats) => {
|
|
|
|
if (error && error.code === "ENOENT") {
|
|
|
|
dialog.hide();
|
|
|
|
callback(filePath);
|
|
|
|
} else if (error) {
|
|
|
|
dialog.error = error.message;
|
|
|
|
} else if (stats.isDirectory()) {
|
|
|
|
dialog.error = `A directory named "${fileName}" already exists.`;
|
|
|
|
} else {
|
|
|
|
dialog.error = undefined;
|
|
|
|
|
|
|
|
const confirmDialog = new DialogBox({
|
|
|
|
message: `A file named "${fileName}" already exists. Do you want to replace it?`,
|
|
|
|
detail: `The file already exists in "${parentName}". Replacing it will overwrite its contents.`,
|
|
|
|
buttons: ["Cancel", "Replace"],
|
|
|
|
});
|
|
|
|
|
|
|
|
confirmDialog.onAction((action) => {
|
|
|
|
if (action.buttonIndex === 1) {
|
|
|
|
confirmDialog.hide();
|
|
|
|
|
|
|
|
return callback(filePath);
|
|
|
|
}
|
|
|
|
|
|
|
|
confirmDialog.hide();
|
|
|
|
dialog.show();
|
|
|
|
});
|
|
|
|
|
|
|
|
dialog.hide();
|
|
|
|
confirmDialog.show();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
dialog.show();
|
|
|
|
}
|
|
|
|
|
|
|
|
public showOpenDialog(): void {
|
|
|
|
throw new Error("not implemented");
|
|
|
|
}
|
|
|
|
|
|
|
|
public showMessageBox(_: void, options: Electron.MessageBoxOptions, callback: (button: number | undefined, checked: boolean) => void): void {
|
|
|
|
const dialog = new DialogBox(options);
|
|
|
|
dialog.onAction((action) => {
|
|
|
|
dialog.hide();
|
|
|
|
callback(action.buttonIndex, false);
|
|
|
|
});
|
|
|
|
dialog.show();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class WebFrame {
|
|
|
|
public getZoomFactor(): number {
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
public getZoomLevel(): number {
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
public setZoomLevel(): void {
|
|
|
|
// Nothing.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class Screen {
|
|
|
|
public getAllDisplays(): [] {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class WebRequest extends EventEmitter {
|
|
|
|
public onBeforeRequest(): void {
|
|
|
|
throw new Error("not implemented");
|
|
|
|
}
|
|
|
|
|
|
|
|
public onBeforeSendHeaders(): void {
|
|
|
|
throw new Error("not implemented");
|
|
|
|
}
|
|
|
|
|
|
|
|
public onHeadersReceived(): void {
|
|
|
|
throw new Error("not implemented");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class Session extends EventEmitter {
|
|
|
|
public webRequest = new WebRequest();
|
|
|
|
|
|
|
|
public resolveProxy(url: string, callback: (proxy: string) => void): void {
|
|
|
|
// TODO: not sure what this actually does.
|
|
|
|
callback(url);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class WebContents extends EventEmitter {
|
|
|
|
public session = new Session();
|
|
|
|
}
|
|
|
|
|
|
|
|
class BrowserWindow extends EventEmitter {
|
|
|
|
public webContents = new WebContents();
|
|
|
|
private representedFilename: string = "";
|
|
|
|
|
|
|
|
public static getFocusedWindow(): undefined {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
public focus(): void {
|
|
|
|
window.focus();
|
|
|
|
}
|
|
|
|
|
|
|
|
public show(): void {
|
|
|
|
window.focus();
|
|
|
|
}
|
|
|
|
|
|
|
|
public reload(): void {
|
|
|
|
location.reload();
|
|
|
|
}
|
|
|
|
|
|
|
|
public isMaximized(): boolean {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
public setFullScreen(fullscreen: boolean): void {
|
|
|
|
if (fullscreen) {
|
|
|
|
document.documentElement.requestFullscreen();
|
|
|
|
} else {
|
|
|
|
document.exitFullscreen();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public isFullScreen(): boolean {
|
|
|
|
return document.fullscreenEnabled;
|
|
|
|
}
|
|
|
|
|
|
|
|
public isFocused(): boolean {
|
|
|
|
return document.hasFocus();
|
|
|
|
}
|
|
|
|
|
|
|
|
public setMenuBarVisibility(): void {
|
|
|
|
throw new Error("not implemented");
|
|
|
|
}
|
|
|
|
|
|
|
|
public setAutoHideMenuBar(): void {
|
|
|
|
throw new Error("not implemented");
|
|
|
|
}
|
|
|
|
|
|
|
|
public setRepresentedFilename(filename: string): void {
|
|
|
|
this.representedFilename = filename;
|
|
|
|
}
|
|
|
|
|
|
|
|
public getRepresentedFilename(): string {
|
|
|
|
return this.representedFilename;
|
|
|
|
}
|
|
|
|
|
|
|
|
public setTitle(value: string): void {
|
|
|
|
document.title = value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* We won't be able to do a 1 to 1 fill because things like moveItemToTrash for
|
|
|
|
* example returns a boolean while we need a promise.
|
|
|
|
*/
|
|
|
|
class ElectronFill {
|
|
|
|
public readonly shell = new Shell();
|
|
|
|
public readonly clipboard = new Clipboard();
|
|
|
|
public readonly app = new App();
|
|
|
|
public readonly dialog = new Dialog();
|
|
|
|
public readonly webFrame = new WebFrame();
|
|
|
|
public readonly screen = new Screen();
|
|
|
|
|
|
|
|
private readonly rendererToMainEmitter = new EventEmitter();
|
|
|
|
private readonly mainToRendererEmitter = new EventEmitter();
|
|
|
|
|
|
|
|
public get BrowserWindow(): typeof BrowserWindow {
|
|
|
|
return BrowserWindow;
|
|
|
|
}
|
|
|
|
|
|
|
|
// tslint:disable no-any
|
|
|
|
public get ipcRenderer(): object {
|
|
|
|
return {
|
|
|
|
send: (str: string, ...args: any[]): void => {
|
|
|
|
this.rendererToMainEmitter.emit(str, {
|
|
|
|
sender: module.exports.ipcMain,
|
|
|
|
}, ...args);
|
|
|
|
},
|
|
|
|
on: (str: string, listener: (...args: any[]) => void): void => {
|
|
|
|
this.mainToRendererEmitter.on(str, listener);
|
|
|
|
},
|
|
|
|
once: (str: string, listener: (...args: any[]) => void): void => {
|
|
|
|
this.mainToRendererEmitter.once(str, listener);
|
|
|
|
},
|
|
|
|
removeListener: (str: string, listener: (...args: any[]) => void): void => {
|
|
|
|
this.mainToRendererEmitter.removeListener(str, listener);
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
public get ipcMain(): object {
|
|
|
|
return {
|
|
|
|
send: (str: string, ...args: any[]): void => {
|
|
|
|
this.mainToRendererEmitter.emit(str, {
|
|
|
|
sender: module.exports.ipcRenderer,
|
|
|
|
}, ...args);
|
|
|
|
},
|
|
|
|
on: (str: string, listener: (...args: any[]) => void): void => {
|
|
|
|
this.rendererToMainEmitter.on(str, listener);
|
|
|
|
},
|
|
|
|
once: (str: string, listener: (...args: any[]) => void): void => {
|
|
|
|
this.rendererToMainEmitter.once(str, listener);
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
// tslint:enable no-any
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = new ElectronFill();
|