| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * @fileoverview Adds listeners that are used to handle forms, enabling autofill |
| * and the replacement method to dismiss the keyboard needed because of the |
| * Autofill keyboard accessory. |
| * Contains method needed to access the forms and their elements. |
| */ |
| |
| goog.provide('__crWeb.form'); |
| |
| /** |
| * Namespace for this file. It depends on |__gCrWeb| having already been |
| * injected. String 'form' is used in |__gCrWeb['form']| as it needs to be |
| * accessed in Objective-C code. |
| */ |
| __gCrWeb.form = {}; |
| |
| // Store common namespace object in a global __gCrWeb object referenced by a |
| // string, so it does not get renamed by closure compiler during the |
| // minification. |
| __gCrWeb['form'] = __gCrWeb.form; |
| |
| /** Beginning of anonymous object */ |
| (function() { |
| // Skip iframes that have different origins from the main frame. For such |
| // frames no form related actions (eg. filling, saving) are supported. |
| try { |
| // The following line generates exception for iframes that have different |
| // origin that. |
| // TODO(crbug.com/792642): implement sending messages instead of using |
| // window.top, when messaging framework is ready. |
| if (!window.top.document) return; |
| } catch (error) { |
| return; |
| } |
| |
| /** |
| * Prefix used in references to form elements that have no 'id' or 'name' |
| */ |
| __gCrWeb.form.kNamelessFormIDPrefix = 'gChrome~form~'; |
| |
| /** |
| * Prefix used in references to field elements that have no 'id' or 'name' but |
| * are included in a form. |
| */ |
| __gCrWeb.form.kNamelessFieldIDPrefix = 'gChrome~field~'; |
| |
| /** |
| * The MutationObserver tracking form related changes. |
| */ |
| __gCrWeb.form.formMutationObserver = null; |
| |
| /** |
| * The form mutation message scheduled to be sent to browser. |
| */ |
| __gCrWeb.form.formMutationMessageToSend = null; |
| |
| /** |
| * A message scheduled to be sent to host on the next runloop. |
| */ |
| __gCrWeb.form.messageToSend = null; |
| |
| /** |
| * The last HTML element that was focused by the user. |
| */ |
| __gCrWeb.form.lastFocusedElement = null; |
| |
| /** |
| * A WeakMap to track if the current value of a field was entered by user or |
| * programmatically. |
| * If the map is null, the source of changed is not track. |
| */ |
| __gCrWeb.form.wasEditedByUser = null; |
| |
| /** |
| * Based on Element::isFormControlElement() (WebKit) |
| * @param {Element} element A DOM element. |
| * @return {boolean} true if the |element| is a form control element. |
| */ |
| __gCrWeb.form.isFormControlElement = function(element) { |
| var tagName = element.tagName; |
| return ( |
| tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA'); |
| }; |
| |
| /** |
| * Returns an array of control elements in a form. |
| * |
| * This method is based on the logic in method |
| * void WebFormElement::getFormControlElements( |
| * WebVector<WebFormControlElement>&) const |
| * in chromium/src/third_party/WebKit/Source/WebKit/chromium/src/ |
| * WebFormElement.cpp. |
| * |
| * @param {Element} form A form element for which the control elements are |
| * returned. |
| * @return {Array<FormControlElement>} |
| */ |
| __gCrWeb.form.getFormControlElements = function(form) { |
| if (!form) { |
| return []; |
| } |
| var results = []; |
| // Get input and select elements from form.elements. |
| // According to |
| // http://www.w3.org/TR/2011/WD-html5-20110525/forms.html, form.elements are |
| // the "listed elements whose form owner is the form element, with the |
| // exception of input elements whose type attribute is in the Image Button |
| // state, which must, for historical reasons, be excluded from this |
| // particular collection." In WebFormElement.cpp, this method is implemented |
| // by returning elements in form's associated elements that have tag 'INPUT' |
| // or 'SELECT'. Check if input Image Buttons are excluded in that |
| // implementation. Note for Autofill, as input Image Button is not |
| // considered as autofillable elements, there is no impact on Autofill |
| // feature. |
| var elements = form.elements; |
| for (var i = 0; i < elements.length; i++) { |
| if (__gCrWeb.form.isFormControlElement(elements[i])) { |
| results.push(/** @type {FormControlElement} */ (elements[i])); |
| } |
| } |
| return results; |
| }; |
| |
| /** |
| * Returns the field's |id| attribute if not space only; otherwise the |
| * form's |name| attribute if the field is part of a form. Otherwhise, |
| * generate a technical identifier |
| * |
| * It is the identifier that should be used for the specified |element| when |
| * storing Autofill data. This identifier will be used when filling the field |
| * to lookup this field. The pair (getFormIdentifier, getFieldIdentifier) must |
| * be unique on the page. |
| * The following elements are considered to generate the identifier: |
| * - the id of the element |
| * - the name of the element if the element is part of a form |
| * - the order of the element in the form if the element is part of the form. |
| * - generate a xpath to the element and use it as an ID. |
| * |
| * Note: if this method returns '', the field will not be accessible and |
| * cannot be autofilled. |
| * |
| * It aims to provide the logic in |
| * WebString nameForAutofill() const; |
| * in chromium/src/third_party/WebKit/Source/WebKit/chromium/public/ |
| * WebFormControlElement.h |
| * |
| * @param {Element} element An element of which the name for Autofill will be |
| * returned. |
| * @return {string} the name for Autofill. |
| */ |
| __gCrWeb.form.getFieldIdentifier = function(element) { |
| if (!element) { |
| return ''; |
| } |
| var trimmedIdentifier = element.id; |
| if (trimmedIdentifier) { |
| return __gCrWeb.common.trim(trimmedIdentifier); |
| } |
| if (element.form) { |
| // The name of an element is only relevant as an identifier if the element |
| // is part of a form. |
| trimmedIdentifier = element.name; |
| if (trimmedIdentifier) { |
| trimmedIdentifier = __gCrWeb.common.trim(trimmedIdentifier); |
| if (trimmedIdentifier.length > 0) { |
| return trimmedIdentifier; |
| } |
| } |
| |
| var elements = __gCrWeb.form.getFormControlElements(element.form); |
| for (var index = 0; index < elements.length; index++) { |
| if (elements[index] === element) { |
| return __gCrWeb.form.kNamelessFieldIDPrefix + index; |
| } |
| } |
| } |
| // Element is not part of a form and has no name or id, or usable attribute. |
| // As best effort, try to find the closest ancestor with an id, then |
| // check the index of the element in the descendants of the ancestors with |
| // the same type. |
| var ancestor = element.parentNode; |
| while (!!ancestor && ancestor.nodeType == Node.ELEMENT_NODE && |
| (!ancestor.hasAttribute('id') || |
| __gCrWeb.common.trim(ancestor.id) == '')) { |
| ancestor = ancestor.parentNode; |
| } |
| var query = element.tagName; |
| var ancestor_id = ''; |
| if (!ancestor || ancestor.nodeType != Node.ELEMENT_NODE) { |
| ancestor = document.body; |
| } |
| if (ancestor.hasAttribute('id')) { |
| ancestor_id = '#' + __gCrWeb.common.trim(ancestor.id); |
| } |
| var descendants = ancestor.querySelectorAll(element.tagName); |
| var i = 0; |
| for (i = 0; i < descendants.length; i++) { |
| if (descendants[i] === element) { |
| return __gCrWeb.form.kNamelessFieldIDPrefix + ancestor_id + '~' + |
| element.tagName + '~' + i; |
| } |
| } |
| |
| return ''; |
| }; |
| |
| /** |
| * Returns the field's |name| attribute if not space only; otherwise the |
| * field's |id| attribute. |
| * |
| * The name will be used as a hint to infer the autofill type of the field. |
| * |
| * It aims to provide the logic in |
| * WebString nameForAutofill() const; |
| * in chromium/src/third_party/WebKit/Source/WebKit/chromium/public/ |
| * WebFormControlElement.h |
| * |
| * @param {Element} element An element of which the name for Autofill will be |
| * returned. |
| * @return {string} the name for Autofill. |
| */ |
| __gCrWeb.form.getFieldName = function(element) { |
| if (!element) { |
| return ''; |
| } |
| var trimmedName = element.name; |
| if (trimmedName) { |
| trimmedName = __gCrWeb.common.trim(trimmedName); |
| if (trimmedName.length > 0) { |
| return trimmedName; |
| } |
| } |
| trimmedName = element.id; |
| if (trimmedName) { |
| return __gCrWeb.common.trim(trimmedName); |
| } |
| return ''; |
| }; |
| |
| /** |
| * Returns the form's |name| attribute if non-empty; otherwise the form's |id| |
| * attribute, or the index of the form (with prefix) in document.forms. |
| * |
| * It is partially based on the logic in |
| * const string16 GetFormIdentifier(const blink::WebFormElement& form) |
| * in chromium/src/components/autofill/renderer/form_autofill_util.h. |
| * |
| * @param {Element} form An element for which the identifier is returned. |
| * @return {string} a string that represents the element's identifier. |
| */ |
| __gCrWeb.form.getFormIdentifier = function(form) { |
| if (!form) return ''; |
| var name = form.getAttribute('name'); |
| if (name && name.length != 0 && |
| form.ownerDocument.forms.namedItem(name) === form) { |
| return name; |
| } |
| name = form.getAttribute('id'); |
| if (name && name.length != 0 && |
| form.ownerDocument.getElementById(name) === form) { |
| return name; |
| } |
| // A form name must be supplied, because the element will later need to be |
| // identified from the name. A last resort is to take the index number of |
| // the form in document.forms. ids are not supposed to begin with digits (by |
| // HTML 4 spec) so this is unlikely to match a true id. |
| for (var idx = 0; idx != document.forms.length; idx++) { |
| if (document.forms[idx] == form) { |
| return __gCrWeb.form.kNamelessFormIDPrefix + idx; |
| } |
| } |
| return ''; |
| }; |
| |
| /** |
| * Returns the form element from an ID obtained from getFormIdentifier. |
| * |
| * This works on a 'best effort' basis since DOM changes can always change the |
| * actual element that the ID refers to. |
| * |
| * @param {string} name An ID string obtained via getFormIdentifier. |
| * @return {HTMLFormElement} The original form element, if it can be determined. |
| */ |
| __gCrWeb.form.getFormElementFromIdentifier = function(name) { |
| // First attempt is from the name / id supplied. |
| var form = document.forms.namedItem(name); |
| if (form) { |
| if (form.nodeType !== Node.ELEMENT_NODE) return null; |
| return (form); |
| } |
| // Second attempt is from the prefixed index position of the form in |
| // document.forms. |
| if (name.indexOf(__gCrWeb.form.kNamelessFormIDPrefix) == 0) { |
| var nameAsInteger = |
| 0 | name.substring(__gCrWeb.form.kNamelessFormIDPrefix.length); |
| if (__gCrWeb.form.kNamelessFormIDPrefix + nameAsInteger == name && |
| nameAsInteger < document.forms.length) { |
| return document.forms[nameAsInteger]; |
| } |
| } |
| return null; |
| }; |
| |
| /** |
| * Schedule |mesg| to be sent on next runloop. |
| * If called multiple times on the same runloop, only the last message is really |
| * sent. |
| */ |
| var sendMessageOnNextLoop_ = function(mesg) { |
| if (!__gCrWeb.form.messageToSend) { |
| setTimeout(function() { |
| __gCrWeb.message.invokeOnHost(__gCrWeb.form.messageToSend); |
| __gCrWeb.form.messageToSend = null; |
| }, 0); |
| } |
| __gCrWeb.form.messageToSend = mesg; |
| } |
| |
| /** |
| * Focus, input, change, keyup and blur events for form elements (form and input |
| * elements) are messaged to the main application for broadcast to |
| * WebStateObservers. |
| * Events will be included in a message to be sent in a future runloop (without |
| * delay). If an event is already scheduled to be sent, it is replaced by |evt|. |
| * Notably, 'blur' event will not be transmitted to the main application if they |
| * are triggered by the focus of another element as the 'focus' event will |
| * replace it. |
| * Only the events targetting the active element (or the previously active in |
| * case of 'blur') are sent to the main application. |
| * This is done with a single event handler for each type being added to the |
| * main document element which checks the source element of the event; this |
| * is much easier to manage than adding handlers to individual elements. |
| * @private |
| */ |
| var formActivity_ = function(evt) { |
| var target = evt.target; |
| var value = target.value || ''; |
| var fieldType = target.type || ''; |
| if (evt.type != 'blur') { |
| __gCrWeb.form.lastFocusedElement = document.activeElement; |
| } |
| if (['change', 'input'].includes(evt.type)) { |
| __gCrWeb.form.wasEditedByUser.set(target, evt.isTrusted); |
| } |
| if (target != __gCrWeb.form.lastFocusedElement) return; |
| var msg = { |
| 'command': 'form.activity', |
| 'formName': __gCrWeb.form.getFormIdentifier(evt.target.form), |
| 'fieldName': __gCrWeb.form.getFieldName(target), |
| 'fieldIdentifier': __gCrWeb.form.getFieldIdentifier(target), |
| 'fieldType': fieldType, |
| 'type': evt.type, |
| 'value': value |
| }; |
| sendMessageOnNextLoop_(msg); |
| }; |
| |
| |
| /** |
| * Capture form submit actions. |
| */ |
| var submitHandler_ = function(evt) { |
| var action; |
| if (evt['defaultPrevented']) return; |
| action = evt.target.getAttribute('action'); |
| // Default action is to re-submit to same page. |
| if (!action) { |
| action = document.location.href; |
| } |
| __gCrWeb.message.invokeOnHost({ |
| 'command': 'document.submit', |
| 'formName': __gCrWeb.form.getFormIdentifier(evt.srcElement), |
| 'href': getFullyQualifiedUrl_(action) |
| }); |
| }; |
| |
| |
| /** @private |
| * @param {string} originalURL |
| * @return {string} |
| */ |
| var getFullyQualifiedUrl_ = function(originalURL) { |
| // A dummy anchor (never added to the document) is used to obtain the |
| // fully-qualified URL of |originalURL|. |
| var anchor = document.createElement('a'); |
| anchor.href = originalURL; |
| return anchor.href; |
| }; |
| |
| /** |
| * Schedules |msg| to be sent after |delay|. Until |msg| is sent, further calls |
| * to this function are ignored. |
| */ |
| var sendFormMutationMessageAfterDelay_ = function(msg, delay) { |
| if (__gCrWeb.form.formMutationMessageToSend) |
| return; |
| |
| __gCrWeb.form.formMutationMessageToSend = msg; |
| setTimeout(function() { |
| __gCrWeb.message.invokeOnHost(__gCrWeb.form.formMutationMessageToSend); |
| __gCrWeb.form.formMutationMessageToSend = null; |
| }, delay); |
| }; |
| |
| var attachListeners_ = function() { |
| /** |
| * Focus events performed on the 'capture' phase otherwise they are often |
| * not received. |
| * Input and change performed on the 'capture' phase as they are needed to |
| * detect if the current value is entered by the user. |
| */ |
| document.addEventListener('focus', formActivity_, true); |
| document.addEventListener('blur', formActivity_, true); |
| document.addEventListener('change', formActivity_, true); |
| document.addEventListener('input', formActivity_, true); |
| |
| /** |
| * Other events are watched at the bubbling phase as this seems adequate in |
| * practice and it is less obtrusive to page scripts than capture phase. |
| */ |
| document.addEventListener('keyup', formActivity_, false); |
| document.addEventListener('submit',submitHandler_, false); |
| }; |
| |
| // Attach the listeners immediatly to try to catch early actions of the user. |
| attachListeners_(); |
| |
| // Initial page loading can remove the listeners. Schedule a reattach after page |
| // build. |
| setTimeout(attachListeners_, 1000); |
| |
| /** |
| * Installs a MutationObserver to track form related changes. Waits |delay| |
| * milliseconds before sending a message to browser. A delay is used because |
| * form mutations are likely to come in batches. An undefined or zero value for |
| * |delay| would stop the MutationObserver, if any. |
| * @suppress {checkTypes} Required for for...of loop on mutations. |
| */ |
| __gCrWeb.form['trackFormMutations'] = function(delay) { |
| if (__gCrWeb.form.formMutationObserver) { |
| __gCrWeb.form.formMutationObserver.disconnect(); |
| __gCrWeb.form.formMutationObserver = null; |
| } |
| |
| if (!delay) |
| return; |
| |
| __gCrWeb.form.formMutationObserver = |
| new MutationObserver(function(mutations) { |
| for (var i = 0; i < mutations.length; i++) { |
| var mutation = mutations[i]; |
| // Only process mutations to the tree of nodes. |
| if (mutation.type != 'childList') |
| continue; |
| var addedElements = []; |
| for (var j = 0; j < mutation.addedNodes.length; j++) { |
| var node = mutation.addedNodes[j]; |
| // Ignore non-element nodes. |
| if (node.nodeType != Node.ELEMENT_NODE) |
| continue; |
| addedElements.push(node); |
| [].push.apply( |
| addedElements, [].slice.call(node.getElementsByTagName('*'))); |
| } |
| var form_changed = addedElements.find(function(element) { |
| return element.tagName.match(/(FORM|INPUT|SELECT|OPTION)/); |
| }); |
| if (form_changed) { |
| var msg = { |
| 'command': 'form.activity', |
| 'formName': '', |
| 'fieldName': '', |
| 'fieldIdentifier': '', |
| 'fieldType': '', |
| 'type': 'form_changed', |
| 'value': '' |
| }; |
| return sendFormMutationMessageAfterDelay_(msg, delay); |
| } |
| } |
| }); |
| __gCrWeb.form.formMutationObserver.observe( |
| document, {childList: true, subtree: true}); |
| }; |
| |
| /** |
| * Enables or disables the tracking of input event sources. |
| */ |
| __gCrWeb.form['toggleTrackingUserEditedFields'] = function(track) { |
| if (track) { |
| __gCrWeb.form.wasEditedByUser = |
| __gCrWeb.form.wasEditedByUser || new WeakMap(); |
| } else { |
| __gCrWeb.form.wasEditedByUser = null; |
| } |
| } |
| |
| /** |
| * Returns whether the last |input| or |change| event on |element| was triggered |
| * by a user action (was "trusted"). |
| */ |
| __gCrWeb.form['fieldWasEditedByUser'] = function(element) { |
| if (__gCrWeb.form.wasEditedByUser === null) { |
| // Input event sources is not tracked. |
| // Return true to preserve previous behavior. |
| return true; |
| } |
| if (!__gCrWeb.form.wasEditedByUser.has(element)) { |
| return false; |
| } |
| return __gCrWeb.form.wasEditedByUser.get(element); |
| } |
| |
| /** Flush the message queue. */ |
| if (__gCrWeb.message) { |
| __gCrWeb.message.invokeQueues(); |
| } |
| |
| }()); // End of anonymous object |