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