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 |