x/tools/cmd/heapview: add a heading to the page

This CL's purpose is to introduce the dependency on the HTML Custom
Elements polyfill. Like we've done so far, I'm trying to keep
dependencies light by using current or polyfilling future webcomponents
standards.

Change-Id: I11d14db367b697cdd527fb66b9d7d160ac244b78
Reviewed-on: https://go-review.googlesource.com/25494
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
This commit is contained in:
Michael Matloob 2016-08-04 14:24:04 -04:00
parent 7ef02fdb22
commit 337c0124d7
8 changed files with 684 additions and 2 deletions

View File

@ -2,6 +2,49 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
/**
* A hamburger menu element.
*/
class HamburgerElement extends HTMLElement {
connectedCallback() {
this.innerHTML = '&#9776'; // Unicode character for hamburger menu.
}
}
window.customElements.define('heap-hamburger', HamburgerElement);
/**
* A heading for the page with a hamburger menu and a title.
*/
export class HeadingElement extends HTMLElement {
connectedCallback() {
this.style.display = 'block';
this.style.backgroundColor = '#2196F3';
this.style.webkitUserSelect = 'none';
this.style.cursor = 'default';
this.style.color = '#FFFFFF';
this.style.padding = '10px';
this.innerHTML = `
<div style="margin:0px; font-size:2em"><heap-hamburger></heap-hamburger> Go Heap Viewer</div>
`;
}
}
window.customElements.define('heap-heading', HeadingElement);
/**
* Reset body's margin and padding, and set font.
*/
function clearStyle() {
document.head.innerHTML += `
<style>
* {font-family: Roboto,Helvetica}
body {margin: 0px; padding:0px}
</style>
`;
}
export function main() { export function main() {
document.title = 'Go Heap Viewer'; document.title = 'Go Heap Viewer';
clearStyle();
document.body.appendChild(new HeadingElement());
} }

View File

@ -4,9 +4,14 @@
import {main} from './main'; import {main} from './main';
describe("main", () => { describe('main', () => {
it('sets the document\'s title', () => { it('sets the document\'s title', () => {
main(); main();
expect(document.title).toBe('Go Heap Viewer'); expect(document.title).toBe('Go Heap Viewer');
}); });
it('has a heading', () => {
main();
expect(document.querySelector('heap-heading')).toBeDefined();
});
}); });

View File

@ -29,6 +29,7 @@
}, },
"scripts": { "scripts": {
"test": "karma start testing/karma.conf.js", "test": "karma start testing/karma.conf.js",
"format": "find . | grep '\\(test_main\\.js\\|\\.ts\\)$' | xargs clang-format -i" "format": "find . | grep '\\(test_main\\.js\\|\\.ts\\)$' | xargs clang-format -i",
"lint": "tslint --project ."
} }
} }

View File

