OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 // This module implements WebView (<webview>) as a custom element that wraps a | |
6 // BrowserPlugin object element. The object element is hidden within | |
7 // the shadow DOM of the WebView element. | |
8 | |
9 var DocumentNatives = requireNative('document_natives'); | |
10 var GuestViewInternal = | |
11 require('binding').Binding.create('guestViewInternal').generate(); | |
12 var IdGenerator = requireNative('id_generator'); | |
13 // TODO(lazyboy): Rename this to WebViewInternal and call WebViewInternal | |
14 // something else. | |
15 var WebView = require('webViewInternal').WebView; | |
16 var WebViewEvents = require('webViewEvents').WebViewEvents; | |
17 var guestViewInternalNatives = requireNative('guest_view_internal'); | |
18 | |
19 // Attributes. | |
20 var WEB_VIEW_ATTRIBUTE_ALLOWTRANSPARENCY = 'allowtransparency'; | |
21 var WEB_VIEW_ATTRIBUTE_AUTOSIZE = 'autosize'; | |
22 var WEB_VIEW_ATTRIBUTE_MAXHEIGHT = 'maxheight'; | |
23 var WEB_VIEW_ATTRIBUTE_MAXWIDTH = 'maxwidth'; | |
24 var WEB_VIEW_ATTRIBUTE_MINHEIGHT = 'minheight'; | |
25 var WEB_VIEW_ATTRIBUTE_MINWIDTH = 'minwidth'; | |
26 var WEB_VIEW_ATTRIBUTE_PARTITION = 'partition'; | |
27 var AUTO_SIZE_ATTRIBUTES = [ | |
28 WEB_VIEW_ATTRIBUTE_AUTOSIZE, | |
29 WEB_VIEW_ATTRIBUTE_MAXHEIGHT, | |
30 WEB_VIEW_ATTRIBUTE_MAXWIDTH, | |
31 WEB_VIEW_ATTRIBUTE_MINHEIGHT, | |
32 WEB_VIEW_ATTRIBUTE_MINWIDTH | |
33 ]; | |
34 | |
35 // Error messages. | |
36 var ERROR_MSG_ALREADY_NAVIGATED = | |
37 'The object has already navigated, so its partition cannot be changed.'; | |
38 var ERROR_MSG_CANNOT_INJECT_SCRIPT = '<webview>: ' + | |
39 'Script cannot be injected into content until the page has loaded.'; | |
40 var ERROR_MSG_CONTENTWINDOW_NOT_AVAILABLE = '<webview>: ' + | |
41 'contentWindow is not available at this time. It will become available ' + | |
42 'when the page has finished loading.'; | |
43 var ERROR_MSG_INVALID_PARTITION_ATTRIBUTE = 'Invalid partition attribute.'; | |
44 | |
45 // Represents the state of the storage partition. | |
46 function Partition() { | |
47 this.validPartitionId = true; | |
48 this.persistStorage = false; | |
49 this.storagePartitionId = ''; | |
50 } | |
51 | |
52 Partition.prototype.toAttribute = function() { | |
53 if (!this.validPartitionId) { | |
54 return ''; | |
55 } | |
56 return (this.persistStorage ? 'persist:' : '') + this.storagePartitionId; | |
57 }; | |
58 | |
59 Partition.prototype.fromAttribute = function(value, hasNavigated) { | |
60 var result = {}; | |
61 if (hasNavigated) { | |
62 result.error = ERROR_MSG_ALREADY_NAVIGATED; | |
63 return result; | |
64 } | |
65 if (!value) { | |
66 value = ''; | |
67 } | |
68 | |
69 var LEN = 'persist:'.length; | |
70 if (value.substr(0, LEN) == 'persist:') { | |
71 value = value.substr(LEN); | |
72 if (!value) { | |
73 this.validPartitionId = false; | |
74 result.error = ERROR_MSG_INVALID_PARTITION_ATTRIBUTE; | |
75 return result; | |
76 } | |
77 this.persistStorage = true; | |
78 } else { | |
79 this.persistStorage = false; | |
80 } | |
81 | |
82 this.storagePartitionId = value; | |
83 return result; | |
84 }; | |
85 | |
86 // Represents the internal state of the WebView node. | |
87 function WebViewInternal(webviewNode) { | |
88 privates(webviewNode).internal = this; | |
89 this.webviewNode = webviewNode; | |
90 this.attached = false; | |
91 this.pendingGuestCreation = false; | |
92 this.elementAttached = false; | |
93 | |
94 this.beforeFirstNavigation = true; | |
95 this.contentWindow = null; | |
96 this.validPartitionId = true; | |
97 // Used to save some state upon deferred attachment. | |
98 // If <object> bindings is not available, we defer attachment. | |
99 // This state contains whether or not the attachment request was for | |
100 // newwindow. | |
101 this.deferredAttachState = null; | |
102 | |
103 // on* Event handlers. | |
104 this.on = {}; | |
105 | |
106 this.browserPluginNode = this.createBrowserPluginNode(); | |
107 var shadowRoot = this.webviewNode.createShadowRoot(); | |
108 this.partition = new Partition(); | |
109 | |
110 this.setupWebViewSrcAttributeMutationObserver(); | |
111 this.setupFocusPropagation(); | |
112 this.setupWebviewNodeProperties(); | |
113 | |
114 this.viewInstanceId = IdGenerator.GetNextId(); | |
115 | |
116 new WebViewEvents(this, this.viewInstanceId); | |
117 | |
118 shadowRoot.appendChild(this.browserPluginNode); | |
119 } | |
120 | |
121 WebViewInternal.prototype.createBrowserPluginNode = function() { | |
122 // We create BrowserPlugin as a custom element in order to observe changes | |
123 // to attributes synchronously. | |
124 var browserPluginNode = new WebViewInternal.BrowserPlugin(); | |
125 privates(browserPluginNode).internal = this; | |
126 return browserPluginNode; | |
127 }; | |
128 | |
129 WebViewInternal.prototype.getGuestInstanceId = function() { | |
130 return this.guestInstanceId; | |
131 }; | |
132 | |
133 // Resets some state upon reattaching <webview> element to the DOM. | |
134 WebViewInternal.prototype.reset = function() { | |
135 // If guestInstanceId is defined then the <webview> has navigated and has | |
136 // already picked up a partition ID. Thus, we need to reset the initialization | |
137 // state. However, it may be the case that beforeFirstNavigation is false BUT | |
138 // guestInstanceId has yet to be initialized. This means that we have not | |
139 // heard back from createGuest yet. We will not reset the flag in this case so | |
140 // that we don't end up allocating a second guest. | |
141 if (this.guestInstanceId) { | |
142 GuestViewInternal.destroyGuest(this.guestInstanceId); | |
143 this.guestInstanceId = undefined; | |
144 this.beforeFirstNavigation = true; | |
145 this.validPartitionId = true; | |
146 this.partition.validPartitionId = true; | |
147 this.contentWindow = null; | |
148 } | |
149 this.internalInstanceId = 0; | |
150 }; | |
151 | |
152 // Sets the <webview>.request property. | |
153 WebViewInternal.prototype.setRequestPropertyOnWebViewNode = function(request) { | |
154 Object.defineProperty( | |
155 this.webviewNode, | |
156 'request', | |
157 { | |
158 value: request, | |
159 enumerable: true | |
160 } | |
161 ); | |
162 }; | |
163 | |
164 WebViewInternal.prototype.setupFocusPropagation = function() { | |
165 if (!this.webviewNode.hasAttribute('tabIndex')) { | |
166 // <webview> needs a tabIndex in order to be focusable. | |
167 // TODO(fsamuel): It would be nice to avoid exposing a tabIndex attribute | |
168 // to allow <webview> to be focusable. | |
169 // See http://crbug.com/231664. | |
170 this.webviewNode.setAttribute('tabIndex', -1); | |
171 } | |
172 this.webviewNode.addEventListener('focus', function(e) { | |
173 // Focus the BrowserPlugin when the <webview> takes focus. | |
174 this.browserPluginNode.focus(); | |
175 }.bind(this)); | |
176 this.webviewNode.addEventListener('blur', function(e) { | |
177 // Blur the BrowserPlugin when the <webview> loses focus. | |
178 this.browserPluginNode.blur(); | |
179 }.bind(this)); | |
180 }; | |
181 | |
182 // Validation helper function for executeScript() and insertCSS(). | |
183 WebViewInternal.prototype.validateExecuteCodeCall = function() { | |
184 if (!this.guestInstanceId) { | |
185 throw new Error(ERROR_MSG_CANNOT_INJECT_SCRIPT); | |
186 } | |
187 }; | |
188 | |
189 WebViewInternal.prototype.setupAutoSizeProperties = function() { | |
190 $Array.forEach(AUTO_SIZE_ATTRIBUTES, function(attributeName) { | |
191 this[attributeName] = this.webviewNode.getAttribute(attributeName); | |
192 Object.defineProperty(this.webviewNode, attributeName, { | |
193 get: function() { | |
194 return this[attributeName]; | |
195 }.bind(this), | |
196 set: function(value) { | |
197 this.webviewNode.setAttribute(attributeName, value); | |
198 }.bind(this), | |
199 enumerable: true | |
200 }); | |
201 }.bind(this), this); | |
202 }; | |
203 | |
204 WebViewInternal.prototype.setupWebviewNodeProperties = function() { | |
205 this.setupAutoSizeProperties(); | |
206 | |
207 Object.defineProperty(this.webviewNode, | |
208 WEB_VIEW_ATTRIBUTE_ALLOWTRANSPARENCY, { | |
209 get: function() { | |
210 return this.allowtransparency; | |
211 }.bind(this), | |
212 set: function(value) { | |
213 this.webviewNode.setAttribute(WEB_VIEW_ATTRIBUTE_ALLOWTRANSPARENCY, | |
214 value); | |
215 }.bind(this), | |
216 enumerable: true | |
217 }); | |
218 | |
219 // We cannot use {writable: true} property descriptor because we want a | |
220 // dynamic getter value. | |
221 Object.defineProperty(this.webviewNode, 'contentWindow', { | |
222 get: function() { | |
223 if (this.contentWindow) { | |
224 return this.contentWindow; | |
225 } | |
226 window.console.error(ERROR_MSG_CONTENTWINDOW_NOT_AVAILABLE); | |
227 }.bind(this), | |
228 // No setter. | |
229 enumerable: true | |
230 }); | |
231 | |
232 Object.defineProperty(this.webviewNode, 'name', { | |
233 get: function() { | |
234 return this.name; | |
235 }.bind(this), | |
236 set: function(value) { | |
237 this.webviewNode.setAttribute('name', value); | |
238 }.bind(this), | |
239 enumerable: true | |
240 }); | |
241 | |
242 Object.defineProperty(this.webviewNode, 'partition', { | |
243 get: function() { | |
244 return this.partition.toAttribute(); | |
245 }.bind(this), | |
246 set: function(value) { | |
247 var result = this.partition.fromAttribute(value, this.hasNavigated()); | |
248 if (result.error) { | |
249 throw result.error; | |
250 } | |
251 this.webviewNode.setAttribute('partition', value); | |
252 }.bind(this), | |
253 enumerable: true | |
254 }); | |
255 | |
256 this.src = this.webviewNode.getAttribute('src'); | |
257 Object.defineProperty(this.webviewNode, 'src', { | |
258 get: function() { | |
259 return this.src; | |
260 }.bind(this), | |
261 set: function(value) { | |
262 this.webviewNode.setAttribute('src', value); | |
263 }.bind(this), | |
264 // No setter. | |
265 enumerable: true | |
266 }); | |
267 }; | |
268 | |
269 // The purpose of this mutation observer is to catch assignment to the src | |
270 // attribute without any changes to its value. This is useful in the case | |
271 // where the webview guest has crashed and navigating to the same address | |
272 // spawns off a new process. | |
273 WebViewInternal.prototype.setupWebViewSrcAttributeMutationObserver = | |
274 function() { | |
275 this.srcAndPartitionObserver = new MutationObserver(function(mutations) { | |
276 $Array.forEach(mutations, function(mutation) { | |
277 var oldValue = mutation.oldValue; | |
278 var newValue = this.webviewNode.getAttribute(mutation.attributeName); | |
279 if (oldValue != newValue) { | |
280 return; | |
281 } | |
282 this.handleWebviewAttributeMutation( | |
283 mutation.attributeName, oldValue, newValue); | |
284 }.bind(this)); | |
285 }.bind(this)); | |
286 var params = { | |
287 attributes: true, | |
288 attributeOldValue: true, | |
289 attributeFilter: ['src', 'partition'] | |
290 }; | |
291 this.srcAndPartitionObserver.observe(this.webviewNode, params); | |
292 }; | |
293 | |
294 // This observer monitors mutations to attributes of the <webview> and | |
295 // updates the BrowserPlugin properties accordingly. In turn, updating | |
296 // a BrowserPlugin property will update the corresponding BrowserPlugin | |
297 // attribute, if necessary. See BrowserPlugin::UpdateDOMAttribute for more | |
298 // details. | |
299 WebViewInternal.prototype.handleWebviewAttributeMutation = | |
300 function(name, oldValue, newValue) { | |
301 if (AUTO_SIZE_ATTRIBUTES.indexOf(name) > -1) { | |
302 this[name] = newValue; | |
303 if (!this.guestInstanceId) { | |
304 return; | |
305 } | |
306 // Convert autosize attribute to boolean. | |
307 var autosize = this.webviewNode.hasAttribute(WEB_VIEW_ATTRIBUTE_AUTOSIZE); | |
308 GuestViewInternal.setAutoSize(this.guestInstanceId, { | |
309 'enableAutoSize': autosize, | |
310 'min': { | |
311 'width': parseInt(this.minwidth || 0), | |
312 'height': parseInt(this.minheight || 0) | |
313 }, | |
314 'max': { | |
315 'width': parseInt(this.maxwidth || 0), | |
316 'height': parseInt(this.maxheight || 0) | |
317 } | |
318 }); | |
319 return; | |
320 } else if (name == WEB_VIEW_ATTRIBUTE_ALLOWTRANSPARENCY) { | |
321 // We treat null attribute (attribute removed) and the empty string as | |
322 // one case. | |
323 oldValue = oldValue || ''; | |
324 newValue = newValue || ''; | |
325 | |
326 if (oldValue === newValue) { | |
327 return; | |
328 } | |
329 this.allowtransparency = newValue != ''; | |
330 | |
331 if (!this.guestInstanceId) { | |
332 return; | |
333 } | |
334 | |
335 WebView.setAllowTransparency(this.guestInstanceId, this.allowtransparency); | |
336 return; | |
337 } else if (name == 'name') { | |
338 // We treat null attribute (attribute removed) and the empty string as | |
339 // one case. | |
340 oldValue = oldValue || ''; | |
341 newValue = newValue || ''; | |
342 | |
343 if (oldValue === newValue) { | |
344 return; | |
345 } | |
346 this.name = newValue; | |
347 if (!this.guestInstanceId) { | |
348 return; | |
349 } | |
350 WebView.setName(this.guestInstanceId, newValue); | |
351 return; | |
352 } else if (name == 'src') { | |
353 // We treat null attribute (attribute removed) and the empty string as | |
354 // one case. | |
355 oldValue = oldValue || ''; | |
356 newValue = newValue || ''; | |
357 // Once we have navigated, we don't allow clearing the src attribute. | |
358 // Once <webview> enters a navigated state, it cannot be return back to a | |
359 // placeholder state. | |
360 if (newValue == '' && oldValue != '') { | |
361 // src attribute changes normally initiate a navigation. We suppress | |
362 // the next src attribute handler call to avoid reloading the page | |
363 // on every guest-initiated navigation. | |
364 this.ignoreNextSrcAttributeChange = true; | |
365 this.webviewNode.setAttribute('src', oldValue); | |
366 return; | |
367 } | |
368 this.src = newValue; | |
369 if (this.ignoreNextSrcAttributeChange) { | |
370 // Don't allow the src mutation observer to see this change. | |
371 this.srcAndPartitionObserver.takeRecords(); | |
372 this.ignoreNextSrcAttributeChange = false; | |
373 return; | |
374 } | |
375 var result = {}; | |
376 this.parseSrcAttribute(result); | |
377 | |
378 if (result.error) { | |
379 throw result.error; | |
380 } | |
381 } else if (name == 'partition') { | |
382 // Note that throwing error here won't synchronously propagate. | |
383 this.partition.fromAttribute(newValue, this.hasNavigated()); | |
384 } | |
385 }; | |
386 | |
387 WebViewInternal.prototype.handleBrowserPluginAttributeMutation = | |
388 function(name, oldValue, newValue) { | |
389 if (name == 'internalinstanceid' && !oldValue && !!newValue) { | |
390 this.browserPluginNode.removeAttribute('internalinstanceid'); | |
391 this.internalInstanceId = parseInt(newValue); | |
392 | |
393 if (!!this.guestInstanceId && this.guestInstanceId != 0) { | |
394 var isNewWindow = this.deferredAttachState ? | |
395 this.deferredAttachState.isNewWindow : false; | |
396 var params = this.buildAttachParams(isNewWindow); | |
397 guestViewInternalNatives.AttachGuest( | |
398 this.internalInstanceId, | |
399 this.guestInstanceId, | |
400 params, | |
401 function(w) { | |
402 this.contentWindow = w; | |
403 }.bind(this) | |
404 ); | |
405 } | |
406 | |
407 return; | |
408 } | |
409 }; | |
410 | |
411 WebViewInternal.prototype.onSizeChanged = function(webViewEvent) { | |
412 var newWidth = webViewEvent.newWidth; | |
413 var newHeight = webViewEvent.newHeight; | |
414 | |
415 var node = this.webviewNode; | |
416 | |
417 var width = node.offsetWidth; | |
418 var height = node.offsetHeight; | |
419 | |
420 // Check the current bounds to make sure we do not resize <webview> | |
421 // outside of current constraints. | |
422 var maxWidth; | |
423 if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MAXWIDTH) && | |
424 node[WEB_VIEW_ATTRIBUTE_MAXWIDTH]) { | |
425 maxWidth = node[WEB_VIEW_ATTRIBUTE_MAXWIDTH]; | |
426 } else { | |
427 maxWidth = width; | |
428 } | |
429 | |
430 var minWidth; | |
431 if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MINWIDTH) && | |
432 node[WEB_VIEW_ATTRIBUTE_MINWIDTH]) { | |
433 minWidth = node[WEB_VIEW_ATTRIBUTE_MINWIDTH]; | |
434 } else { | |
435 minWidth = width; | |
436 } | |
437 if (minWidth > maxWidth) { | |
438 minWidth = maxWidth; | |
439 } | |
440 | |
441 var maxHeight; | |
442 if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MAXHEIGHT) && | |
443 node[WEB_VIEW_ATTRIBUTE_MAXHEIGHT]) { | |
444 maxHeight = node[WEB_VIEW_ATTRIBUTE_MAXHEIGHT]; | |
445 } else { | |
446 maxHeight = height; | |
447 } | |
448 | |
449 var minHeight; | |
450 if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MINHEIGHT) && | |
451 node[WEB_VIEW_ATTRIBUTE_MINHEIGHT]) { | |
452 minHeight = node[WEB_VIEW_ATTRIBUTE_MINHEIGHT]; | |
453 } else { | |
454 minHeight = height; | |
455 } | |
456 if (minHeight > maxHeight) { | |
457 minHeight = maxHeight; | |
458 } | |
459 | |
460 if (!this.webviewNode.hasAttribute(WEB_VIEW_ATTRIBUTE_AUTOSIZE) || | |
461 (newWidth >= minWidth && | |
462 newWidth <= maxWidth && | |
463 newHeight >= minHeight && | |
464 newHeight <= maxHeight)) { | |
465 node.style.width = newWidth + 'px'; | |
466 node.style.height = newHeight + 'px'; | |
467 // Only fire the DOM event if the size of the <webview> has actually | |
468 // changed. | |
469 this.dispatchEvent(webViewEvent); | |
470 } | |
471 }; | |
472 | |
473 // Returns if <object> is in the render tree. | |
474 WebViewInternal.prototype.isPluginInRenderTree = function() { | |
475 return !!this.internalInstanceId && this.internalInstanceId != 0; | |
476 }; | |
477 | |
478 WebViewInternal.prototype.hasNavigated = function() { | |
479 return !this.beforeFirstNavigation; | |
480 }; | |
481 | |
482 WebViewInternal.prototype.parseSrcAttribute = function(result) { | |
483 if (!this.partition.validPartitionId) { | |
484 result.error = ERROR_MSG_INVALID_PARTITION_ATTRIBUTE; | |
485 return; | |
486 } | |
487 this.src = this.webviewNode.getAttribute('src'); | |
488 | |
489 if (!this.src) { | |
490 return; | |
491 } | |
492 | |
493 if (this.guestInstanceId == undefined) { | |
494 if (this.beforeFirstNavigation) { | |
495 this.beforeFirstNavigation = false; | |
496 this.createGuest(); | |
497 } | |
498 return; | |
499 } | |
500 | |
501 // Navigate to |this.src|. | |
502 WebView.navigate(this.guestInstanceId, this.src); | |
503 }; | |
504 | |
505 WebViewInternal.prototype.parseAttributes = function() { | |
506 if (!this.elementAttached) { | |
507 return; | |
508 } | |
509 var hasNavigated = this.hasNavigated(); | |
510 var attributeValue = this.webviewNode.getAttribute('partition'); | |
511 var result = this.partition.fromAttribute(attributeValue, hasNavigated); | |
512 this.parseSrcAttribute(result); | |
513 }; | |
514 | |
515 WebViewInternal.prototype.createGuest = function() { | |
516 if (this.pendingGuestCreation) { | |
517 return; | |
518 } | |
519 var storagePartitionId = | |
520 this.webviewNode.getAttribute(WEB_VIEW_ATTRIBUTE_PARTITION) || | |
521 this.webviewNode[WEB_VIEW_ATTRIBUTE_PARTITION]; | |
522 var params = { | |
523 'storagePartitionId': storagePartitionId | |
524 }; | |
525 GuestViewInternal.createGuest( | |
526 'webview', | |
527 params, | |
528 function(guestInstanceId) { | |
529 this.pendingGuestCreation = false; | |
530 if (!this.elementAttached) { | |
531 GuestViewInternal.destroyGuest(guestInstanceId); | |
532 return; | |
533 } | |
534 this.attachWindow(guestInstanceId, false); | |
535 }.bind(this) | |
536 ); | |
537 this.pendingGuestCreation = true; | |
538 }; | |
539 | |
540 WebViewInternal.prototype.onFrameNameChanged = function(name) { | |
541 this.name = name || ''; | |
542 if (this.name === '') { | |
543 this.webviewNode.removeAttribute('name'); | |
544 } else { | |
545 this.webviewNode.setAttribute('name', this.name); | |
546 } | |
547 }; | |
548 | |
549 WebViewInternal.prototype.dispatchEvent = function(webViewEvent) { | |
550 return this.webviewNode.dispatchEvent(webViewEvent); | |
551 }; | |
552 | |
553 // Adds an 'on<event>' property on the webview, which can be used to set/unset | |
554 // an event handler. | |
555 WebViewInternal.prototype.setupEventProperty = function(eventName) { | |
556 var propertyName = 'on' + eventName.toLowerCase(); | |
557 Object.defineProperty(this.webviewNode, propertyName, { | |
558 get: function() { | |
559 return this.on[propertyName]; | |
560 }.bind(this), | |
561 set: function(value) { | |
562 if (this.on[propertyName]) | |
563 this.webviewNode.removeEventListener(eventName, this.on[propertyName]); | |
564 this.on[propertyName] = value; | |
565 if (value) | |
566 this.webviewNode.addEventListener(eventName, value); | |
567 }.bind(this), | |
568 enumerable: true | |
569 }); | |
570 }; | |
571 | |
572 // Updates state upon loadcommit. | |
573 WebViewInternal.prototype.onLoadCommit = function( | |
574 baseUrlForDataUrl, currentEntryIndex, entryCount, | |
575 processId, url, isTopLevel) { | |
576 this.baseUrlForDataUrl = baseUrlForDataUrl; | |
577 this.currentEntryIndex = currentEntryIndex; | |
578 this.entryCount = entryCount; | |
579 this.processId = processId; | |
580 var oldValue = this.webviewNode.getAttribute('src'); | |
581 var newValue = url; | |
582 if (isTopLevel && (oldValue != newValue)) { | |
583 // Touching the src attribute triggers a navigation. To avoid | |
584 // triggering a page reload on every guest-initiated navigation, | |
585 // we use the flag ignoreNextSrcAttributeChange here. | |
586 this.ignoreNextSrcAttributeChange = true; | |
587 this.webviewNode.setAttribute('src', newValue); | |
588 } | |
589 }; | |
590 | |
591 WebViewInternal.prototype.onAttach = function(storagePartitionId) { | |
592 this.webviewNode.setAttribute('partition', storagePartitionId); | |
593 this.partition.fromAttribute(storagePartitionId, this.hasNavigated()); | |
594 }; | |
595 | |
596 WebViewInternal.prototype.buildAttachParams = function(isNewWindow) { | |
597 var params = { | |
598 'allowtransparency': this.allowtransparency || false, | |
599 'autosize': this.webviewNode.hasAttribute(WEB_VIEW_ATTRIBUTE_AUTOSIZE), | |
600 'instanceId': this.viewInstanceId, | |
601 'maxheight': parseInt(this.maxheight || 0), | |
602 'maxwidth': parseInt(this.maxwidth || 0), | |
603 'minheight': parseInt(this.minheight || 0), | |
604 'minwidth': parseInt(this.minwidth || 0), | |
605 'name': this.name, | |
606 // We don't need to navigate new window from here. | |
607 'src': isNewWindow ? undefined : this.src, | |
608 // If we have a partition from the opener, that will also be already | |
609 // set via this.onAttach(). | |
610 'storagePartitionId': this.partition.toAttribute(), | |
611 'userAgentOverride': this.userAgentOverride | |
612 }; | |
613 return params; | |
614 }; | |
615 | |
616 WebViewInternal.prototype.attachWindow = function(guestInstanceId, | |
617 isNewWindow) { | |
618 this.guestInstanceId = guestInstanceId; | |
619 var params = this.buildAttachParams(isNewWindow); | |
620 | |
621 if (!this.isPluginInRenderTree()) { | |
622 this.deferredAttachState = {isNewWindow: isNewWindow}; | |
623 return true; | |
624 } | |
625 | |
626 this.deferredAttachState = null; | |
627 return guestViewInternalNatives.AttachGuest( | |
628 this.internalInstanceId, | |
629 this.guestInstanceId, | |
630 params, function(w) { | |
631 this.contentWindow = w; | |
632 }.bind(this) | |
633 ); | |
634 }; | |
635 | |
636 // ----------------------------------------------------------------------------- | |
637 // Public-facing API methods. | |
638 | |
639 | |
640 // Navigates to the previous history entry. | |
641 WebViewInternal.prototype.back = function(callback) { | |
642 return this.go(-1, callback); | |
643 }; | |
644 | |
645 // Returns whether there is a previous history entry to navigate to. | |
646 WebViewInternal.prototype.canGoBack = function() { | |
647 return this.entryCount > 1 && this.currentEntryIndex > 0; | |
648 }; | |
649 | |
650 // Returns whether there is a subsequent history entry to navigate to. | |
651 WebViewInternal.prototype.canGoForward = function() { | |
652 return this.currentEntryIndex >= 0 && | |
653 this.currentEntryIndex < (this.entryCount - 1); | |
654 }; | |
655 | |
656 // Clears browsing data for the WebView partition. | |
657 WebViewInternal.prototype.clearData = function() { | |
658 if (!this.guestInstanceId) { | |
659 return; | |
660 } | |
661 var args = $Array.concat([this.guestInstanceId], $Array.slice(arguments)); | |
662 $Function.apply(WebView.clearData, null, args); | |
663 }; | |
664 | |
665 // Injects JavaScript code into the guest page. | |
666 WebViewInternal.prototype.executeScript = function(var_args) { | |
667 this.validateExecuteCodeCall(); | |
668 var webviewSrc = this.src; | |
669 if (this.baseUrlForDataUrl != '') { | |
670 webviewSrc = this.baseUrlForDataUrl; | |
671 } | |
672 var args = $Array.concat([this.guestInstanceId, webviewSrc], | |
673 $Array.slice(arguments)); | |
674 $Function.apply(WebView.executeScript, null, args); | |
675 }; | |
676 | |
677 // Initiates a find-in-page request. | |
678 WebViewInternal.prototype.find = function(search_text, options, callback) { | |
679 if (!this.guestInstanceId) { | |
680 return; | |
681 } | |
682 WebView.find(this.guestInstanceId, search_text, options, callback); | |
683 }; | |
684 | |
685 // Navigates to the subsequent history entry. | |
686 WebViewInternal.prototype.forward = function(callback) { | |
687 return this.go(1, callback); | |
688 }; | |
689 | |
690 // Returns Chrome's internal process ID for the guest web page's current | |
691 // process. | |
692 WebViewInternal.prototype.getProcessId = function() { | |
693 return this.processId; | |
694 }; | |
695 | |
696 // Returns the user agent string used by the webview for guest page requests. | |
697 WebViewInternal.prototype.getUserAgent = function() { | |
698 return this.userAgentOverride || navigator.userAgent; | |
699 }; | |
700 | |
701 // Gets the current zoom factor. | |
702 WebViewInternal.prototype.getZoom = function(callback) { | |
703 if (!this.guestInstanceId) { | |
704 return; | |
705 } | |
706 WebView.getZoom(this.guestInstanceId, callback); | |
707 }; | |
708 | |
709 // Navigates to a history entry using a history index relative to the current | |
710 // navigation. | |
711 WebViewInternal.prototype.go = function(relativeIndex, callback) { | |
712 if (!this.guestInstanceId) { | |
713 return; | |
714 } | |
715 WebView.go(this.guestInstanceId, relativeIndex, callback); | |
716 }; | |
717 | |
718 // Injects CSS into the guest page. | |
719 WebViewInternal.prototype.insertCSS = function(var_args) { | |
720 this.validateExecuteCodeCall(); | |
721 var webviewSrc = this.src; | |
722 if (this.baseUrlForDataUrl != '') { | |
723 webviewSrc = this.baseUrlForDataUrl; | |
724 } | |
725 var args = $Array.concat([this.guestInstanceId, webviewSrc], | |
726 $Array.slice(arguments)); | |
727 $Function.apply(WebView.insertCSS, null, args); | |
728 }; | |
729 | |
730 // Indicates whether or not the webview's user agent string has been overridden. | |
731 WebViewInternal.prototype.isUserAgentOverridden = function() { | |
732 return !!this.userAgentOverride && | |
733 this.userAgentOverride != navigator.userAgent; | |
734 }; | |
735 | |
736 // Prints the contents of the webview. | |
737 WebViewInternal.prototype.print = function() { | |
738 this.executeScript({code: 'window.print();'}); | |
739 }; | |
740 | |
741 // Reloads the current top-level page. | |
742 WebViewInternal.prototype.reload = function() { | |
743 if (!this.guestInstanceId) { | |
744 return; | |
745 } | |
746 WebView.reload(this.guestInstanceId); | |
747 }; | |
748 | |
749 // Override the user agent string used by the webview for guest page requests. | |
750 WebViewInternal.prototype.setUserAgentOverride = function(userAgentOverride) { | |
751 this.userAgentOverride = userAgentOverride; | |
752 if (!this.guestInstanceId) { | |
753 // If we are not attached yet, then we will pick up the user agent on | |
754 // attachment. | |
755 return; | |
756 } | |
757 WebView.overrideUserAgent(this.guestInstanceId, userAgentOverride); | |
758 }; | |
759 | |
760 // Changes the zoom factor of the page. | |
761 WebViewInternal.prototype.setZoom = function(zoomFactor, callback) { | |
762 if (!this.guestInstanceId) { | |
763 return; | |
764 } | |
765 WebView.setZoom(this.guestInstanceId, zoomFactor, callback); | |
766 }; | |
767 | |
768 // Stops loading the current navigation if one is in progress. | |
769 WebViewInternal.prototype.stop = function() { | |
770 if (!this.guestInstanceId) { | |
771 return; | |
772 } | |
773 WebView.stop(this.guestInstanceId); | |
774 }; | |
775 | |
776 // Ends the current find session. | |
777 WebViewInternal.prototype.stopFinding = function(action) { | |
778 if (!this.guestInstanceId) { | |
779 return; | |
780 } | |
781 WebView.stopFinding(this.guestInstanceId, action); | |
782 }; | |
783 | |
784 // Forcibly kills the guest web page's renderer process. | |
785 WebViewInternal.prototype.terminate = function() { | |
786 if (!this.guestInstanceId) { | |
787 return; | |
788 } | |
789 WebView.terminate(this.guestInstanceId); | |
790 }; | |
791 | |
792 // ----------------------------------------------------------------------------- | |
793 | |
794 // Registers browser plugin <object> custom element. | |
795 function registerBrowserPluginElement() { | |
796 var proto = Object.create(HTMLObjectElement.prototype); | |
797 | |
798 proto.createdCallback = function() { | |
799 this.setAttribute('type', 'application/browser-plugin'); | |
800 this.setAttribute('id', 'browser-plugin-' + IdGenerator.GetNextId()); | |
801 // The <object> node fills in the <webview> container. | |
802 this.style.width = '100%'; | |
803 this.style.height = '100%'; | |
804 }; | |
805 | |
806 proto.attributeChangedCallback = function(name, oldValue, newValue) { | |
807 var internal = privates(this).internal; | |
808 if (!internal) { | |
809 return; | |
810 } | |
811 internal.handleBrowserPluginAttributeMutation(name, oldValue, newValue); | |
812 }; | |
813 | |
814 proto.attachedCallback = function() { | |
815 // Load the plugin immediately. | |
816 var unused = this.nonExistentAttribute; | |
817 }; | |
818 | |
819 WebViewInternal.BrowserPlugin = | |
820 DocumentNatives.RegisterElement('browserplugin', {extends: 'object', | |
821 prototype: proto}); | |
822 | |
823 delete proto.createdCallback; | |
824 delete proto.attachedCallback; | |
825 delete proto.detachedCallback; | |
826 delete proto.attributeChangedCallback; | |
827 } | |
828 | |
829 // Registers <webview> custom element. | |
830 function registerWebViewElement() { | |
831 var proto = Object.create(HTMLElement.prototype); | |
832 | |
833 proto.createdCallback = function() { | |
834 new WebViewInternal(this); | |
835 }; | |
836 | |
837 proto.attributeChangedCallback = function(name, oldValue, newValue) { | |
838 var internal = privates(this).internal; | |
839 if (!internal) { | |
840 return; | |
841 } | |
842 internal.handleWebviewAttributeMutation(name, oldValue, newValue); | |
843 }; | |
844 | |
845 proto.detachedCallback = function() { | |
846 var internal = privates(this).internal; | |
847 if (!internal) { | |
848 return; | |
849 } | |
850 internal.elementAttached = false; | |
851 internal.reset(); | |
852 }; | |
853 | |
854 proto.attachedCallback = function() { | |
855 var internal = privates(this).internal; | |
856 if (!internal) { | |
857 return; | |
858 } | |
859 if (!internal.elementAttached) { | |
860 internal.elementAttached = true; | |
861 internal.parseAttributes(); | |
862 } | |
863 }; | |
864 | |
865 // Public-facing API methods. | |
866 var methods = [ | |
867 'back', | |
868 'canGoBack', | |
869 'canGoForward', | |
870 'clearData', | |
871 'executeScript', | |
872 'find', | |
873 'forward', | |
874 'getProcessId', | |
875 'getUserAgent', | |
876 'getZoom', | |
877 'go', | |
878 'insertCSS', | |
879 'isUserAgentOverridden', | |
880 'print', | |
881 'reload', | |
882 'setUserAgentOverride', | |
883 'setZoom', | |
884 'stop', | |
885 'stopFinding', | |
886 'terminate' | |
887 ]; | |
888 | |
889 // Add the experimental API methods, if available. | |
890 var experimentalMethods = | |
891 WebViewInternal.maybeGetExperimentalAPIs(); | |
892 methods = $Array.concat(methods, experimentalMethods); | |
893 | |
894 // Forward proto.foo* method calls to WebViewInternal.foo*. | |
895 var createHandler = function(m) { | |
896 return function(var_args) { | |
897 var internal = privates(this).internal; | |
898 return $Function.apply(internal[m], internal, arguments); | |
899 }; | |
900 }; | |
901 for (var i = 0; methods[i]; ++i) { | |
902 proto[methods[i]] = createHandler(methods[i]); | |
903 } | |
904 | |
905 window.WebView = | |
906 DocumentNatives.RegisterElement('webview', {prototype: proto}); | |
907 | |
908 // Delete the callbacks so developers cannot call them and produce unexpected | |
909 // behavior. | |
910 delete proto.createdCallback; | |
911 delete proto.attachedCallback; | |
912 delete proto.detachedCallback; | |
913 delete proto.attributeChangedCallback; | |
914 } | |
915 | |
916 var useCapture = true; | |
917 window.addEventListener('readystatechange', function listener(event) { | |
918 if (document.readyState == 'loading') | |
919 return; | |
920 | |
921 registerBrowserPluginElement(); | |
922 registerWebViewElement(); | |
923 window.removeEventListener(event.type, listener, useCapture); | |
924 }, useCapture); | |
925 | |
926 // Implemented when the ChromeWebView API is available. | |
927 WebViewInternal.prototype.maybeGetChromeWebViewEvents = function() {}; | |
928 | |
929 // Implemented when the experimental WebView API is available. | |
930 WebViewInternal.maybeGetExperimentalAPIs = function() {}; | |
931 WebViewInternal.prototype.maybeGetExperimentalEvents = function() {}; | |
932 WebViewInternal.prototype.setupExperimentalContextMenus = function() {}; | |
933 | |
934 // Exports. | |
935 exports.WebView = WebView; | |
936 exports.WebViewInternal = WebViewInternal; | |
OLD | NEW |