1// Copyright 2014 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 5var DocumentNatives = requireNative('document_natives'); 6var ExtensionOptionsEvents = 7 require('extensionOptionsEvents').ExtensionOptionsEvents; 8var GuestViewInternal = 9 require('binding').Binding.create('guestViewInternal').generate(); 10var IdGenerator = requireNative('id_generator'); 11var utils = require('utils'); 12var guestViewInternalNatives = requireNative('guest_view_internal'); 13 14// Mapping of the autosize attribute names to default values 15var AUTO_SIZE_ATTRIBUTES = { 16 'autosize': 'on', 17 'maxheight': window.innerHeight, 18 'maxwidth': window.innerWidth, 19 'minheight': 32, 20 'minwidth': 32 21}; 22 23function ExtensionOptionsInternal(extensionoptionsNode) { 24 privates(extensionoptionsNode).internal = this; 25 this.extensionoptionsNode = extensionoptionsNode; 26 this.viewInstanceId = IdGenerator.GetNextId(); 27 28 this.autosizeDeferred = false; 29 30 // on* Event handlers. 31 this.eventHandlers = {}; 32 33 // setupEventProperty is normally called in extension_options_events.js to 34 // register events, but the createfailed event is registered here because 35 // the event is fired from here instead of through 36 // extension_options_events.js. 37 this.setupEventProperty('createfailed'); 38 new ExtensionOptionsEvents(this, this.viewInstanceId); 39 40 this.setupNodeProperties(); 41 42 this.parseExtensionAttribute(); 43 44 // Once the browser plugin has been created, the guest view will be created 45 // and attached. See handleBrowserPluginAttributeMutation(). 46 this.browserPluginNode = this.createBrowserPluginNode(); 47 var shadowRoot = this.extensionoptionsNode.createShadowRoot(); 48 shadowRoot.appendChild(this.browserPluginNode); 49}; 50 51ExtensionOptionsInternal.prototype.attachWindow = function() { 52 return guestViewInternalNatives.AttachGuest( 53 this.internalInstanceId, 54 this.guestInstanceId, 55 { 56 'autosize': this.extensionoptionsNode.hasAttribute('autosize'), 57 'instanceId': this.viewInstanceId, 58 'maxheight': parseInt(this.maxheight || 0), 59 'maxwidth': parseInt(this.maxwidth || 0), 60 'minheight': parseInt(this.minheight || 0), 61 'minwidth': parseInt(this.minwidth || 0) 62 }); 63}; 64 65ExtensionOptionsInternal.prototype.createBrowserPluginNode = function() { 66 var browserPluginNode = new ExtensionOptionsInternal.BrowserPlugin(); 67 privates(browserPluginNode).internal = this; 68 return browserPluginNode; 69}; 70 71ExtensionOptionsInternal.prototype.createGuest = function() { 72 var params = { 73 'extensionId': this.extensionId, 74 }; 75 GuestViewInternal.createGuest( 76 'extensionoptions', 77 params, 78 function(guestInstanceId) { 79 if (guestInstanceId == 0) { 80 // Fire a createfailed event here rather than in ExtensionOptionsGuest 81 // because the guest will not be created, and cannot fire an event. 82 this.initCalled = false; 83 var createFailedEvent = new Event('createfailed', { bubbles: true }); 84 this.dispatchEvent(createFailedEvent); 85 } else { 86 this.guestInstanceId = guestInstanceId; 87 this.attachWindow(); 88 } 89 }.bind(this)); 90}; 91 92ExtensionOptionsInternal.prototype.dispatchEvent = 93 function(extensionOptionsEvent) { 94 return this.extensionoptionsNode.dispatchEvent(extensionOptionsEvent); 95}; 96 97ExtensionOptionsInternal.prototype.handleExtensionOptionsAttributeMutation = 98 function(name, oldValue, newValue) { 99 // We treat null attribute (attribute removed) and the empty string as 100 // one case. 101 oldValue = oldValue || ''; 102 newValue = newValue || ''; 103 104 if (oldValue === newValue) 105 return; 106 107 if (name == 'extension' && !oldValue && newValue) { 108 this.extensionId = newValue; 109 // If the browser plugin is not ready then don't create the guest until 110 // it is ready (in handleBrowserPluginAttributeMutation). 111 if (!this.internalInstanceId) 112 return; 113 114 // If a guest view does not exist then create one. 115 if (!this.guestInstanceId) { 116 this.createGuest(); 117 return; 118 } 119 // TODO(ericzeng): Implement navigation to another guest view if we want 120 // that functionality. 121 } else if (AUTO_SIZE_ATTRIBUTES.hasOwnProperty(name) > -1) { 122 this[name] = newValue; 123 this.resetSizeConstraintsIfInvalid(); 124 125 if (!this.guestInstanceId) 126 return; 127 128 GuestViewInternal.setAutoSize(this.guestInstanceId, { 129 'enableAutoSize': this.extensionoptionsNode.hasAttribute('autosize'), 130 'min': { 131 'width': parseInt(this.minwidth || 0), 132 'height': parseInt(this.minheight || 0) 133 }, 134 'max': { 135 'width': parseInt(this.maxwidth || 0), 136 'height': parseInt(this.maxheight || 0) 137 } 138 }); 139 } 140}; 141 142ExtensionOptionsInternal.prototype.handleBrowserPluginAttributeMutation = 143 function(name, oldValue, newValue) { 144 if (name == 'internalinstanceid' && !oldValue && !!newValue) { 145 this.internalInstanceId = parseInt(newValue); 146 this.browserPluginNode.removeAttribute('internalinstanceid'); 147 if (this.extensionId) 148 this.createGuest(); 149 150 } 151}; 152 153ExtensionOptionsInternal.prototype.onSizeChanged = 154 function(newWidth, newHeight, oldWidth, oldHeight) { 155 if (this.autosizeDeferred) { 156 this.deferredAutoSizeState = { 157 newWidth: newWidth, 158 newHeight: newHeight, 159 oldWidth: oldWidth, 160 oldHeight: oldHeight 161 }; 162 } else { 163 this.resize(newWidth, newHeight, oldWidth, oldHeight); 164 } 165}; 166 167ExtensionOptionsInternal.prototype.parseExtensionAttribute = function() { 168 if (this.extensionoptionsNode.hasAttribute('extension')) { 169 this.extensionId = this.extensionoptionsNode.getAttribute('extension'); 170 return true; 171 } 172 return false; 173}; 174 175ExtensionOptionsInternal.prototype.resize = 176 function(newWidth, newHeight, oldWidth, oldHeight) { 177 this.browserPluginNode.style.width = newWidth + 'px'; 178 this.browserPluginNode.style.height = newHeight + 'px'; 179 180 // Do not allow the options page's dimensions to shrink so that the options 181 // page has a consistent UI. If the new size is larger than the minimum, 182 // make that the new minimum size. 183 if (newWidth > this.minwidth) 184 this.minwidth = newWidth; 185 if (newHeight > this.minheight) 186 this.minheight = newHeight; 187 188 GuestViewInternal.setAutoSize(this.guestInstanceId, { 189 'enableAutoSize': this.extensionoptionsNode.hasAttribute('autosize'), 190 'min': { 191 'width': parseInt(this.minwidth || 0), 192 'height': parseInt(this.minheight || 0) 193 }, 194 'max': { 195 'width': parseInt(this.maxwidth || 0), 196 'height': parseInt(this.maxheight || 0) 197 } 198 }); 199}; 200 201// Adds an 'on<event>' property on the view, which can be used to set/unset 202// an event handler. 203ExtensionOptionsInternal.prototype.setupEventProperty = function(eventName) { 204 var propertyName = 'on' + eventName.toLowerCase(); 205 var extensionoptionsNode = this.extensionoptionsNode; 206 Object.defineProperty(extensionoptionsNode, propertyName, { 207 get: function() { 208 return this.eventHandlers[propertyName]; 209 }.bind(this), 210 set: function(value) { 211 if (this.eventHandlers[propertyName]) 212 extensionoptionsNode.removeEventListener( 213 eventName, this.eventHandlers[propertyName]); 214 this.eventHandlers[propertyName] = value; 215 if (value) 216 extensionoptionsNode.addEventListener(eventName, value); 217 }.bind(this), 218 enumerable: true 219 }); 220}; 221 222ExtensionOptionsInternal.prototype.setupNodeProperties = function() { 223 utils.forEach(AUTO_SIZE_ATTRIBUTES, function(attributeName) { 224 // Get the size constraints from the <extensionoptions> tag, or use the 225 // defaults if not specified 226 if (this.extensionoptionsNode.hasAttribute(attributeName)) { 227 this[attributeName] = 228 this.extensionoptionsNode.getAttribute(attributeName); 229 } else { 230 this[attributeName] = AUTO_SIZE_ATTRIBUTES[attributeName]; 231 } 232 233 Object.defineProperty(this.extensionoptionsNode, attributeName, { 234 get: function() { 235 return this[attributeName]; 236 }.bind(this), 237 set: function(value) { 238 this.extensionoptionsNode.setAttribute(attributeName, value); 239 }.bind(this), 240 enumerable: true 241 }); 242 }, this); 243 244 this.resetSizeConstraintsIfInvalid(); 245 246 Object.defineProperty(this.extensionoptionsNode, 'extension', { 247 get: function() { 248 return this.extensionId; 249 }.bind(this), 250 set: function(value) { 251 this.extensionoptionsNode.setAttribute('extension', value); 252 }.bind(this), 253 enumerable: true 254 }); 255}; 256 257ExtensionOptionsInternal.prototype.resetSizeConstraintsIfInvalid = function () { 258 if (this.minheight > this.maxheight || this.minheight < 0) { 259 this.minheight = AUTO_SIZE_ATTRIBUTES.minheight; 260 this.maxheight = AUTO_SIZE_ATTRIBUTES.maxheight; 261 } 262 if (this.minwidth > this.maxwidth || this.minwidth < 0) { 263 this.minwidth = AUTO_SIZE_ATTRIBUTES.minwidth; 264 this.maxwidth = AUTO_SIZE_ATTRIBUTES.maxwidth; 265 } 266}; 267 268/** 269 * Toggles whether the element should automatically resize to its preferred 270 * size. If set to true, when the element receives new autosize dimensions, 271 * it passes them to the embedder in a sizechanged event, but does not resize 272 * itself to those dimensions until the embedder calls resumeDeferredAutoSize. 273 * This allows the embedder to defer the resizing until it is ready. 274 * When set to false, the element resizes whenever it receives new autosize 275 * dimensions. 276 */ 277ExtensionOptionsInternal.prototype.setDeferAutoSize = function(value) { 278 if (!value) 279 resumeDeferredAutoSize(); 280 this.autosizeDeferred = value; 281}; 282 283/** 284 * Allows the element to resize to most recent set of autosize dimensions if 285 * autosizing is being deferred. 286 */ 287ExtensionOptionsInternal.prototype.resumeDeferredAutoSize = function() { 288 if (this.autosizeDeferred) { 289 this.resize(this.deferredAutoSizeState.newWidth, 290 this.deferredAutoSizeState.newHeight, 291 this.deferredAutoSizeState.oldWidth, 292 this.deferredAutoSizeState.oldHeight); 293 } 294}; 295 296function registerBrowserPluginElement() { 297 var proto = Object.create(HTMLObjectElement.prototype); 298 299 proto.createdCallback = function() { 300 this.setAttribute('type', 'application/browser-plugin'); 301 this.style.width = '100%'; 302 this.style.height = '100%'; 303 }; 304 305 proto.attributeChangedCallback = function(name, oldValue, newValue) { 306 var internal = privates(this).internal; 307 if (!internal) { 308 return; 309 } 310 internal.handleBrowserPluginAttributeMutation(name, oldValue, newValue); 311 }; 312 313 proto.attachedCallback = function() { 314 // Load the plugin immediately. 315 var unused = this.nonExistentAttribute; 316 }; 317 318 ExtensionOptionsInternal.BrowserPlugin = 319 DocumentNatives.RegisterElement('extensionoptionsplugin', 320 {extends: 'object', prototype: proto}); 321 delete proto.createdCallback; 322 delete proto.attachedCallback; 323 delete proto.detachedCallback; 324 delete proto.attributeChangedCallback; 325} 326 327function registerExtensionOptionsElement() { 328 var proto = Object.create(HTMLElement.prototype); 329 330 proto.createdCallback = function() { 331 new ExtensionOptionsInternal(this); 332 }; 333 334 proto.attributeChangedCallback = function(name, oldValue, newValue) { 335 var internal = privates(this).internal; 336 if (!internal) 337 return; 338 internal.handleExtensionOptionsAttributeMutation(name, oldValue, newValue); 339 }; 340 341 var methods = [ 342 'setDeferAutoSize', 343 'resumeDeferredAutoSize' 344 ]; 345 346 // Forward proto.foo* method calls to ExtensionOptionsInternal.foo*. 347 for (var i = 0; methods[i]; ++i) { 348 var createHandler = function(m) { 349 return function(var_args) { 350 var internal = privates(this).internal; 351 return $Function.apply(internal[m], internal, arguments); 352 }; 353 }; 354 proto[methods[i]] = createHandler(methods[i]); 355 } 356 357 window.ExtensionOptions = 358 DocumentNatives.RegisterElement('extensionoptions', {prototype: proto}); 359 360 // Delete the callbacks so developers cannot call them and produce unexpected 361 // behavior. 362 delete proto.createdCallback; 363 delete proto.attachedCallback; 364 delete proto.detachedCallback; 365 delete proto.attributeChangedCallback; 366} 367 368var useCapture = true; 369window.addEventListener('readystatechange', function listener(event) { 370 if (document.readyState == 'loading') 371 return; 372 373 registerBrowserPluginElement(); 374 registerExtensionOptionsElement(); 375 window.removeEventListener(event.type, listener, useCapture); 376}, useCapture); 377