@ -7,6 +7,7 @@ module.exports = config => {
frameworks: ['jasmine'], frameworks: ['jasmine'],
basePath: '../../../..', basePath: '../../../..',
files: [ files: [
'third_party/webcomponents/customelements.js',
'third_party/typescript/typescript.js', 'third_party/typescript/typescript.js',
'third_party/moduleloader/moduleloader.js', 'third_party/moduleloader/moduleloader.js',
'cmd/heapview/client/testing/test_main.js', 'cmd/heapview/client/testing/test_main.js',

View File

@ -4,6 +4,9 @@
// Configure module loader. // Configure module loader.
System.transpiler = 'typescript' System.transpiler = 'typescript'
System.typescriptOptions = {
target: ts.ScriptTarget.ES2015
};
System.locate = (load) => load.name + '.ts'; System.locate = (load) => load.name + '.ts';
// Determine set of test files. // Determine set of test files.

View File

@ -18,10 +18,12 @@ import (
var port = flag.Int("port", 8080, "service port") var port = flag.Int("port", 8080, "service port")
var index = `<!DOCTYPE html> var index = `<!DOCTYPE html>
<script src="js/customelements.js"></script>
<script src="js/typescript.js"></script> <script src="js/typescript.js"></script>
<script src="js/moduleloader.js"></script> <script src="js/moduleloader.js"></script>
<script> <script>
System.transpiler = 'typescript'; System.transpiler = 'typescript';
System.typescriptOptions = {target: ts.ScriptTarget.ES2015};
System.locate = (load) => load.name + '.ts'; System.locate = (load) => load.name + '.ts';
</script> </script>
<script type="module"> <script type="module">
@ -55,6 +57,9 @@ var addHandlers = func() {
http.HandleFunc("/js/moduleloader.js", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/js/moduleloader.js", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, filepath.Join(toolsDir(), "third_party/moduleloader/moduleloader.js")) http.ServeFile(w, r, filepath.Join(toolsDir(), "third_party/moduleloader/moduleloader.js"))
}) })
http.HandleFunc("/js/customelements.js", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, filepath.Join(toolsDir(), "third_party/webcomponents/customelements.js"))
})
// Serve index.html using html string above. // Serve index.html using html string above.
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {

27
third_party/webcomponents/LICENSE vendored Normal file
View File

@ -0,0 +1,27 @@
Copyright (c) 2015 The Polymer Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,597 @@
/**
* @license
* Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
* 2.3
* http://w3c.github.io/webcomponents/spec/custom/#dfn-element-definition
* @typedef {{
* name: string,
* localName: string,
* constructor: Function,
* connectedCallback: Function,
* disconnectedCallback: Function,
* attributeChangedCallback: Function,
* observedAttributes: Array<string>,
* }}
*/
var CustomElementDefinition;
(function() {
'use strict';
var doc = document;
var win = window;
// name validation
// https://html.spec.whatwg.org/multipage/scripting.html#valid-custom-element-name
/**
* @const
* @type {Array<string>}
*/
var reservedTagList = [
'annotation-xml',
'color-profile',
'font-face',
'font-face-src',
'font-face-uri',
'font-face-format',
'font-face-name',
'missing-glyph',
];
/** @const */
var customNameValidation = /^[a-z][.0-9_a-z]*-[\-.0-9_a-z]*$/;
function isValidCustomElementName(name) {
return customNameValidation.test(name) && reservedTagList.indexOf(name) === -1;
}
function createTreeWalker(root) {
// IE 11 requires the third and fourth arguments be present. If the third
// arg is null, it applies the default behaviour. However IE also requires
// the fourth argument be present even though the other browsers ignore it.
return doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, null, false);
}
function isElement(node) {
return node.nodeType === Node.ELEMENT_NODE
}
/**
* A registry of custom element definitions.
*
* See https://html.spec.whatwg.org/multipage/scripting.html#customelementsregistry
*
* @constructor
* @property {boolean} polyfilled Whether this registry is polyfilled
* @property {boolean} enableFlush Set to true to enable the flush() method
* to work. This should only be done for tests, as it causes a memory leak.
*/
function CustomElementsRegistry() {
/** @private {Map<string, CustomElementDefinition>} **/
this._definitions = new Map();
/** @private {Map<Function, CustomElementDefinition>} **/
this._constructors = new Map();
this._whenDefinedMap = new Map();
/** @private {Set<MutationObserver>} **/
this._observers = new Set();
/** @private {MutationObserver} **/
this._attributeObserver =
new MutationObserver(this._handleAttributeChange.bind(this));
/** @private {HTMLElement} **/
this._newInstance = null;
this.polyfilled = true;
this.enableFlush = false;
this._observeRoot(document);
}
CustomElementsRegistry.prototype = {
// HTML spec part 4.13.4
// https://html.spec.whatwg.org/multipage/scripting.html#dom-customelementsregistry-define
define: function(name, constructor, options) {
name = name.toString().toLowerCase();
// 1:
if (typeof constructor !== 'function') {
throw new TypeError('constructor must be a Constructor');
}
// 2. If constructor is an interface object whose corresponding interface
// either is HTMLElement or has HTMLElement in its set of inherited
// interfaces, throw a TypeError and abort these steps.
//
// It doesn't appear possible to check this condition from script
// 3:
if (!isValidCustomElementName(name)) {
throw new SyntaxError(`The element name '${name}' is not valid.`);
}
// 4, 5:
// Note: we don't track being-defined names and constructors because
// define() isn't normally reentrant. The only time user code can run
// during define() is when getting callbacks off the prototype, which
// would be highly-unusual. We can make define() reentrant-safe if needed.
if (this._definitions.has(name)) {
throw new Error(`An element with name '${name}' is already defined`);
}
// 6, 7:
if (this._constructors.has(constructor)) {
throw new Error(`Definition failed for '${name}': ` +
`The constructor is already used.`);
}
// 8:
var localName = name;
// 9, 10: We do not support extends currently.
// 11, 12, 13: Our define() isn't rentrant-safe
// 14.1:
var prototype = constructor.prototype;
// 14.2:
if (typeof prototype !== 'object') {
throw new TypeError(`Definition failed for '${name}': ` +
`constructor.prototype must be an object`);
}
function getCallback(calllbackName) {
var callback = prototype[calllbackName];
if (callback !== undefined && typeof callback !== 'function') {
throw new Error(`${localName} '${calllbackName}' is not a Function`);
}
return callback;
}
// 3, 4:
var connectedCallback = getCallback('connectedCallback');
// 5, 6:
var disconnectedCallback = getCallback('disconnectedCallback');
// Divergence from spec: we always throw if attributeChangedCallback is
// not a function, and always get observedAttributes.
// 7, 9.1:
var attributeChangedCallback = getCallback('attributeChangedCallback');
// 8, 9.2, 9.3:
var observedAttributes = constructor['observedAttributes'] || [];
// 15:
// @type {CustomElementDefinition}
var definition = {
name: name,
localName: localName,
constructor: constructor,
connectedCallback: connectedCallback,
disconnectedCallback: disconnectedCallback,
attributeChangedCallback: attributeChangedCallback,
observedAttributes: observedAttributes,
};
// 16:
this._definitions.set(localName, definition);
this._constructors.set(constructor, localName);
// 17, 18, 19:
this._addNodes(doc.childNodes);
// 20:
var deferred = this._whenDefinedMap.get(localName);
if (deferred) {
deferred.resolve(undefined);
this._whenDefinedMap.delete(localName);
}
},
/**
* Returns the constructor defined for `name`, or `null`.
*
* @param {string} name
* @return {Function|undefined}
*/
get: function(name) {
// https://html.spec.whatwg.org/multipage/scripting.html#custom-elements-api
var def = this._definitions.get(name);
return def ? def.constructor : undefined;
},
/**
* Returns a `Promise` that resolves when a custom element for `name` has
* been defined.
*
* @param {string} name
* @return {Promise}
*/
whenDefined: function(name) {
// https://html.spec.whatwg.org/multipage/scripting.html#dom-customelementsregistry-whendefined
if (!customNameValidation.test(name)) {
return Promise.reject(
new SyntaxError(`The element name '${name}' is not valid.`));
}
if (this._definitions.has(name)) {
return Promise.resolve();
}
var deferred = {
promise: null,
};
deferred.promise = new Promise(function(resolve, _) {
deferred.resolve = resolve;
});
this._whenDefinedMap.set(name, deferred);
return deferred.promise;
},
/**
* Causes all pending mutation records to be processed, and thus all
* customization, upgrades and custom element reactions to be called.
* `enableFlush` must be true for this to work. Only use during tests!
*/
flush: function() {
if (this.enableFlush) {
console.warn("flush!!!");
this._observers.forEach(function(observer) {
this._handleMutations(observer.takeRecords());
}, this);
}
},
_setNewInstance: function(instance) {
this._newInstance = instance;
},
/**
* Observes a DOM root for mutations that trigger upgrades and reactions.
* @private
*/
_observeRoot: function(root) {
root.__observer = new MutationObserver(this._handleMutations.bind(this));
root.__observer.observe(root, {childList: true, subtree: true});
if (this.enableFlush) {
// this is memory leak, only use in tests
this._observers.add(root.__observer);
}
},
/**
* @private
*/
_unobserveRoot: function(root) {
if (root.__observer) {
root.__observer.disconnect();
root.__observer = null;
if (this.enableFlush) {
this._observers.delete(root.__observer);
}
}
},
/**
* @private
*/
_handleMutations: function(mutations) {
for (var i = 0; i < mutations.length; i++) {
var mutation = mutations[i];
if (mutation.type === 'childList') {
// Note: we can't get an ordering between additions and removals, and
// so might diverge from spec reaction ordering
this._addNodes(mutation.addedNodes);
this._removeNodes(mutation.removedNodes);
}
}
},
/**
* @param {NodeList} nodeList
* @private
*/
_addNodes: function(nodeList) {
for (var i = 0; i < nodeList.length; i++) {
var root = nodeList[i];
if (!isElement(root)) {
continue;
}
// Since we're adding this node to an observed tree, we can unobserve
this._unobserveRoot(root);
var walker = createTreeWalker(root);
do {
var node = /** @type {HTMLElement} */ (walker.currentNode);
var definition = this._definitions.get(node.localName);
if (definition) {
if (!node.__upgraded) {
this._upgradeElement(node, definition, true);
}
if (node.__upgraded && !node.__attached) {
node.__attached = true;
if (definition && definition.connectedCallback) {
definition.connectedCallback.call(node);
}
}
}
if (node.shadowRoot) {
// TODO(justinfagnani): do we need to check that the shadowRoot
// is observed?
this._addNodes(node.shadowRoot.childNodes);
}
if (node.tagName === 'LINK') {
var onLoad = (function() {
var link = node;
return function() {
link.removeEventListener('load', onLoad);
this._observeRoot(link.import);
this._addNodes(link.import.childNodes);
}.bind(this);
}).bind(this)();
if (node.import) {
onLoad();
} else {
node.addEventListener('load', onLoad);
}
}
} while (walker.nextNode())
}
},
/**
* @param {NodeList} nodeList
* @private
*/
_removeNodes: function(nodeList) {
for (var i = 0; i < nodeList.length; i++) {
var root = nodeList[i];
if (!isElement(root)) {
continue;
}
// Since we're detatching this element from an observed root, we need to
// reobserve it.
// TODO(justinfagnani): can we do this in a microtask so we don't thrash
// on creating and destroying MutationObservers on batch DOM mutations?
this._observeRoot(root);
var walker = createTreeWalker(root);
do {
var node = walker.currentNode;
if (node.__upgraded && node.__attached) {
node.__attached = false;
var definition = this._definitions.get(node.localName);
if (definition && definition.disconnectedCallback) {
definition.disconnectedCallback.call(node);
}
}
} while (walker.nextNode())
}
},
/**
* Upgrades or customizes a custom element.
*
* @param {HTMLElement} element
* @param {CustomElementDefinition} definition
* @param {boolean} callConstructor
* @private
*/
_upgradeElement: function(element, definition, callConstructor) {
var prototype = definition.constructor.prototype;
element.__proto__ = prototype;
if (callConstructor) {
this._setNewInstance(element);
element.__upgraded = true;
new (definition.constructor)();
console.assert(this._newInstance == null);
}
var observedAttributes = definition.observedAttributes;
if (definition.attributeChangedCallback && observedAttributes.length > 0) {
this._attributeObserver.observe(element, {
attributes: true,
attributeOldValue: true,
attributeFilter: observedAttributes,
});
// Trigger attributeChangedCallback for existing attributes.
// https://html.spec.whatwg.org/multipage/scripting.html#upgrades
for (var i = 0; i < observedAttributes.length; i++) {
var name = observedAttributes[i];
if (element.hasAttribute(name)) {
var value = element.getAttribute(name);
element.attributeChangedCallback(name, null, value);
}
}
}
},
/**
* @private
*/
_handleAttributeChange: function(mutations) {
for (var i = 0; i < mutations.length; i++) {
var mutation = mutations[i];
if (mutation.type === 'attributes') {
var name = mutation.attributeName;
var oldValue = mutation.oldValue;
var target = mutation.target;
var newValue = target.getAttribute(name);
var namespace = mutation.attributeNamespace;
target['attributeChangedCallback'](name, oldValue, newValue, namespace);
}
}
},
}
// Closure Compiler Exports
window['CustomElementsRegistry'] = CustomElementsRegistry;
CustomElementsRegistry.prototype['define'] = CustomElementsRegistry.prototype.define;
CustomElementsRegistry.prototype['get'] = CustomElementsRegistry.prototype.get;
CustomElementsRegistry.prototype['whenDefined'] = CustomElementsRegistry.prototype.whenDefined;
CustomElementsRegistry.prototype['flush'] = CustomElementsRegistry.prototype.flush;
CustomElementsRegistry.prototype['polyfilled'] = CustomElementsRegistry.prototype.polyfilled;
CustomElementsRegistry.prototype['enableFlush'] = CustomElementsRegistry.prototype.enableFlush;
// patch window.HTMLElement
var origHTMLElement = win.HTMLElement;
win.HTMLElement = function HTMLElement() {
var customElements = win['customElements'];
if (customElements._newInstance) {
var i = customElements._newInstance;
customElements._newInstance = null;
return i;
}
if (this.constructor) {
var tagName = customElements._constructors.get(this.constructor);
return doc._createElement(tagName, false);
}
throw new Error('unknown constructor. Did you call customElements.define()?');
}
win.HTMLElement.prototype = Object.create(origHTMLElement.prototype);
Object.defineProperty(win.HTMLElement.prototype, 'constructor', {value: win.HTMLElement});
// patch all built-in subclasses of HTMLElement to inherit from the new HTMLElement
// See https://html.spec.whatwg.org/multipage/indices.html#element-interfaces
/** @const */
var htmlElementSubclasses = [
'Button',
'Canvas',
'Data',
'Head',
'Mod',
'TableCell',
'TableCol',
'Anchor',
'Area',
'Base',
'Body',
'BR',
'DataList',
'Details',
'Dialog',
'Div',
'DList',
'Embed',
'FieldSet',
'Form',
'Heading',
'HR',
'Html',
'IFrame',
'Image',
'Input',
'Keygen',
'Label',
'Legend',
'LI',
'Link',
'Map',
'Media',
'Menu',
'MenuItem',
'Meta',
'Meter',
'Object',
'OList',
'OptGroup',
'Option',
'Output',
'Paragraph',
'Param',
'Picture',
'Pre',
'Progress',
'Quote',
'Script',
'Select',
'Slot',
'Source',
'Span',
'Style',
'TableCaption',
'Table',
'TableRow',
'TableSection',
'Template',
'TextArea',
'Time',
'Title',
'Track',
'UList',
'Unknown',
];
for (var i = 0; i < htmlElementSubclasses.length; i++) {
var ctor = window['HTML' + htmlElementSubclasses[i] + 'Element'];
if (ctor) {
ctor.prototype.__proto__ = win.HTMLElement.prototype;
}
}
// patch doc.createElement
var rawCreateElement = doc.createElement;
doc._createElement = function(tagName, callConstructor) {
var customElements = win['customElements'];
var element = rawCreateElement.call(doc, tagName);
var definition = customElements._definitions.get(tagName.toLowerCase());
if (definition) {
customElements._upgradeElement(element, definition, callConstructor);
}
customElements._observeRoot(element);
return element;
};
doc.createElement = function(tagName) {
return doc._createElement(tagName, true);
}
// patch doc.createElementNS
var HTMLNS = 'http://www.w3.org/1999/xhtml';
var _origCreateElementNS = doc.createElementNS;
doc.createElementNS = function(namespaceURI, qualifiedName) {
if (namespaceURI === 'http://www.w3.org/1999/xhtml') {
return doc.createElement(qualifiedName);
} else {
return _origCreateElementNS.call(document, namespaceURI, qualifiedName);
}
};
// patch Element.attachShadow
var _origAttachShadow = Element.prototype['attachShadow'];
if (_origAttachShadow) {
Object.defineProperty(Element.prototype, 'attachShadow', {
value: function(options) {
var root = _origAttachShadow.call(this, options);
var customElements = win['customElements'];
customElements._observeRoot(root);
return root;
},
});
}
/** @type {CustomElementsRegistry} */
window['customElements'] = new CustomElementsRegistry();
})();