Add ICU message format support
[chromium-blink-merge.git] / ios / web / web_state / js / resources / common.js
blobe5fbc83609a194a18dad0a5707200ee6b1e8c740
1 // Copyright 2013 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.
5 // This file provides common methods that can be shared by other JavaScripts.
7 goog.provide('__crWeb.common');
9 goog.require('__crWeb.base');
12 /**
13  * Namespace for this file. It depends on |__gCrWeb| having already been
14  * injected. String 'common' is used in |__gCrWeb['common']| as it needs to be
15  * accessed in Objective-C code.
16  */
17 __gCrWeb['common'] = {};
19 /* Beginning of anonymous object. */
20 (function() {
21   /**
22    * JSON safe object to protect against custom implementation of Object.toJSON
23    * in host pages.
24    * @constructor
25    */
26   __gCrWeb['common'].JSONSafeObject = function JSONSafeObject() {
27   };
29   /**
30    * Protect against custom implementation of Object.toJSON in host pages.
31    */
32   __gCrWeb['common'].JSONSafeObject.prototype.toJSON = null;
34   /**
35    * Retain the original JSON.stringify method where possible to reduce the
36    * impact of sites overriding it
37    */
38   __gCrWeb.common.JSONStringify = JSON.stringify;
40   /**
41    * Prefix used in references to form elements that have no 'id' or 'name'
42    */
43   __gCrWeb.common.kNamelessFormIDPrefix = 'gChrome~';
45   /**
46    * Tests an element's visiblity. This test is expensive so should be used
47    * sparingly.
48    * @param {Element} element A DOM element.
49    * @return {boolean} true if the |element| is currently part of the visible
50    * DOM.
51    */
52   __gCrWeb.common.isElementVisible = function(element) {
53     /** @type {Node} */
54     var node = element;
55     while (node && node !== document) {
56       if (node.nodeType === Node.ELEMENT_NODE) {
57         var style = window.getComputedStyle(/** @type {Element} */(node));
58         if (style.display === 'none' || style.visibility === 'hidden') {
59           return false;
60         }
61       }
62       // Move up the tree and test again.
63       node = node.parentNode;
64     }
65     // Test reached the top of the DOM without finding a concealed ancestor.
66     return true;
67   };
69   /**
70    * Based on Element::isFormControlElement() (WebKit)
71    * @param {Element} element A DOM element.
72    * @return {boolean} true if the |element| is a form control element.
73    */
74   __gCrWeb.common.isFormControlElement = function(element) {
75     var tagName = element.tagName;
76     return (tagName === 'INPUT' ||
77             tagName === 'SELECT' ||
78             tagName === 'TEXTAREA');
79   };
81   /**
82    * Detects focusable elements.
83    * @param {Element} element A DOM element.
84    * @return {boolean} true if the |element| is focusable.
85    */
86   __gCrWeb.common.isFocusable = function(element) {
87     // When the disabled or hidden attributes are present, controls do not
88     // receive focus.
89     if (element.hasAttribute('disabled') || element.hasAttribute('hidden'))
90       return false;
91     return __gCrWeb.common.isFormControlElement(element);
92   };
94   /**
95    * Returns an array of control elements in a form.
96    *
97    * This method is based on the logic in method
98    *     void WebFormElement::getFormControlElements(
99    *         WebVector<WebFormControlElement>&) const
100    * in chromium/src/third_party/WebKit/Source/WebKit/chromium/src/
101    * WebFormElement.cpp.
102    *
103    * @param {Element} form A form element for which the control elements are
104    *   returned.
105    * @return {Array<Element>}
106    */
107   __gCrWeb.common.getFormControlElements = function(form) {
108     if (!form) {
109       return [];
110     }
111     var results = [];
112     // Get input and select elements from form.elements.
113     // TODO(chenyu): according to
114     // http://www.w3.org/TR/2011/WD-html5-20110525/forms.html, form.elements are
115     // the "listed elements whose form owner is the form element, with the
116     // exception of input elements whose type attribute is in the Image Button
117     // state, which must, for historical reasons, be excluded from this
118     // particular collection." In WebFormElement.cpp, this method is implemented
119     // by returning elements in form's associated elements that have tag 'INPUT'
120     // or 'SELECT'. Check if input Image Buttons are excluded in that
121     // implementation. Note for Autofill, as input Image Button is not
122     // considered as autofillable elements, there is no impact on Autofill
123     // feature.
124     var elements = form.elements;
125     for (var i = 0; i < elements.length; i++) {
126       if (__gCrWeb.common.isFormControlElement(elements[i])) {
127         results.push(elements[i]);
128       }
129     }
130     return results;
131   };
133   /**
134    * Returns true if an element can be autocompleted.
135    *
136    * This method aims to provide the same logic as method
137    *     bool autoComplete() const
138    * in chromium/src/third_party/WebKit/Source/WebKit/chromium/src/
139    * WebFormElement.cpp.
140    *
141    * @param {Element} element An element to check if it can be autocompleted.
142    * @return {boolean} true if element can be autocompleted.
143    */
144   __gCrWeb.common.autoComplete = function(element) {
145     if (!element) {
146       return false;
147     }
148     if (__gCrWeb.common.getLowerCaseAttribute(
149         element, 'autocomplete') == 'off') {
150       return false;
151     }
152     if (__gCrWeb.common.getLowerCaseAttribute(
153             element.form, 'autocomplete') == 'off') {
154       return false;
155     }
156     return true;
157   };
159   /**
160    * Returns if an element is a text field.
161    * This returns true for all of textfield-looking types such as text,
162    * password, search, email, url, and number.
163    *
164    * This method aims to provide the same logic as method
165    *     bool WebInputElement::isTextField() const
166    * in chromium/src/third_party/WebKit/Source/WebKit/chromium/src/
167    * WebInputElement.cpp, where this information is from
168    *     bool HTMLInputElement::isTextField() const
169    *     {
170    *       return m_inputType->isTextField();
171    *     }
172    * (chromium/src/third_party/WebKit/Source/WebCore/html/HTMLInputElement.cpp)
173    *
174    * The implementation here is based on the following:
175    *
176    * - Method bool InputType::isTextField() defaults to be false and it is
177    *   override to return true only in HTMLInputElement's subclass
178    *   TextFieldInputType (chromium/src/third_party/WebKit/Source/WebCore/html/
179    *   TextFieldInputType.h).
180    *
181    * - The implementation here considers all the subclasses of
182    *   TextFieldInputType: NumberInputType and BaseTextInputType, which has
183    *   subclasses EmailInputType, PasswordInputType, SearchInputType,
184    *   TelephoneInputType, TextInputType, URLInputType. (All these classes are
185    *   defined in chromium/src/third_party/WebKit/Source/WebCore/html/)
186    *
187    * @param {Element} element An element to examine if it is a text field.
188    * @return {boolean} true if element has type=text.
189    */
190   __gCrWeb.common.isTextField = function(element) {
191     if (!element) {
192       return false;
193     }
194     if (element.type === 'hidden') {
195       return false;
196     }
197     return element.type === 'text' ||
198            element.type === 'email' ||
199            element.type === 'password' ||
200            element.type === 'search' ||
201            element.type === 'tel' ||
202            element.type === 'url' ||
203            element.type === 'number';
204   };
206   /**
207    * Sets the checked value of an input and dispatches an change event if
208    * |shouldSendChangeEvent|.
209    *
210    * This is a simplified version of the implementation of
211    *
212    *     void setChecked(bool nowChecked, TextFieldEventBehavior eventBehavior)
213    *
214    * in chromium/src/third_party/WebKit/Source/WebKit/chromium/src/
215    * WebInputElement.cpp, which calls
216    *     void HTMLInputElement::setChecked(
217    *         bool nowChecked, TextFieldEventBehavior eventBehavior)
218    * in chromium/src/third_party/WebKit/Source/core/html/HTMLInputElement.cpp.
219    *
220    * @param {boolean} nowChecked The new checked value of the input element.
221    * @param {Element} input The input element of which the value is set.
222    * @param {boolean} shouldSendChangeEvent Whether a change event should be
223    *     dispatched.
224    */
225   __gCrWeb.common.setInputElementChecked = function(
226       nowChecked, input, shouldSendChangeEvent) {
227     var checkedChanged = input.checked !== nowChecked;
228     input.checked = nowChecked;
229     if (checkedChanged) {
230       __gCrWeb.common.createAndDispatchHTMLEvent(input, 'change', true, false);
231     }
232   };
234   /**
235    * Sets the value of an input and dispatches an change event if
236    * |shouldSendChangeEvent|.
237    *
238    * It is based on the logic in
239    *
240    *     void setValue(const WebString&, bool sendChangeEvent = false)
241    *
242    * in chromium/src/third_party/WebKit/Source/WebKit/chromium/src/
243    * WebInputElement.cpp, which calls
244    *    void setValue(const String& value, TextFieldEventBehavior eventBehavior)
245    * in chromium/src/third_party/WebKit/Source/core/html/HTMLInputElement.cpp.
246    *
247    * @param {string} value The value the input element will be set.
248    * @param {Element} input The input element of which the value is set.
249    * @param {boolean} shouldSendChangeEvent Whether a change event should be
250    *     dispatched.
251    */
252   __gCrWeb.common.setInputElementValue = function(
253       value, input, shouldSendChangeEvent) {
254     // In HTMLInputElement.cpp there is a check on canSetValue(value), which
255     // returns false only for file input. As file input is not relevant for
256     // autofill and this method is only used for autofill for now, there is no
257     // such check in this implementation.
258     var sanitizedValue = __gCrWeb.common.sanitizeValueForInputElement(
259         value, input);
260     var valueChanged = sanitizedValue !== input.value;
261     input.value = sanitizedValue;
262     if (valueChanged) {
263       __gCrWeb.common.createAndDispatchHTMLEvent(input, 'change', true, false);
264     }
265   };
267   /**
268    * Returns a sanitized value of proposedValue for a given input element type.
269    * The logic is based on
270    *
271    *      String sanitizeValue(const String&) const
272    *
273    * in chromium/src/third_party/WebKit/Source/core/html/InputType.h
274    *
275    * @param {string} proposedValue The proposed value.
276    * @param {Element} element The element for which the proposedValue is to be
277    *     sanitized.
278    * @return {string} The sanitized value.
279    */
280    __gCrWeb.common.sanitizeValueForInputElement = function(
281        proposedValue, element) {
282     if (!proposedValue) {
283       return '';
284     }
286     // Method HTMLInputElement::sanitizeValue() calls InputType::sanitizeValue()
287     // (chromium/src/third_party/WebKit/Source/core/html/InputType.cpp) for
288     // non-null proposedValue. InputType::sanitizeValue() returns the original
289     // proposedValue by default and it is overridden in classes
290     // BaseDateAndTimeInputType, ColorInputType, RangeInputType and
291     // TextFieldInputType (all are in
292     // chromium/src/third_party/WebKit/Source/core/html/). Currently only
293     // TextFieldInputType is relevant and sanitizeValue() for other types of
294     // input elements has not been implemented.
295     if (__gCrWeb.common.isTextField(element)) {
296       return __gCrWeb.common.sanitizeValueForTextFieldInputType(
297           proposedValue, element);
298     }
299     return proposedValue;
300    };
302   /**
303    * Returns a sanitized value for a text field.
304    *
305    * The logic is based on |String sanitizeValue(const String&)|
306    * in chromium/src/third_party/WebKit/Source/core/html/TextFieldInputType.h
307    * Note this method is overridden in EmailInputType and NumberInputType.
308    *
309    * @param {string} proposedValue The proposed value.
310    * @param {Element} element The element for which the proposedValue is to be
311    *     sanitized.
312    * @return {string} The sanitized value.
313    */
314   __gCrWeb.common.sanitizeValueForTextFieldInputType = function(
315       proposedValue, element) {
316     var textFieldElementType = element.type;
317     if (textFieldElementType === 'email') {
318       return __gCrWeb.common.sanitizeValueForEmailInputType(
319           proposedValue, element);
320     } else if (textFieldElementType === 'number') {
321       return __gCrWeb.common.sanitizeValueForNumberInputType(proposedValue);
322     }
323     var valueWithLineBreakRemoved = proposedValue.replace(/(\r\n|\n|\r)/gm, '');
324     // TODO(chenyu): Should we also implement numCharactersInGraphemeClusters()
325     // in chromium/src/third_party/WebKit/Source/core/platform/text/
326     // TextBreakIterator.cpp and call it here when computing newLength?
327     // Different from the implementation in TextFieldInputType.h, where a limit
328     // on the text length is considered due to
329     // https://bugs.webkit.org/show_bug.cgi?id=14536, no such limit is
330     // considered here for now.
331     var newLength = valueWithLineBreakRemoved.length;
332     // This logic is from method String limitLength() in TextFieldInputType.h
333     for (var i = 0; i < newLength; ++i) {
334       var current = valueWithLineBreakRemoved[i];
335       if (current < ' ' && current != '\t') {
336         newLength = i;
337         break;
338       }
339     }
340     return valueWithLineBreakRemoved.substring(0, newLength);
341   };
343   /**
344    * Returns the sanitized value for an email input.
345    *
346    * The logic is based on
347    *
348    *     String EmailInputType::sanitizeValue(const String& proposedValue) const
349    *
350    * in chromium/src/third_party/WebKit/Source/core/html/EmailInputType.cpp
351    *
352    * @param {string} proposedValue The proposed value.
353    * @param {Element} element The element for which the proposedValue is to be
354    *     sanitized.
355    * @return {string} The sanitized value.
356    */
357   __gCrWeb.common.sanitizeValueForEmailInputType = function(
358       proposedValue, element) {
359     var valueWithLineBreakRemoved = proposedValue.replace(/(\r\n|\n\r)/gm, '');
361     if (!element.multiple) {
362       return __gCrWeb.common.trim(proposedValue);
363     }
364     var addresses = valueWithLineBreakRemoved.split(',');
365     for (var i = 0; i < addresses.length; ++i) {
366       addresses[i] = __gCrWeb.common.trim(addresses[i]);
367     }
368     return addresses.join(',');
369   };
371   /**
372    * Returns the sanitized value of a proposed value for a number input.
373    *
374    * The logic is based on
375    *
376    *     String NumberInputType::sanitizeValue(const String& proposedValue)
377    *         const
378    *
379    * in chromium/src/third_party/WebKit/Source/core/html/NumberInputType.cpp
380    *
381    * Note in this implementation method Number() is used in the place of method
382    * parseToDoubleForNumberType() called in NumberInputType.cpp.
383    *
384    * @param {string} proposedValue The proposed value.
385    * @return {string} The sanitized value.
386    */
387   __gCrWeb.common.sanitizeValueForNumberInputType = function(proposedValue) {
388     var sanitizedValue = Number(proposedValue);
389     if (isNaN(sanitizedValue)) {
390       return '';
391     }
392     return sanitizedValue.toString();
393   };
395   /**
396    * Trims any whitespace from the start and end of a string.
397    * Used in preference to String.prototype.trim as this can be overridden by
398    * sites.
399    *
400    * @param {string} str The string to be trimmed.
401    * @return {string} The string after trimming.
402    */
403   __gCrWeb.common.trim = function(str) {
404     return str.replace(/^\s+|\s+$/g, '');
405   };
407   /**
408    * Returns the name that should be used for the specified |element| when
409    * storing Autofill data. Various attributes are used to attempt to identify
410    * the element, beginning with 'name' and 'id' attributes. Providing a
411    * uniquely reversible identifier for any element is a non-trivial problem;
412    * this solution attempts to satisfy the majority of cases.
413    *
414    * It aims to provide the logic in
415    *     WebString nameForAutofill() const;
416    * in chromium/src/third_party/WebKit/Source/WebKit/chromium/public/
417    *  WebFormControlElement.h
418    *
419    * @param {Element} element An element of which the name for Autofill will be
420    *     returned.
421    * @return {string} the name for Autofill.
422    */
423   __gCrWeb.common.nameForAutofill = function(element) {
424     if (!element) {
425       return '';
426     }
427     var trimmedName = element.name;
428     if (trimmedName) {
429       trimmedName = __gCrWeb.common.trim(trimmedName);
430       if (trimmedName.length > 0) {
431         return trimmedName;
432       }
433     }
434     trimmedName = element.getAttribute('id');
435     if (trimmedName) {
436       return __gCrWeb.common.trim(trimmedName);
437     }
438     return '';
439   };
441   /**
442    * Acquires the specified DOM |attribute| from the DOM |element| and returns
443    * its lower-case value, or null if not present.
444    * @param {Element} element A DOM element.
445    * @param {string} attribute An attribute name.
446    * @return {?string} Lowercase value of DOM element or null if not present.
447    */
448   __gCrWeb.common.getLowerCaseAttribute = function(element, attribute) {
449     if (!element) {
450       return null;
451     }
452     var value = element.getAttribute(attribute);
453     if (value) {
454       return value.toLowerCase();
455     }
456     return null;
457   };
459   /**
460    * Converts a relative URL into an absolute URL.
461    * @param {Object} doc Document.
462    * @param {string} relativeURL Relative URL.
463    * @return {string} Absolute URL.
464    */
465   __gCrWeb.common.absoluteURL = function(doc, relativeURL) {
466     // In the case of data: URL-based pages, relativeURL === absoluteURL.
467     if (doc.location.protocol === 'data:') {
468       return doc.location.href;
469     }
470     var urlNormalizer = doc['__gCrWebURLNormalizer'];
471     if (!urlNormalizer) {
472       urlNormalizer = doc.createElement('a');
473       doc['__gCrWebURLNormalizer'] = urlNormalizer;
474     }
476     // Use the magical quality of the <a> element. It automatically converts
477     // relative URLs into absolute ones.
478     urlNormalizer.href = relativeURL;
479     return urlNormalizer.href;
480   };
482   /**
483    * Extracts the webpage URL from the given URL by removing the query
484    * and the reference (aka fragment) from the URL.
485    * @param {string} url Web page URL.
486    * @return {string} Web page URL with query and reference removed.
487    */
488   __gCrWeb.common.removeQueryAndReferenceFromURL = function(url) {
489     var queryIndex = url.indexOf('?');
490     if (queryIndex != -1) {
491       return url.substring(0, queryIndex);
492     }
494     var hashIndex = url.indexOf('#');
495     if (hashIndex != -1) {
496       return url.substring(0, hashIndex);
497     }
498     return url;
499   };
501   /**
502    * Returns the form's |name| attribute if non-empty; otherwise the form's |id|
503    * attribute, or the index of the form (with prefix) in document.forms.
504    *
505    * It is partially based on the logic in
506    *     const string16 GetFormIdentifier(const blink::WebFormElement& form)
507    * in chromium/src/components/autofill/renderer/form_autofill_util.h.
508    *
509    * @param {Element} form An element for which the identifier is returned.
510    * @return {string} a string that represents the element's identifier.
511    */
512   __gCrWeb.common.getFormIdentifier = function(form) {
513     if (!form)
514       return '';
515     var name = form.getAttribute('name');
516     if (name && name.length != 0) {
517       return name;
518     }
519     name = form.getAttribute('id');
520     if (name) {
521       return name;
522     }
523     // A form name must be supplied, because the element will later need to be
524     // identified from the name. A last resort is to take the index number of
525     // the form in document.forms. ids are not supposed to begin with digits (by
526     // HTML 4 spec) so this is unlikely to match a true id.
527     for (var idx = 0; idx != document.forms.length; idx++) {
528       if (document.forms[idx] == form) {
529         return __gCrWeb.common.kNamelessFormIDPrefix + idx;
530       }
531     }
532     return '';
533   };
535   /**
536    * Returns the form element from an ID obtained from getFormIdentifier.
537    *
538    * This works on a 'best effort' basis since DOM changes can always change the
539    * actual element that the ID refers to.
540    *
541    * @param {string} name An ID string obtained via getFormIdentifier.
542    * @return {Element} The original form element, if it can be determined.
543    */
544   __gCrWeb.common.getFormElementFromIdentifier = function(name) {
545     // First attempt is from the name / id supplied.
546     var form = document.forms.namedItem(name);
547     if (form) {
548       if (form.nodeType !== Node.ELEMENT_NODE)
549         return null;
550       return /** @type {Element} */(form);
551     }
552     // Second attempt is from the prefixed index position of the form in
553     // document.forms.
554     if (name.indexOf(__gCrWeb.common.kNamelessFormIDPrefix) == 0) {
555       var nameAsInteger = 0 |
556           name.substring(__gCrWeb.common.kNamelessFormIDPrefix.length);
557       if (__gCrWeb.common.kNamelessFormIDPrefix + nameAsInteger == name &&
558           nameAsInteger < document.forms.length) {
559         return document.forms[nameAsInteger];
560       }
561     }
562     return null;
563   };
565   /**
566    * Creates and dispatches an HTML event.
567    *
568    * @param {Element} element The element for which an event is created.
569    * @param {string} type The type of the event.
570    * @param {boolean} bubbles A boolean indicating whether the event should
571    *     bubble up through the event chain or not.
572    * @param {boolean} cancelable A boolean indicating whether the event can be
573    *     canceled.
574    */
575   __gCrWeb.common.createAndDispatchHTMLEvent = function(
576       element, type, bubbles, cancelable) {
577     var changeEvent = element.ownerDocument.createEvent('HTMLEvents');
578     changeEvent.initEvent(type, bubbles, cancelable);
580     // A timer is used to avoid reentering JavaScript evaluation.
581     window.setTimeout(function() {
582       element.dispatchEvent(changeEvent);
583     }, 0);
584   };
586   /**
587    * Retrieves favicon information.
588    *
589    * @return {Object} Object containing favicon data.
590    */
591   __gCrWeb.common.getFavicons = function() {
592     var favicons = [];
593     var hasFavicon = false;
594     favicons.toJSON = null;  // Never inherit Array.prototype.toJSON.
595     var links = document.getElementsByTagName('link');
596     var linkCount = links.length;
597     for (var i = 0; i < linkCount; ++i) {
598       if (links[i].rel) {
599         var rel = links[i].rel.toLowerCase();
600         if (rel == 'shortcut icon' ||
601             rel == 'icon' ||
602             rel == 'apple-touch-icon' ||
603             rel == 'apple-touch-icon-precomposed') {
604           var favicon = {
605             rel: links[i].rel.toLowerCase(),
606             href: links[i].href
607           };
608           favicons.push(favicon);
609           if (rel == 'icon' || rel == 'shortcut icon') {
610             hasFavicon = true;
611           }
612         }
613       }
614     }
615     if (!hasFavicon) {
616       // If an HTTP(S)? webpage does not reference a "favicon" then search
617       // for a file named "favicon.ico" at the root of the website (legacy).
618       // http://en.wikipedia.org/wiki/Favicon
619       var location = document.location;
620       if (location.protocol == 'http:' || location.protocol == 'https:') {
621         var favicon = {
622           rel: 'icon',
623           href: location.origin + '/favicon.ico'
624         };
625         favicons.push(favicon);
626       }
627     }
628     return favicons;
629   };
631   /**
632    * Checks whether an <object> node is plugin content (as <object> can also be
633    * used to embed images).
634    * @param {HTMLElement} node The <object> node to check.
635    * @return {boolean} Whether the node appears to be a plugin.
636    * @private
637    */
638   var objectNodeIsPlugin_ = function(node) {
639     return node.hasAttribute('classid') ||
640            (node.hasAttribute('type') && node.type.indexOf('image/') != 0);
641   };
643   /**
644    * Checks whether plugin a node has fallback content.
645    * @param {HTMLElement} node The node to check.
646    * @return {boolean} Whether the node has fallback.
647    * @private
648    */
649   var pluginHasFallbackContent_ = function(node) {
650     return node.textContent.trim().length > 0 ||
651            node.getElementsByTagName('img').length > 0;
652   };
654   /**
655    * Returns a list of plugin elements in the document that have no fallback
656    * content. For nested plugins, only the innermost plugin element is returned.
657    * @return {Array} A list of plugin elements.
658    * @private
659    */
660   var findPluginNodesWithoutFallback_ = function() {
661     var pluginNodes = [];
662     var objects = document.getElementsByTagName('object');
663     var objectCount = objects.length;
664     for (var i = 0; i < objectCount; i++) {
665       var object = objects[i];
666       if (objectNodeIsPlugin_(object) &&
667           !pluginHasFallbackContent_(object)) {
668         pluginNodes.push(object);
669       }
670     }
671     var applets = document.getElementsByTagName('applet');
672     var appletsCount = applets.length;
673     for (var i = 0; i < appletsCount; i++) {
674       var applet = applets[i];
675       if (!pluginHasFallbackContent_(applet)) {
676         pluginNodes.push(applet);
677       }
678     }
679     return pluginNodes;
680   };
682   /**
683    * Finds and stores any plugins that don't have placeholders.
684    * Returns true if any plugins without placeholders are found.
685    */
686   __gCrWeb.common.updatePluginPlaceholders = function() {
687     var plugins = findPluginNodesWithoutFallback_();
688     if (plugins.length > 0) {
689       // Store the list of plugins in a known place for the replacement script
690       // to use, then trigger it.
691       __gCrWeb['placeholderTargetPlugins'] = plugins;
692       return true;
693     }
694     return false;
695   };
696 }());  // End of anonymous object