| // 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); |