Skip to content

Commit

Permalink
#271 Support for Visual Studio Code in Web Browsers.
Browse files Browse the repository at this point in the history
  • Loading branch information
mhutchie committed Jun 13, 2020
1 parent faa39de commit a0d6cc2
Show file tree
Hide file tree
Showing 12 changed files with 191 additions and 22 deletions.
31 changes: 24 additions & 7 deletions src/gitGraphView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,12 +621,12 @@ export class GitGraphView implements vscode.Disposable {
}

if (this.dataSource.isGitExecutableUnknown()) {
body = `<body class="unableToLoad" style="${colorVars}">
body = `<body class="unableToLoad">
<h2>Unable to load Git Graph</h2>
<p class="unableToLoadMessage">${UNABLE_TO_FIND_GIT_MSG}</p>
</body>`;
} else if (numRepos > 0) {
body = `<body style="${colorVars}">
body = `<body>
<div id="view">
<div id="controls">
<span id="repoControl"><span class="unselectable">Repo: </span><div id="repoDropdown" class="dropdown"></div></span>
Expand All @@ -645,10 +645,10 @@ export class GitGraphView implements vscode.Disposable {
</div>
<div id="scrollShadow"></div>
<script nonce="${nonce}">var globalState = ${JSON.stringify(globalState)}, initialState = ${JSON.stringify(initialState)};</script>
<script src="${this.getMediaUri('out.min.js')}"></script>
<script nonce="${nonce}" src="${this.getMediaUri('out.min.js')}"></script>
</body>`;
} else {
body = `<body class="unableToLoad" style="${colorVars}">
body = `<body class="unableToLoad">
<h2>Unable to load Git Graph</h2>
<p class="unableToLoadMessage">No Git repositories were found in the current workspace when it was last scanned by Git Graph.</p>
<p>If your repositories are in subfolders of the open workspace folder(s), make sure you have set the Git Graph Setting "git-graph.maxDepthOfRepoSearch" appropriately (read the <a href="https://github.com/mhutchie/vscode-git-graph/wiki/Extension-Settings#max-depth-of-repo-search" target="_blank">documentation</a> for more information).</p>
Expand All @@ -663,11 +663,11 @@ export class GitGraphView implements vscode.Disposable {
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${this.panel.webview.cspSource} 'unsafe-inline'; script-src ${this.panel.webview.cspSource} 'nonce-${nonce}'; img-src data:;">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${standardiseCspSource(this.panel.webview.cspSource)} 'unsafe-inline'; script-src 'nonce-${nonce}'; img-src data:;">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="${this.getMediaUri('out.min.css')}">
<title>Git Graph</title>
<style>${colorParams}</style>
<style>body{${colorVars}} ${colorParams}</style>
</head>
${body}
</html>`;
Expand Down Expand Up @@ -728,4 +728,21 @@ export class GitGraphView implements vscode.Disposable {
public respondWithAvatar(email: string, image: string) {
this.sendMessage({ command: 'fetchAvatar', email: email, image: image });
}
}
}

/**
* Standardise the CSP Source provided by Visual Studio Code for use with the Webview. It is idempotent unless called with http/https URI's, in which case it keeps only the authority portion of the http/https URI. This is necessary to be compatible with some web browser environments.
* @param cspSource The value provide by Visual Studio Code.
* @returns The standardised CSP Source.
*/
export function standardiseCspSource(cspSource: string) {
if (cspSource.startsWith('http://') || cspSource.startsWith('https://')) {
const pathIndex = cspSource.indexOf('/', 8), queryIndex = cspSource.indexOf('?', 8), fragmentIndex = cspSource.indexOf('#', 8);
let endOfAuthorityIndex = pathIndex;
if (queryIndex > -1 && (queryIndex < endOfAuthorityIndex || endOfAuthorityIndex === -1)) endOfAuthorityIndex = queryIndex;
if (fragmentIndex > -1 && (fragmentIndex < endOfAuthorityIndex || endOfAuthorityIndex === -1)) endOfAuthorityIndex = fragmentIndex;
return endOfAuthorityIndex > -1 ? cspSource.substring(0, endOfAuthorityIndex) : cspSource;
} else {
return cspSource;
}
}
118 changes: 118 additions & 0 deletions tests/gitGraphView.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import * as vscode from './mocks/vscode';
jest.mock('vscode', () => vscode, { virtual: true });

import { standardiseCspSource } from '../src/gitGraphView';

describe('standardiseCspSource', () => {
it('Should not affect vscode-resource scheme-only sources', () => {
// Run
const result = standardiseCspSource('vscode-resource:');

// Assert
expect(result).toBe('vscode-resource:');
});

it('Should not affect file scheme-only sources', () => {
// Run
const result = standardiseCspSource('file:');

// Assert
expect(result).toBe('file:');
});

it('Should not affect http scheme-only sources', () => {
// Run
const result = standardiseCspSource('http:');

// Assert
expect(result).toBe('http:');
});

it('Should not affect https scheme-only sources', () => {
// Run
const result = standardiseCspSource('https:');

// Assert
expect(result).toBe('https:');
});

it('Should not affect file scheme sources', () => {
// Run
const result = standardiseCspSource('file://server');

// Assert
expect(result).toBe('file://server');
});

it('Should not affect http host-only sources', () => {
// Run
const result = standardiseCspSource('http://www.mhutchie.com');

// Assert
expect(result).toBe('http://www.mhutchie.com');
});

it('Should not affect https host-only sources', () => {
// Run
const result = standardiseCspSource('https://www.mhutchie.com');

// Assert
expect(result).toBe('https://www.mhutchie.com');
});

it('Should not affect https host-only IP sources', () => {
// Run
const result = standardiseCspSource('https://192.168.1.101');

// Assert
expect(result).toBe('https://192.168.1.101');
});

it('Should remove the path component from http sources', () => {
// Run
const result = standardiseCspSource('http://www.mhutchie.com/path/to/file');

// Assert
expect(result).toBe('http://www.mhutchie.com');
});

it('Should remove the path component from https sources', () => {
// Run
const result = standardiseCspSource('https://www.mhutchie.com/path/to/file');

// Assert
expect(result).toBe('https://www.mhutchie.com');
});

it('Should remove the path component from https IP sources', () => {
// Run
const result = standardiseCspSource('https://192.168.1.101:8080/path/to/file');

// Assert
expect(result).toBe('https://192.168.1.101:8080');
});

it('Should remove the query from http/https sources', () => {
// Run
const result = standardiseCspSource('https://www.mhutchie.com?query');

// Assert
expect(result).toBe('https://www.mhutchie.com');
});

it('Should remove the fragment from http/https sources', () => {
// Run
const result = standardiseCspSource('https://www.mhutchie.com#fragment');

// Assert
expect(result).toBe('https://www.mhutchie.com');
});

it('Should remove the path, query & fragment from http/https sources', () => {
// Run
const result = standardiseCspSource('https://www.mhutchie.com:443/path/to/file?query#fragment');

// Assert
expect(result).toBe('https://www.mhutchie.com:443');
});
});
6 changes: 4 additions & 2 deletions web/dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,16 +375,18 @@ class CustomSelect {
};
document.addEventListener('click', this.clickHandler, true);

currentElem.addEventListener('keydown', e => {
currentElem.addEventListener('keydown', (e) => {
if (this.open && e.key === 'Tab') {
this.render(false);
} else if (this.open && (e.key === 'Enter' || e.key === 'Escape')) {
this.render(false);
e.stopPropagation();
handledEvent(e);
} else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
this.setSelectedItem(this.selectedItem > 0 ? this.selectedItem - 1 : this.data.options.length - 1);
handledEvent(e);
} else if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
this.setSelectedItem(this.selectedItem < this.data.options.length - 1 ? this.selectedItem + 1 : 0);
handledEvent(e);
}
});

Expand Down
3 changes: 2 additions & 1 deletion web/findWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,13 @@ class FindWidget {
this.inputElem = <HTMLInputElement>document.getElementById('findInput')!;
let keyupTimeout: NodeJS.Timer | null = null;
this.inputElem.addEventListener('keyup', (e) => {
if (e.key === 'Enter' && this.text !== '') {
if ((e.keyCode ? e.keyCode === 13 : e.key === 'Enter') && this.text !== '') {
if (e.shiftKey) {
this.prev();
} else {
this.next();
}
handledEvent(e);
} else {
if (keyupTimeout !== null) clearTimeout(keyupTimeout);
keyupTimeout = setTimeout(() => {
Expand Down
23 changes: 12 additions & 11 deletions web/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1495,7 +1495,7 @@ class GitGraphView {
cols[i].style.width = columnWidths[parseInt(cols[i].dataset.col!)] + 'px';
}
this.tableElem.className = 'fixedLayout';
this.tableElem.style.cssText = '';
this.tableElem.style.removeProperty(CSS_PROP_LIMIT_GRAPH_WIDTH);
this.graph.limitMaxWidth(columnWidths[0] + COLUMN_LEFT_RIGHT_PADDING);
};

Expand Down Expand Up @@ -1526,10 +1526,10 @@ class GitGraphView {
this.graph.limitMaxWidth(maxWidth);
graphWidth = maxWidth;
this.tableElem.className += ' limitGraphWidth';
this.tableElem.style.cssText = '--limitGraphWidth:' + maxWidth + 'px;';
this.tableElem.style.setProperty(CSS_PROP_LIMIT_GRAPH_WIDTH, maxWidth + 'px');
} else {
this.graph.limitMaxWidth(-1);
this.tableElem.style.cssText = '';
this.tableElem.style.removeProperty(CSS_PROP_LIMIT_GRAPH_WIDTH);
}

if (colWidth < Math.max(graphWidth, 64)) {
Expand Down Expand Up @@ -1592,7 +1592,7 @@ class GitGraphView {
});

colHeadersElem.addEventListener('contextmenu', (e: MouseEvent) => {
e.stopPropagation();
handledEvent(e);

const toggleColumnState = (col: number, defaultWidth: number) => {
columnWidths[col] = columnWidths[col] !== COLUMN_HIDDEN ? COLUMN_HIDDEN : columnWidths[0] === COLUMN_AUTO ? COLUMN_AUTO : defaultWidth - COLUMN_LEFT_RIGHT_PADDING;
Expand Down Expand Up @@ -1793,22 +1793,20 @@ class GitGraphView {
}

private observeKeyboardEvents() {
const handledEvent = (e: KeyboardEvent) => {
e.preventDefault();
e.stopPropagation();
};

document.addEventListener('keydown', (e) => {
if (dialog.isOpen()) {
if (e.key === 'Escape') {
dialog.close();
handledEvent(e);
} else if (e.keyCode ? e.keyCode === 13 : e.key === 'Enter') {
// Use keyCode === 13 to detect 'Enter' events if available (for compatibility with IME Keyboards used by Chinese / Japanese / Korean users)
dialog.submit();
handledEvent(e);
}
} else if (contextMenu.isOpen()) {
if (e.key === 'Escape') {
contextMenu.close();
handledEvent(e);
}
} else if (this.expandedCommit !== null && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
let curHashIndex = this.commitLookup[this.expandedCommit.commitHash], newHashIndex = -1;
Expand Down Expand Up @@ -1851,10 +1849,13 @@ class GitGraphView {
} else if (e.key === 'Escape') {
if (this.settingsWidget.isVisible()) {
this.settingsWidget.close();
handledEvent(e);
} else if (this.findWidget.isVisible()) {
this.findWidget.close();
handledEvent(e);
} else if (this.expandedCommit !== null) {
this.closeCommitDetails(true);
handledEvent(e);
}
}
});
Expand Down Expand Up @@ -1963,7 +1964,7 @@ class GitGraphView {

if ((eventElem = <HTMLElement>eventTarget.closest('.gitRef')) !== null) {
// .gitRef was right clicked
e.stopPropagation();
handledEvent(e);
const commitElem = <HTMLElement>eventElem.closest('.commit')!;
const commit = this.getCommitOfElem(commitElem);
if (commit === null) return;
Expand Down Expand Up @@ -2000,7 +2001,7 @@ class GitGraphView {

} else if ((eventElem = <HTMLElement>eventTarget.closest('.commit')) !== null) {
// .commit was right clicked
e.stopPropagation();
handledEvent(e);
const commit = this.getCommitOfElem(eventElem);
if (commit === null) return;

Expand Down
3 changes: 3 additions & 0 deletions web/styles/contextMenu.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
margin:0;
padding:4px 0;
z-index:10;
-webkit-user-select:none;
user-select:none;
}

.contextMenu li{
cursor:default;
-webkit-user-select:none;
user-select:none;
}

Expand Down Expand Up @@ -43,6 +45,7 @@
top:50%;
margin-top:-8px;
left:15px;
-webkit-user-select:none;
user-select:none;
}

Expand Down
4 changes: 4 additions & 0 deletions web/styles/dialog.css
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
padding-top:8px;
text-align:left;
white-space:nowrap;
-webkit-user-select:none;
user-select:none;
}

Expand Down Expand Up @@ -60,6 +61,7 @@
}

.dialogContent .dialogFormCheckbox > label, .dialogContent .dialogFormRadio > label{
-webkit-user-select:none;
user-select:none;
cursor:pointer;
}
Expand Down Expand Up @@ -206,6 +208,7 @@
text-overflow:ellipsis;
white-space:nowrap;
overflow:hidden;
-webkit-user-select:none;
user-select:none;
cursor:pointer;
}
Expand Down Expand Up @@ -252,6 +255,7 @@
background-color:rgba(127,127,127,0.05);
overflow:hidden;
word-wrap:break-word;
-webkit-user-select:none;
user-select:none;
cursor:pointer;
}
Expand Down
4 changes: 4 additions & 0 deletions web/styles/dropdown.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
padding-left:10px;
padding-right:20px;
cursor:pointer;
-webkit-user-select:none;
user-select:none;
white-space:nowrap;
overflow-x:hidden;
Expand Down Expand Up @@ -63,6 +64,7 @@
position:relative;
cursor:pointer;
padding:4px 10px;
-webkit-user-select:none;
user-select:none;
text-overflow:ellipsis;
white-space:nowrap;
Expand Down Expand Up @@ -124,6 +126,7 @@
.dropdownFilter{
position:relative;
padding:3px 3px;
-webkit-user-select:none;
user-select:none;
}
.dropdownFilterInput{
Expand All @@ -142,6 +145,7 @@
.dropdownNoResults{
position:relative;
padding:4px 10px;
-webkit-user-select:none;
user-select:none;
font-style:italic;
}
Loading

0 comments on commit a0d6cc2

Please sign in to comment.