// Copyright (c) 2014 The Polymer Project Authors. All rights reserved. // This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt // The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt // The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt // Code distributed by Google as part of the polymer project is also // subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt (function(global) { 'use strict'; var filter = Array.prototype.filter.call.bind(Array.prototype.filter); function getTreeScope(node) { while (node.parentNode) { node = node.parentNode; } return typeof node.getElementById === 'function' ? node : null; } Node.prototype.bind = function(name, observable) { console.error('Unhandled binding to Node: ', this, name, observable); }; Node.prototype.bindFinished = function() {}; function updateBindings(node, name, binding) { var bindings = node.bindings_; if (!bindings) bindings = node.bindings_ = {}; if (bindings[name]) binding[name].close(); return bindings[name] = binding; } function returnBinding(node, name, binding) { return binding; } function sanitizeValue(value) { return value == null ? '' : value; } function updateText(node, value) { node.data = sanitizeValue(value); } function textBinding(node) { return function(value) { return updateText(node, value); }; } var maybeUpdateBindings = returnBinding; Object.defineProperty(Platform, 'enableBindingsReflection', { get: function() { return maybeUpdateBindings === updateBindings; }, set: function(enable) { maybeUpdateBindings = enable ? updateBindings : returnBinding; return enable; }, configurable: true }); Text.prototype.bind = function(name, value, oneTime) { if (name !== 'textContent') return Node.prototype.bind.call(this, name, value, oneTime); if (oneTime) return updateText(this, value); var observable = value; updateText(this, observable.open(textBinding(this))); return maybeUpdateBindings(this, name, observable); } function updateAttribute(el, name, conditional, value) { if (conditional) { if (value) el.setAttribute(name, ''); else el.removeAttribute(name); return; } el.setAttribute(name, sanitizeValue(value)); } function attributeBinding(el, name, conditional) { return function(value) { updateAttribute(el, name, conditional, value); }; } Element.prototype.bind = function(name, value, oneTime) { var conditional = name[name.length - 1] == '?'; if (conditional) { this.removeAttribute(name); name = name.slice(0, -1); } if (oneTime) return updateAttribute(this, name, conditional, value); var observable = value; updateAttribute(this, name, conditional, observable.open(attributeBinding(this, name, conditional))); return maybeUpdateBindings(this, name, observable); }; var checkboxEventType; (function() { // Attempt to feature-detect which event (change or click) is fired first // for checkboxes. var div = document.createElement('div'); var checkbox = div.appendChild(document.createElement('input')); checkbox.setAttribute('type', 'checkbox'); var first; var count = 0; checkbox.addEventListener('click', function(e) { count++; first = first || 'click'; }); checkbox.addEventListener('change', function() { count++; first = first || 'change'; }); var event = document.createEvent('MouseEvent'); event.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); checkbox.dispatchEvent(event); // WebKit/Blink don't fire the change event if the element is outside the // document, so assume 'change' for that case. checkboxEventType = count == 1 ? 'change' : first; })(); function getEventForInputType(element) { switch (element.type) { case 'checkbox': return checkboxEventType; case 'radio': case 'select-multiple': case 'select-one': return 'change'; case 'range': if (/Trident|MSIE/.test(navigator.userAgent)) return 'change'; default: return 'input'; } } function updateInput(input, property, value, santizeFn) { input[property] = (santizeFn || sanitizeValue)(value); } function inputBinding(input, property, santizeFn) { return function(value) { return updateInput(input, property, value, santizeFn); } } function noop() {} function bindInputEvent(input, property, observable, postEventFn) { var eventType = getEventForInputType(input); function eventHandler() { observable.setValue(input[property]); observable.discardChanges(); (postEventFn || noop)(input); Platform.performMicrotaskCheckpoint(); } input.addEventListener(eventType, eventHandler); return { close: function() { input.removeEventListener(eventType, eventHandler); observable.close(); }, observable_: observable } } function booleanSanitize(value) { return Boolean(value); } // |element| is assumed to be an HTMLInputElement with |type| == 'radio'. // Returns an array containing all radio buttons other than |element| that // have the same |name|, either in the form that |element| belongs to or, // if no form, in the document tree to which |element| belongs. // // This implementation is based upon the HTML spec definition of a // "radio button group": // http://www.whatwg.org/specs/web-apps/current-work/multipage/number-state.html#radio-button-group // function getAssociatedRadioButtons(element) { if (element.form) { return filter(element.form.elements, function(el) { return el != element && el.tagName == 'INPUT' && el.type == 'radio' && el.name == element.name; }); } else { var treeScope = getTreeScope(element); if (!treeScope) return []; var radios = treeScope.querySelectorAll( 'input[type="radio"][name="' + element.name + '"]'); return filter(radios, function(el) { return el != element && !el.form; }); } } function checkedPostEvent(input) { // Only the radio button that is getting checked gets an event. We // therefore find all the associated radio buttons and update their // check binding manually. if (input.tagName === 'INPUT' && input.type === 'radio') { getAssociatedRadioButtons(input).forEach(function(radio) { var checkedBinding = radio.bindings_.checked; if (checkedBinding) { // Set the value directly to avoid an infinite call stack. checkedBinding.observable_.setValue(false); } }); } } HTMLInputElement.prototype.bind = function(name, value, oneTime) { if (name !== 'value' && name !== 'checked') return HTMLElement.prototype.bind.call(this, name, value, oneTime); this.removeAttribute(name); var sanitizeFn = name == 'checked' ? booleanSanitize : sanitizeValue; var postEventFn = name == 'checked' ? checkedPostEvent : noop; if (oneTime) return updateInput(this, name, value, sanitizeFn); var observable = value; var binding = bindInputEvent(this, name, observable, postEventFn); updateInput(this, name, observable.open(inputBinding(this, name, sanitizeFn)), sanitizeFn); // Checkboxes may need to update bindings of other checkboxes. return updateBindings(this, name, binding); } HTMLTextAreaElement.prototype.bind = function(name, value, oneTime) { if (name !== 'value') return HTMLElement.prototype.bind.call(this, name, value, oneTime); this.removeAttribute('value'); if (oneTime) return updateInput(this, 'value', value); var observable = value; var binding = bindInputEvent(this, 'value', observable); updateInput(this, 'value', observable.open(inputBinding(this, 'value', sanitizeValue))); return maybeUpdateBindings(this, name, binding); } function updateOption(option, value) { var parentNode = option.parentNode;; var select; var selectBinding; var oldValue; if (parentNode instanceof HTMLSelectElement && parentNode.bindings_ && parentNode.bindings_.value) { select = parentNode; selectBinding = select.bindings_.value; oldValue = select.value; } option.value = sanitizeValue(value); if (select && select.value != oldValue) { selectBinding.observable_.setValue(select.value); selectBinding.observable_.discardChanges(); Platform.performMicrotaskCheckpoint(); } } function optionBinding(option) { return function(value) { updateOption(option, value); } } HTMLOptionElement.prototype.bind = function(name, value, oneTime) { if (name !== 'value') return HTMLElement.prototype.bind.call(this, name, value, oneTime); this.removeAttribute('value'); if (oneTime) return updateOption(this, value); var observable = value; var binding = bindInputEvent(this, 'value', observable); updateOption(this, observable.open(optionBinding(this))); return maybeUpdateBindings(this, name, binding); } HTMLSelectElement.prototype.bind = function(name, value, oneTime) { if (name === 'selectedindex') name = 'selectedIndex'; if (name !== 'selectedIndex' && name !== 'value') return HTMLElement.prototype.bind.call(this, name, value, oneTime); this.removeAttribute(name); if (oneTime) return updateInput(this, name, value); var observable = value; var binding = bindInputEvent(this, name, observable); updateInput(this, name, observable.open(inputBinding(this, name))); // Option update events may need to access select bindings. return updateBindings(this, name, binding); } })(this);