OLD | NEW |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 library polymer.polymer_element; | 5 library polymer.polymer_element; |
6 | 6 |
7 import 'dart:async'; | 7 export 'polymer.dart' show PolymerElement, registerPolymerElement; |
8 import 'dart:html'; | |
9 import 'dart:mirrors'; | |
10 import 'dart:js' as js; | |
11 | |
12 import 'package:custom_element/custom_element.dart'; | |
13 import 'package:mdv/mdv.dart' show NodeBinding; | |
14 import 'package:observe/observe.dart'; | |
15 import 'package:observe/src/microtask.dart'; | |
16 import 'package:polymer_expressions/polymer_expressions.dart'; | |
17 | |
18 import 'src/utils.dart' show toCamelCase, toHyphenedName; | |
19 | |
20 /** | |
21 * Registers a [PolymerElement]. This is similar to [registerCustomElement] | |
22 * but it is designed to work with the `<element>` element and adds additional | |
23 * features. | |
24 */ | |
25 void registerPolymerElement(String localName, PolymerElement create()) { | |
26 registerCustomElement(localName, () => create().._initialize(localName)); | |
27 } | |
28 | |
29 /** | |
30 * *Warning*: many features of this class are not fully implemented. | |
31 * | |
32 * The base class for Polymer elements. It provides convience features on top | |
33 * of the custom elements web standard. | |
34 * | |
35 * Currently it supports publishing attributes via: | |
36 * | |
37 * <element name="..." attributes="foo, bar, baz"> | |
38 * | |
39 * Any attribute published this way can be used in a data binding expression, | |
40 * and it should contain a corresponding DOM field. | |
41 * | |
42 * *Warning*: due to dart2js mirror limititations, the mapping from HTML | |
43 * attribute to element property is a conversion from `dash-separated-words` | |
44 * to camelCase, rather than searching for a property with the same name. | |
45 */ | |
46 // TODO(jmesserly): fix the dash-separated-words issue. Polymer uses lowercase. | |
47 class PolymerElement extends CustomElement with _EventsMixin { | |
48 // This is a partial port of: | |
49 // https://github.com/Polymer/polymer/blob/stable/src/attrs.js | |
50 // https://github.com/Polymer/polymer/blob/stable/src/bindProperties.js | |
51 // https://github.com/Polymer/polymer/blob/7936ff8/src/declaration/events.js | |
52 // https://github.com/Polymer/polymer/blob/7936ff8/src/instance/events.js | |
53 // TODO(jmesserly): we still need to port more of the functionality | |
54 | |
55 /// The one syntax to rule them all. | |
56 static BindingDelegate _polymerSyntax = new PolymerExpressions(); | |
57 // TODO(sigmund): delete. The next line is only added to avoid warnings from | |
58 // the analyzer (see http://dartbug.com/11672) | |
59 Element get host => super.host; | |
60 | |
61 bool get applyAuthorStyles => false; | |
62 bool get resetStyleInheritance => false; | |
63 | |
64 /** | |
65 * The declaration of this polymer-element, used to extract template contents | |
66 * and other information. | |
67 */ | |
68 static Map<String, Element> _declarations = {}; | |
69 static Element getDeclaration(String localName) { | |
70 if (localName == null) return null; | |
71 var element = _declarations[localName]; | |
72 if (element == null) { | |
73 element = document.query('polymer-element[name="$localName"]'); | |
74 _declarations[localName] = element; | |
75 } | |
76 return element; | |
77 } | |
78 | |
79 Map<String, PathObserver> _publishedAttrs; | |
80 Map<String, StreamSubscription> _bindings; | |
81 final List<String> _localNames = []; | |
82 | |
83 void _initialize(String localName) { | |
84 if (localName == null) return; | |
85 | |
86 var declaration = getDeclaration(localName); | |
87 if (declaration == null) return; | |
88 | |
89 var extendee = declaration.attributes['extends']; | |
90 if (extendee != null) { | |
91 // Skip normal tags, only initialize parent custom elements. | |
92 if (extendee.contains('-')) _initialize(extendee); | |
93 } | |
94 | |
95 _parseHostEvents(declaration); | |
96 _parseLocalEvents(declaration); | |
97 _publishAttributes(declaration); | |
98 | |
99 var templateContent = declaration.query('template').content; | |
100 _shimStyling(templateContent, localName); | |
101 | |
102 _localNames.add(localName); | |
103 } | |
104 | |
105 void _publishAttributes(elementElement) { | |
106 _bindings = {}; | |
107 _publishedAttrs = {}; | |
108 | |
109 var attrs = elementElement.attributes['attributes']; | |
110 if (attrs != null) { | |
111 // attributes='a b c' or attributes='a,b,c' | |
112 for (var name in attrs.split(attrs.contains(',') ? ',' : ' ')) { | |
113 name = name.trim(); | |
114 | |
115 // TODO(jmesserly): PathObserver is overkill here; it helps avoid | |
116 // "new Symbol" and other mirrors-related warnings. | |
117 _publishedAttrs[name] = new PathObserver(this, toCamelCase(name)); | |
118 } | |
119 } | |
120 } | |
121 | |
122 void created() { | |
123 // TODO(jmesserly): this breaks until we get some kind of type conversion. | |
124 // _publishedAttrs.forEach((name, propObserver) { | |
125 // var value = attributes[name]; | |
126 // if (value != null) propObserver.value = value; | |
127 // }); | |
128 _initShadowRoot(); | |
129 _addHostListeners(); | |
130 } | |
131 | |
132 /** | |
133 * Creates the document fragment to use for each instance of the custom | |
134 * element, given the `<template>` node. By default this is equivalent to: | |
135 * | |
136 * template.createInstance(this, polymerSyntax); | |
137 * | |
138 * Where polymerSyntax is a singleton `PolymerExpressions` instance from the | |
139 * [polymer_expressions](https://pub.dartlang.org/packages/polymer_expressions
) | |
140 * package. | |
141 * | |
142 * You can override this method to change the instantiation behavior of the | |
143 * template, for example to use a different data-binding syntax. | |
144 */ | |
145 DocumentFragment instanceTemplate(Element template) => | |
146 template.createInstance(this, _polymerSyntax); | |
147 | |
148 void _initShadowRoot() { | |
149 for (var localName in _localNames) { | |
150 var declaration = getDeclaration(localName); | |
151 var root = createShadowRoot(localName); | |
152 _addInstanceListeners(root, localName); | |
153 | |
154 root.applyAuthorStyles = applyAuthorStyles; | |
155 root.resetStyleInheritance = resetStyleInheritance; | |
156 | |
157 var templateNode = declaration.query('template'); | |
158 if (templateNode == null) return; | |
159 | |
160 // Create the contents of the element's ShadowRoot, and add them. | |
161 root.nodes.add(instanceTemplate(templateNode)); | |
162 } | |
163 } | |
164 | |
165 NodeBinding createBinding(String name, model, String path) { | |
166 var propObserver = _publishedAttrs[name]; | |
167 if (propObserver != null) { | |
168 return new _PolymerBinding(this, name, model, path, propObserver); | |
169 } | |
170 return super.createBinding(name, model, path); | |
171 } | |
172 | |
173 /** | |
174 * Using Polymer's platform/src/ShadowCSS.js passing the style tag's content. | |
175 */ | |
176 void _shimStyling(DocumentFragment template, String localName) { | |
177 if (js.context == null) return; | |
178 | |
179 var platform = js.context['Platform']; | |
180 if (platform == null) return; | |
181 | |
182 var style = template.query('style'); | |
183 if (style == null) return; | |
184 | |
185 var shadowCss = platform['ShadowCSS']; | |
186 if (shadowCss == null) return; | |
187 | |
188 // TODO(terry): Remove calls to shimShadowDOMStyling2 and replace with | |
189 // shimShadowDOMStyling when we support unwrapping dart:html | |
190 // Element to a JS DOM node. | |
191 var shimShadowDOMStyling2 = shadowCss['shimShadowDOMStyling2']; | |
192 if (shimShadowDOMStyling2 == null) return; | |
193 | |
194 var scopedCSS = shimShadowDOMStyling2.apply(shadowCss, | |
195 [style.text, localName]); | |
196 | |
197 // TODO(terry): Remove when shimShadowDOMStyling is called we don't need to | |
198 // replace original CSS with scoped CSS shimShadowDOMStyling | |
199 // does that. | |
200 style.text = scopedCSS; | |
201 } | |
202 } | |
203 | |
204 class _PolymerBinding extends NodeBinding { | |
205 final PathObserver _publishedAttr; | |
206 | |
207 _PolymerBinding(node, property, model, path, PathObserver this._publishedAttr) | |
208 : super(node, property, model, path); | |
209 | |
210 void boundValueChanged(newValue) { | |
211 _publishedAttr.value = newValue; | |
212 } | |
213 } | |
214 | |
215 /** | |
216 * Polymer features to handle the syntactic sugar on-* to declare to | |
217 * automatically map event handlers to instance methods of the [PolymerElement]. | |
218 * This mixin is a port of: | |
219 * https://github.com/Polymer/polymer/blob/7936ff8/src/declaration/events.js | |
220 * https://github.com/Polymer/polymer/blob/7936ff8/src/instance/events.js | |
221 */ | |
222 abstract class _EventsMixin { | |
223 // TODO(sigmund): implement the Dart equivalent of 'inheritDelegates' | |
224 // Notes about differences in the implementation below: | |
225 // - _templateDelegates: polymer stores the template delegates directly on | |
226 // the template node (see in parseLocalEvents: 't.delegates = {}'). Here we | |
227 // simply use a separate map, where keys are the name of the | |
228 // custom-element. | |
229 // - _listenLocal we return true/false and propagate that up, JS | |
230 // implementation does't forward the return value. | |
231 // - we don't keep the side-table (weak hash map) of unhandled events (see | |
232 // handleIfNotHandled) | |
233 // - we don't use event.type to dispatch events, instead we save the event | |
234 // name with the event listeners. We do so to avoid translating back and | |
235 // forth between Dom and Dart event names. | |
236 | |
237 // --------------------------------------------------------------------------- | |
238 // The following section was ported from: | |
239 // https://github.com/Polymer/polymer/blob/7936ff8/src/declaration/events.js | |
240 // --------------------------------------------------------------------------- | |
241 | |
242 /** Maps event names and their associated method in the element class. */ | |
243 final Map<String, String> _delegates = {}; | |
244 | |
245 /** Expected events per element node. */ | |
246 // TODO(sigmund): investigate whether we need more than 1 set of local events | |
247 // per element (why does the js implementation stores 1 per template node?) | |
248 final Map<String, Set<String>> _templateDelegates = | |
249 new Map<String, Set<String>>(); | |
250 | |
251 /** [host] is needed by this mixin, but not defined here. */ | |
252 Element get host; | |
253 | |
254 /** Attribute prefix used for declarative event handlers. */ | |
255 static const _eventPrefix = 'on-'; | |
256 | |
257 /** Whether an attribute declares an event. */ | |
258 static bool _isEvent(String attr) => attr.startsWith(_eventPrefix); | |
259 | |
260 /** Extracts events from the element tag attributes. */ | |
261 void _parseHostEvents(elementElement) { | |
262 for (var attr in elementElement.attributes.keys.where(_isEvent)) { | |
263 _delegates[toCamelCase(attr)] = elementElement.attributes[attr]; | |
264 } | |
265 } | |
266 | |
267 /** Extracts events under the element's <template>. */ | |
268 void _parseLocalEvents(elementElement) { | |
269 var name = elementElement.attributes["name"]; | |
270 if (name == null) return; | |
271 var events = null; | |
272 for (var template in elementElement.queryAll('template')) { | |
273 var content = template.content; | |
274 if (content != null) { | |
275 for (var child in content.children) { | |
276 events = _accumulateEvents(child, events); | |
277 } | |
278 } | |
279 } | |
280 if (events != null) { | |
281 _templateDelegates[name] = events; | |
282 } | |
283 } | |
284 | |
285 /** Returns all events names listened by [element] and it's children. */ | |
286 static Set<String> _accumulateEvents(Element element, [Set<String> events]) { | |
287 events = events == null ? new Set<String>() : events; | |
288 | |
289 // from: accumulateAttributeEvents, accumulateEvent | |
290 events.addAll(element.attributes.keys.where(_isEvent).map(toCamelCase)); | |
291 | |
292 // from: accumulateChildEvents | |
293 for (var child in element.children) { | |
294 _accumulateEvents(child, events); | |
295 } | |
296 | |
297 // from: accumulateTemplatedEvents | |
298 if (element.isTemplate) { | |
299 var content = element.content; | |
300 if (content != null) { | |
301 for (var child in content.children) { | |
302 _accumulateEvents(child, events); | |
303 } | |
304 } | |
305 } | |
306 return events; | |
307 } | |
308 | |
309 // --------------------------------------------------------------------------- | |
310 // The following section was ported from: | |
311 // https://github.com/Polymer/polymer/blob/7936ff8/src/instance/events.js | |
312 // --------------------------------------------------------------------------- | |
313 | |
314 /** Attaches event listeners on the [host] element. */ | |
315 void _addHostListeners() { | |
316 for (var eventName in _delegates.keys) { | |
317 _addNodeListener(host, eventName, | |
318 (e) => _hostEventListener(eventName, e)); | |
319 } | |
320 } | |
321 | |
322 void _addNodeListener(node, String onEvent, Function listener) { | |
323 // If [node] is an element (typically when listening for host events) we | |
324 // use directly the '.onFoo' event stream of the element instance. | |
325 if (node is Element) { | |
326 reflect(node).getField(new Symbol(onEvent)).reflectee.listen(listener); | |
327 return; | |
328 } | |
329 | |
330 // When [node] is not an element, most commonly when [node] is the | |
331 // shadow-root of the polymer-element, we find the appropriate static event | |
332 // stream providers and attach it to [node]. | |
333 var eventProvider = _eventStreamProviders[onEvent]; | |
334 if (eventProvider != null) { | |
335 eventProvider.forTarget(node).listen(listener); | |
336 return; | |
337 } | |
338 | |
339 // When no provider is available, mainly because of custom-events, we use | |
340 // the underlying event listeners from the DOM. | |
341 var eventName = onEvent.substring(2).toLowerCase(); // onOneTwo => onetwo | |
342 // Most events names in Dart match those in JS in lowercase except for some | |
343 // few events listed in this map. We expect these cases to be handled above, | |
344 // but just in case we include them as a safety net here. | |
345 var jsNameFixes = const { | |
346 'animationend': 'webkitAnimationEnd', | |
347 'animationiteration': 'webkitAnimationIteration', | |
348 'animationstart': 'webkitAnimationStart', | |
349 'doubleclick': 'dblclick', | |
350 'fullscreenchange': 'webkitfullscreenchange', | |
351 'fullscreenerror': 'webkitfullscreenerror', | |
352 'keyadded': 'webkitkeyadded', | |
353 'keyerror': 'webkitkeyerror', | |
354 'keymessage': 'webkitkeymessage', | |
355 'needkey': 'webkitneedkey', | |
356 'speechchange': 'webkitSpeechChange', | |
357 }; | |
358 var fixedName = jsNameFixes[eventName]; | |
359 node.on[fixedName != null ? fixedName : eventName].listen(listener); | |
360 } | |
361 | |
362 void _addInstanceListeners(ShadowRoot root, String elementName) { | |
363 var events = _templateDelegates[elementName]; | |
364 if (events == null) return; | |
365 for (var eventName in events) { | |
366 _addNodeListener(root, eventName, | |
367 (e) => _instanceEventListener(eventName, e)); | |
368 } | |
369 } | |
370 | |
371 void _hostEventListener(String eventName, Event event) { | |
372 var method = _delegates[eventName]; | |
373 if (event.bubbles && method != null) { | |
374 _dispatchMethod(this, method, event, host); | |
375 } | |
376 } | |
377 | |
378 void _dispatchMethod(Object receiver, String methodName, Event event, | |
379 Node target) { | |
380 var detail = event is CustomEvent ? (event as CustomEvent).detail : null; | |
381 var args = [event, detail, target]; | |
382 | |
383 var method = new Symbol(methodName); | |
384 // TODO(sigmund): consider making event listeners list all arguments | |
385 // explicitly. Unless VM mirrors are optimized first, this reflectClass call | |
386 // will be expensive once custom elements extend directly from Element (see | |
387 // dartbug.com/11108). | |
388 var methodDecl = reflectClass(receiver.runtimeType).methods[method]; | |
389 if (methodDecl != null) { | |
390 // This will either truncate the argument list or extend it with extra | |
391 // null arguments, so it will match the signature. | |
392 // TODO(sigmund): consider accepting optional arguments when we can tell | |
393 // them appart from named arguments (see http://dartbug.com/11334) | |
394 args.length = methodDecl.parameters.where((p) => !p.isOptional).length; | |
395 } | |
396 reflect(receiver).invoke(method, args); | |
397 performMicrotaskCheckpoint(); | |
398 } | |
399 | |
400 bool _instanceEventListener(String eventName, Event event) { | |
401 if (event.bubbles) { | |
402 if (event.path == null || !ShadowRoot.supported) { | |
403 return _listenLocalNoEventPath(eventName, event); | |
404 } else { | |
405 return _listenLocal(eventName, event); | |
406 } | |
407 } | |
408 return false; | |
409 } | |
410 | |
411 bool _listenLocal(String eventName, Event event) { | |
412 var controller = null; | |
413 for (var target in event.path) { | |
414 // if we hit host, stop | |
415 if (target == host) return true; | |
416 | |
417 // find a controller for the target, unless we already found `host` | |
418 // as a controller | |
419 controller = (controller == host) ? controller : _findController(target); | |
420 | |
421 // if we have a controller, dispatch the event, and stop if the handler | |
422 // returns true | |
423 if (controller != null | |
424 && handleEvent(controller, eventName, event, target)) { | |
425 return true; | |
426 } | |
427 } | |
428 return false; | |
429 } | |
430 | |
431 // TODO(sorvell): remove when ShadowDOM polyfill supports event path. | |
432 // Note that _findController will not return the expected controller when the | |
433 // event target is a distributed node. This is because we cannot traverse | |
434 // from a composed node to a node in shadowRoot. | |
435 // This will be addressed via an event path api | |
436 // https://www.w3.org/Bugs/Public/show_bug.cgi?id=21066 | |
437 bool _listenLocalNoEventPath(String eventName, Event event) { | |
438 var target = event.target; | |
439 var controller = null; | |
440 while (target != null && target != host) { | |
441 controller = (controller == host) ? controller : _findController(target); | |
442 if (controller != null | |
443 && handleEvent(controller, eventName, event, target)) { | |
444 return true; | |
445 } | |
446 target = target.parent; | |
447 } | |
448 return false; | |
449 } | |
450 | |
451 // TODO(sigmund): investigate if this implementation is correct. Polymer looks | |
452 // up the shadow-root that contains [node] and uses a weak-hashmap to find the | |
453 // host associated with that root. This implementation assumes that the | |
454 // [node] is under [host]'s shadow-root. | |
455 Element _findController(Node node) => host.xtag; | |
456 | |
457 bool handleEvent( | |
458 Element controller, String eventName, Event event, Element element) { | |
459 // Note: local events are listened only in the shadow root. This dynamic | |
460 // lookup is used to distinguish determine whether the target actually has a | |
461 // listener, and if so, to determine lazily what's the target method. | |
462 var methodName = element.attributes[toHyphenedName(eventName)]; | |
463 if (methodName != null) { | |
464 _dispatchMethod(controller, methodName, event, element); | |
465 } | |
466 return event.bubbles; | |
467 } | |
468 } | |
469 | |
470 | |
471 /** Event stream providers per event name. */ | |
472 // TODO(sigmund): after dartbug.com/11108 is fixed, consider eliminating this | |
473 // table and using reflection instead. | |
474 const Map<String, EventStreamProvider> _eventStreamProviders = const { | |
475 'onMouseWheel': Element.mouseWheelEvent, | |
476 'onTransitionEnd': Element.transitionEndEvent, | |
477 'onAbort': Element.abortEvent, | |
478 'onBeforeCopy': Element.beforeCopyEvent, | |
479 'onBeforeCut': Element.beforeCutEvent, | |
480 'onBeforePaste': Element.beforePasteEvent, | |
481 'onBlur': Element.blurEvent, | |
482 'onChange': Element.changeEvent, | |
483 'onClick': Element.clickEvent, | |
484 'onContextMenu': Element.contextMenuEvent, | |
485 'onCopy': Element.copyEvent, | |
486 'onCut': Element.cutEvent, | |
487 'onDoubleClick': Element.doubleClickEvent, | |
488 'onDrag': Element.dragEvent, | |
489 'onDragEnd': Element.dragEndEvent, | |
490 'onDragEnter': Element.dragEnterEvent, | |
491 'onDragLeave': Element.dragLeaveEvent, | |
492 'onDragOver': Element.dragOverEvent, | |
493 'onDragStart': Element.dragStartEvent, | |
494 'onDrop': Element.dropEvent, | |
495 'onError': Element.errorEvent, | |
496 'onFocus': Element.focusEvent, | |
497 'onInput': Element.inputEvent, | |
498 'onInvalid': Element.invalidEvent, | |
499 'onKeyDown': Element.keyDownEvent, | |
500 'onKeyPress': Element.keyPressEvent, | |
501 'onKeyUp': Element.keyUpEvent, | |
502 'onLoad': Element.loadEvent, | |
503 'onMouseDown': Element.mouseDownEvent, | |
504 'onMouseMove': Element.mouseMoveEvent, | |
505 'onMouseOut': Element.mouseOutEvent, | |
506 'onMouseOver': Element.mouseOverEvent, | |
507 'onMouseUp': Element.mouseUpEvent, | |
508 'onPaste': Element.pasteEvent, | |
509 'onReset': Element.resetEvent, | |
510 'onScroll': Element.scrollEvent, | |
511 'onSearch': Element.searchEvent, | |
512 'onSelect': Element.selectEvent, | |
513 'onSelectStart': Element.selectStartEvent, | |
514 'onSubmit': Element.submitEvent, | |
515 'onTouchCancel': Element.touchCancelEvent, | |
516 'onTouchEnd': Element.touchEndEvent, | |
517 'onTouchEnter': Element.touchEnterEvent, | |
518 'onTouchLeave': Element.touchLeaveEvent, | |
519 'onTouchMove': Element.touchMoveEvent, | |
520 'onTouchStart': Element.touchStartEvent, | |
521 'onFullscreenChange': Element.fullscreenChangeEvent, | |
522 'onFullscreenError': Element.fullscreenErrorEvent, | |
523 'onAutocomplete': FormElement.autocompleteEvent, | |
524 'onAutocompleteError': FormElement.autocompleteErrorEvent, | |
525 'onSpeechChange': InputElement.speechChangeEvent, | |
526 'onCanPlay': MediaElement.canPlayEvent, | |
527 'onCanPlayThrough': MediaElement.canPlayThroughEvent, | |
528 'onDurationChange': MediaElement.durationChangeEvent, | |
529 'onEmptied': MediaElement.emptiedEvent, | |
530 'onEnded': MediaElement.endedEvent, | |
531 'onLoadStart': MediaElement.loadStartEvent, | |
532 'onLoadedData': MediaElement.loadedDataEvent, | |
533 'onLoadedMetadata': MediaElement.loadedMetadataEvent, | |
534 'onPause': MediaElement.pauseEvent, | |
535 'onPlay': MediaElement.playEvent, | |
536 'onPlaying': MediaElement.playingEvent, | |
537 'onProgress': MediaElement.progressEvent, | |
538 'onRateChange': MediaElement.rateChangeEvent, | |
539 'onSeeked': MediaElement.seekedEvent, | |
540 'onSeeking': MediaElement.seekingEvent, | |
541 'onShow': MediaElement.showEvent, | |
542 'onStalled': MediaElement.stalledEvent, | |
543 'onSuspend': MediaElement.suspendEvent, | |
544 'onTimeUpdate': MediaElement.timeUpdateEvent, | |
545 'onVolumeChange': MediaElement.volumeChangeEvent, | |
546 'onWaiting': MediaElement.waitingEvent, | |
547 'onKeyAdded': MediaElement.keyAddedEvent, | |
548 'onKeyError': MediaElement.keyErrorEvent, | |
549 'onKeyMessage': MediaElement.keyMessageEvent, | |
550 'onNeedKey': MediaElement.needKeyEvent, | |
551 'onWebGlContextLost': CanvasElement.webGlContextLostEvent, | |
552 'onWebGlContextRestored': CanvasElement.webGlContextRestoredEvent, | |
553 'onPointerLockChange': Document.pointerLockChangeEvent, | |
554 'onPointerLockError': Document.pointerLockErrorEvent, | |
555 'onReadyStateChange': Document.readyStateChangeEvent, | |
556 'onSelectionChange': Document.selectionChangeEvent, | |
557 'onSecurityPolicyViolation': Document.securityPolicyViolationEvent, | |
558 }; | |
OLD | NEW |