-
Notifications
You must be signed in to change notification settings - Fork 2
/
element.js
193 lines (172 loc) · 6.18 KB
/
element.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
/**
* @license element 0.0.1+ Copyright jQuery Foundation and other contributors.
* Released under MIT license, http://github.com/requirejs/element/LICENSE
*/
/*jshint browser: true */
/*globals define */
define(function(require, exports, module) {
'use strict';
var slice = Array.prototype.slice,
callbackSuffix = 'Callback',
callbackSuffixLength = callbackSuffix.length,
charRegExp = /[^a-z]/g,
idToTag = function(id) {
return id.toLowerCase().replace(charRegExp, '-');
},
moduleConfig = module.config();
if (moduleConfig.hasOwnProperty('idToTag')) {
idToTag = moduleConfig.idToTag;
}
/**
* Converts an attribute like a-long-attr to aLongAttr
* @param {String} attrName The attribute name
* @return {String}
*/
function makePropName(attrName) {
var parts = attrName.split('-');
for (var i = 1; i < parts.length; i++) {
parts[i] = parts[i].charAt(0).toUpperCase() + parts[i].substring(1);
}
return parts.join('');
}
/**
* Given an attribute name, set the corresponding property
* name on the custom element instance, if it has such a
* property.
* @param {Object} instance the custom element instance.
* @param {String} attrName the attribute name.
* @param {String} attrValue The attribute value.
*/
function setPropFromAttr(instance, attrName, attrValue) {
var proto = Object.getPrototypeOf(instance),
propName = makePropName(attrName),
descriptor = Object.getOwnPropertyDescriptor(proto, propName);
// Only check immediate prototype for a property that
// matches, to avoid calling base setters that may be
// on original HTML-based element that could cause
// bad effects. Needs more testing for those cases to
// confirm, but since element is a mixin approach, this
// approach is safe.
if (descriptor && descriptor.set) {
instance[propName] = attrValue;
}
}
function makePropFn(prop) {
return function() {
var i, ret,
args = slice.call(arguments),
fns = this._element.props[prop];
for (i = 0; i < fns.length; i++) {
ret = fns[i].apply(this, args);
}
// Last function wins on the return value.
return ret;
};
}
function mixFnProp(proto, prop, value, operation) {
if (proto.hasOwnProperty(prop)) {
var existing = proto._element.props[prop];
if (!existing) {
existing = proto._element.props[prop] = [proto[prop]];
proto[prop] = makePropFn(prop);
}
operation = operation || 'push';
existing[operation](value);
} else {
proto[prop] = value;
}
}
function mix(proto, mixin) {
// Allow a top level of a mixin to be an array of other
// mixins.
if (Array.isArray(mixin)) {
mixin.forEach(function(mixin) {
mix(proto, mixin);
});
return;
}
Object.keys(mixin).forEach(function(key) {
var suffixIndex,
descriptor = Object.getOwnPropertyDescriptor(mixin, key);
// Any property that ends in Callback, like the custom element
// lifecycle events, can be multiplexed.
suffixIndex = key.indexOf(callbackSuffix);
if (suffixIndex > 0 &&
suffixIndex === key.length - callbackSuffixLength) {
mixFnProp(proto, key, descriptor.value);
} else {
Object.defineProperty(proto, key, descriptor);
}
});
}
/**
* Main module export. These methods are visible to
* any module.
*/
var element = {
/**
* The AMD loader plugin API. Called by an AMD loader
* to handle 'element!' resources.
* @param {String} id module ID to load.
* @param {Function} req context-specific `require` function.
* @param {Function} onload function to call once loading is complete.
* @param {Object} config config from the loader. Normally just has
* config.isBuild if in a build scenario.
*/
load: function(id, req, onload, config) {
// Normal dependency request.
req([id], function(mod) {
// For builds do nothing else. Also if no module export or
// it is a function because the module already called
// document.register itself, then do not bother with the
// other work.
if (config.isBuild || !mod || typeof mod === 'function') {
return onload();
}
// Create the prototype for the custom element.
// Allow the module to be an array of mixins.
// If it is an array, then mix them all in to the
// prototype.
var proto = Object.create(HTMLElement.prototype);
// Define a property to hold all the element-specific information
Object.defineProperty(proto, '_element', {
enumerable: false,
configurable: false,
writable: false,
value: {}
});
proto._element.props = {};
mix(proto, mod);
// Wire attributes to this element's custom/getter setters.
// Because of the 'unshift' use, this will actually execute
// before the templateCreatedCallback, which is good. The
// exterior API should set up the internal state before
// other parts of createdCallback run.
mixFnProp(proto, 'createdCallback', function attrCreated() {
var i, item,
attrs = this.attributes;
for (i = 0; i < attrs.length; i++) {
item = attrs.item(i);
setPropFromAttr(this, item.nodeName, item.value);
}
}, 'unshift');
// Listen for attribute changed calls, and just trigger getter/setter
// calling if matching property. Make sure it is the first one in
// the listener set.
mixFnProp(proto, 'attributeChangedCallback',
function attrChanged(name, oldValue, newValue) {
// Only called if value has changed, so no need to check
// oldValue !== newValue
setPropFromAttr(this, name, newValue);
}, 'unshift');
// Translate any characters that are unfit for custom element
// names to dashes
var tagId = idToTag(id);
onload(document.registerElement(tagId, {
prototype: proto
}));
});
}
};
return element;
});