-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathtrex.js
214 lines (191 loc) · 6.24 KB
/
trex.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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
/**
* Core implementation file of TRex
*/
/**
* Defines a new class of elements.
* Usually associated with a new HTML tag, e.g. <textbox>.
*
* @param newElementSelector {String} e.g. "hbox"
* Which tags this implements, and to which elements
* this implementation should be attached to.
* @param parentElementSelector {String} e.g. "box"
* Parent class in OO class hierarchy from which to inherit behavior.
* @param prototype {Object} Will be used as prototype for the
* element object.
* Must have a function |constructor()|.
* |this.base| will be defined and point to the implementation of
* |parentElementSelector|.
*/
function defineElementClass(newElementSelector, parentElementSelector, prototype) {
assert(typeof(newElementSelector) == "string", "Need tag name");
assert( !gSelectorClassMapping[newElementSelector],
"Tag name <" + newElementSelector + "> already defined");
var parentPrototype = ElementPrototype;
if (parentElementSelector) {
assert(typeof(parentElementSelector) == "string", "Need tag name for parent");
parentPrototype = gSelectorClassMapping[parentElementSelector];
assert(parentElementSelector, "No class registered for <" + parentElementSelector + ">");
}
prototype._forTag = newElementSelector;
prototype._baseTag = parentElementSelector;
prototype.base = parentPrototype;
// TODO replace this (without changing API of this function)
prototype.__proto__ = parentPrototype;
gSelectorClassMapping[newElementSelector] = prototype;
// Rest of logic is in Element.trex getter.
}
/**
* Set by calling defineElementClass()
* Used by Element.trex getter
*
* Map {tag name -> class prototype {ElementPrototype}}
*/
var gSelectorClassMapping = {};
/**
* Parent for all element classes
*/
var ElementPrototype = {
/**
* Prototype object of parent class
*/
base : null,
/**
* Back reference to the element this is attached to.
* Set by Element.trex getter
* {DOMElement}
*/
el : null,
/**
* List of HTML attributes that are mapped to JS properties.
* This list should include all attributes that are defined
* by this element class. Set them using .push() in the ctor.
*
* See this._attributeToPropertyName() to get the attribute name.
*
* Map property name {String} -> true
*/
_mappedProperties : null,
/**
* Map property name {String} -> value
*/
_propertyValue : null,
_setterRunning : null,
/**
* Called by each class constructor to define
* properties and attributes that this class implements.
*/
_addProperty : function(propertyName) {
this._mappedProperties[propertyName] = true;
var attrValue = this.el.getAttribute(this._propertyToAttributeName(propertyName));
if (attrValue) {
this[propertyName] = attrValue;
}
},
_propertyToAttributeName : function(propertyName) {
return propertyName; // TODO convert "S" to "-s"
},
_attributeToPropertyName : function(attributeName) {
return attributeName; // TODO convert "-s" to "S"
},
baseGet : function(propertyName) {
if (this.base) {
return this.base.__lookupGetter__(propertyName).call(this);
}
return this._propertyValue[propertyName];
},
baseSet : function(propertyName, value) {
if (this.base) {
// TODO loops. ditto above.
return this.base.__lookupSetter__(propertyName).call(this, value);
}
var attributeName = this._propertyToAttributeName(propertyName);
if (this.el.getAttribute(attributeName) != value && // avoid loop TODO test for non-strings
!this._setterRunning[propertyName]) { // double-protection against loops
this._setterRunning[propertyName] = true;
this.el.setAttribute(attributeName, value + "");
this._setterRunning[propertyName] = false;
}
return this._propertyValue[propertyName] = value;
},
/**
* Called when a new instance of this kind of element
* is instantiated.
* Called by Element.trex getter.
*/
constructor : function() {
console.log("ctor el: " + this.el);
this._mappedProperties = {};
this._propertyValue = {};
this._setterRunning = {};
// <https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver>
var self = this;
this._attrObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
var attributeName = mutation.attributeName;
if (attributeName) {
var propertyName = self._attributeToPropertyName(attributeName);
self[propertyName] = self.el.getAttribute(attributeName); // calls setter
}
});
});
this._attrObserver.observe(this.el, { attributes : true });
this._addProperty("hidden");
},
}
/**
* The DOMElement.trex getter implementation
*/
Object.defineProperty(Element.prototype, "trex", {
get : function() {
if (this._trex) {
return this._trex;
}
var tagName = this.nodeName.toLowerCase();
var prototype = gSelectorClassMapping[tagName];
assert(prototype, "No class registered for <" + tagName + ">");
this._trex = Object.create(prototype);
console.log(dumpObject(this._trex, "obj", 4));
this._trex.el = this;
this._trex.constructor();
return this._trex;
},
// no setter
});
function assert(test, errorMsg) {
console.assert(test, errorMsg);
if (!test) {
throw new Error(errorMsg ? errorMsg : "Bug: assertion failed");
}
}
defineElementClass("#element", null, {
constructor : function() {
console.log("element constructor for " + this._forTag + " with base " + this._baseTag);
// TODO loops. ditto below.
this.base.constructor.call(this);
},
get hidden () {
return this.baseGet("hidden");
},
set hidden (value) {
this.baseSet("hidden", sanitize.boolean(value));
// implemented in CSS
},
});
defineElementClass("box", "#element", {
constructor : function() {
console.log("box constructor for " + this._forTag + " with base " + this._baseTag);
// TODO loops. ditto below.
this.base.constructor.call(this);
},
});
defineElementClass("hbox", "box", {
constructor : function() {
console.log("hbox constructor for " + this.el);
this.base.constructor.call(this);
},
});
defineElementClass("vbox", "box", {
constructor : function() {
this.base.constructor.call(this);
},
});