diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 00381ed..298e63e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,9 +30,9 @@ Other projects require a similar agreement: jQuery, Firefox, Apache, Node, and m cd $REPO npm install - gulp build + npm run build -The builds will be placed into the `dist/` directory if all goes well. +The builds will be placed into the custom-elements.min.js file if all goes well. 1. Commit your code and make a pull request. diff --git a/DEVELOPING.md b/DEVELOPING.md index 9b5f4f8..f5da492 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -11,10 +11,8 @@ reactions. The source references old versions of the spec. ### To do 1. Implement Node#isConnected - 2. Implement built-in element extension (is=) - 3. Add reaction callback ordering tests - 4. Reorganize tests to be closer to spec structure - 5. Performance tests + 2. Reorganize tests to be closer to spec structure + 3. Performance tests ## Building & Running Tests diff --git a/bower.json b/bower.json index 8238110..56ea4db 100644 --- a/bower.json +++ b/bower.json @@ -16,7 +16,8 @@ "license": "BSD", "ignore": [], "devDependencies": { - "web-component-tester": "^4.0.1", - "webcomponentsjs": "webcomponents/webcomponentsjs#^0.7.22" + "web-component-tester": "6.0.0-prerelease.3", + "webcomponentsjs": "webcomponents/webcomponentsjs#v1", + "es6-promise": "stefanpenner/es6-promise#^4.0.0" } } diff --git a/package.json b/package.json index c2d6567..47f8f0c 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,10 @@ }, "homepage": "http://webcomponents.org", "devDependencies": { + "bower": "^1.8.0", "google-closure-compiler": "^20160822.1.0", "gulp": "^3.8.8", "gulp-sourcemaps": "^1.6.0", - "web-component-tester": "^4.0.1" + "web-component-tester": "6.0.0-prerelease.3" } } diff --git a/src/README.md b/src/README.md index 9b5f4f8..f5da492 100644 --- a/src/README.md +++ b/src/README.md @@ -11,10 +11,8 @@ reactions. The source references old versions of the spec. ### To do 1. Implement Node#isConnected - 2. Implement built-in element extension (is=) - 3. Add reaction callback ordering tests - 4. Reorganize tests to be closer to spec structure - 5. Performance tests + 2. Reorganize tests to be closer to spec structure + 3. Performance tests ## Building & Running Tests diff --git a/src/custom-elements.js b/src/custom-elements.js index 2bdddb9..0e95296 100644 --- a/src/custom-elements.js +++ b/src/custom-elements.js @@ -138,7 +138,7 @@ let Deferred; /** @private {!Map} **/ this._definitions = new Map(); - /** @private {!Map} **/ + /** @private {!Map} **/ this._constructors = new Map(); /** @private {!Map} **/ @@ -182,17 +182,11 @@ let Deferred; 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: + // 2: const nameError = checkValidCustomElementName(name); if (nameError) throw nameError; - // 4, 5: + // 3: // 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 @@ -201,25 +195,44 @@ let Deferred; throw new Error(`An element with name '${name}' is already defined`); } - // 6, 7: + // 4: if (this._constructors.has(constructor)) { throw new Error(`Definition failed for '${name}': ` + `The constructor is already used.`); } - // 8: + // 5: /** @type {string} */ - const localName = name; + let localName = name; + + // 6: + let _extends = options ? options.extends : null; + + // 7: + if (_extends) { + // 7.1: + const extendsNameError = checkValidCustomElementName(options.extends); + if (!extendsNameError) { + throw new Error(`Cannot extend '${options.extends}': A custom element cannot extend a custom element.`); + } + + // 7.2: + const el = document.createElement(options.extends); + if (el instanceof window['HTMLUnknownElement']) { + throw new Error(`Cannot extend '${options.extends}': is not a real HTMLElement`); + } - // 9, 10: We do not support extends currently. + // 7.3: + localName = _extends; + } - // 11, 12, 13: Our define() isn't rentrant-safe + // 8, 9: Our define() isn't rentrant-safe - // 14.1: + // 10.1: /** @type {Object} */ const prototype = constructor.prototype; - // 14.2: + // 10.2: if (typeof prototype !== 'object') { throw new TypeError(`Definition failed for '${name}': ` + `constructor.prototype must be an object`); @@ -237,23 +250,18 @@ let Deferred; return callback; } - // 3, 4: + // 10.3, 10.4: const connectedCallback = getCallback('connectedCallback'); - // 5, 6: const disconnectedCallback = getCallback('disconnectedCallback'); - // Divergence from spec: we always throw if attributeChangedCallback is - // not a function. - - // 7, 9.1: const attributeChangedCallback = getCallback('attributeChangedCallback'); - // 8, 9.2, 9.3: + // 10.5, 10.6: const observedAttributes = (attributeChangedCallback && constructor['observedAttributes']) || []; - // 15: + // 11: /** @type {CustomElementDefinition} */ const definition = { name: name, @@ -265,14 +273,14 @@ let Deferred; observedAttributes: observedAttributes, }; - // 16: - this._definitions.set(localName, definition); - this._constructors.set(constructor, localName); + // 12: + this._definitions.set(name, definition); + this._constructors.set(constructor, definition); - // 17, 18, 19: + // 13, 14, 15: this._upgradeDoc(); - // 20: + // 16: /** @type {Deferred} **/ const deferred = this._whenDefinedMap.get(localName); if (deferred) { @@ -451,6 +459,9 @@ let Deferred; const walker = createTreeWalker(root); do { const node = /** @type {!HTMLElement} */ (walker.currentNode); + if (node.getAttribute('is')) { + node['is'] = node.getAttribute('is'); + } this._addElement(node, visitedNodes); } while (walker.nextNode()) } @@ -465,7 +476,7 @@ let Deferred; visitedNodes.add(element); /** @type {?CustomElementDefinition} */ - const definition = this._definitions.get(element.localName); + const definition = getDefinition(this._definitions, element); if (definition) { if (!element[_upgradedProp]) { this._upgradeElement(element, definition, true); @@ -562,7 +573,7 @@ let Deferred; const node = walker.currentNode; if (node[_upgradedProp] && node[_attachedProp]) { node[_attachedProp] = false; - const definition = this._definitions.get(node.localName); + const definition = getDefinition(this._definitions, node); if (definition && definition.disconnectedCallback) { definition.disconnectedCallback.call(node); } @@ -621,7 +632,7 @@ let Deferred; const target = /** @type {HTMLElement} */(mutation.target); // We should be gaurenteed to have a definition because this mutation // observer is only observing custom elements observedAttributes - const definition = this._definitions.get(target.localName); + const definition = getDefinition(this._definitions, target); const name = /** @type {!string} */(mutation.attributeName); const oldValue = mutation.oldValue; const newValue = target.getAttribute(name); @@ -635,6 +646,21 @@ let Deferred; } } + /** + * @param {!Map} definitions + * @param {!Node|!HTMLElement|null} node + * @return {CustomElementDefinition|null} + */ + function getDefinition(definitions, node) { + const name = typeof node['is'] === 'string' ? node['is'] : node.localName; + const definition = definitions.get(name); + if (definition) { + return definition.localName === node.localName || definition.localName === node['is'] ? definition : null; + } else { + return null; + } + } + // Closure Compiler Exports window['CustomElementRegistry'] = CustomElementRegistry; CustomElementRegistry.prototype['define'] = CustomElementRegistry.prototype.define; @@ -646,37 +672,42 @@ let Deferred; CustomElementRegistry.prototype['_observeRoot'] = CustomElementRegistry.prototype._observeRoot; CustomElementRegistry.prototype['_addImport'] = CustomElementRegistry.prototype._addImport; - // patch window.HTMLElement + patchElement('HTMLElement') + 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 (let index in htmlElementSubclasses) { + patchElement(`HTML${htmlElementSubclasses[Number(index)]}Element`); + } - /** @const */ - const origHTMLElement = window.HTMLElement; - CustomElementRegistry.prototype['nativeHTMLElement'] = origHTMLElement; - /** - * @type {function(new: HTMLElement)} - */ - const newHTMLElement = function HTMLElement() { - const customElements = _customElements(); + function patchElement(varName) { + /** @const */ + const origHTMLElement = window[varName]; + if (!origHTMLElement) { + return; + } + CustomElementRegistry.prototype[`native${varName}`] = origHTMLElement; + /** + * @type {function(new: HTMLElement)} + */ + const newHTMLElement = function() { + const customElements = _customElements(); - // If there's an being upgraded, return that - if (customElements._newInstance) { - const i = customElements._newInstance; - customElements._newInstance = null; - return i; - } - if (this.constructor) { - // Find the tagname of the constructor and create a new element with it - const tagName = customElements._constructors.get(this.constructor); - return _createElement(document, tagName, undefined, false); - } - throw new Error('Unknown constructor. Did you call customElements.define()?'); + // If there's an being upgraded, return that + if (customElements._newInstance) { + const i = customElements._newInstance; + customElements._newInstance = null; + return i; + } + if (this.constructor) { + // Find the tagname of the constructor and create a new element with it + const constructorInfo = customElements._constructors.get(this.constructor); + const options = constructorInfo.name !== constructorInfo.localName ? {is: constructorInfo.name} : undefined; + return _createElement(document, constructorInfo.localName, options, false); + } + throw new Error('Unknown constructor. Did you call customElements.define()?'); + } + window[varName] = newHTMLElement; + window[varName].prototype = origHTMLElement.prototype; } - window.HTMLElement = newHTMLElement; - // By setting the patched HTMLElement's prototype property to the native - // HTMLElement's prototype we make sure that: - // document.createElement('a') instanceof HTMLElement - // works because instanceof uses HTMLElement.prototype, which is on the - // ptototype chain of built-in elements. - window.HTMLElement.prototype = origHTMLElement.prototype; // patch doc.createElement // TODO(justinfagnani): why is the cast neccessary? @@ -697,9 +728,19 @@ let Deferred; */ function _createElement(doc, tagName, options, callConstructor) { const customElements = _customElements(); + let isAttr; + if (options && options.is) { + // We're going to take care of setting the is attribute ourselves + isAttr = options.is; + delete options.is; + } const element = options ? _nativeCreateElement.call(doc, tagName, options) : _nativeCreateElement.call(doc, tagName); - const definition = customElements._definitions.get(tagName.toLowerCase()); + if (isAttr) { + element.setAttribute('is', isAttr); + element.is = isAttr; + } + const definition = getDefinition(customElements._definitions, element); if (definition) { customElements._upgradeElement(element, definition, callConstructor); } @@ -772,7 +813,7 @@ let Deferred; // Bail if this wasn't a fully upgraded custom element if (element[_upgradedProp] == true) { - const definition = _customElements()._definitions.get(element.localName); + const definition = getDefinition(_customElements()._definitions, element); const observedAttributes = definition.observedAttributes; const attributeChangedCallback = definition.attributeChangedCallback; if (attributeChangedCallback && observedAttributes.indexOf(name) >= 0) { diff --git a/src/native-shim.js b/src/native-shim.js index d55bfec..735f3d7 100644 --- a/src/native-shim.js +++ b/src/native-shim.js @@ -61,6 +61,9 @@ (() => { 'use strict'; + // Do nothing if `customElements` does not exist. + if (!window.customElements) return; + const NativeHTMLElement = window.HTMLElement; const nativeDefine = window.customElements.define; const nativeGet = window.customElements.get; diff --git a/tests/html/imports.html b/tests/html/imports.html index 8dbe461..ebf207d 100644 --- a/tests/html/imports.html +++ b/tests/html/imports.html @@ -12,7 +12,7 @@ Custom Elements: imports integration + - + @@ -17,6 +18,12 @@ suite('Native Shim', () => { + // Do nothing if the browser does not natively support custom elements. + if (!window.customElements) { + test.skip('The native shim does not apply to this browser.', () => {}); + return; + } + test('works with createElement()', () => { function ES5Element1() { return HTMLElement.apply(this); diff --git a/tests/index.html b/tests/index.html index 269062d..70005e4 100644 --- a/tests/index.html +++ b/tests/index.html @@ -11,9 +11,10 @@ Custom Elements Tests - + +