0
|
1 // Copyright (C) 2008 Google Inc.
|
|
2 //
|
|
3 // Licensed under the Apache License, Version 2.0 (the "License");
|
|
4 // you may not use this file except in compliance with the License.
|
|
5 // You may obtain a copy of the License at
|
|
6 //
|
|
7 // http://www.apache.org/licenses/LICENSE-2.0
|
|
8 //
|
|
9 // Unless required by applicable law or agreed to in writing, software
|
|
10 // distributed under the License is distributed on an "AS IS" BASIS,
|
|
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12 // See the License for the specific language governing permissions and
|
|
13 // limitations under the License.
|
|
14
|
|
15 /**
|
|
16 * @fileoverview
|
|
17 * A set of utility functions that implement browser feature testing to unify
|
|
18 * certain DOM behaviors, and a set of recommendations about when to use these
|
|
19 * functions as opposed to the native DOM functions.
|
|
20 *
|
|
21 * @author ihab.awad@gmail.com
|
|
22 * @author jasvir@gmail.com
|
|
23 * @provides bridal
|
|
24 * @requires ___, cajita, document, html, html4, navigator
|
|
25 */
|
|
26
|
|
27 var bridal = (function() {
|
|
28
|
|
29 ////////////////////////////////////////////////////////////////////////////
|
|
30 // Private section
|
|
31 ////////////////////////////////////////////////////////////////////////////
|
|
32
|
|
33 var isOpera = navigator.userAgent.indexOf('Opera') === 0;
|
|
34 var isIE = !isOpera && navigator.userAgent.indexOf('MSIE') !== -1;
|
|
35 var isWebkit = !isOpera && navigator.userAgent.indexOf('WebKit') !== -1;
|
|
36
|
|
37 var features = {
|
|
38 attachEvent: !!(document.createElement('div').attachEvent),
|
|
39 setAttributeExtraParam: isIE,
|
|
40 /**
|
|
41 * Does the extended form of extendedCreateElement work?
|
|
42 * From http://msdn.microsoft.com/en-us/library/ms536389.aspx :<blockquote>
|
|
43 * You can also specify all the attributes inside the createElement
|
|
44 * method by using an HTML string for the method argument.
|
|
45 * The following example demonstrates how to dynamically create two
|
|
46 * radio buttons utilizing this technique.
|
|
47 * <pre>
|
|
48 * ...
|
|
49 * var newRadioButton = document.createElement(
|
|
50 * "<INPUT TYPE='RADIO' NAME='RADIOTEST' VALUE='First Choice'>")
|
|
51 * </pre>
|
|
52 * </blockquote>
|
|
53 */
|
|
54 extendedCreateElement: (
|
|
55 function () {
|
|
56 try {
|
|
57 var inp = document.createElement('<input name="x" type="radio">');
|
|
58 return inp.name === 'x' && inp.type === 'radio';
|
|
59 } catch (ex) {
|
|
60 return false;
|
|
61 }
|
|
62 })()
|
|
63 };
|
|
64
|
|
65 var CUSTOM_EVENT_TYPE_SUFFIX = '_custom___';
|
|
66 function tameEventType(type, opt_isCustom) {
|
|
67 type = String(type);
|
|
68 if (endsWith__.test(type)) {
|
|
69 throw new Error('Invalid event type ' + type);
|
|
70 }
|
|
71 if (opt_isCustom
|
|
72 || html4.atype.SCRIPT !== html4.ATTRIBS['*:on' + type]) {
|
|
73 type = type + CUSTOM_EVENT_TYPE_SUFFIX;
|
|
74 }
|
|
75 return type;
|
|
76 }
|
|
77
|
|
78 function eventHandlerTypeFilter(handler, tameType) {
|
|
79 // This does not need to check that handler is callable by untrusted code
|
|
80 // since the handler will invoke plugin_dispatchEvent which will do that
|
|
81 // check on the untrusted function reference.
|
|
82 return function (event) {
|
|
83 if (tameType === event.eventType___) {
|
|
84 return handler.call(this, event);
|
|
85 }
|
|
86 };
|
|
87 }
|
|
88
|
|
89 var endsWith__ = /__$/;
|
|
90 function constructClone(node, deep) {
|
|
91 var clone;
|
|
92 if (node.nodeType === 1) {
|
|
93 // From http://blog.pengoworks.com/index.cfm/2007/7/16/IE6--IE7-quirks-with-cloneNode-and-form-elements
|
|
94 // It turns out IE 6/7 doesn't properly clone some form elements
|
|
95 // when you use the cloneNode(true) and the form element is a
|
|
96 // checkbox, radio or select element.
|
|
97 // JQuery provides a clone method which attempts to fix this and an issue
|
|
98 // with event listeners. According to the source code for JQuery's clone
|
|
99 // method ( http://docs.jquery.com/Manipulation/clone#true ):
|
|
100 // IE copies events bound via attachEvent when
|
|
101 // using cloneNode. Calling detachEvent on the
|
|
102 // clone will also remove the events from the orignal
|
|
103 // We do not need to deal with XHTML DOMs and so can skip the clean step
|
|
104 // that jQuery does.
|
|
105 var tagDesc = node.tagName;
|
|
106 // Copying form state is not strictly mentioned in DOM2's spec of
|
|
107 // cloneNode, but all implementations do it. The value copying
|
|
108 // can be interpreted as fixing implementations' failure to have
|
|
109 // the value attribute "reflect" the input's value as determined by the
|
|
110 // value property.
|
|
111 switch (node.tagName) {
|
|
112 case 'INPUT':
|
|
113 tagDesc = '<input name="' + html.escapeAttrib(node.name)
|
|
114 + '" type="' + html.escapeAttrib(node.type)
|
|
115 + '" value="' + html.escapeAttrib(node.defaultValue) + '"'
|
|
116 + (node.defaultChecked ? ' checked="checked">' : '>');
|
|
117 break;
|
|
118 case 'OPTION':
|
|
119 tagDesc = '<option '
|
|
120 + (node.defaultSelected ? ' selected="selected">' : '>');
|
|
121 break;
|
|
122 case 'TEXTAREA':
|
|
123 tagDesc = '<textarea value="'
|
|
124 + html.escapeAttrib(node.defaultValue) + '">';
|
|
125 break;
|
|
126 }
|
|
127
|
|
128 clone = document.createElement(tagDesc);
|
|
129
|
|
130 var attrs = node.attributes;
|
|
131 for (var i = 0, attr; (attr = attrs[i]); ++i) {
|
|
132 if (attr.specified && !endsWith__.test(attr.name)) {
|
|
133 clone.setAttribute(attr.nodeName, attr.nodeValue);
|
|
134 }
|
|
135 }
|
|
136 } else {
|
|
137 clone = node.cloneNode(false);
|
|
138 }
|
|
139 if (deep) {
|
|
140 // TODO(mikesamuel): should we whitelist nodes here, to e.g. prevent
|
|
141 // untrusted code from reloading an already loaded script by cloning
|
|
142 // a script node that somehow exists in a tree accessible to it?
|
|
143 for (var child = node.firstChild; child; child = child.nextSibling) {
|
|
144 var cloneChild = constructClone(child, deep);
|
|
145 clone.appendChild(cloneChild);
|
|
146 }
|
|
147 }
|
|
148 return clone;
|
|
149 }
|
|
150
|
|
151 function fixupClone(node, clone) {
|
|
152 for (var child = node.firstChild, cloneChild = clone.firstChild; cloneChild;
|
|
153 child = child.nextSibling, cloneChild = cloneChild.nextSibling) {
|
|
154 fixupClone(child, cloneChild);
|
|
155 }
|
|
156 if (node.nodeType === 1) {
|
|
157 switch (node.tagName) {
|
|
158 case 'INPUT':
|
|
159 clone.value = node.value;
|
|
160 clone.checked = node.checked;
|
|
161 break;
|
|
162 case 'OPTION':
|
|
163 clone.selected = node.selected;
|
|
164 clone.value = node.value;
|
|
165 break;
|
|
166 case 'TEXTAREA':
|
|
167 clone.value = node.value;
|
|
168 break;
|
|
169 }
|
|
170 }
|
|
171
|
|
172 // Do not copy listeners since DOM2 specifies that only attributes and
|
|
173 // children are copied, and that children should only be copied if the
|
|
174 // deep flag is set.
|
|
175 // The children are handled in constructClone.
|
|
176 var originalAttribs = node.attributes___;
|
|
177 if (originalAttribs) {
|
|
178 var attribs = {};
|
|
179 clone.attributes___ = attribs;
|
|
180 cajita.forOwnKeys(originalAttribs, ___.func(function (k, v) {
|
|
181 switch (typeof v) {
|
|
182 case 'string': case 'number': case 'boolean':
|
|
183 attribs[k] = v;
|
|
184 break;
|
|
185 }
|
|
186 }));
|
|
187 }
|
|
188 }
|
|
189
|
|
190 ////////////////////////////////////////////////////////////////////////////
|
|
191 // Public section
|
|
192 ////////////////////////////////////////////////////////////////////////////
|
|
193
|
|
194 function untameEventType(type) {
|
|
195 var suffix = CUSTOM_EVENT_TYPE_SUFFIX;
|
|
196 var tlen = type.length, slen = suffix.length;
|
|
197 var end = tlen - slen;
|
|
198 if (end >= 0 && suffix === type.substring(end)) {
|
|
199 type = type.substring(0, end);
|
|
200 }
|
|
201 return type;
|
|
202 }
|
|
203
|
|
204 function initEvent(event, type, bubbles, cancelable) {
|
|
205 type = tameEventType(type, true);
|
|
206 bubbles = Boolean(bubbles);
|
|
207 cancelable = Boolean(cancelable);
|
|
208
|
|
209 if (event.initEvent) { // Non-IE
|
|
210 event.initEvent(type, bubbles, cancelable);
|
|
211 } else if (bubbles && cancelable) { // IE
|
|
212 event.eventType___ = type;
|
|
213 } else {
|
|
214 // TODO(mikesamuel): can bubbling and cancelable on events be simulated
|
|
215 // via http://msdn.microsoft.com/en-us/library/ms533545(VS.85).aspx
|
|
216 throw new Error(
|
|
217 'Browser does not support non-bubbling/uncanceleable events');
|
|
218 }
|
|
219 }
|
|
220
|
|
221 function dispatchEvent(element, event) {
|
|
222 // TODO(mikesamuel): when we change event dispatching to happen
|
|
223 // asynchronously, we should exempt custom events since those
|
|
224 // need to return a useful value, and there may be code bracketing
|
|
225 // them which could observe asynchronous dispatch.
|
|
226
|
|
227 // "The return value of dispatchEvent indicates whether any of
|
|
228 // the listeners which handled the event called
|
|
229 // preventDefault. If preventDefault was called the value is
|
|
230 // false, else the value is true."
|
|
231 if (element.dispatchEvent) {
|
|
232 return Boolean(element.dispatchEvent(event));
|
|
233 } else {
|
|
234 // Only dispatches custom events as when tameEventType(t) !== t.
|
|
235 element.fireEvent('ondataavailable', event);
|
|
236 return Boolean(event.returnValue);
|
|
237 }
|
|
238 }
|
|
239
|
|
240 /**
|
|
241 * Add an event listener function to an element.
|
|
242 *
|
|
243 * <p>Replaces
|
|
244 * W3C <code>Element::addEventListener</code> and
|
|
245 * IE <code>Element::attachEvent</code>.
|
|
246 *
|
|
247 * @param {HTMLElement} element a native DOM element.
|
|
248 * @param {string} type a string identifying the event type.
|
|
249 * @param {boolean Element::function (event)} handler an event handler.
|
|
250 * @param {boolean} useCapture whether the user wishes to initiate capture.
|
|
251 * @return {boolean Element::function (event)} the handler added. May be
|
|
252 * a wrapper around the input.
|
|
253 */
|
|
254 function addEventListener(element, type, handler, useCapture) {
|
|
255 type = String(type);
|
|
256 var tameType = tameEventType(type);
|
|
257 if (features.attachEvent) {
|
|
258 // TODO(ihab.awad): How do we emulate 'useCapture' here?
|
|
259 if (type !== tameType) {
|
|
260 var wrapper = eventHandlerTypeFilter(handler, tameType);
|
|
261 element.attachEvent('ondataavailable', wrapper);
|
|
262 return wrapper;
|
|
263 } else {
|
|
264 element.attachEvent('on' + type, handler);
|
|
265 return handler;
|
|
266 }
|
|
267 } else {
|
|
268 // FF2 fails if useCapture not passed or is not a boolean.
|
|
269 element.addEventListener(tameType, handler, useCapture);
|
|
270 return handler;
|
|
271 }
|
|
272 }
|
|
273
|
|
274 /**
|
|
275 * Remove an event listener function from an element.
|
|
276 *
|
|
277 * <p>Replaces
|
|
278 * W3C <code>Element::removeEventListener</code> and
|
|
279 * IE <code>Element::detachEvent</code>.
|
|
280 *
|
|
281 * @param element a native DOM element.
|
|
282 * @param type a string identifying the event type.
|
|
283 * @param handler a function acting as an event handler.
|
|
284 * @param useCapture whether the user wishes to initiate capture.
|
|
285 */
|
|
286 function removeEventListener(element, type, handler, useCapture) {
|
|
287 type = String(type);
|
|
288 var tameType = tameEventType(type);
|
|
289 if (features.attachEvent) {
|
|
290 // TODO(ihab.awad): How do we emulate 'useCapture' here?
|
|
291 if (tameType !== type) {
|
|
292 element.detachEvent('ondataavailable', handler);
|
|
293 } else {
|
|
294 element.detachEvent('on' + type, handler);
|
|
295 }
|
|
296 } else {
|
|
297 element.removeEventListener(tameType, handler, useCapture);
|
|
298 }
|
|
299 }
|
|
300
|
|
301 /**
|
|
302 * Clones a node per {@code Node.clone()}.
|
|
303 * <p>
|
|
304 * Returns a duplicate of this node, i.e., serves as a generic copy
|
|
305 * constructor for nodes. The duplicate node has no parent;
|
|
306 * (parentNode is null.).
|
|
307 * <p>
|
|
308 * Cloning an Element copies all attributes and their values,
|
|
309 * including those generated by the XML processor to represent
|
|
310 * defaulted attributes, but this method does not copy any text it
|
|
311 * contains unless it is a deep clone, since the text is contained
|
|
312 * in a child Text node. Cloning an Attribute directly, as opposed
|
|
313 * to be cloned as part of an Element cloning operation, returns a
|
|
314 * specified attribute (specified is true). Cloning any other type
|
|
315 * of node simply returns a copy of this node.
|
|
316 * <p>
|
|
317 * Note that cloning an immutable subtree results in a mutable copy,
|
|
318 * but the children of an EntityReference clone are readonly. In
|
|
319 * addition, clones of unspecified Attr nodes are specified. And,
|
|
320 * cloning Document, DocumentType, Entity, and Notation nodes is
|
|
321 * implementation dependent.
|
|
322 *
|
|
323 * @param {boolean} deep If true, recursively clone the subtree
|
|
324 * under the specified node; if false, clone only the node itself
|
|
325 * (and its attributes, if it is an Element).
|
|
326 *
|
|
327 * @return {Node} The duplicate node.
|
|
328 */
|
|
329 function cloneNode(node, deep) {
|
|
330 var clone;
|
|
331 if (!document.all) { // Not IE 6 or IE 7
|
|
332 clone = node.cloneNode(deep);
|
|
333 } else {
|
|
334 clone = constructClone(node, deep);
|
|
335 }
|
|
336 fixupClone(node, clone);
|
|
337 return clone;
|
|
338 }
|
|
339
|
|
340 /**
|
|
341 * Create a <code>style</code> element for a document containing some
|
|
342 * specified CSS text. Does not add the element to the document: the client
|
|
343 * may do this separately if desired.
|
|
344 *
|
|
345 * <p>Replaces directly creating the <code>style</code> element and
|
|
346 * populating its contents.
|
|
347 *
|
|
348 * @param document a DOM document.
|
|
349 * @param cssText a string containing a well-formed stylesheet production.
|
|
350 * @return a <code>style</code> element for the specified document.
|
|
351 */
|
|
352 function createStylesheet(document, cssText) {
|
|
353 // Courtesy Stoyan Stefanov who documents the derivation of this at
|
|
354 // http://www.phpied.com/dynamic-script-and-style-elements-in-ie/ and
|
|
355 // http://yuiblog.com/blog/2007/06/07/style/
|
|
356 var styleSheet = document.createElement('style');
|
|
357 styleSheet.setAttribute('type', 'text/css');
|
|
358 if (styleSheet.styleSheet) { // IE
|
|
359 styleSheet.styleSheet.cssText = cssText;
|
|
360 } else { // the world
|
|
361 styleSheet.appendChild(document.createTextNode(cssText));
|
|
362 }
|
|
363 return styleSheet;
|
|
364 }
|
|
365
|
|
366 /**
|
|
367 * Set an attribute on a DOM node.
|
|
368 *
|
|
369 * <p>Replaces DOM <code>Node::setAttribute</code>.
|
|
370 *
|
|
371 * @param {HTMLElement} element a DOM element.
|
|
372 * @param {string} name the name of an attribute.
|
|
373 * @param {string} value the value of an attribute.
|
|
374 */
|
|
375 function setAttribute(element, name, value) {
|
|
376 switch (name) {
|
|
377 case 'style':
|
|
378 if ((typeof element.style.cssText) === 'string') {
|
|
379 // Setting the 'style' attribute does not work for IE, but
|
|
380 // setting cssText works on IE 6, Firefox, and IE 7.
|
|
381 element.style.cssText = value;
|
|
382 return value;
|
|
383 }
|
|
384 break;
|
|
385 case 'class':
|
|
386 element.className = value;
|
|
387 return value;
|
|
388 case 'for':
|
|
389 element.htmlFor = value;
|
|
390 return value;
|
|
391 }
|
|
392 if (features.setAttributeExtraParam) {
|
|
393 element.setAttribute(name, value, 0);
|
|
394 } else {
|
|
395 element.setAttribute(name, value);
|
|
396 }
|
|
397 return value;
|
|
398 }
|
|
399
|
|
400 /**
|
|
401 * See <a href="http://www.w3.org/TR/cssom-view/#the-getclientrects"
|
|
402 * >ElementView.getBoundingClientRect()</a>.
|
|
403 * @return {Object} duck types as a TextRectangle with numeric fields
|
|
404 * {@code left}, {@code right}, {@code top}, and {@code bottom}.
|
|
405 */
|
|
406 function getBoundingClientRect(el) {
|
|
407 var doc = el.ownerDocument;
|
|
408 // Use the native method if present.
|
|
409 if (el.getBoundingClientRect) {
|
|
410 var cRect = el.getBoundingClientRect();
|
|
411 if (isIE) {
|
|
412 // IE has an unnecessary border, which can be mucked with by styles, so
|
|
413 // the amount of border is not predictable.
|
|
414 // Depending on whether the document is in quirks or standards mode,
|
|
415 // the border will be present on either the HTML or BODY elements.
|
|
416 var fixupLeft = doc.documentElement.clientLeft + doc.body.clientLeft;
|
|
417 cRect.left -= fixupLeft;
|
|
418 cRect.right -= fixupLeft;
|
|
419 var fixupTop = doc.documentElement.clientTop + doc.body.clientTop;
|
|
420 cRect.top -= fixupTop;
|
|
421 cRect.bottom -= fixupTop;
|
|
422 }
|
|
423 return ({
|
|
424 top: +cRect.top,
|
|
425 left: +cRect.left,
|
|
426 right: +cRect.right,
|
|
427 bottom: +cRect.bottom
|
|
428 });
|
|
429 }
|
|
430
|
|
431 // Otherwise, try using the deprecated gecko method, or emulate it in
|
|
432 // horribly inefficient ways.
|
|
433
|
|
434 // http://code.google.com/p/doctype/wiki/ArticleClientViewportElement
|
|
435 var viewport = (isIE && doc.compatMode === 'CSS1Compat')
|
|
436 ? doc.body : doc.documentElement;
|
|
437
|
|
438 // Figure out the position relative to the viewport.
|
|
439 // From http://code.google.com/p/doctype/wiki/ArticlePageOffset
|
|
440 var pageX = 0, pageY = 0;
|
|
441 if (el === viewport) {
|
|
442 // The viewport is the origin.
|
|
443 } else if (doc.getBoxObjectFor) { // Handles Firefox < 3
|
|
444 var elBoxObject = doc.getBoxObjectFor(el);
|
|
445 var viewPortBoxObject = doc.getBoxObjectFor(viewport);
|
|
446 pageX = elBoxObject.screenX - viewPortBoxObject.screenX;
|
|
447 pageY = elBoxObject.screenY - viewPortBoxObject.screenY;
|
|
448 } else {
|
|
449 // Walk the offsetParent chain adding up offsets.
|
|
450 for (var op = el; (op && op !== el); op = op.offsetParent) {
|
|
451 pageX += op.offsetLeft;
|
|
452 pageY += op.offsetTop;
|
|
453 if (op !== el) {
|
|
454 pageX += op.clientLeft || 0;
|
|
455 pageY += op.clientTop || 0;
|
|
456 }
|
|
457 if (isWebkit) {
|
|
458 // On webkit the offsets for position:fixed elements are off by the
|
|
459 // scroll offset.
|
|
460 var opPosition = doc.defaultView.getComputedStyle(op, 'position');
|
|
461 if (opPosition === 'fixed') {
|
|
462 pageX += doc.body.scrollLeft;
|
|
463 pageY += doc.body.scrollTop;
|
|
464 }
|
|
465 break;
|
|
466 }
|
|
467 }
|
|
468
|
|
469 // Opera & (safari absolute) incorrectly account for body offsetTop
|
|
470 if ((isWebkit
|
|
471 && doc.defaultView.getComputedStyle(el, 'position') === 'absolute')
|
|
472 || isOpera) {
|
|
473 pageY -= doc.body.offsetTop;
|
|
474 }
|
|
475
|
|
476 // Accumulate the scroll positions for everything but the body element
|
|
477 for (var op = el; (op = op.offsetParent) && op !== doc.body;) {
|
|
478 pageX -= op.scrollLeft;
|
|
479 // see https://bugs.opera.com/show_bug.cgi?id=249965
|
|
480 if (!isOpera || op.tagName !== 'TR') {
|
|
481 pageY -= op.scrollTop;
|
|
482 }
|
|
483 }
|
|
484 }
|
|
485
|
|
486 // Figure out the viewport container so we can subtract the window's
|
|
487 // scroll offsets.
|
|
488 var scrollEl = !isWebkit && doc.compatMode === 'CSS1Compat'
|
|
489 ? doc.documentElement
|
|
490 : doc.body;
|
|
491
|
|
492 var left = pageX - scrollEl.scrollLeft, top = pageY - scrollEl.scrollTop;
|
|
493 return ({
|
|
494 top: top,
|
|
495 left: left,
|
|
496 right: left + el.clientWidth,
|
|
497 bottom: top + el.clientHeight
|
|
498 });
|
|
499 }
|
|
500
|
|
501 /**
|
|
502 * Returns the value of the named attribute on element.
|
|
503 *
|
|
504 * @param {HTMLElement} element a DOM element.
|
|
505 * @param {string} name the name of an attribute.
|
|
506 */
|
|
507 function getAttribute(element, name) {
|
|
508 switch (name) {
|
|
509 case 'style':
|
|
510 if ((typeof element.style.cssText) === 'string') {
|
|
511 return element.style.cssText;
|
|
512 }
|
|
513 break;
|
|
514 case 'class':
|
|
515 return element.className;
|
|
516 case 'for':
|
|
517 return element.htmlFor;
|
|
518 }
|
|
519 return element.getAttribute(name);
|
|
520 }
|
|
521
|
|
522 function getAttributeNode(element, name) {
|
|
523 return element.getAttributeNode(name);
|
|
524 }
|
|
525
|
|
526 function hasAttribute(element, name) {
|
|
527 if (element.hasAttribute) { // Non IE
|
|
528 return element.hasAttribute(name);
|
|
529 } else {
|
|
530 var attr = getAttributeNode(element, name);
|
|
531 return attr !== null && attr.specified;
|
|
532 }
|
|
533 }
|
|
534
|
|
535 return {
|
|
536 addEventListener: addEventListener,
|
|
537 removeEventListener: removeEventListener,
|
|
538 initEvent: initEvent,
|
|
539 dispatchEvent: dispatchEvent,
|
|
540 cloneNode: cloneNode,
|
|
541 createStylesheet: createStylesheet,
|
|
542 setAttribute: setAttribute,
|
|
543 getAttribute: getAttribute,
|
|
544 getAttributeNode: getAttributeNode,
|
|
545 hasAttribute: hasAttribute,
|
|
546 getBoundingClientRect: getBoundingClientRect,
|
|
547 untameEventType: untameEventType,
|
|
548 extendedCreateElementFeature: features.extendedCreateElement
|
|
549 };
|
|
550 })();
|