extension_options.js revision 5f1c94371a64b3196d4be9466099bb892df9b88e
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'); 12 13// Mapping of the autosize attribute names to default values 14var AUTO_SIZE_ATTRIBUTES = { 15 'autosize': 'on', 16 'maxheight': 600, 17 'maxwidth': 800, 18 'minheight': 32, 19 'minwidth': 80 20}; 21 22function ExtensionOptionsInternal(extensionoptionsNode) { 23 privates(extensionoptionsNode).internal = this; 24 this.extensionoptionsNode = extensionoptionsNode; 25 this.viewInstanceId = IdGenerator.GetNextId(); 26 27 // on* Event handlers. 28 this.eventHandlers = {}; 29 new ExtensionOptionsEvents(this, this.viewInstanceId); 30 31 this.setupNodeProperties(); 32 33 if (this.parseExtensionAttribute()) 34 this.init(); 35}; 36 37ExtensionOptionsInternal.prototype.attachWindow = function(instanceId) { 38 this.instanceId = instanceId; 39 var params = { 40 'autosize': this.autosize, 41 'instanceId': this.viewInstanceId, 42 'maxheight': parseInt(this.maxheight || 0), 43 'maxwidth': parseInt(this.maxwidth || 0), 44 'minheight': parseInt(this.minheight || 0), 45 'minwidth': parseInt(this.minwidth || 0) 46 } 47 return this.browserPluginNode['-internal-attach'](instanceId, params); 48}; 49 50ExtensionOptionsInternal.prototype.createBrowserPluginNode = function() { 51 var browserPluginNode = new ExtensionOptionsInternal.BrowserPlugin(); 52 privates(browserPluginNode).internal = this; 53 return browserPluginNode; 54}; 55 56ExtensionOptionsInternal.prototype.createGuest = function() { 57 var params = { 58 'extensionId': this.extensionId, 59 }; 60 GuestViewInternal.createGuest( 61 'extensionoptions', 62 params, 63 function(instanceId) { 64 if (instanceId == 0) { 65 this.initCalled = false; 66 } else { 67 this.attachWindow(instanceId); 68 GuestViewInternal.setAutoSize(this.instanceId, { 69 'enableAutoSize': 70 this.extensionoptionsNode.hasAttribute('autosize'), 71 'min': { 72 'width': parseInt(this.minwidth || 0), 73 'height': parseInt(this.minheight || 0) 74 }, 75 'max': { 76 'width': parseInt(this.maxwidth || 0), 77 'height': parseInt(this.maxheight || 0) 78 } 79 }); 80 } 81 }.bind(this)); 82}; 83 84ExtensionOptionsInternal.prototype.dispatchEvent = 85 function(extensionOptionsEvent) { 86 return this.extensionoptionsNode.dispatchEvent(extensionOptionsEvent); 87}; 88 89ExtensionOptionsInternal.prototype.handleExtensionOptionsAttributeMutation = 90 function(name, oldValue, newValue) { 91 // We treat null attribute (attribute removed) and the empty string as 92 // one case. 93 oldValue = oldValue || ''; 94 newValue = newValue || ''; 95 96 if (oldValue === newValue) 97 return; 98 99 if (name == 'extension') { 100 this.extensionId = newValue; 101 // Create new guest view if one hasn't been created for this element. 102 if (!this.instanceId && this.parseExtensionAttribute()) 103 this.init(); 104 // TODO(ericzeng): Implement navigation to another guest view if we want 105 // that functionality. 106 } else if (AUTO_SIZE_ATTRIBUTES.hasOwnProperty(name) > -1) { 107 this[name] = newValue; 108 this.resetSizeConstraintsIfInvalid(); 109 110 if (!this.instanceId) 111 return; 112 113 GuestViewInternal.setAutoSize(this.instanceId, { 114 'enableAutoSize': this.extensionoptionsNode.hasAttribute('autosize'), 115 'min': { 116 'width': parseInt(this.minwidth || 0), 117 'height': parseInt(this.minheight || 0) 118 }, 119 'max': { 120 'width': parseInt(this.maxwidth || 0), 121 'height': parseInt(this.maxheight || 0) 122 } 123 }); 124 } 125}; 126 127ExtensionOptionsInternal.prototype.init = function() { 128 if (this.initCalled) 129 return; 130 131 this.initCalled = true; 132 this.browserPluginNode = this.createBrowserPluginNode(); 133 var shadowRoot = this.extensionoptionsNode.createShadowRoot(); 134 shadowRoot.appendChild(this.browserPluginNode); 135 this.createGuest(); 136}; 137 138ExtensionOptionsInternal.prototype.onSizeChanged = function(width, height) { 139 this.browserPluginNode.style.width = width + 'px'; 140 this.browserPluginNode.style.height = height + 'px'; 141}; 142 143ExtensionOptionsInternal.prototype.parseExtensionAttribute = function() { 144 if (this.extensionoptionsNode.hasAttribute('extension')) { 145 var extensionId = this.extensionoptionsNode.getAttribute('extension'); 146 // Only allow extensions to embed their own options page (if it has one). 147 if (chrome.runtime.id == extensionId && 148 chrome.runtime.getManifest().hasOwnProperty('options_page')) { 149 this.extensionId = extensionId; 150 return true; 151 } 152 } 153 return false; 154}; 155 156// Adds an 'on<event>' property on the view, which can be used to set/unset 157// an event handler. 158ExtensionOptionsInternal.prototype.setupEventProperty = function(eventName) { 159 var propertyName = 'on' + eventName.toLowerCase(); 160 var self = this; 161 var extensionoptionsNode = this.extensionoptionsNode; 162 Object.defineProperty(extensionoptionsNode, propertyName, { 163 get: function() { 164 return self.eventHandlers[propertyName]; 165 }, 166 set: function(value) { 167 if (self.eventHandlers[propertyName]) 168 extensionoptionsNode.removeEventListener( 169 eventName, self.eventHandlers[propertyName]); 170 self.eventHandlers[propertyName] = value; 171 if (value) 172 extensionoptionsNode.addEventListener(eventName, value); 173 }, 174 enumerable: true 175 }); 176}; 177 178ExtensionOptionsInternal.prototype.setupNodeProperties = function() { 179 utils.forEach(AUTO_SIZE_ATTRIBUTES, function(attributeName) { 180 // Get the size constraints from the <extensionoptions> tag, or use the 181 // defaults if not specified 182 if (this.extensionoptionsNode.hasAttribute(attributeName)) { 183 this[attributeName] = 184 this.extensionoptionsNode.getAttribute(attributeName); 185 } else { 186 this[attributeName] = AUTO_SIZE_ATTRIBUTES[attributeName]; 187 } 188 189 Object.defineProperty(this.extensionoptionsNode, attributeName, { 190 get: function() { 191 return this[attributeName]; 192 }.bind(this), 193 set: function(value) { 194 this.extensionoptionsNode.setAttribute(attributeName, value); 195 }.bind(this), 196 enumerable: true 197 }); 198 }, this); 199 200 this.resetSizeConstraintsIfInvalid(); 201 202 Object.defineProperty(this.extensionoptionsNode, 'extension', { 203 get: function() { 204 return this.extensionId; 205 }.bind(this), 206 set: function(value) { 207 this.extensionoptionsNode.setAttribute('extension', value); 208 }.bind(this), 209 enumerable: true 210 }); 211}; 212 213ExtensionOptionsInternal.prototype.resetSizeConstraintsIfInvalid = function () { 214 if (this.minheight > this.maxheight || this.minheight < 0) { 215 this.minheight = AUTO_SIZE_ATTRIBUTES.minheight; 216 this.maxheight = AUTO_SIZE_ATTRIBUTES.maxheight; 217 } 218 if (this.minwidth > this.maxwidth || this.minwidth < 0) { 219 this.minwidth = AUTO_SIZE_ATTRIBUTES.minwidth; 220 this.maxwidth = AUTO_SIZE_ATTRIBUTES.maxwidth; 221 } 222} 223 224function registerBrowserPluginElement() { 225 var proto = Object.create(HTMLObjectElement.prototype); 226 227 proto.createdCallback = function() { 228 this.setAttribute('type', 'application/browser-plugin'); 229 this.style.width = '100%'; 230 this.style.height = '100%'; 231 }; 232 233 proto.attachedCallback = function() { 234 // Load the plugin immediately. 235 var unused = this.nonExistentAttribute; 236 }; 237 238 ExtensionOptionsInternal.BrowserPlugin = 239 DocumentNatives.RegisterElement('extensionoptionsplugin', 240 {extends: 'object', prototype: proto}); 241 delete proto.createdCallback; 242 delete proto.attachedCallback; 243 delete proto.detachedCallback; 244 delete proto.attributeChangedCallback; 245} 246 247function registerExtensionOptionsElement() { 248 var proto = Object.create(HTMLElement.prototype); 249 250 proto.createdCallback = function() { 251 new ExtensionOptionsInternal(this); 252 }; 253 254 proto.attributeChangedCallback = function(name, oldValue, newValue) { 255 var internal = privates(this).internal; 256 if (!internal) 257 return; 258 internal.handleExtensionOptionsAttributeMutation(name, oldValue, newValue); 259 }; 260 261 window.ExtensionOptions = 262 DocumentNatives.RegisterElement('extensionoptions', {prototype: proto}); 263 264 // Delete the callbacks so developers cannot call them and produce unexpected 265 // behavior. 266 delete proto.createdCallback; 267 delete proto.attachedCallback; 268 delete proto.detachedCallback; 269 delete proto.attributeChangedCallback; 270} 271 272var useCapture = true; 273window.addEventListener('readystatechange', function listener(event) { 274 if (document.readyState == 'loading') 275 return; 276 277 registerBrowserPluginElement(); 278 registerExtensionOptionsElement(); 279 window.removeEventListener(event.type, listener, useCapture); 280}, useCapture); 281