| 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 |