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

Custom elements support in domino #96

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .jshintrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"predef": [ "describe", "specify", "it" ],
"predef": [ "describe", "specify", "it" , "Promise", "Symbol"],
"node": true,
"bitwise": true,
"eqeqeq": true,
Expand All @@ -14,5 +14,6 @@
"proto": true,
"strict": true,
"undef": true,
"unused": "vars"
"unused": "vars",
"esversion": 6
}
155 changes: 155 additions & 0 deletions lib/CustomElementRegistry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
"use strict";
var xml = require('./xmlnames');

var forbiddenNames = [
'annotation-xml',
'color-profile',
'font-face',
'font-face-src',
'font-face-uri',
'font-face-format',
'font-face-name',
'missing-glyph'];

var forbiddenExtendsNames = [
'bgsound',
'blink',
'isindex',
'multicol',
'nextid',
'spacer',
'elementnametobeunknownelement'
];

function isInvalidCustomElementName(name) {
return !name || !xml.isValidQName(name) || name.indexOf('-') < 0 || forbiddenNames.indexOf(name) > -1 || name.toLowerCase() !== name;
}

function CustomElementRegistry(registry, document) {

Choose a reason for hiding this comment

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

I am totally new to domino, but shouldn't you just use Class?

Copy link
Author

Choose a reason for hiding this comment

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

I'm also new :) but as I see domino should work with node 0.8 so in code the class syntax is not used, unfortunately, I had to use class syntax in one place and I do not know how to avoid it :(

Choose a reason for hiding this comment

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

Haven't noticed anything regarding this at package.json or in the readme.
@fgnass
Though it makes sense to have engine key in package json. Semantic increment of version shouldn't hurt anyone since Classes supported from 4th version.

Copy link
Author

Choose a reason for hiding this comment

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

@rootical
Please look at travis :(

this._parent = registry;
this._document = document;
this._registry = {};
this._clonesRegistry = {};
this._options = {};
this._promises = {};
}

/**
* @param {string} name
* @param {Function} constructor
* @param options
*/
CustomElementRegistry.prototype.define = function(name, constructor, options) {
var err;
if (!constructor) {
err = new TypeError('Provide name and constructor');
throw err;
}

if (!constructor) throw new TypeError('Provide constructor');
if (typeof constructor !== 'function') throw new TypeError('Constructor have to be a function');
if (!constructor.prototype) throw new TypeError('Constructor');
if (typeof constructor.prototype === 'string') throw new TypeError('Constructor prototype is string');

if (isInvalidCustomElementName(name)) {
err = new SyntaxError('Invalid name');
err.code = 12;
throw err;
}

if (this._registry[name]) {
err = new Error('Name already used');
err.name = 'NOT_SUPPORTED_ERR';
err.code = 9;
throw err;
}

var _registry = this._registry;
Object.keys(this._registry).forEach(function(_name) {
if (constructor === _registry[_name]) {
err = new Error('Constructor already used');
err.name = 'NOT_SUPPORTED_ERR';
err.code = 9;
throw err;
}
});

if (options && options.extends && (!isInvalidCustomElementName(options.extends) || forbiddenExtendsNames.indexOf(options.extends > -1))) {
err = new Error('Extend have to be build in type');
err.name = 'NOT_SUPPORTED_ERR';
err.code = 9;
throw err;
}

['connectedCallback',
'disconnectedCallback',
'adoptedCallback',
'attributeChangedCallback'].forEach(function(prop) {
var type = typeof constructor.prototype[prop];
if (type !== 'undefined' && type !== 'function') {
throw new TypeError(name + ' have to be function');
}
});

this._registry[name] = constructor;


// we have to clone class to fallow the spec (after define observedAttributes and attributeChangedCallback mutation
// do not have effect)
this._clonesRegistry[name] = class extends constructor {};

if (constructor.observedAttributes) {
let observedAttributes;
if (Array.isArray(constructor.observedAttributes)) {
observedAttributes = constructor.observedAttributes.slice();
} else if (constructor.observedAttributes && constructor.observedAttributes[Symbol.iterator]) {
observedAttributes = Array.from(constructor.observedAttributes[Symbol.iterator]());
}
Object.defineProperty(this._clonesRegistry[name], 'observedAttributes', {
get: function() {
return observedAttributes;
}
});
}

this._clonesRegistry[name].prototype.attributeChangedCallback = constructor.prototype.attributeChangedCallback;
this._clonesRegistry[name].prototype.connectedCallback = constructor.prototype.connectedCallback;
this._clonesRegistry[name].prototype.disconnectedCallback = constructor.prototype.disconnectedCallback;

this._options[name] = options;
if (this._promises[name]) {
this._promises[name].resolve();
}

if (this._document) {
// TODO apply new item to existing content
}

};

CustomElementRegistry.prototype.get = function(name) {
if (this._clonesRegistry[name]) {
return this._clonesRegistry[name];
} else if (this._parent) {
return this._parent.get(name);
}
};

CustomElementRegistry.prototype.whenDefined = function(name) {
if (this._promises[name]) {
return this._promises[name];
} else if (this._registry[name]) {
return Promise.resolve();
} else {
var resolve;
var promise = new Promise(function (res){
resolve = res;
});
promise.resolve = resolve;
this._promises[name] = promise;
return promise;

}
};

module.exports = CustomElementRegistry;
8 changes: 5 additions & 3 deletions lib/DOMImplementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ var xml = require('./xmlnames');

// Each document must have its own instance of the domimplementation object
// Even though these objects have no state
function DOMImplementation() {}
function DOMImplementation(customElementRegistry) {
this._customElementRegistry = customElementRegistry;
}


// Feature/version pairs that DOMImplementation.hasFeature() returns
Expand Down Expand Up @@ -41,7 +43,7 @@ DOMImplementation.prototype = {
// namespace and doctype are propertly set. See this thread:
// http://lists.w3.org/Archives/Public/www-dom/2011AprJun/0132.html
//
var d = new Document(false, null);
var d = new Document(false, null, this._customElementRegistry);
var e;

if (qualifiedName)
Expand All @@ -60,7 +62,7 @@ DOMImplementation.prototype = {
},

createHTMLDocument: function createHTMLDocument(titleText) {
var d = new Document(true, null);
var d = new Document(true, null, this._customElementRegistry);
d.appendChild(new DocumentType('html'));
var html = d.createElement('html');
d.appendChild(html);
Expand Down
11 changes: 8 additions & 3 deletions lib/Document.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,18 @@ var html = require('./htmlelts');
var svg = require('./svg');
var utils = require('./utils');
var MUTATE = require('./MutationConstants');
var CustomElementsRegistry = require("./CustomElementRegistry");
var ce = require("./customElements");
var NAMESPACE = utils.NAMESPACE;
var isApiWritable = require("./config").isApiWritable;

function Document(isHTML, address) {
function Document(isHTML, address, customElementsRegistry) {
this.nodeType = Node.DOCUMENT_NODE;
this.isHTML = isHTML;
this._address = address || 'about:blank';
this.readyState = 'loading';
this.implementation = new DOMImplementation();
this.implementation = new DOMImplementation(customElementsRegistry);
this._customElementRegistry = new CustomElementsRegistry(customElementsRegistry, this);

// DOMCore says that documents are always associated with themselves
this.ownerDocument = null; // ... but W3C tests expect null
Expand Down Expand Up @@ -618,7 +621,7 @@ Document.prototype = Object.create(Node.prototype, {
_templateDoc: { get: function() {
if (!this._templateDocCache) {
// "associated inert template document"
var newDoc = new Document(this.isHTML, this._address);
var newDoc = new Document(this.isHTML, this._address, this._customElementRegistry);
this._templateDocCache = newDoc._templateDocCache = newDoc;
}
return this._templateDocCache;
Expand Down Expand Up @@ -689,6 +692,7 @@ function root(n) {
// into the document
if (n._roothook) n._roothook();
}
ce.notifyRooted(n);
}

function uproot(n) {
Expand All @@ -699,6 +703,7 @@ function uproot(n) {
}
n.ownerDocument._nodes[n._nid] = undefined;
n._nid = undefined;
ce.notifyUpRooted(n);
}

function recursivelyRoot(node) {
Expand Down
10 changes: 10 additions & 0 deletions lib/Element.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ var DOMTokenList = require('./DOMTokenList');
var select = require('./select');
var ChildNode = require('./ChildNode');
var NonDocumentTypeChildNode = require('./NonDocumentTypeChildNode');
var ce = require('./customElements');

function Element(doc, localName, namespaceURI, prefix) {
this.nodeType = Node.ELEMENT_NODE;
Expand Down Expand Up @@ -531,6 +532,8 @@ Element.prototype = Object.create(Node.prototype, {

// Mutation event
if (this.rooted) this.ownerDocument.mutateRemoveAttr(attr);
// CEv1
ce.notifyCustomElementsSetAttrCallback(this, qname, attr.value, null);
}},

removeAttributeNS: { value: function removeAttributeNS(ns, lname) {
Expand All @@ -552,6 +555,8 @@ Element.prototype = Object.create(Node.prototype, {
attr.onchange(this, attr.localName, attr.value, null);
// Mutation event
if (this.rooted) this.ownerDocument.mutateRemoveAttr(attr);
// CEv1
ce.notifyCustomElementsSetAttrCallback(this, lname, attr.value, null, ns);
}},

// This 'raw' version of getAttribute is used by the getter functions
Expand Down Expand Up @@ -647,6 +652,10 @@ Element.prototype = Object.create(Node.prototype, {
// the content attribute 'class'.
className: attributes.property({name: 'class'}),

// Define getters and setters for a 'contentEditable' property that reflects
// the content attribute 'contenteditable'.
contentEditable: attributes.property({name: 'contenteditable'}),

classList: { get: function() {
var self = this;
if (this._classList) {
Expand Down Expand Up @@ -749,6 +758,7 @@ Attr.prototype = {
// Generate a mutation event if the element is rooted
if (this.ownerElement.rooted)
this.ownerElement.ownerDocument.mutateAttr(this, oldval);
ce.notifyCustomElementsSetAttrCallback(this.ownerElement, this.localName, oldval, value, this.namespaceURI);
},

// Legacy aliases (see gh#70 and https://dom.spec.whatwg.org/#interface-attr)
Expand Down
9 changes: 8 additions & 1 deletion lib/HTMLParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -2109,9 +2109,16 @@ function HTMLParser(address, fragmentContext, options) {
}
};

// We are taking the custom elements registry from options if there is
// is needed beacause we have to have same same 'elements namespace' as
// place where we are parsing in innerHTML
var customElementsRegistry;
if (options && options.customElementsRegistry) {
customElementsRegistry = options.customElementsRegistry;
}

// This is the document we'll be building up
var doc = new Document(true, address);
var doc = new Document(true, address, customElementsRegistry);

// The document needs to know about the parser, for document.write().
// This _parser property will be deleted when we're done parsing.
Expand Down
2 changes: 2 additions & 0 deletions lib/Node.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module.exports = Node;

var EventTarget = require('./EventTarget');
var utils = require('./utils');
var ce = require('./customElements');
var NAMESPACE = utils.NAMESPACE;

// All nodes have a nodeType and an ownerDocument.
Expand Down Expand Up @@ -423,6 +424,7 @@ Node.prototype = Object.create(EventTarget.prototype, {
cloneNode: { value: function(deep) {
// Clone this node
var clone = this.clone();
ce.notifyCloned(clone);

// Handle the recursive case if necessary
if (deep && this.firstChild) {
Expand Down
8 changes: 7 additions & 1 deletion lib/Window.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,13 @@ Window.prototype = Object.create(EventTarget.prototype, {
// XXX This is a completely broken implementation
getComputedStyle: { value: function getComputedStyle(elt) {
return elt.style;
}}
}},

customElements: {
get: function() {
return this.document._customElementRegistry;
}
}

});

Expand Down
44 changes: 44 additions & 0 deletions lib/customElements.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"use strict";
module.exports.preventNotify = false;

module.exports.notifyCustomElementsSetAttrCallback = function(node, name, oldValue, newValue, ns) {
if (node.constructor.observedAttributes && node.constructor.observedAttributes.indexOf(name) > -1) {
if (node.attributeChangedCallback) {
if (oldValue === undefined) oldValue = null;
if (newValue === undefined) newValue = null;
node.attributeChangedCallback(name, oldValue, newValue, ns || null);
}
}
};

module.exports.notifyCloned = function(n) {
//nyi();
};

module.exports.notifyRooted = function(n) {
if (!exports.preventNotify) {
// CEv1
if (n.adoptedCallback && n._lastDocument && (n._lastDocument !== n.ownerDocument)) {
n.adoptedCallback(n._lastDocument, n.ownerDocument);
delete n._lastDocument;
}
// CEv1
if (n.connectedCallback) {
n.connectedCallback();
}
}
};

module.exports.notifyUpRooted = function(n) {
if (!exports.preventNotify) {
// CEv1
if (n.adoptedCallback) {
n._lastDocument = n.ownerDocument;
}
// CEv1
if (n.disconnectedCallback) {
n.disconnectedCallback();
}
}
};

Loading