| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | |
| 2 // This code may only be used under the BSD style license found at http://polyme
r.github.io/LICENSE.txt | |
| 3 // The complete set of authors may be found at http://polymer.github.io/AUTHORS.
txt | |
| 4 // The complete set of contributors may be found at http://polymer.github.io/CON
TRIBUTORS.txt | |
| 5 // Code distributed by Google as part of the polymer project is also | |
| 6 // subject to an additional IP rights grant found at http://polymer.github.io/PA
TENTS.txt | |
| 7 | |
| 8 (function(global) { | |
| 9 'use strict'; | |
| 10 | |
| 11 var filter = Array.prototype.filter.call.bind(Array.prototype.filter); | |
| 12 | |
| 13 function getTreeScope(node) { | |
| 14 while (node.parentNode) { | |
| 15 node = node.parentNode; | |
| 16 } | |
| 17 | |
| 18 return typeof node.getElementById === 'function' ? node : null; | |
| 19 } | |
| 20 | |
| 21 Node.prototype.bind = function(name, observable) { | |
| 22 console.error('Unhandled binding to Node: ', this, name, observable); | |
| 23 }; | |
| 24 | |
| 25 Node.prototype.bindFinished = function() {}; | |
| 26 | |
| 27 function updateBindings(node, name, binding) { | |
| 28 var bindings = node.bindings_; | |
| 29 if (!bindings) | |
| 30 bindings = node.bindings_ = {}; | |
| 31 | |
| 32 if (bindings[name]) | |
| 33 binding[name].close(); | |
| 34 | |
| 35 return bindings[name] = binding; | |
| 36 } | |
| 37 | |
| 38 function returnBinding(node, name, binding) { | |
| 39 return binding; | |
| 40 } | |
| 41 | |
| 42 function sanitizeValue(value) { | |
| 43 return value == null ? '' : value; | |
| 44 } | |
| 45 | |
| 46 function updateText(node, value) { | |
| 47 node.data = sanitizeValue(value); | |
| 48 } | |
| 49 | |
| 50 function textBinding(node) { | |
| 51 return function(value) { | |
| 52 return updateText(node, value); | |
| 53 }; | |
| 54 } | |
| 55 | |
| 56 var maybeUpdateBindings = returnBinding; | |
| 57 | |
| 58 Object.defineProperty(Platform, 'enableBindingsReflection', { | |
| 59 get: function() { | |
| 60 return maybeUpdateBindings === updateBindings; | |
| 61 }, | |
| 62 set: function(enable) { | |
| 63 maybeUpdateBindings = enable ? updateBindings : returnBinding; | |
| 64 return enable; | |
| 65 }, | |
| 66 configurable: true | |
| 67 }); | |
| 68 | |
| 69 Text.prototype.bind = function(name, value, oneTime) { | |
| 70 if (name !== 'textContent') | |
| 71 return Node.prototype.bind.call(this, name, value, oneTime); | |
| 72 | |
| 73 if (oneTime) | |
| 74 return updateText(this, value); | |
| 75 | |
| 76 var observable = value; | |
| 77 updateText(this, observable.open(textBinding(this))); | |
| 78 return maybeUpdateBindings(this, name, observable); | |
| 79 } | |
| 80 | |
| 81 function updateAttribute(el, name, conditional, value) { | |
| 82 if (conditional) { | |
| 83 if (value) | |
| 84 el.setAttribute(name, ''); | |
| 85 else | |
| 86 el.removeAttribute(name); | |
| 87 return; | |
| 88 } | |
| 89 | |
| 90 el.setAttribute(name, sanitizeValue(value)); | |
| 91 } | |
| 92 | |
| 93 function attributeBinding(el, name, conditional) { | |
| 94 return function(value) { | |
| 95 updateAttribute(el, name, conditional, value); | |
| 96 }; | |
| 97 } | |
| 98 | |
| 99 Element.prototype.bind = function(name, value, oneTime) { | |
| 100 var conditional = name[name.length - 1] == '?'; | |
| 101 if (conditional) { | |
| 102 this.removeAttribute(name); | |
| 103 name = name.slice(0, -1); | |
| 104 } | |
| 105 | |
| 106 if (oneTime) | |
| 107 return updateAttribute(this, name, conditional, value); | |
| 108 | |
| 109 | |
| 110 var observable = value; | |
| 111 updateAttribute(this, name, conditional, | |
| 112 observable.open(attributeBinding(this, name, conditional))); | |
| 113 | |
| 114 return maybeUpdateBindings(this, name, observable); | |
| 115 }; | |
| 116 | |
| 117 var checkboxEventType; | |
| 118 (function() { | |
| 119 // Attempt to feature-detect which event (change or click) is fired first | |
| 120 // for checkboxes. | |
| 121 var div = document.createElement('div'); | |
| 122 var checkbox = div.appendChild(document.createElement('input')); | |
| 123 checkbox.setAttribute('type', 'checkbox'); | |
| 124 var first; | |
| 125 var count = 0; | |
| 126 checkbox.addEventListener('click', function(e) { | |
| 127 count++; | |
| 128 first = first || 'click'; | |
| 129 }); | |
| 130 checkbox.addEventListener('change', function() { | |
| 131 count++; | |
| 132 first = first || 'change'; | |
| 133 }); | |
| 134 | |
| 135 var event = document.createEvent('MouseEvent'); | |
| 136 event.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, | |
| 137 false, false, false, 0, null); | |
| 138 checkbox.dispatchEvent(event); | |
| 139 // WebKit/Blink don't fire the change event if the element is outside the | |
| 140 // document, so assume 'change' for that case. | |
| 141 checkboxEventType = count == 1 ? 'change' : first; | |
| 142 })(); | |
| 143 | |
| 144 function getEventForInputType(element) { | |
| 145 switch (element.type) { | |
| 146 case 'checkbox': | |
| 147 return checkboxEventType; | |
| 148 case 'radio': | |
| 149 case 'select-multiple': | |
| 150 case 'select-one': | |
| 151 return 'change'; | |
| 152 case 'range': | |
| 153 if (/Trident|MSIE/.test(navigator.userAgent)) | |
| 154 return 'change'; | |
| 155 default: | |
| 156 return 'input'; | |
| 157 } | |
| 158 } | |
| 159 | |
| 160 function updateInput(input, property, value, santizeFn) { | |
| 161 input[property] = (santizeFn || sanitizeValue)(value); | |
| 162 } | |
| 163 | |
| 164 function inputBinding(input, property, santizeFn) { | |
| 165 return function(value) { | |
| 166 return updateInput(input, property, value, santizeFn); | |
| 167 } | |
| 168 } | |
| 169 | |
| 170 function noop() {} | |
| 171 | |
| 172 function bindInputEvent(input, property, observable, postEventFn) { | |
| 173 var eventType = getEventForInputType(input); | |
| 174 | |
| 175 function eventHandler() { | |
| 176 observable.setValue(input[property]); | |
| 177 observable.discardChanges(); | |
| 178 (postEventFn || noop)(input); | |
| 179 Platform.performMicrotaskCheckpoint(); | |
| 180 } | |
| 181 input.addEventListener(eventType, eventHandler); | |
| 182 | |
| 183 return { | |
| 184 close: function() { | |
| 185 input.removeEventListener(eventType, eventHandler); | |
| 186 observable.close(); | |
| 187 }, | |
| 188 | |
| 189 observable_: observable | |
| 190 } | |
| 191 } | |
| 192 | |
| 193 function booleanSanitize(value) { | |
| 194 return Boolean(value); | |
| 195 } | |
| 196 | |
| 197 // |element| is assumed to be an HTMLInputElement with |type| == 'radio'. | |
| 198 // Returns an array containing all radio buttons other than |element| that | |
| 199 // have the same |name|, either in the form that |element| belongs to or, | |
| 200 // if no form, in the document tree to which |element| belongs. | |
| 201 // | |
| 202 // This implementation is based upon the HTML spec definition of a | |
| 203 // "radio button group": | |
| 204 // http://www.whatwg.org/specs/web-apps/current-work/multipage/number-state.
html#radio-button-group | |
| 205 // | |
| 206 function getAssociatedRadioButtons(element) { | |
| 207 if (element.form) { | |
| 208 return filter(element.form.elements, function(el) { | |
| 209 return el != element && | |
| 210 el.tagName == 'INPUT' && | |
| 211 el.type == 'radio' && | |
| 212 el.name == element.name; | |
| 213 }); | |
| 214 } else { | |
| 215 var treeScope = getTreeScope(element); | |
| 216 if (!treeScope) | |
| 217 return []; | |
| 218 var radios = treeScope.querySelectorAll( | |
| 219 'input[type="radio"][name="' + element.name + '"]'); | |
| 220 return filter(radios, function(el) { | |
| 221 return el != element && !el.form; | |
| 222 }); | |
| 223 } | |
| 224 } | |
| 225 | |
| 226 function checkedPostEvent(input) { | |
| 227 // Only the radio button that is getting checked gets an event. We | |
| 228 // therefore find all the associated radio buttons and update their | |
| 229 // check binding manually. | |
| 230 if (input.tagName === 'INPUT' && | |
| 231 input.type === 'radio') { | |
| 232 getAssociatedRadioButtons(input).forEach(function(radio) { | |
| 233 var checkedBinding = radio.bindings_.checked; | |
| 234 if (checkedBinding) { | |
| 235 // Set the value directly to avoid an infinite call stack. | |
| 236 checkedBinding.observable_.setValue(false); | |
| 237 } | |
| 238 }); | |
| 239 } | |
| 240 } | |
| 241 | |
| 242 HTMLInputElement.prototype.bind = function(name, value, oneTime) { | |
| 243 if (name !== 'value' && name !== 'checked') | |
| 244 return HTMLElement.prototype.bind.call(this, name, value, oneTime); | |
| 245 | |
| 246 this.removeAttribute(name); | |
| 247 var sanitizeFn = name == 'checked' ? booleanSanitize : sanitizeValue; | |
| 248 var postEventFn = name == 'checked' ? checkedPostEvent : noop; | |
| 249 | |
| 250 if (oneTime) | |
| 251 return updateInput(this, name, value, sanitizeFn); | |
| 252 | |
| 253 | |
| 254 var observable = value; | |
| 255 var binding = bindInputEvent(this, name, observable, postEventFn); | |
| 256 updateInput(this, name, | |
| 257 observable.open(inputBinding(this, name, sanitizeFn)), | |
| 258 sanitizeFn); | |
| 259 | |
| 260 // Checkboxes may need to update bindings of other checkboxes. | |
| 261 return updateBindings(this, name, binding); | |
| 262 } | |
| 263 | |
| 264 HTMLTextAreaElement.prototype.bind = function(name, value, oneTime) { | |
| 265 if (name !== 'value') | |
| 266 return HTMLElement.prototype.bind.call(this, name, value, oneTime); | |
| 267 | |
| 268 this.removeAttribute('value'); | |
| 269 | |
| 270 if (oneTime) | |
| 271 return updateInput(this, 'value', value); | |
| 272 | |
| 273 var observable = value; | |
| 274 var binding = bindInputEvent(this, 'value', observable); | |
| 275 updateInput(this, 'value', | |
| 276 observable.open(inputBinding(this, 'value', sanitizeValue))); | |
| 277 return maybeUpdateBindings(this, name, binding); | |
| 278 } | |
| 279 | |
| 280 function updateOption(option, value) { | |
| 281 var parentNode = option.parentNode;; | |
| 282 var select; | |
| 283 var selectBinding; | |
| 284 var oldValue; | |
| 285 if (parentNode instanceof HTMLSelectElement && | |
| 286 parentNode.bindings_ && | |
| 287 parentNode.bindings_.value) { | |
| 288 select = parentNode; | |
| 289 selectBinding = select.bindings_.value; | |
| 290 oldValue = select.value; | |
| 291 } | |
| 292 | |
| 293 option.value = sanitizeValue(value); | |
| 294 | |
| 295 if (select && select.value != oldValue) { | |
| 296 selectBinding.observable_.setValue(select.value); | |
| 297 selectBinding.observable_.discardChanges(); | |
| 298 Platform.performMicrotaskCheckpoint(); | |
| 299 } | |
| 300 } | |
| 301 | |
| 302 function optionBinding(option) { | |
| 303 return function(value) { | |
| 304 updateOption(option, value); | |
| 305 } | |
| 306 } | |
| 307 | |
| 308 HTMLOptionElement.prototype.bind = function(name, value, oneTime) { | |
| 309 if (name !== 'value') | |
| 310 return HTMLElement.prototype.bind.call(this, name, value, oneTime); | |
| 311 | |
| 312 this.removeAttribute('value'); | |
| 313 | |
| 314 if (oneTime) | |
| 315 return updateOption(this, value); | |
| 316 | |
| 317 var observable = value; | |
| 318 var binding = bindInputEvent(this, 'value', observable); | |
| 319 updateOption(this, observable.open(optionBinding(this))); | |
| 320 return maybeUpdateBindings(this, name, binding); | |
| 321 } | |
| 322 | |
| 323 HTMLSelectElement.prototype.bind = function(name, value, oneTime) { | |
| 324 if (name === 'selectedindex') | |
| 325 name = 'selectedIndex'; | |
| 326 | |
| 327 if (name !== 'selectedIndex' && name !== 'value') | |
| 328 return HTMLElement.prototype.bind.call(this, name, value, oneTime); | |
| 329 | |
| 330 this.removeAttribute(name); | |
| 331 | |
| 332 if (oneTime) | |
| 333 return updateInput(this, name, value); | |
| 334 | |
| 335 var observable = value; | |
| 336 var binding = bindInputEvent(this, name, observable); | |
| 337 updateInput(this, name, | |
| 338 observable.open(inputBinding(this, name))); | |
| 339 | |
| 340 // Option update events may need to access select bindings. | |
| 341 return updateBindings(this, name, binding); | |
| 342 } | |
| 343 })(this); | |
| OLD | NEW |