Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
379 lines
No EOL
9.2 KiB
JavaScript
Executable file
379 lines
No EOL
9.2 KiB
JavaScript
Executable file
;(function(define){define(function(require,exports,module){
|
|
'use strict';
|
|
|
|
/**
|
|
* Locals
|
|
*/
|
|
|
|
var textContent = Object.getOwnPropertyDescriptor(Node.prototype, 'textContent');
|
|
var innerHTML = Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML');
|
|
var removeAttribute = Element.prototype.removeAttribute;
|
|
var setAttribute = Element.prototype.setAttribute;
|
|
var noop = function() {};
|
|
|
|
/**
|
|
* Register a new component.
|
|
*
|
|
* @param {String} name
|
|
* @param {Object} props
|
|
* @return {constructor}
|
|
* @public
|
|
*/
|
|
exports.register = function(name, props) {
|
|
var baseProto = getBaseProto(props.extends);
|
|
|
|
// Clean up
|
|
delete props.extends;
|
|
|
|
// Pull out CSS that needs to be in the light-dom
|
|
if (props.template) {
|
|
var output = processCss(props.template, name);
|
|
|
|
props.template = document.createElement('template');
|
|
props.template.innerHTML = output.template;
|
|
props.lightCss = output.lightCss;
|
|
|
|
props.globalCss = props.globalCss || '';
|
|
props.globalCss += output.globalCss;
|
|
}
|
|
|
|
// Inject global CSS into the document,
|
|
// and delete as no longer needed
|
|
injectGlobalCss(props.globalCss);
|
|
delete props.globalCss;
|
|
|
|
// Merge base getter/setter attributes with the user's,
|
|
// then define the property descriptors on the prototype.
|
|
var descriptors = Object.assign(props.attrs || {}, base.descriptors);
|
|
|
|
// Store the orginal descriptors somewhere
|
|
// a little more private and delete the original
|
|
props._attrs = props.attrs;
|
|
delete props.attrs;
|
|
|
|
// Create the prototype, extended from base and
|
|
// define the descriptors directly on the prototype
|
|
var proto = createProto(baseProto, props);
|
|
Object.defineProperties(proto, descriptors);
|
|
|
|
// Register the custom-element and return the constructor
|
|
return document.registerElement(name, { prototype: proto });
|
|
};
|
|
|
|
var base = {
|
|
properties: {
|
|
GaiaComponent: true,
|
|
attributeChanged: noop,
|
|
attached: noop,
|
|
detached: noop,
|
|
created: noop,
|
|
|
|
createdCallback: function() {
|
|
if (this.rtl) { addDirObserver(); }
|
|
injectLightCss(this);
|
|
this.created();
|
|
},
|
|
|
|
/**
|
|
* It is very common to want to keep object
|
|
* properties in-sync with attributes,
|
|
* for example:
|
|
*
|
|
* el.value = 'foo';
|
|
* el.setAttribute('value', 'foo');
|
|
*
|
|
* So we support an object on the prototype
|
|
* named 'attrs' to provide a consistent
|
|
* way for component authors to define
|
|
* these properties. When an attribute
|
|
* changes we keep the attr[name]
|
|
* up-to-date.
|
|
*
|
|
* @param {String} name
|
|
* @param {String||null} from
|
|
* @param {String||null} to
|
|
*/
|
|
attributeChangedCallback: function(name, from, to) {
|
|
var prop = toCamelCase(name);
|
|
if (this._attrs && this._attrs[prop]) { this[prop] = to; }
|
|
this.attributeChanged(name, from, to);
|
|
},
|
|
|
|
attachedCallback: function() { this.attached(); },
|
|
detachedCallback: function() { this.detached(); },
|
|
|
|
/**
|
|
* A convenient method for setting up
|
|
* a shadow-root using the defined template.
|
|
*
|
|
* @return {ShadowRoot}
|
|
*/
|
|
setupShadowRoot: function() {
|
|
if (!this.template) { return; }
|
|
var node = document.importNode(this.template.content, true);
|
|
this.createShadowRoot().appendChild(node);
|
|
return this.shadowRoot;
|
|
},
|
|
|
|
/**
|
|
* Sets an attribute internally
|
|
* and externally. This is so that
|
|
* we can style internal shadow-dom
|
|
* content.
|
|
*
|
|
* @param {String} name
|
|
* @param {String} value
|
|
*/
|
|
setAttr: function(name, value) {
|
|
var internal = this.shadowRoot.firstElementChild;
|
|
setAttribute.call(internal, name, value);
|
|
setAttribute.call(this, name, value);
|
|
},
|
|
|
|
/**
|
|
* Removes an attribute internally
|
|
* and externally. This is so that
|
|
* we can style internal shadow-dom
|
|
* content.
|
|
*
|
|
* @param {String} name
|
|
* @param {String} value
|
|
*/
|
|
removeAttr: function(name) {
|
|
var internal = this.shadowRoot.firstElementChild;
|
|
removeAttribute.call(internal, name);
|
|
removeAttribute.call(this, name);
|
|
}
|
|
},
|
|
|
|
descriptors: {
|
|
textContent: {
|
|
set: function(value) {
|
|
textContent.set.call(this, value);
|
|
if (this.lightStyle) { this.appendChild(this.lightStyle); }
|
|
},
|
|
|
|
get: textContent.get
|
|
},
|
|
|
|
innerHTML: {
|
|
set: function(value) {
|
|
innerHTML.set.call(this, value);
|
|
if (this.lightStyle) { this.appendChild(this.lightStyle); }
|
|
},
|
|
|
|
get: innerHTML.get
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The default base prototype to use
|
|
* when `extends` is undefined.
|
|
*
|
|
* @type {Object}
|
|
*/
|
|
var defaultPrototype = createProto(HTMLElement.prototype, base.properties);
|
|
|
|
/**
|
|
* Returns a suitable prototype based
|
|
* on the object passed.
|
|
*
|
|
* @param {HTMLElementPrototype|undefined} proto
|
|
* @return {HTMLElementPrototype}
|
|
* @private
|
|
*/
|
|
function getBaseProto(proto) {
|
|
if (!proto) { return defaultPrototype; }
|
|
proto = proto.prototype || proto;
|
|
return !proto.GaiaComponent
|
|
? createProto(proto, base.properties)
|
|
: proto;
|
|
}
|
|
|
|
/**
|
|
* Extends the given proto and mixes
|
|
* in the given properties.
|
|
*
|
|
* @param {Object} proto
|
|
* @param {Object} props
|
|
* @return {Object}
|
|
*/
|
|
function createProto(proto, props) {
|
|
return Object.assign(Object.create(proto), props);
|
|
}
|
|
|
|
/**
|
|
* Detects presence of shadow-dom
|
|
* CSS selectors.
|
|
*
|
|
* @return {Boolean}
|
|
*/
|
|
var hasShadowCSS = (function() {
|
|
var div = document.createElement('div');
|
|
try { div.querySelector(':host'); div.querySelector('::content'); return true; }
|
|
catch (e) { return false; }
|
|
})();
|
|
|
|
/**
|
|
* Regexs used to extract shadow-css
|
|
*
|
|
* @type {Object}
|
|
*/
|
|
var regex = {
|
|
shadowCss: /(?:\:host|\:\:content)[^{]*\{[^}]*\}/g,
|
|
':host': /(?:\:host)/g,
|
|
':host()': /\:host\((.+)\)(?: \:\:content)?/g,
|
|
':host-context': /\:host-context\((.+)\)([^{,]+)?/g,
|
|
'::content': /(?:\:\:content)/g
|
|
};
|
|
|
|
/**
|
|
* Extracts the :host and ::content rules
|
|
* from the shadow-dom CSS and rewrites
|
|
* them to work from the <style scoped>
|
|
* injected at the root of the component.
|
|
*
|
|
* @return {String}
|
|
*/
|
|
function processCss(template, name) {
|
|
var globalCss = '';
|
|
var lightCss = '';
|
|
|
|
if (!hasShadowCSS) {
|
|
template = template.replace(regex.shadowCss, function(match) {
|
|
var hostContext = regex[':host-context'].exec(match);
|
|
|
|
if (hostContext) {
|
|
globalCss += match
|
|
.replace(regex['::content'], '')
|
|
.replace(regex[':host-context'], '$1 ' + name + '$2')
|
|
.replace(/ +/g, ' '); // excess whitespace
|
|
} else {
|
|
lightCss += match
|
|
.replace(regex[':host()'], name + '$1')
|
|
.replace(regex[':host'], name)
|
|
.replace(regex['::content'], name);
|
|
}
|
|
|
|
return '';
|
|
});
|
|
}
|
|
|
|
return {
|
|
template: template,
|
|
lightCss: lightCss,
|
|
globalCss: globalCss
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Some CSS rules, such as @keyframes
|
|
* and @font-face don't work inside
|
|
* scoped or shadow <style>. So we
|
|
* have to put them into 'global'
|
|
* <style> in the head of the
|
|
* document.
|
|
*
|
|
* @param {String} css
|
|
*/
|
|
function injectGlobalCss(css) {
|
|
if (!css) return;
|
|
var style = document.createElement('style');
|
|
style.innerHTML = css.trim();
|
|
headReady().then(() => {
|
|
document.head.appendChild(style)
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* Resolves a promise once document.head is ready.
|
|
*
|
|
* @private
|
|
*/
|
|
function headReady() {
|
|
return new Promise(resolve => {
|
|
if (document.head) { return resolve(); }
|
|
window.addEventListener('load', function fn() {
|
|
window.removeEventListener('load', fn);
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* The Gecko platform doesn't yet have
|
|
* `::content` or `:host`, selectors,
|
|
* without these we are unable to style
|
|
* user-content in the light-dom from
|
|
* within our shadow-dom style-sheet.
|
|
*
|
|
* To workaround this, we clone the <style>
|
|
* node into the root of the component,
|
|
* so our selectors are able to target
|
|
* light-dom content.
|
|
*
|
|
* @private
|
|
*/
|
|
function injectLightCss(el) {
|
|
if (hasShadowCSS) { return; }
|
|
el.lightStyle = document.createElement('style');
|
|
el.lightStyle.setAttribute('scoped', '');
|
|
el.lightStyle.innerHTML = el.lightCss;
|
|
el.appendChild(el.lightStyle);
|
|
}
|
|
|
|
/**
|
|
* Convert hyphen separated
|
|
* string to camel-case.
|
|
*
|
|
* Example:
|
|
*
|
|
* toCamelCase('foo-bar'); //=> 'fooBar'
|
|
*
|
|
* @param {Sring} string
|
|
* @return {String}
|
|
*/
|
|
function toCamelCase(string) {
|
|
return string.replace(/-(.)/g, function replacer(string, p1) {
|
|
return p1.toUpperCase();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Observer (singleton)
|
|
*
|
|
* @type {MutationObserver|undefined}
|
|
*/
|
|
var dirObserver;
|
|
|
|
/**
|
|
* Observes the document `dir` (direction)
|
|
* attribute and dispatches a global event
|
|
* when it changes.
|
|
*
|
|
* Components can listen to this event and
|
|
* make internal changes if need be.
|
|
*
|
|
* @private
|
|
*/
|
|
function addDirObserver() {
|
|
if (dirObserver) { return; }
|
|
|
|
dirObserver = new MutationObserver(onChanged);
|
|
dirObserver.observe(document.documentElement, {
|
|
attributeFilter: ['dir'],
|
|
attributes: true
|
|
});
|
|
|
|
function onChanged(mutations) {
|
|
document.dispatchEvent(new Event('dirchanged'));
|
|
}
|
|
}
|
|
|
|
});})(typeof define=='function'&&define.amd?define
|
|
:(function(n,w){'use strict';return typeof module=='object'?function(c){
|
|
c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){
|
|
return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-component',this)); |