OLD | NEW |
---|---|
(Empty) | |
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 | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 /** | |
6 * Support for interoperating with JavaScript. | |
Jennifer Messerly
2015/06/12 21:03:05
this is a straight up copy from the 1.9.0-dev.4.0
| |
7 * | |
8 * This library provides access to JavaScript objects from Dart, allowing | |
9 * Dart code to get and set properties, and call methods of JavaScript objects | |
10 * and invoke JavaScript functions. The library takes care of converting | |
11 * between Dart and JavaScript objects where possible, or providing proxies if | |
12 * conversion isn't possible. | |
13 * | |
14 * This library does not yet make Dart objects usable from JavaScript, their | |
15 * methods and proeprties are not accessible, though it does allow Dart | |
16 * functions to be passed into and called from JavaScript. | |
17 * | |
18 * [JsObject] is the core type and represents a proxy of a JavaScript object. | |
19 * JsObject gives access to the underlying JavaScript objects properties and | |
20 * methods. `JsObject`s can be acquired by calls to JavaScript, or they can be | |
21 * created from proxies to JavaScript constructors. | |
22 * | |
23 * The top-level getter [context] provides a [JsObject] that represents the | |
24 * global object in JavaScript, usually `window`. | |
25 * | |
26 * The following example shows an alert dialog via a JavaScript call to the | |
27 * global function `alert()`: | |
28 * | |
29 * import 'dart:js'; | |
30 * | |
31 * main() => context.callMethod('alert', ['Hello from Dart!']); | |
32 * | |
33 * This example shows how to create a [JsObject] from a JavaScript constructor | |
34 * and access its properties: | |
35 * | |
36 * import 'dart:js'; | |
37 * | |
38 * main() { | |
39 * var object = new JsObject(context['Object']); | |
40 * object['greeting'] = 'Hello'; | |
41 * object['greet'] = (name) => "${object['greeting']} $name"; | |
42 * var message = object.callMethod('greet', ['JavaScript']); | |
43 * context['console'].callMethod('log', [message]); | |
44 * } | |
45 * | |
46 * ## Proxying and automatic conversion | |
47 * | |
48 * When setting properties on a JsObject or passing arguments to a Javascript | |
49 * method or function, Dart objects are automatically converted or proxied to | |
50 * JavaScript objects. When accessing JavaScript properties, or when a Dart | |
51 * closure is invoked from JavaScript, the JavaScript objects are also | |
52 * converted to Dart. | |
53 * | |
54 * Functions and closures are proxied in such a way that they are callable. A | |
55 * Dart closure assigned to a JavaScript property is proxied by a function in | |
56 * JavaScript. A JavaScript function accessed from Dart is proxied by a | |
57 * [JsFunction], which has a [apply] method to invoke it. | |
58 * | |
59 * The following types are transferred directly and not proxied: | |
60 * | |
61 * * "Basic" types: `null`, `bool`, `num`, `String`, `DateTime` | |
62 * * `Blob` | |
63 * * `Event` | |
64 * * `HtmlCollection` | |
65 * * `ImageData` | |
66 * * `KeyRange` | |
67 * * `Node` | |
68 * * `NodeList` | |
69 * * `TypedData`, including its subclasses like `Int32List`, but _not_ | |
70 * `ByteBuffer` | |
71 * * `Window` | |
72 * | |
73 * ## Converting collections with JsObject.jsify() | |
74 * | |
75 * To create a JavaScript collection from a Dart collection use the | |
76 * [JsObject.jsify] constructor, which converts Dart [Map]s and [Iterable]s | |
77 * into JavaScript Objects and Arrays. | |
78 * | |
79 * The following expression creats a new JavaScript object with the properties | |
80 * `a` and `b` defined: | |
81 * | |
82 * var jsMap = new JsObject.jsify({'a': 1, 'b': 2}); | |
83 * | |
84 * This expression creates a JavaScript array: | |
85 * | |
86 * var jsArray = new JsObject.jsify([1, 2, 3]); | |
87 */ | |
88 library dart.js; | |
89 | |
90 import 'dart:html' show Blob, Event, ImageData, Node, Window; | |
91 import 'dart:collection' show HashMap, ListMixin; | |
92 import 'dart:indexed_db' show KeyRange; | |
93 import 'dart:typed_data' show TypedData; | |
94 | |
95 import 'dart:_foreign_helper' show JS, DART_CLOSURE_TO_JS; | |
96 import 'dart:_interceptors' show JavaScriptObject, UnknownJavaScriptObject; | |
97 import 'dart:_js_helper' show Primitives, convertDartClosureToJS, | |
98 getIsolateAffinityTag; | |
99 | |
100 final JsObject context = _wrapToDart(JS('', 'self')); | |
101 | |
102 _convertDartFunction(Function f, {bool captureThis: false}) { | |
103 return JS('', | |
104 'function(_call, f, captureThis) {' | |
105 'return function() {' | |
106 'return _call(f, captureThis, this, ' | |
107 'Array.prototype.slice.apply(arguments));' | |
108 '}' | |
109 '}(#, #, #)', DART_CLOSURE_TO_JS(_callDartFunction), f, captureThis); | |
110 } | |
111 | |
112 _callDartFunction(callback, bool captureThis, self, List arguments) { | |
113 if (captureThis) { | |
114 arguments = [self]..addAll(arguments); | |
115 } | |
116 var dartArgs = new List.from(arguments.map(_convertToDart)); | |
117 return _convertToJS(Function.apply(callback, dartArgs)); | |
118 } | |
119 | |
120 /** | |
121 * Proxies a JavaScript object to Dart. | |
122 * | |
123 * The properties of the JavaScript object are accessible via the `[]` and | |
124 * `[]=` operators. Methods are callable via [callMethod]. | |
125 */ | |
126 class JsObject { | |
127 // The wrapped JS object. | |
128 final dynamic _jsObject; | |
129 | |
130 // This shoud only be called from _wrapToDart | |
131 JsObject._fromJs(this._jsObject) { | |
132 assert(_jsObject != null); | |
133 } | |
134 | |
135 /** | |
136 * Constructs a new JavaScript object from [constructor] and returns a proxy | |
137 * to it. | |
138 */ | |
139 factory JsObject(JsFunction constructor, [List arguments]) { | |
140 var constr = _convertToJS(constructor); | |
141 if (arguments == null) { | |
142 return _wrapToDart(JS('', 'new #()', constr)); | |
143 } | |
144 // The following code solves the problem of invoking a JavaScript | |
145 // constructor with an unknown number arguments. | |
146 // First bind the constructor to the argument list using bind.apply(). | |
147 // The first argument to bind() is the binding of 'this', so add 'null' to | |
148 // the arguments list passed to apply(). | |
149 // After that, use the JavaScript 'new' operator which overrides any binding | |
150 // of 'this' with the new instance. | |
151 var args = [null]..addAll(arguments.map(_convertToJS)); | |
152 var factoryFunction = JS('', '#.bind.apply(#, #)', constr, constr, args); | |
153 // Without this line, calling factoryFunction as a constructor throws | |
154 JS('String', 'String(#)', factoryFunction); | |
155 // This could return an UnknownJavaScriptObject, or a native | |
156 // object for which there is an interceptor | |
157 var jsObj = JS('JavaScriptObject', 'new #()', factoryFunction); | |
158 | |
159 return _wrapToDart(jsObj); | |
160 } | |
161 | |
162 /** | |
163 * Constructs a [JsObject] that proxies a native Dart object; _for expert use | |
164 * only_. | |
165 * | |
166 * Use this constructor only if you wish to get access to JavaScript | |
167 * properties attached to a browser host object, such as a Node or Blob, that | |
168 * is normally automatically converted into a native Dart object. | |
169 * | |
170 * An exception will be thrown if [object] either is `null` or has the type | |
171 * `bool`, `num`, or `String`. | |
172 */ | |
173 factory JsObject.fromBrowserObject(object) { | |
174 if (object is num || object is String || object is bool || object == null) { | |
175 throw new ArgumentError( | |
176 "object cannot be a num, string, bool, or null"); | |
177 } | |
178 return _wrapToDart(_convertToJS(object)); | |
179 } | |
180 | |
181 /** | |
182 * Recursively converts a JSON-like collection of Dart objects to a | |
183 * collection of JavaScript objects and returns a [JsObject] proxy to it. | |
184 * | |
185 * [object] must be a [Map] or [Iterable], the contents of which are also | |
186 * converted. Maps and Iterables are copied to a new JavaScript object. | |
187 * Primitives and other transferrable values are directly converted to their | |
188 * JavaScript type, and all other objects are proxied. | |
189 */ | |
190 factory JsObject.jsify(object) { | |
191 if ((object is! Map) && (object is! Iterable)) { | |
192 throw new ArgumentError("object must be a Map or Iterable"); | |
193 } | |
194 return _wrapToDart(_convertDataTree(object)); | |
195 } | |
196 | |
197 static _convertDataTree(data) { | |
198 var _convertedObjects = new HashMap.identity(); | |
199 | |
200 _convert(o) { | |
201 if (_convertedObjects.containsKey(o)) { | |
202 return _convertedObjects[o]; | |
203 } | |
204 if (o is Map) { | |
205 final convertedMap = JS('=Object', '{}'); | |
206 _convertedObjects[o] = convertedMap; | |
207 for (var key in o.keys) { | |
208 JS('=Object', '#[#]=#', convertedMap, key, _convert(o[key])); | |
209 } | |
210 return convertedMap; | |
211 } else if (o is Iterable) { | |
212 var convertedList = []; | |
213 _convertedObjects[o] = convertedList; | |
214 convertedList.addAll(o.map(_convert)); | |
215 return convertedList; | |
216 } else { | |
217 return _convertToJS(o); | |
218 } | |
219 } | |
220 | |
221 return _convert(data); | |
222 } | |
223 | |
224 /** | |
225 * Returns the value associated with [property] from the proxied JavaScript | |
226 * object. | |
227 * | |
228 * The type of [property] must be either [String] or [num]. | |
229 */ | |
230 dynamic operator[](property) { | |
231 if (property is! String && property is! num) { | |
232 throw new ArgumentError("property is not a String or num"); | |
233 } | |
234 return _convertToDart(JS('', '#[#]', _jsObject, property)); | |
235 } | |
236 | |
237 /** | |
238 * Sets the value associated with [property] on the proxied JavaScript | |
239 * object. | |
240 * | |
241 * The type of [property] must be either [String] or [num]. | |
242 */ | |
243 operator[]=(property, value) { | |
244 if (property is! String && property is! num) { | |
245 throw new ArgumentError("property is not a String or num"); | |
246 } | |
247 JS('', '#[#]=#', _jsObject, property, _convertToJS(value)); | |
248 } | |
249 | |
250 int get hashCode => 0; | |
251 | |
252 bool operator==(other) => other is JsObject && | |
253 JS('bool', '# === #', _jsObject, other._jsObject); | |
254 | |
255 /** | |
256 * Returns `true` if the JavaScript object contains the specified property | |
257 * either directly or though its prototype chain. | |
258 * | |
259 * This is the equivalent of the `in` operator in JavaScript. | |
260 */ | |
261 bool hasProperty(property) { | |
262 if (property is! String && property is! num) { | |
263 throw new ArgumentError("property is not a String or num"); | |
264 } | |
265 return JS('bool', '# in #', property, _jsObject); | |
266 } | |
267 | |
268 /** | |
269 * Removes [property] from the JavaScript object. | |
270 * | |
271 * This is the equivalent of the `delete` operator in JavaScript. | |
272 */ | |
273 void deleteProperty(property) { | |
274 if (property is! String && property is! num) { | |
275 throw new ArgumentError("property is not a String or num"); | |
276 } | |
277 JS('bool', 'delete #[#]', _jsObject, property); | |
278 } | |
279 | |
280 /** | |
281 * Returns `true` if the JavaScript object has [type] in its prototype chain. | |
282 * | |
283 * This is the equivalent of the `instanceof` operator in JavaScript. | |
284 */ | |
285 bool instanceof(JsFunction type) { | |
286 return JS('bool', '# instanceof #', _jsObject, _convertToJS(type)); | |
287 } | |
288 | |
289 /** | |
290 * Returns the result of the JavaScript objects `toString` method. | |
291 */ | |
292 String toString() { | |
293 try { | |
294 return JS('String', 'String(#)', _jsObject); | |
295 } catch(e) { | |
296 return super.toString(); | |
297 } | |
298 } | |
299 | |
300 /** | |
301 * Calls [method] on the JavaScript object with the arguments [args] and | |
302 * returns the result. | |
303 * | |
304 * The type of [method] must be either [String] or [num]. | |
305 */ | |
306 dynamic callMethod(method, [List args]) { | |
307 if (method is! String && method is! num) { | |
308 throw new ArgumentError("method is not a String or num"); | |
309 } | |
310 return _convertToDart(JS('', '#[#].apply(#, #)', _jsObject, method, | |
311 _jsObject, | |
312 args == null ? null : new List.from(args.map(_convertToJS)))); | |
313 } | |
314 } | |
315 | |
316 /** | |
317 * Proxies a JavaScript Function object. | |
318 */ | |
319 class JsFunction extends JsObject { | |
320 | |
321 /** | |
322 * Returns a [JsFunction] that captures its 'this' binding and calls [f] | |
323 * with the value of this passed as the first argument. | |
324 */ | |
325 factory JsFunction.withThis(Function f) { | |
326 var jsFunc = _convertDartFunction(f, captureThis: true); | |
327 return new JsFunction._fromJs(jsFunc); | |
328 } | |
329 | |
330 JsFunction._fromJs(jsObject) : super._fromJs(jsObject); | |
331 | |
332 /** | |
333 * Invokes the JavaScript function with arguments [args]. If [thisArg] is | |
334 * supplied it is the value of `this` for the invocation. | |
335 */ | |
336 dynamic apply(List args, { thisArg }) => | |
337 _convertToDart(JS('', '#.apply(#, #)', _jsObject, | |
338 _convertToJS(thisArg), | |
339 args == null ? null : new List.from(args.map(_convertToJS)))); | |
340 } | |
341 | |
342 /** | |
343 * A [List] that proxies a JavaScript array. | |
344 */ | |
345 class JsArray<E> extends JsObject with ListMixin<E> { | |
346 | |
347 /** | |
348 * Creates a new JavaScript array. | |
349 */ | |
350 JsArray() : super._fromJs([]); | |
351 | |
352 /** | |
353 * Creates a new JavaScript array and initializes it to the contents of | |
354 * [other]. | |
355 */ | |
356 JsArray.from(Iterable<E> other) | |
357 : super._fromJs([]..addAll(other.map(_convertToJS))); | |
358 | |
359 JsArray._fromJs(jsObject) : super._fromJs(jsObject); | |
360 | |
361 _checkIndex(int index) { | |
362 if (index is int && (index < 0 || index >= length)) { | |
363 throw new RangeError.range(index, 0, length); | |
364 } | |
365 } | |
366 | |
367 _checkInsertIndex(int index) { | |
368 if (index is int && (index < 0 || index >= length + 1)) { | |
369 throw new RangeError.range(index, 0, length); | |
370 } | |
371 } | |
372 | |
373 static _checkRange(int start, int end, int length) { | |
374 if (start < 0 || start > length) { | |
375 throw new RangeError.range(start, 0, length); | |
376 } | |
377 if (end < start || end > length) { | |
378 throw new RangeError.range(end, start, length); | |
379 } | |
380 } | |
381 | |
382 // Methods required by ListMixin | |
383 | |
384 E operator [](index) { | |
385 // TODO(justinfagnani): fix the semantics for non-ints | |
386 // dartbug.com/14605 | |
387 if (index is num && index == index.toInt()) { | |
388 _checkIndex(index); | |
389 } | |
390 return super[index]; | |
391 } | |
392 | |
393 void operator []=(index, E value) { | |
394 // TODO(justinfagnani): fix the semantics for non-ints | |
395 // dartbug.com/14605 | |
396 if (index is num && index == index.toInt()) { | |
397 _checkIndex(index); | |
398 } | |
399 super[index] = value; | |
400 } | |
401 | |
402 int get length { | |
403 // Check the length honours the List contract. | |
404 var len = JS('', '#.length', _jsObject); | |
405 // JavaScript arrays have lengths which are unsigned 32-bit integers. | |
406 if (JS('bool', 'typeof # === "number" && (# >>> 0) === #', len, len, len)) { | |
407 return JS('int', '#', len); | |
408 } | |
409 throw new StateError('Bad JsArray length'); | |
410 } | |
411 | |
412 void set length(int length) { super['length'] = length; } | |
413 | |
414 | |
415 // Methods overriden for better performance | |
416 | |
417 void add(E value) { | |
418 callMethod('push', [value]); | |
419 } | |
420 | |
421 void addAll(Iterable<E> iterable) { | |
422 var list = (JS('bool', '# instanceof Array', iterable)) | |
423 ? iterable | |
424 : new List.from(iterable); | |
425 callMethod('push', list); | |
426 } | |
427 | |
428 void insert(int index, E element) { | |
429 _checkInsertIndex(index); | |
430 callMethod('splice', [index, 0, element]); | |
431 } | |
432 | |
433 E removeAt(int index) { | |
434 _checkIndex(index); | |
435 return callMethod('splice', [index, 1])[0]; | |
436 } | |
437 | |
438 E removeLast() { | |
439 if (length == 0) throw new RangeError(-1); | |
440 return callMethod('pop'); | |
441 } | |
442 | |
443 void removeRange(int start, int end) { | |
444 _checkRange(start, end, length); | |
445 callMethod('splice', [start, end - start]); | |
446 } | |
447 | |
448 void setRange(int start, int end, Iterable<E> iterable, [int skipCount = 0]) { | |
449 _checkRange(start, end, length); | |
450 int length = end - start; | |
451 if (length == 0) return; | |
452 if (skipCount < 0) throw new ArgumentError(skipCount); | |
453 var args = [start, length]..addAll(iterable.skip(skipCount).take(length)); | |
454 callMethod('splice', args); | |
455 } | |
456 | |
457 void sort([int compare(E a, E b)]) { | |
458 // Note: arr.sort(null) is a type error in FF | |
459 callMethod('sort', compare == null ? [] : [compare]); | |
460 } | |
461 } | |
462 | |
463 // property added to a Dart object referencing its JS-side DartObject proxy | |
464 final String _DART_OBJECT_PROPERTY_NAME = | |
465 getIsolateAffinityTag(r'_$dart_dartObject'); | |
466 final String _DART_CLOSURE_PROPERTY_NAME = | |
467 getIsolateAffinityTag(r'_$dart_dartClosure'); | |
468 | |
469 // property added to a JS object referencing its Dart-side JsObject proxy | |
470 const _JS_OBJECT_PROPERTY_NAME = r'_$dart_jsObject'; | |
471 const _JS_FUNCTION_PROPERTY_NAME = r'$dart_jsFunction'; | |
472 | |
473 bool _defineProperty(o, String name, value) { | |
474 if (_isExtensible(o) && | |
475 // TODO(ahe): Calling _hasOwnProperty to work around | |
476 // https://code.google.com/p/dart/issues/detail?id=21331. | |
477 !_hasOwnProperty(o, name)) { | |
478 try { | |
479 JS('void', 'Object.defineProperty(#, #, { value: #})', o, name, value); | |
480 return true; | |
481 } catch (e) { | |
482 // object is native and lies about being extensible | |
483 // see https://bugzilla.mozilla.org/show_bug.cgi?id=775185 | |
484 } | |
485 } | |
486 return false; | |
487 } | |
488 | |
489 bool _hasOwnProperty(o, String name) { | |
490 return JS('bool', 'Object.prototype.hasOwnProperty.call(#, #)', o, name); | |
491 } | |
492 | |
493 bool _isExtensible(o) => JS('bool', 'Object.isExtensible(#)', o); | |
494 | |
495 Object _getOwnProperty(o, String name) { | |
496 if (_hasOwnProperty(o, name)) { | |
497 return JS('', '#[#]', o, name); | |
498 } | |
499 return null; | |
500 } | |
501 | |
502 bool _isLocalObject(o) => JS('bool', '# instanceof Object', o); | |
503 | |
504 // The shared constructor function for proxies to Dart objects in JavaScript. | |
505 final _dartProxyCtor = JS('', 'function DartObject(o) { this.o = o; }'); | |
506 | |
507 dynamic _convertToJS(dynamic o) { | |
508 // Note: we don't write `if (o == null) return null;` to make sure dart2js | |
509 // doesn't convert `return null;` into `return;` (which would make `null` be | |
510 // `undefined` in Javascprit). See dartbug.com/20305 for details. | |
511 if (o == null || o is String || o is num || o is bool) { | |
512 return o; | |
513 } else if (o is Blob || o is Event || o is KeyRange || o is ImageData | |
514 || o is Node || o is TypedData || o is Window) { | |
515 return o; | |
516 } else if (o is DateTime) { | |
517 return Primitives.lazyAsJsDate(o); | |
518 } else if (o is JsObject) { | |
519 return o._jsObject; | |
520 } else if (o is Function) { | |
521 return _getJsProxy(o, _JS_FUNCTION_PROPERTY_NAME, (o) { | |
522 var jsFunction = _convertDartFunction(o); | |
523 // set a property on the JS closure referencing the Dart closure | |
524 _defineProperty(jsFunction, _DART_CLOSURE_PROPERTY_NAME, o); | |
525 return jsFunction; | |
526 }); | |
527 } else { | |
528 var ctor = _dartProxyCtor; | |
529 return _getJsProxy(o, _JS_OBJECT_PROPERTY_NAME, | |
530 (o) => JS('', 'new #(#)', ctor, o)); | |
531 } | |
532 } | |
533 | |
534 Object _getJsProxy(o, String propertyName, createProxy(o)) { | |
535 var jsProxy = _getOwnProperty(o, propertyName); | |
536 if (jsProxy == null) { | |
537 jsProxy = createProxy(o); | |
538 _defineProperty(o, propertyName, jsProxy); | |
539 } | |
540 return jsProxy; | |
541 } | |
542 | |
543 // converts a Dart object to a reference to a native JS object | |
544 // which might be a DartObject JS->Dart proxy | |
545 Object _convertToDart(o) { | |
546 if (JS('bool', '# == null', o) || | |
547 JS('bool', 'typeof # == "string"', o) || | |
548 JS('bool', 'typeof # == "number"', o) || | |
549 JS('bool', 'typeof # == "boolean"', o)) { | |
550 return o; | |
551 } else if (_isLocalObject(o) | |
552 && (o is Blob || o is Event || o is KeyRange || o is ImageData | |
553 || o is Node || o is TypedData || o is Window)) { | |
554 // long line: dart2js doesn't allow string concatenation in the JS() form | |
555 return JS('Blob|Event|KeyRange|ImageData|Node|TypedData|Window', '#', o); | |
556 } else if (JS('bool', '# instanceof Date', o)) { | |
557 var ms = JS('num', '#.getTime()', o); | |
558 return new DateTime.fromMillisecondsSinceEpoch(ms); | |
559 } else if (JS('bool', '#.constructor === #', o, _dartProxyCtor)) { | |
560 return JS('', '#.o', o); | |
561 } else { | |
562 return _wrapToDart(o); | |
563 } | |
564 } | |
565 | |
566 JsObject _wrapToDart(o) { | |
567 if (JS('bool', 'typeof # == "function"', o)) { | |
568 return _getDartProxy(o, _DART_CLOSURE_PROPERTY_NAME, | |
569 (o) => new JsFunction._fromJs(o)); | |
570 } else if (JS('bool', '# instanceof Array', o)) { | |
571 return _getDartProxy(o, _DART_OBJECT_PROPERTY_NAME, | |
572 (o) => new JsArray._fromJs(o)); | |
573 } else { | |
574 return _getDartProxy(o, _DART_OBJECT_PROPERTY_NAME, | |
575 (o) => new JsObject._fromJs(o)); | |
576 } | |
577 } | |
578 | |
579 Object _getDartProxy(o, String propertyName, createProxy(o)) { | |
580 var dartProxy = _getOwnProperty(o, propertyName); | |
581 // Temporary fix for dartbug.com/15193 | |
582 // In some cases it's possible to see a JavaScript object that | |
583 // came from a different context and was previously proxied to | |
584 // Dart in that context. The JS object will have a cached proxy | |
585 // but it won't be a valid Dart object in this context. | |
586 // For now we throw away the cached proxy, but we should be able | |
587 // to cache proxies from multiple JS contexts and Dart isolates. | |
588 if (dartProxy == null || !_isLocalObject(o)) { | |
589 dartProxy = createProxy(o); | |
590 _defineProperty(o, propertyName, dartProxy); | |
591 } | |
592 return dartProxy; | |
593 } | |
OLD | NEW |