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 |