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

Viewer: Lighthouse 2.0 support #2521

Merged
merged 13 commits into from
Jun 22, 2017
2 changes: 2 additions & 0 deletions lighthouse-core/closure/closure-type-checking.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ gulp.task('compile-report', () => {
return gulp.src([
// externs
'closure/third_party/commonjs.js',
'closure/typedefs/viewer-externs.js',

'lib/file-namer.js',
'report/v2/renderer/*.js',
'../lighthouse-viewer/app/src/viewer-ui-features.js',
])

// Ignore `module.exports` and `self.ClassName = ClassName` statements.
Expand Down
23 changes: 23 additions & 0 deletions lighthouse-core/closure/typedefs/viewer-externs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* @license Copyright 2017 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
'use strict';

/**
* Typing externs file needed for Viewer compilation.
* @externs
*/

/**
* @struct
* @constructor
*/
function ReportGenerator() {}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rather than trying to compile ReportGenerator (with its fs use), just use externs for the one method we care about


/**
* @param {!ReportRenderer.ReportJSON} reportJson
* @return {string}
*/
ReportGenerator.prototype.generateReportHtml = function(reportJson) {};
11 changes: 1 addition & 10 deletions lighthouse-core/report/v2/renderer/report-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,14 @@ class ReportRenderer {
/**
* @param {!DOM} dom
* @param {!CategoryRenderer} categoryRenderer
* @param {?ReportUIFeatures=} uiFeatures
*/
constructor(dom, categoryRenderer, uiFeatures = null) {
constructor(dom, categoryRenderer) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reportUIFeatures is removed from the report-renderer constructor, which should make devtools subclassing without it slightly easier cc @paulirish

/** @private {!DOM} */
this._dom = dom;
/** @private {!CategoryRenderer} */
this._categoryRenderer = categoryRenderer;
/** @private {!Document|!Element} */
this._templateContext = this._dom.document();
/** @private {ReportUIFeatures} */
this._uiFeatures = uiFeatures;
}

/**
Expand All @@ -39,12 +36,6 @@ class ReportRenderer {
container.textContent = ''; // Remove previous report.
const element = container.appendChild(this._renderReport(report));

// Hook in JS features and page-level event listeners after the report
// is in the document.
if (this._uiFeatures) {
this._uiFeatures.initFeatures(report);
}

return /** @type {!Element} **/ (element);
}

Expand Down
66 changes: 60 additions & 6 deletions lighthouse-core/report/v2/renderer/report-ui-features.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* the report.
*/

/* globals self URL Blob CustomEvent getFilenamePrefix */
/* globals self URL Blob CustomEvent getFilenamePrefix window */

class ReportUIFeatures {

Expand All @@ -20,9 +20,9 @@ class ReportUIFeatures {
constructor(dom) {
/** @type {!ReportRenderer.ReportJSON} */
this.json; // eslint-disable-line no-unused-expressions
/** @private {!DOM} */
/** @protected {!DOM} */
this._dom = dom;
/** @private {!Document} */
/** @protected {!Document} */
this._document = this._dom.document();
/** @private {boolean} */
this._copyAttempt = false;
Expand Down Expand Up @@ -188,9 +188,7 @@ class ReportUIFeatures {
break;
}
case 'save-html': {
this._resetUIState();

const htmlStr = this._document.documentElement.outerHTML;
const htmlStr = this.getReportHtml();
try {
this._saveFile(new Blob([htmlStr], {type: 'text/html'}));
} catch (/** @type {!Error} */ e) {
Expand All @@ -200,6 +198,14 @@ class ReportUIFeatures {
}
break;
}
case 'open-viewer': {
this.sendJsonReport();
break;
}
case 'save-gist': {
this.saveAsGist();
break;
}
}

this.closeExportDropdown();
Expand All @@ -216,6 +222,34 @@ class ReportUIFeatures {
}
}

/**
* Opens a new tab to the online viewer and sends the local page's JSON results
* to the online viewer using postMessage.
* @protected
*/
sendJsonReport() {
const VIEWER_ORIGIN = 'https://googlechrome.github.io';
const VIEWER_URL = `${VIEWER_ORIGIN}/lighthouse/viewer/`;

// Chrome doesn't allow us to immediately postMessage to a popup right
// after it's created. Normally, we could also listen for the popup window's
// load event, however it is cross-domain and won't fire. Instead, listen
// for a message from the target app saying "I'm open".
window.addEventListener('message', function msgHandler(/** @type {!Event} */ e) {
const messageEvent = /** @type {!MessageEvent<{opened: boolean}>} */ (e);
if (messageEvent.origin !== VIEWER_ORIGIN) {
return;
}

if (messageEvent.data.opened) {
popup.postMessage({lhresults: this.json}, VIEWER_ORIGIN);
window.removeEventListener('message', msgHandler);
}
}.bind(this));

const popup = /** @type {!Window} */ (window.open(VIEWER_URL, '_blank'));
}

/**
* Expands audit details when user prints via keyboard shortcut.
* @param {!Event} e
Expand Down Expand Up @@ -265,9 +299,29 @@ class ReportUIFeatures {
});
}
}

/**
* Returns the html that recreates this report.
* @return {string}
* @protected
*/
getReportHtml() {
this._resetUIState();
return this._document.documentElement.outerHTML;
}

/**
* Save json as a gist. Unimplemented in base UI features.
* @protected
*/
saveAsGist() {
throw new Error('Cannot save as gist from base report');
}

/**
* Downloads a file (blob) using a[download].
* @param {!Blob|!File} blob The file to save.
* @private
*/
_saveFile(blob) {
const filename = getFilenamePrefix({
Expand Down
21 changes: 21 additions & 0 deletions lighthouse-core/report/v2/report-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,27 @@ const REPORT_CSS = fs.readFileSync(__dirname + '/report-styles.css', 'utf8');
const REPORT_TEMPLATES = fs.readFileSync(__dirname + '/templates.html', 'utf8');

class ReportGeneratorV2 {
/**
* @return {string}
*/
static get reportJs() {
return REPORT_JAVASCRIPT;
}

/**
* @return {string}
*/
static get reportCss() {
return REPORT_CSS;
}

/**
* @return {string}
*/
static get reportTemplates() {
return REPORT_TEMPLATES;
}

/**
* Computes the weighted-average of the score of the list of items.
* @param {!Array<{score: number|undefined, weight: number|undefined}} items
Expand Down
8 changes: 6 additions & 2 deletions lighthouse-core/report/v2/report-template.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,15 @@
const dom = new DOM(document);
const detailsRenderer = new DetailsRenderer(dom);
const categoryRenderer = new CategoryRenderer(dom, detailsRenderer);
const features = new ReportUIFeatures(dom);
const renderer = new ReportRenderer(dom, categoryRenderer, features);
const renderer = new ReportRenderer(dom, categoryRenderer);

const container = document.querySelector('main');
renderer.renderReport(window.__LIGHTHOUSE_JSON__, container);

// Hook in JS features and page-level event listeners after the report
// is in the document.
const features = new ReportUIFeatures(dom);
features.initFeatures(window.__LIGHTHOUSE_JSON__);
});

document.addEventListener('lh-analytics', e => {
Expand Down
6 changes: 6 additions & 0 deletions lighthouse-core/report/v2/templates.html
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,10 @@ <h1 class="leftnav__header__title">Lighthouse</h1>
background-size: 16px;
background-position: 9px 50%;
}
/* save-as-gist option hidden in report */
.lh-export--gist {
display: none;
}
.lh-config {
display: flex;
}
Expand Down Expand Up @@ -321,6 +325,8 @@ <h1 class="leftnav__header__title">Lighthouse</h1>
<a href="#" class="report-icon report-icon--copy" data-action="copy">Copy JSON</a>
<a href="#" class="report-icon report-icon--download" data-action="save-html">Save as HTML</a>
<a href="#" class="report-icon report-icon--download" data-action="save-json">Save as JSON</a>
<a href="#" class="report-icon report-icon--open lh-export--viewer" data-action="open-viewer">Open in Viewer</a>
<a href="#" class="report-icon report-icon--open lh-export--gist" data-action="save-gist">Save as Gist</a>
</div>
</div>
</div>
Expand Down
17 changes: 10 additions & 7 deletions lighthouse-viewer/app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
-->

<!doctype html>
<html data-report-context="viewer">
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
Expand All @@ -15,11 +15,12 @@
<meta name="theme-color" content="#304ffe">
<link rel="stylesheet" href="styles/viewer.css">
</head>
<body>
<body class="lh-root lh-vars">
<div hidden>%%LIGHTHOUSE_TEMPLATES%%</div>

<div class="drop_zone"></div>

<output></output>
<main><!-- report populated here --></main>

<div class="viewer-placeholder">
<div class="viewer-placeholder-inner">
Expand All @@ -36,11 +37,13 @@ <h1 class="viewer-placeholder__heading">Lighthouse Report Viewer</h1>
</div>
</div>

<div class="log-wrapper">
<div id="log"></div>
</div>
<div id="lh-log"></div>

<input id="hidden-file-input" type="file" hidden accept="application/json">

<script src="src/main.js" async></script>
<script src="https://www.gstatic.com/firebasejs/4.1.2/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/4.1.2/firebase-auth.js"></script>
<script src="src/viewer.js"></script>
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
Expand Down
63 changes: 63 additions & 0 deletions lighthouse-viewer/app/src/drag-and-drop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* @license Copyright 2017 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
'use strict';

/**
* Manages drag and drop file input for the page.
*/
class DragAndDrop {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

slightly simpler than the old FileUploader. Handles only drag and dropping of files (the hidden input is now just in the page and its handler and readFile are now in the viewer main class)

/**
* @param {function(!File)} fileHandlerCallback Invoked when the user chooses a new file.
*/
constructor(fileHandlerCallback) {
this._dropZone = document.querySelector('.drop_zone');
this._fileHandlerCallback = fileHandlerCallback;
this._dragging = false;

this._addListeners();
}

_addListeners() {
// The mouseleave event is more reliable than dragleave when the user drops
// the file outside the window.
document.addEventListener('mouseleave', _ => {
if (!this._dragging) {
return;
}
this._resetDraggingUI();
});

document.addEventListener('dragover', e => {
e.stopPropagation();
e.preventDefault();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as an aside, what do each of these e.stopProgagation/preventDefaults do/why are they necessary? I built another drag/drop recently and I got away without some of them, but I don't know what exactly is required to pull this off...

cc: @ebidel for the original impl :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This dude told me to do it: https://www.html5rocks.com/en/tutorials/dnd/basics/

TBH, I don't think we need them anymore in a dragover handler. Back in the day, there were browser quirks and I noted that e.preventDefault() was needed to even start a drag. But we're also dragging files (as opposed to links or something in-page) and at the document level. MDN mentions e.preventDefault in dragover, but I betcha that's not accurate.

Anyways, doesn't hurt to be explicit the method is handing the event.

e.dataTransfer.dropEffect = 'copy'; // Explicitly show as copy action.
});

document.addEventListener('dragenter', _ => {
this._dropZone.classList.add('dropping');
this._dragging = true;
});

document.addEventListener('drop', e => {
e.stopPropagation();
e.preventDefault();

this._resetDraggingUI();

// Note, this ignores multiple files in the drop, only taking the first.
this._fileHandlerCallback(e.dataTransfer.files[0]);
});
}

_resetDraggingUI() {
this._dropZone.classList.remove('dropping');
this._dragging = false;
}
}

if (typeof module !== 'undefined' && module.exports) {
module.exports = DragAndDrop;
}
Loading