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