-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtracker.js
More file actions
495 lines (414 loc) · 19.1 KB
/
Copy pathtracker.js
File metadata and controls
495 lines (414 loc) · 19.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
steal('can/control', 'can/observe', 'can/util', function(Control, Observe, Util) {
var _getAttr = function(name, attr) {
return attr ? attr : name;
},
// Checks if an object is a can.Observe
_isObserve = function(o) {
return o instanceof Observe;
},
// Checks if an object is an HTMLElement
_isElement = function(o) {
return ( typeof HTMLElement === "object" ? o instanceof HTMLElement : //DOM2
o && typeof o === "object" && o.nodeType === 1 && typeof o.nodeName==="string"
);
},
// Checks if an object is an HTMLElement
_isHTMLElement = function(e) {
return e instanceof HTMLElement;
},
// Checks if an object is one of the supported form elements
_isFormElement = function(o) {
var formElements = ["input", "textarea", "select"];
if(_isHTMLElement(o.get(0))) {
return (can.inArray(o.prop("tagName").toLowerCase(), formElements) !== -1);
}
else return false;
},
// Determines the mode of the plugin
_getMode = function(o, attr) {
if(_isObserve(o)) {
if(attr) {
return "observe";
}
else {
steal.dev.warn("Attribute to link to not specified. Either set 'name' attribute on this.element or use 'attr' option");
return "simple";
}
}
else if(_isFormElement(can.$(o))) {
return "form";
}
else if(o instanceof Object) {
if(attr) {
steal.dev.warn("LinkedObj is a plain object. Only one-way binding will work. Consider converting to can.Observe");
return "object";
}
else {
steal.dev.warn("Attribute to link to not specified. Either set 'name' attribute on this.element or use 'attr' option");
return "simple";
}
}
else {
steal.dev.warn("Tracker in simple mode");
return "simple";
}
};
/**
* Tracker is a plugin used to link a form element with a can.Observe
* @type {[type]}
*/
var Tracker = window.Tracker = Control({
defaults: {
attr: undefined,
_attrNameToWatch: "", // This is use internally for templated event bind to the attribute on the linkedObj we are tracking
initFromLinkedObj: true, // On init, set the this.element's value to the value it's linked to in the linkedObj
dirtyClass: "dirty", // Class added to this.element when it's value has changed from it's original value
changeCallback: can.noop, // Function to be executed when the form element's value is changed
multiSelectAsString: false
}
}, {
/**
* Initializes the Tracker plugin
*
* @param {HTMLElement} [element] the element this instance operates on
* @param {Object} [options] option values for the control
*/
'init': function(element, options) {
var self = this,
el = this.element,
tagName = el.prop("tagName"),
inputType,
linkedObj,
valueHolder;
if(!tagName) {
steal.dev.warn("Tracker is for form elements");
this.destroy();
return;
}
tagName = tagName.toLowerCase();
// This plugin is only for inputs, textareas and selects
// TODO: What about datalist, keygen and output?
if(!(tagName === "input" || tagName === "textarea" || tagName === "select")) {
steal.dev.warn("Tracker is for form elements");
this.destroy();
return;
}
// Shortcut to linkedObj
linkedObj = this.options.linkedObj;
// Store the name associated with this.element
this._elementName = el.prop("name");
// Determine the attribute/property name to link to in the linkedObj
// This can be specified as an option, otherwise the name property of this.element is used
this._attr = _getAttr(this._elementName, this.options.attr);
// TODO: Warn if this._attr is empty???
// Determine the mode for this instance. Valid modes include observe, form and simple
// The mode is based on the linkedObj
// observe means the linkedObj is a can.Observe (or can.Model)
// form means the linkedObj is a form element
// object means the linkedObj is a plain object
// simple means there is no linkedObj
this._mode = _getMode(linkedObj, this._attr);
// Sets up the appropriate internal values and then rebinds the control's event handlers
this._setupBindings();
// Generate a function that will be used for updating the linkedObj when our target changes
// The definition of this function will differ depending on the type/mode of linkedObj
this._updateLinkedObjFn = this._getUpdateLinkedObjFn();
// Generate a function that will be used to update our target (this.element) when the linkedObj changes
this._updateTargetFn = this._getUpdateTargetFn();
// What type of input is this.element?
inputType = el.prop("type").toLowerCase();
if(inputType === "checkbox") { // Checkboxes
// If initFromLinkedObj is true, set the target's value based on the mode
if(this._mode === "observe" && this.options.initFromLinkedObj) {
el.prop("checked", linkedObj.attr(this._attr));
// Warn when the value we are setting a checkbox with is not a boolean
if(typeof linkedObj.attr(this._attr) !== "boolean") {
steal.dev.warn("Linked attribure is not a boolean. You may experience strange results");
}
}
else if(this._mode === "object" && this.options.initFromLinkedObj) {
el.prop("checked", linkedObj[this._attr]);
}
// Getter/setter for target (this.element)
this._val = function(val) {
if(val === undefined) {
return el.prop("checked");
}
else {
// Should I only accept true? How about any truthy values?
// If not true or false, maybe don't change the value?
// For now, this is how prop works
el.prop("checked", val).trigger("change");
//el.prop("checked", val===true || val===1);
return this;
}
};
}
else if(inputType === "radio") { // Radio buttons
if(this._mode === "observe" && this.options.initFromLinkedObj) {
el.filter("[value='" + linkedObj.attr(this._attr) + "']").prop("checked", true);
}
else if(this._mode === "object" && this.options.initFromLinkedObj) {
el.filter("[value='" + linkedObj[this._attr] + "']").prop("checked", true);
}
// Getter/setter
this._val = function(val) {
if(val === undefined) {
// Get the selected radio button
return el.filter(":checked").val();
}
else {
// Select the radio button
el.filter("[value='" + val + "']").prop("checked", true).trigger("change");
return this;
}
};
}
else if(tagName === "select" && el.prop('multiple')) { // Select (multiple) dropdowns
if(this._mode === "observe" && this.options.initFromLinkedObj) {
if(self.options.multiSelectAsString) {
el.val(linkedObj.attr(this._attr).toString().replace(/\s*,\s*/gm, ',').split(","));
}
else {
valueHolder = linkedObj.attr(this._attr);
// Check if the value is an array
if(!can.isArray(valueHolder)) {
steal.dev.warn("Multi-select expect array input");
}
el.val(linkedObj.attr(this._attr));
}
}
else if(this._mode === "object" && this.options.initFromLinkedObj) {
el.val(linkedObj[this._attr]);
}
// Getter/setter
this._val = function(val) {
if(val === undefined) {
// Get the selected option(s)
return self.options.multiSelectAsString ? el.val().toString() : el.val();
}
else {
// FIXME: Make sure options actually exist in select
el.val(self.options.multiSelectAsString ? val.toString().replace(/\s*,\s*/gm, ',').split(",") : val).trigger('change');
return this;
}
};
}
else if(inputType === "text" ||
inputType === "password" ||
tagName === "textarea" ||
tagName === "select") { // Textboxes, passwords, selects and textareas
if(this._mode === "observe" && this.options.initFromLinkedObj) {
el.val(linkedObj.attr(this._attr));
}
else if(this._mode === "object" && this.options.initFromLinkedObj) {
el.val(linkedObj[this._attr]);
}
// Getter/setter
this._val = function(val) {
if(val === undefined) {
// Cannot cache this value because the input can be changed programmatically by some other code using
// $input.val() and doing so does not trigger a change event so we wouldn't know about the change. Doh!
return el.val();
}
else {
// If it's a select dropdown, make sure the new value has an option
if(tagName === "select") {
if(el.find('option[value="' + val + '"]').length) {
el.val(val).trigger('change');
}
else {
steal.dev.warn("No such option exists in select: " + val);
}
}
else el.val(val).trigger('change');
return this;
}
};
}
// Keep track of the original/initial value so we reset or know if it's been changed
this._original = this._val.call(this);
},
/**
* Sets up internal values and rebinds the control's event handlers
* We are using the "_attrNameToWatch" option for templated event binding to bind to the
* appropriate attribute of the linkedObj
*
* When the linkedObj is a can.Observe, we want to bind to a specific attribute of the observe
* When the linkedObj is a form element, we want to bind to the "change" event of that form element
* When the linkedObj is a plain object, we convert it to a can.Observe for internal use
*/
'_setupBindings': function() {
var linkedObj = this.options.linkedObj;
if(this._mode === "observe") {
// So we can listen for changes to this specific attribute of the observe
this.options._attrNameToWatch = this._attr;
}
else if(this._mode === "form") {
// Wrap linkedObj in a jQuery object (if it exists)
if(linkedObj) {
linkedObj = can.$(linkedObj);
}
// Make sure we are not trying to link to ourself
if(this.element.get(0) === linkedObj.get(0)) {
linkedObj = undefined;
// Set the _mode to simple
this._mode = "simple";
steal.dev.warn("Cannot track back to yourself (form mode)");
}
else {
// For a form element we want to listen for "change" events
this.options._attrNameToWatch = "change";
}
}
else if(this._mode === "object") {
// Save the object in case we need/want it later
this.saveObject = linkedObj;
// Convert the object into a can.Observe so we can bind to changes
this.options.linkedObj = new can.Observe(linkedObj);
// Listen for changes to this specific attribute of the observe
this.options._attrNameToWatch = this._attr;
}
// Rebind event handlers
this.on();
},
/**
* Generate the function that will be used to update the linkedObj when the target is changed
* @return {Function}
*/
'_getUpdateLinkedObjFn': function() {
var self = this;
return (function() {
if(self._mode === "observe") { // LinkedObj is a can.Observe
return function(newVal) {
// Update the can.Observe
self.options.linkedObj.attr(self._attr, newVal);
};
}
else if(self._mode === "form") { // LinkedObj is a form element
return function(newVal) {
// FIXME: Need to update this because different form elements are updated differently
self.options.linkedObj.val(newVal);
};
}
else if(self._mode === "object") { // LinkedObj is a plain object
return function(newVal) {
self.saveObject[self._attr] = newVal;
self.options.linkedObj.attr(self._attr, newVal);
};
}
else {
return can.noop;
}
})();
},
/**
* Generate the function that will update the target (this.element) when the linkedObj changes
* @return {Function}
*/
'_getUpdateTargetFn': function() {
var self = this;
return (function() {
if(self._mode === "observe") { // linkedObj is a can.Observe
return function(o, ev, newVal, oldVal) {
//if(newVal !== self._val.call(self)) {
if(!Util.Object.same(newVal, self._val.call(self))) {
self._val.call(self, newVal);
// FIXME: Need to keep an eye on this change as this is going to result in an attempted update to the can.Observe
//self.element.trigger('change');
}
self.element[self.changed() ? "addClass" : "removeClass"](self.options.dirtyClass);
};
}
else if(self._mode === "form") { // linkedObj is a form element
return function(el, ev) {
var newVal = $(el).val();
if(newVal !== self._val.call(self)) {
self._val.call(self, newVal);
}
self.element[self.changed() ? "addClass" : "removeClass"](self.options.dirtyClass);
};
}
else if(self._mode === "object") { // linkedObj is a plain object
return function(o, ev, newVal, oldVal) {
if(newVal !== self._val.call(self)) {
self._val.call(self, newVal);
}
self.element[self.changed() ? "addClass" : "removeClass"](self.options.dirtyClass);
};
}
else {
return can.noop;
}
})();
},
/**
* Get or set the value of the target
* Getting will get the value from this.element
* Setting will set both this.element and linkedObj
*
* @param {Object} [val] if provided, sets the value
* @return {Object} the observable or the attribute property
*/
'val': function(val) {
return this._val.call(this, val);
},
'original': function() {
return this._original;
},
/**
* Has this.element's value changed since tracker has been employed
* @return {Boolean}
*/
'changed': function() {
return !(Util.Object.same(this._original, this._val.call(this)));
},
/**
* Resets this.element to its original value
* This is reset linkedObj too
* @return {Tracker}
*/
'reset': function() {
this._val.call(this, this._original);
return this;
},
/**
* Get the linked object (linkedObj)
* @return {Object}
*/
'getLinkedObj': function() {
return this.options.linkedObj;
},
/**
* Event handler for when this.element changes
*
* @param {HTMLElement} [el]
* @param {[jQuery.Event} [ev]
*/
'change': function(el, ev) {
var newVal = this._val.call(this);
steal.dev.log('The target has been changed: ' + newVal);
// Add or remove the dirty class depending on if the element is changed
this.element[this.changed() ? "addClass" : "removeClass"](this.options.dirtyClass);
// Execute the change callback
this.options.changeCallback(newVal);
// Execute the function that will update the linked object
this._updateLinkedObjFn.apply(this, [newVal]);
},
/**
* Event handler for when linkedObj changes
* "_attrNameToWatch" is set in _setBindings
*/
'{linkedObj} {_attrNameToWatch}': function() {
// Execute the function that will update target object (this.element)
this._updateTargetFn.apply(this, arguments);
},
'destroy': function() {
// Remove the dirty class
this.element.removeClass(this.options.dirtyClass);
// Call the super's destroy
can.Control.prototype.destroy.call(this);
}
});
return Tracker;
});