blob: 1eb272591c1d43a2825ac6a5643006184f77e56f [file] [log] [blame]
// 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');
goog.require('__crWeb.message');
/**
* 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~';
/**
* 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 form's |name| attribute if not space only; otherwise the
* form's |id| attribute.
*
* It is the name that should be used for the specified |element| when
* storing Autofill data. Various attributes are used to attempt to identify
* the element, beginning with 'name' and 'id' attributes. If both name and id
* are empty and the field is in a form, returns
* __gCrWeb.form.kNamelessFieldIDPrefix + index of the field in the form.
* Providing a uniquely reversible identifier for any element is a non-trivial
* problem; this solution attempts to satisfy the majority of cases.
*
* 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 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);
}
if (element.form) {
var elements = __gCrWeb.form.getFormControlElements(element.form);
for (var index = 0; index < elements.length; index++) {
if (elements[index] === element) {
return __gCrWeb.form.kNamelessFieldIDPrefix + index;
}
}
}
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) {
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;
};
/**
* Focus and input events for form elements are messaged to the main
* application for broadcast to WebStateObservers.
* 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 srcElement = evt.srcElement;
var value = srcElement.value || '';
var fieldType = srcElement.type || '';
var msg = {
'command': 'form.activity',
'formName': __gCrWeb.form.getFormIdentifier(evt.srcElement.form),
'fieldName': __gCrWeb.form.getFieldIdentifier(srcElement),
'fieldType': fieldType,
'type': evt.type,
'value': value
};
__gCrWeb.message.invokeOnHost(msg);
};
/**
* Focus events performed on the 'capture' phase otherwise they are often
* not received.
*/
document.addEventListener('focus', formActivity_, true);
document.addEventListener('blur', formActivity_, true);
document.addEventListener('change', formActivity_, true);
/**
* Text input is watched at the bubbling phase as this seems adequate in
* practice and it is less obtrusive to page scripts than capture phase.
*/
document.addEventListener('input', formActivity_, false);
document.addEventListener('keyup', formActivity_, false);
/**
* Capture form submit actions.
*/
document.addEventListener('submit', 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)
});
}, false);
/** @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;
};
/** Flush the message queue. */
if (__gCrWeb.message) {
__gCrWeb.message.invokeQueues();
}
}()); // End of anonymous object