1
OC.Contacts = OC.Contacts || {};
4
(function(window, $, OC) {
7
* An item which binds the appropriate html and event handlers
8
* @param parent the parent ContactList
9
* @param id The integer contact id.
10
* @param metadata An metadata object containing and 'owner' string variable, a 'backend' string variable and an integer 'permissions' variable.
11
* @param data the data used to populate the contact
12
* @param listtemplate the jquery object used to render the contact list item
13
* @param fulltemplate the jquery object used to render the entire contact
14
* @param detailtemplates A map of jquery objects used to render the contact parts e.g. EMAIL, TEL etc.
16
var Contact = function(parent, id, metadata, data, listtemplate, dragtemplate, fulltemplate, detailtemplates) {
17
//console.log('contact:', id, metadata, data); //parent, id, data, listtemplate, fulltemplate);
19
this.storage = parent.storage,
21
this.metadata = metadata,
23
this.$dragTemplate = dragtemplate,
24
this.$listTemplate = listtemplate,
25
this.$fullTemplate = fulltemplate;
26
this.detailTemplates = detailtemplates;
27
this.displayNames = {};
28
this.sortOrder = contacts_sortby || 'fn';
30
this.multi_properties = ['EMAIL', 'TEL', 'IMPP', 'ADR', 'URL'];
33
Contact.prototype.metaData = function() {
36
addressBookId: this.metadata.parent,
37
backend: this.metadata.backend
41
Contact.prototype.getDisplayName = function() {
42
return this.displayNames[this.sortOrder];
45
Contact.prototype.setDisplayMethod = function(method) {
46
if(this.sortOrder === method) {
49
this.sortOrder = method;
50
// ~30% faster than jQuery.
52
this.$listelem.get(0).firstElementChild.getElementsByClassName('nametext')[0].innerHTML = escapeHTML(this.displayNames[method]);
54
var $elem = this.$listelem.find('.nametext').text(escapeHTML(this.displayNames[method]));
55
$elem.text(escapeHTML(this.displayNames[method]));
59
Contact.prototype.getId = function() {
63
Contact.prototype.getOwner = function() {
64
return this.metadata.owner;
67
Contact.prototype.setOwner = function(owner) {
68
this.metadata.owner = owner;
71
Contact.prototype.getPermissions = function() {
72
return this.metadata.permissions;
75
Contact.prototype.hasPermission = function(permission) {
76
//console.log('hasPermission', this.getPermissions(), permission, this.getPermissions() & permission);
77
return (this.getPermissions() & permission);
80
Contact.prototype.getParent = function() {
81
return this.metadata.parent;
84
Contact.prototype.setParent = function(parent) {
85
this.metadata.parent = parent;
88
Contact.prototype.getBackend = function() {
89
return this.metadata.backend;
92
Contact.prototype.setBackend = function(backend) {
93
this.metadata.backend = backend;
96
Contact.prototype.reload = function(data) {
97
console.log('Contact.reload', data);
98
this.id = data.metadata.id;
99
this.metadata = data.metadata;
100
this.data = data.data;
101
/*if(this.$fullelem) {
102
this.$fullelem.replaceWith(this.renderContact(this.groupprops));
106
Contact.prototype.merge = function(mergees) {
107
console.log('Contact.merge, mergees', mergees);
108
if(!mergees instanceof Array && !mergees instanceof Contact) {
109
throw new TypeError('BadArgument: Contact.merge() only takes Contacts');
111
if(mergees instanceof Contact) {
116
// For multi_properties
117
var addIfNotExists = function(name, newproperty) {
118
// If the property isn't set at all just add it and return.
119
if(!self.data[name]) {
120
self.data[name] = [newproperty];
124
$.each(self.data[name], function(idx, property) {
126
// Do a simple string comparison
127
if(property.value.join(';').toLowerCase() === newproperty.value.join(';').toLowerCase()) {
129
return false; // break loop
132
if(property.value.toLowerCase() === newproperty.value.toLowerCase()) {
134
return false; // break loop
141
// Not found, so adding it.
142
self.data[name].push(newproperty);
146
$.each(mergees, function(idx, mergee) {
147
console.log('Contact.merge, mergee', mergee);
148
if(!mergee instanceof Contact) {
149
throw new TypeError('BadArgument: Contact.merge() only takes Contacts');
151
if(mergee === self) {
152
throw new Error('BadArgument: Why should I merge with myself?');
154
$.each(mergee.data, function(name, properties) {
155
if(self.multi_properties.indexOf(name) === -1) {
156
if(self.data[name] && self.data[name].length > 0) {
157
// If the property exists don't touch it.
158
return true; // continue
161
self.data[name] = properties;
164
$.each(properties, function(idx, property) {
165
addIfNotExists(name, property);
169
console.log('Merged', self.data);
174
Contact.prototype.showActions = function(act) {
175
this.$footer.children().hide();
176
if(act && act.length > 0) {
177
this.$footer.children('.'+act.join(',.')).show();
181
Contact.prototype.setAsSaving = function(obj, state) {
185
$(obj).prop('disabled', state);
186
$(obj).toggleClass('loading', state);
188
$(obj).addClass('loading');
190
$(obj).removeClass('loading');
194
Contact.prototype.handleURL = function(obj) {
198
var $container = this.propertyContainerFor(obj);
199
$(document).trigger('request.openurl', {
200
type: $container.data('element'),
201
url: this.valueFor(obj)
206
* Update group name internally. No saving as this is done by groups backend.
208
Contact.prototype.renameGroup = function(from, to) {
209
if(!this.data.CATEGORIES.length) {
210
console.warn(this.getDisplayName(), 'had no groups!?!');
213
var groups = this.data.CATEGORIES[0].value;
215
$.each(groups, function(idx, group) {
216
if(from.toLowerCase() === group.toLowerCase()) {
217
console.log('Updating group name for', self.getDisplayName(), group, to);
218
self.data.CATEGORIES[0].value[idx] = to;
219
return false; // break
222
$(document).trigger('status.contact.updated', {
223
property: 'CATEGORIES',
228
Contact.prototype.pushToUndo = function(params) {
229
// Check if the same property has been changed before
230
// and update it's checksum if so.
231
if(typeof params.oldchecksum !== 'undefined') {
232
$.each(this.undoQueue, function(idx, item) {
233
if(item.checksum === params.oldchecksum) {
234
item.checksum = params.newchecksum;
235
if(params.action === 'delete') {
236
item.action = 'delete';
238
return false; // Break loop
242
this.undoQueue.push({
243
action:params.action,
245
checksum: params.newchecksum,
246
newvalue: params.newvalue,
247
oldvalue: params.oldvalue
249
//console.log('undoQueue', this.undoQueue);
252
Contact.prototype.addProperty = function($option, name) {
253
console.log('Contact.addProperty', name)
261
$elem = this.$fullelem.find('[data-element="' + name.toLowerCase() + '"]');
262
$elem.addClass('new').show();
263
$elem.find('input:not(:checkbox),textarea').first().focus();
264
$option.prop('disabled', true);
269
var $elem = this.renderStandardProperty(name.toLowerCase());
270
var $list = this.$fullelem.find('ul.' + name.toLowerCase());
273
$elem.find('input.value').addClass('new');
274
$elem.find('input:not(:checkbox)').first().focus();
277
var $elem = this.renderAddressProperty();
278
var $list = this.$fullelem.find('ul.' + name.toLowerCase());
281
$elem.find('.display').trigger('click');
282
$elem.find('input.value').addClass('new');
283
$elem.find('input:not(:checkbox)').first().focus();
286
var $elem = this.renderIMProperty();
287
var $list = this.$fullelem.find('ul.' + name.toLowerCase());
290
$elem.find('input.value').addClass('new');
291
$elem.find('input:not(:checkbox)').first().focus();
296
// If there's already a property of this type enable setting as preferred.
297
if(this.multi_properties.indexOf(name) !== -1 && this.data[name] && this.data[name].length > 0) {
298
var selector = 'li[data-element="' + name.toLowerCase() + '"]';
299
$.each(this.$fullelem.find(selector), function(idx, elem) {
300
$(elem).find('input.parameter[value="PREF"]').show();
302
} else if(this.multi_properties.indexOf(name) !== -1) {
303
$elem.find('input.parameter[value="PREF"]').hide();
305
$elem.find('select.type[name="parameters[TYPE][]"]')
308
classes: ['propertytype', 'float', 'label'],
313
Contact.prototype.deleteProperty = function(params) {
314
var obj = params.obj;
318
var element = this.propertyTypeFor(obj);
319
var $container = this.propertyContainerFor(obj);
320
console.log('Contact.deleteProperty, element', element, $container);
325
if(this.multi_properties.indexOf(element) !== -1) {
326
params['checksum'] = this.checksumFor(obj);
327
if(params['checksum'] === 'new' && $.trim(this.valueFor(obj)) === '') {
328
// If there's only one property of this type enable setting as preferred.
329
if(this.data[element].length === 1) {
330
var selector = 'li[data-element="' + element.toLowerCase() + '"]';
331
this.$fullelem.find(selector).find('input.parameter[value="PREF"]').hide();
337
this.setAsSaving(obj, true);
339
$.when(this.storage.patchContact(this.metadata.backend, this.metadata.parent, this.id, params))
340
.then(function(response) {
341
if(!response.error) {
342
if(self.multi_properties.indexOf(element) !== -1) {
343
// First find out if an existing element by looking for checksum
344
var checksum = self.checksumFor(obj);
348
oldchecksum: self.checksumFor(obj),
349
newvalue: self.valueFor(obj)
352
for(var i in self.data[element]) {
353
if(self.data[element][i].checksum === checksum) {
355
self.data[element].splice(self.data[element].indexOf(self.data[element][i]), 1);
360
// If there's only one property of this type enable setting as preferred.
361
if(self.data[element].length === 1) {
362
var selector = 'li[data-element="' + element.toLowerCase() + '"]';
363
self.$fullelem.find(selector).find('input.parameter[value="PREF"]').hide();
370
newvalue: $container.find('input.value').val()
372
self.setAsSaving(obj, false);
373
if(element === 'PHOTO') {
374
self.data.PHOTO[0].value = false;
375
self.data.thumbnail = null;
377
self.$fullelem.find('[data-element="' + element.toLowerCase() + '"]').hide();
378
$container.find('input.value').val('');
379
self.$addMenu.find('option[value="' + element.toUpperCase() + '"]').prop('disabled', false);
382
$(document).trigger('status.contact.updated', {
388
$(document).trigger('status.contacts.error', response);
389
self.setAsSaving(obj, false);
393
.fail(function(response) {
394
console.log(response.message);
395
$(document).trigger('status.contacts.error', response);
401
* @brief Save all properties. Used for merging contacts.
402
* If this is a new contact it will first be saved to the datastore and a
403
* new datastructure will be added to the object.
405
Contact.prototype.saveAll = function(cb) {
406
console.log('Contact.saveAll');
409
this.add({isnew:true}, function(response) {
411
console.warn('No response object');
419
this.setAsSaving(this.$fullelem, true);
420
var data = JSON.stringify(this.data);
421
//console.log('stringified', data);
422
$.when(this.storage.saveAllProperties(this.metadata.backend, this.metadata.parent, this.id, {data:this.data}))
423
.then(function(response) {
424
if(!response.error) {
425
self.data = response.data.data;
426
self.metadata = response.data.metadata;
427
if(typeof cb === 'function') {
431
$(document).trigger('status.contacts.error', {
432
message: response.message
434
if(typeof cb === 'function') {
435
cb({error:true, message:response.message});
438
self.setAsSaving(self.$fullelem, false);
443
* @brief Act on change of a property.
444
* If this is a new contact it will first be saved to the datastore and a
445
* new datastructure will be added to the object.
446
* If the obj argument is not provided 'name' and 'value' MUST be provided
447
* and this is only allowed for single elements like N, FN, CATEGORIES.
448
* @param obj. The form form field that has changed.
449
* @param name. The optional name of the element.
450
* @param value. The optional value.
452
Contact.prototype.saveProperty = function(params) {
453
console.log('Contact.saveProperty', params);
456
this.add({isnew:true}, function(response) {
457
if(!response || response.status === 'error') {
458
console.warn('No response object');
461
self.saveProperty(params);
462
self.showActions(['close', 'add', 'export', 'delete']);
471
args = this.argumentsFor(obj);
472
//args['parameters'] = $.param(this.parametersFor(obj));
473
element = this.propertyTypeFor(obj);
476
element = params.name;
477
var value = utils.isArray(params.value)
478
? $.param(params.value)
479
: encodeURIComponent(params.value);
482
console.log('No arguments. returning');
485
console.log('args', args);
487
this.setAsSaving(obj, true);
488
$.when(this.storage.patchContact(this.metadata.backend, this.metadata.parent, this.id, args))
489
.then(function(response) {
490
if(!response.error) {
491
if(!self.data[element]) {
492
self.data[element] = [];
494
if(self.multi_properties.indexOf(element) !== -1) {
495
// First find out if an existing element by looking for checksum
496
var checksum = self.checksumFor(obj);
497
var value = self.valueFor(obj);
498
var parameters = self.parametersFor(obj);
499
if(parameters['TYPE'] && parameters['TYPE'].indexOf('PREF') !== -1) {
500
parameters['PREF'] = 1;
501
parameters['TYPE'].splice(parameters['TYPE'].indexOf('PREF', 1));
503
if(checksum && checksum !== 'new') {
507
newchecksum: response.data.checksum,
508
oldchecksum: checksum,
510
oldvalue: obj.defaultValue
512
$.each(self.data[element], function(i, el) {
513
if(el.checksum === checksum) {
514
self.data[element][i] = {
517
parameters: parameters,
518
checksum: response.data.checksum
524
$(obj).removeClass('new');
528
newchecksum: response.data.checksum,
531
self.data[element].push({
534
parameters: parameters,
535
checksum: response.data.checksum,
538
self.propertyContainerFor(obj).data('checksum', response.data.checksum);
540
// Save value and parameters internally
541
var value = obj ? self.valueFor(obj) : params.value;
543
action: ((obj && obj.defaultValue) || self.data[element].length) ? 'save' : 'add', // FIXME
549
// We deal with this in addToGroup()
552
// reverse order again.
553
value = $.datepicker.formatDate('yy-mm-dd', $.datepicker.parseDate(datepickerFormatDate, value));
554
self.data[element][0] = {
557
parameters: self.parametersFor(obj),
558
checksum: response.data.checksum
562
if(!self.data.FN || !self.data.FN.length) {
563
self.data.FN = [{name:'FN', value:'', parameters:[]}];
565
self.data.FN[0]['value'] = value;
568
// TODO: Maybe add a method for constructing new elements?
569
self.data.N = [{name:'N',value:['', '', '', '', ''],parameters:[]}];
571
$.each(self.data.N[0]['value'], function(idx, val) {
578
self.data.N[0]['value'] = ['', '', '', '', ''];
579
var nvalue = value.split(' ');
580
// Very basic western style parsing. I'm not gonna implement
581
// https://github.com/android/platform_packages_providers_contactsprovider/blob/master/src/com/android/providers/contacts/NameSplitter.java ;)
582
self.data.N[0]['value'][0] = nvalue.length > 2 && nvalue.slice(nvalue.length-1).toString() || nvalue[1] || '';
583
self.data.N[0]['value'][1] = nvalue[0] || '';
584
self.data.N[0]['value'][2] = nvalue.length > 2 && nvalue.slice(1, nvalue.length-1).join(' ') || '';
585
setTimeout(function() {
586
self.saveProperty({name:'N', value:self.data.N[0].value.join(';')});
587
setTimeout(function() {
588
self.$fullelem.find('.fullname').next('.action.edit').trigger('click');
589
OC.notify({message:t('contacts', 'Is this correct?')});
596
if(!utils.isArray(value)) {
597
value = value.split(';');
598
// Then it is auto-generated from FN.
599
var $nelems = self.$fullelem.find('.n.editor input');
600
$.each(value, function(idx, val) {
601
self.$fullelem.find('#n_' + idx).val(val).get(0).defaultValue = val;
604
var $fullname = self.$fullelem.find('.fullname'), fullname = '';
605
var update_fn = false;
607
self.data.FN = [{name:'FN', value:'', parameters:[]}];
609
/* If FN is empty fill it with the values from N.
610
* As N consists of several fields which each trigger a change/save
611
* also check if the contents of FN equals parts of N and fill
614
if(self.data.FN[0]['value'] === '') {
615
self.data.FN[0]['value'] = value[1] + ' ' + value[0];
616
$fullname.val(self.data.FN[0]['value']);
618
} else if($fullname.val() == value[1] + ' ') {
619
self.data.FN[0]['value'] = value[1] + ' ' + value[0];
620
$fullname.val(self.data.FN[0]['value']);
622
} else if($fullname.val() == ' ' + value[0]) {
623
self.data.FN[0]['value'] = value[1] + ' ' + value[0];
624
$fullname.val(self.data.FN[0]['value']);
628
setTimeout(function() {
629
self.saveProperty({name:'FN', value:self.data.FN[0]['value']});
634
// Auto-fill FN if empty
636
self.data.FN = [{name:'FN', value:value, parameters:[]}];
637
self.$fullelem.find('.fullname').val(value).trigger('change');
641
self.data[element][0] = {
644
parameters: self.parametersFor(obj),
645
checksum: response.data.checksum
652
self.setAsSaving(obj, false);
653
$(document).trigger('status.contact.updated', {
659
$(document).trigger('status.contacts.error', response);
660
self.setAsSaving(obj, false);
667
* Hide contact list element.
669
Contact.prototype.hide = function() {
670
this.getListItemElement().hide();
674
* Show contact list element.
676
Contact.prototype.show = function() {
677
this.getListItemElement().show();
681
* Remove any open contact from the DOM.
683
Contact.prototype.close = function() {
684
$(document).unbind('status.contact.photoupdated');
685
console.log('Contact.close', this);
687
this.$fullelem.hide().remove();
688
this.getListItemElement().show();
689
this.$fullelem = null;
697
* Remove any open contact from the DOM and detach it's list
698
* element from the DOM.
699
* @returns The contact object.
701
Contact.prototype.detach = function() {
703
this.$fullelem.remove();
706
this.$listelem.detach();
712
* Set a contacts list element as (un)checked
713
* @returns The contact object.
715
Contact.prototype.setChecked = function(checked) {
717
this.$listelem.find('input:checkbox').prop('checked', checked);
723
* Set a contact to en/disabled depending on its permissions.
724
* @param boolean enabled
726
Contact.prototype.setEnabled = function(enabled) {
728
this.$fullelem.find('#addproperty').show();
730
this.$fullelem.find('#addproperty,.action.delete,.action.edit').hide();
732
this.enabled = enabled;
733
this.$fullelem.find('.value,.action,.parameter').each(function () {
734
$(this).prop('disabled', !enabled);
736
$(document).trigger('status.contact.enabled', enabled);
740
* Add a contact to data store.
741
* @params params. An object which can contain the optional properties:
742
* aid: The id of the addressbook to add the contact to. Per default it will be added to the first.
743
* fn: The formatted name of the contact.
744
* @param cb Optional callback function which
745
* @returns The callback gets an object as argument with a variable 'status' of either 'success'
746
* or 'error'. On success the 'data' property of that object contains the contact id as 'id', the
747
* addressbook id as 'aid' and the contact data structure as 'details'.
749
Contact.prototype.add = function(params, cb) {
751
$.when(this.storage.addContact(this.metadata.backend, this.metadata.parent))
752
.then(function(response) {
753
if(!response.error) {
754
self.id = String(response.data.metadata.id);
755
self.metadata = response.data.metadata;
756
self.data = response.data.data;
757
self.$groupSelect.multiselect('enable');
758
// Add contact to current group
760
&& ['all', 'fav', 'uncategorized'].indexOf(self.groupprops.currentgroup.id) === -1
762
if(!self.data.CATEGORIES) {
763
self.addToGroup(self.groupprops.currentgroup.name);
764
$(document).trigger('request.contact.addtogroup', {
766
groupid: self.groupprops.currentgroup.id
768
self.$groupSelect.find('option[value="' + self.groupprops.currentgroup.id + '"]')
769
.attr('selected', 'selected');
770
self.$groupSelect.multiselect('refresh');
773
$(document).trigger('status.contact.added', {
778
$(document).trigger('status.contacts.error', response);
781
if(typeof cb == 'function') {
787
* Delete contact from data store and remove it from the DOM
788
* @param cb Optional callback function which
789
* @returns An object with a variable 'status' of either success
792
Contact.prototype.destroy = function(cb) {
794
$.when(this.storage.deleteContact(
795
this.metadata.backend,
796
this.metadata.parent,
798
).then(function(response) {
799
//$.post(OC.filePath('contacts', 'ajax', 'contact/delete.php'),
800
// {id: this.id}, function(response) {
801
if(!response.error) {
803
self.$listelem.remove();
806
self.$fullelem.remove();
809
if(typeof cb == 'function') {
819
Contact.prototype.argumentsFor = function(obj) {
820
console.log('Contact.argumentsFor', $(obj));
822
var ptype = this.propertyTypeFor(obj);
823
args['name'] = ptype;
825
if(this.multi_properties.indexOf(ptype) !== -1) {
826
args['checksum'] = this.checksumFor(obj);
829
if($(obj).hasClass('propertycontainer')) {
830
if($(obj).is('select[data-element="categories"]')) {
832
$.each($(obj).find(':selected'), function(idx, e) {
833
args['value'].push($(e).text());
836
args['value'] = $(obj).val();
839
var $elements = this.propertyContainerFor(obj)
840
.find('input.value,select.value,textarea.value');
841
if($elements.length > 1) {
843
$.each($elements, function(idx, e) {
844
args['value'][parseInt($(e).attr('name').substr(6,1))] = $(e).val();
845
//args['value'].push($(e).val());
848
var value = $elements.val();
849
switch(args['name']) {
852
args['value'] = $.datepicker.formatDate('yy-mm-dd', $.datepicker.parseDate(datepickerFormatDate, value));
855
'status.contacts.error',
856
{message:t('contacts', 'Error parsing date: {date}', {date:value})}
862
args['value'] = value;
867
args['parameters'] = this.parametersFor(obj);
868
console.log('Contact.argumentsFor', args);
872
Contact.prototype.queryStringFor = function(obj) {
873
var q = 'id=' + this.id;
874
var ptype = this.propertyTypeFor(obj);
875
q += '&name=' + ptype;
877
if(this.multi_properties.indexOf(ptype) !== -1) {
878
q += '&checksum=' + this.checksumFor(obj);
881
if($(obj).hasClass('propertycontainer')) {
882
if($(obj).is('select[data-element="categories"]')) {
883
$.each($(obj).find(':selected'), function(idx, e) {
884
q += '&value=' + encodeURIComponent($(e).text());
887
q += '&value=' + encodeURIComponent($(obj).val());
890
var $elements = this.propertyContainerFor(obj)
891
.find('input.value,select.value,textarea.value,.parameter');
892
if($elements.length > 1) {
893
q += '&' + $elements.serialize();
895
q += '&value=' + encodeURIComponent($elements.val());
901
Contact.prototype.propertyContainerFor = function(obj) {
902
return $(obj).hasClass('propertycontainer')
904
: $(obj).parents('.propertycontainer').first();
907
Contact.prototype.checksumFor = function(obj) {
908
return this.propertyContainerFor(obj).data('checksum');
911
Contact.prototype.valueFor = function(obj) {
912
var $container = this.propertyContainerFor(obj);
913
console.assert($container.length > 0, 'Couldn\'t find container for ' + $(obj));
914
return $container.is('input.value')
917
var $elem = $container.find('textarea.value,input.value:not(:checkbox)');
918
console.assert($elem.length > 0, 'Couldn\'t find value for ' + $container.data('element'));
919
if($elem.length === 1) {
921
} else if($elem.length > 1) {
923
$.each($elem, function(idx, e) {
924
retval[parseInt($(e).attr('name').substr(6,1))] = $(e).val();
931
Contact.prototype.parametersFor = function(obj, asText) {
933
$.each(this.propertyContainerFor(obj)
934
.find('select.parameter,input:checkbox:checked.parameter'),
937
var paramname = $elem.data('parameter');
938
if(!parameters[paramname]) {
939
parameters[paramname] = [];
941
if($elem.is(':checkbox')) {
943
parameters[paramname].push($elem.attr('title'));
945
parameters[paramname].push($elem.attr('value'));
947
} else if($elem.is('select')) {
948
$.each($elem.find(':selected'), function(idx, e) {
950
parameters[paramname].push($(e).text());
952
parameters[paramname].push($(e).val());
960
Contact.prototype.propertyTypeFor = function(obj) {
961
var ptype = this.propertyContainerFor(obj).data('element');
962
return ptype ? ptype.toUpperCase() : null;
966
* Render an element item to be shown during drag.
967
* @return A jquery object
969
Contact.prototype.renderDragItem = function() {
970
if(typeof this.$dragelem === 'undefined') {
971
this.$dragelem = this.$dragTemplate.octemplate({
973
name: this.getPreferredValue('FN', '')
976
this.setThumbnail(this.$dragelem);
977
return this.$dragelem;
981
* Render the list item
982
* @return A jquery object to be inserted in the DOM
984
Contact.prototype.renderListItem = function(isnew) {
985
this.displayNames.fn = this.getPreferredValue('FN')
986
|| this.getPreferredValue('ORG', []).pop()
987
|| this.getPreferredValue('EMAIL')
988
|| this.getPreferredValue('TEL');
990
this.displayNames.fl = this.getPreferredValue('N', [this.displayNames.fn])
991
.slice(0, 2).reverse().join(' ');
993
this.displayNames.lf = this.getPreferredValue('N', [this.displayNames.fn])
994
.slice(0, 2).join(', ');
996
this.$listelem = this.$listTemplate.octemplate({
998
parent: this.metadata.parent,
999
backend: this.metadata.backend,
1000
name: this.getDisplayName(),
1001
email: this.getPreferredValue('EMAIL', ''),
1002
tel: this.getPreferredValue('TEL', ''),
1003
adr: this.getPreferredValue('ADR', []).clean('').join(', '),
1004
categories: this.getPreferredValue('CATEGORIES', [])
1005
.clean('').join(' / ')
1007
if(this.getOwner() !== OC.currentUser
1008
&& !(this.metadata.permissions & OC.PERMISSION_UPDATE
1009
|| this.metadata.permissions & OC.PERMISSION_DELETE)) {
1010
this.$listelem.find('input:checkbox').prop('disabled', true).css('opacity', '0');
1013
this.$listelem.find('td.name')
1018
helper: function (e,ui) {
1019
return self.renderDragItem().appendTo('body');
1026
this.setThumbnail();
1028
this.$listelem.data('obj', this);
1029
return this.$listelem;
1033
* Render the full contact
1034
* @return A jquery object to be inserted in the DOM
1036
Contact.prototype.renderContact = function(groupprops) {
1038
this.groupprops = groupprops;
1040
var buildGroupSelect = function(availableGroups) {
1041
//this.$groupSelect.find('option').remove();
1042
$.each(availableGroups, function(idx, group) {
1043
var $option = $('<option value="' + group.id + '">' + group.name + '</option>');
1044
if(self.inGroup(group.name)) {
1045
$option.attr('selected', 'selected');
1047
self.$groupSelect.append($option);
1049
self.$groupSelect.multiselect({
1052
noneSelectedText: self.$groupSelect.attr('title'),
1053
selectedText: t('contacts', '# groups')
1055
self.$groupSelect.bind('multiselectclick', function(event, ui) {
1056
var action = ui.checked ? 'addtogroup' : 'removefromgroup';
1057
console.assert(typeof self.id === 'string', 'ID is not a string')
1058
$(document).trigger('request.contact.' + action, {
1060
groupid: parseInt(ui.value)
1063
self.addToGroup(ui.text);
1065
self.removeFromGroup(ui.text);
1068
if(!self.id || !self.hasPermission(OC.PERMISSION_UPDATE)) {
1069
self.$groupSelect.multiselect('disable');
1073
var buildAddressBookSelect = function(availableAddressBooks) {
1074
console.log('address books', availableAddressBooks.length, availableAddressBooks);
1075
$.each(availableAddressBooks, function(idx, addressBook) {
1076
//console.log('addressBook', idx, addressBook);
1077
var $option = $('<option />')
1078
.val(addressBook.getId())
1079
.text(addressBook.getDisplayName() + '(' + addressBook.getBackend() + ')')
1080
.data('backend', addressBook.getBackend())
1081
.data('owner', addressBook.getOwner());
1082
if(self.metadata.parent === addressBook.getId()
1083
&& self.metadata.backend === addressBook.getBackend()) {
1084
$option.attr('selected', 'selected');
1086
self.$addressBookSelect.append($option);
1088
self.$addressBookSelect.multiselect({
1092
noneSelectedText: self.$addressBookSelect.attr('title')
1094
self.$addressBookSelect.on('multiselectclick', function(event, ui) {
1095
console.log('AddressBook select', ui);
1096
self.$addressBookSelect.val(ui.value);
1097
var opt = self.$addressBookSelect.find(':selected');
1099
console.log('AddressBook', opt);
1100
$(document).trigger('request.contact.move', {
1102
from: {id:self.getParent(), backend:self.getBackend()},
1103
target: {id:opt.val(), backend:opt.data('backend')}
1106
self.setBackend(opt.data('backend'));
1107
self.setParent(opt.val());
1108
self.setOwner(opt.data('owner'));
1112
//self.$addressBookSelect.multiselect('disable');
1118
var n = this.getPreferredValue('N', ['', '', '', '', '']),
1119
bday = this.getPreferredValue('BDAY', '');
1120
if(bday.length >= 10) {
1122
bday = $.datepicker.parseDate('yy-mm-dd', bday.substring(0, 10));
1123
bday = $.datepicker.formatDate(datepickerFormatDate, bday);
1125
var message = t('contacts', 'Error parsing birthday {bday}: {error}', {bday:bday, error: e});
1126
console.warn(message);
1128
$(document).trigger('status.contacts.error', {
1136
favorite:groupprops.favorite ? 'active' : '',
1137
name: this.getPreferredValue('FN', ''),
1138
n0: n[0]||'', n1: n[1]||'', n2: n[2]||'', n3: n[3]||'', n4: n[4]||'',
1139
nickname: this.getPreferredValue('NICKNAME', ''),
1140
title: this.getPreferredValue('TITLE', ''),
1141
org: this.getPreferredValue('ORG', []).clean('').join(', '), // TODO Add parts if more than one.
1143
note: this.getPreferredValue('NOTE', '')
1146
values = {id:'', favorite:'', name:'', nickname:'', title:'', org:'', bday:'', note:'', n0:'', n1:'', n2:'', n3:'', n4:''};
1148
this.$fullelem = this.$fullTemplate.octemplate(values).data('contactobject', this);
1150
this.$footer = this.$fullelem.find('footer');
1152
this.$fullelem.find('.tooltipped.rightwards.onfocus').tipsy({trigger: 'focus', gravity: 'w'});
1153
this.$fullelem.on('submit', function() {
1157
if(this.getOwner() === OC.currentUser) {
1158
this.$groupSelect = this.$fullelem.find('#contactgroups');
1159
buildGroupSelect(groupprops.groups);
1162
var writeableAddressBooks = this.parent.addressBooks.selectByPermission(OC.PERMISSION_CREATE);
1163
if(writeableAddressBooks.length > 1 && this.hasPermission(OC.PERMISSION_DELETE)) {
1164
this.$addressBookSelect = this.$fullelem.find('#contactaddressbooks');
1165
buildAddressBookSelect(writeableAddressBooks);
1168
this.$addMenu = this.$fullelem.find('#addproperty');
1169
this.$addMenu.on('change', function(event) {
1170
//console.log('add', $(this).val());
1171
var $opt = $(this).find('option:selected');
1172
self.addProperty($opt, $(this).val());
1175
var $fullname = this.$fullelem.find('.fullname');
1176
this.$fullelem.find('.singleproperties').on('mouseenter', function() {
1177
$fullname.next('.edit').css('opacity', '1');
1178
}).on('mouseleave', function() {
1179
$fullname.next('.edit').css('opacity', '0');
1181
$fullname.next('.edit').on('click keydown', function(event) {
1182
//console.log('edit name', event);
1183
$('.tipsy').remove();
1184
if(wrongKey(event)) {
1187
$(this).css('opacity', '0');
1188
var $editor = $(this).next('.n.editor').first();
1189
var bodyListener = function(e) {
1190
if($editor.find($(e.target)).length == 0) {
1191
$editor.toggle('blind');
1192
$('body').unbind('click', bodyListener);
1195
$editor.toggle('blind', function() {
1196
$('body').bind('click', bodyListener);
1200
this.$fullelem.on('click keydown', '.delete', function(event) {
1201
$('.tipsy').remove();
1202
if(wrongKey(event)) {
1205
self.deleteProperty({obj:event.target});
1208
this.$fullelem.on('click keydown', '.globe,.mail', function(event) {
1209
$('.tipsy').remove();
1210
if(wrongKey(event)) {
1213
self.handleURL(event.target);
1216
this.$footer.on('click keydown', 'button', function(event) {
1217
$('.tipsy').remove();
1218
if(wrongKey(event)) {
1221
if($(this).is('.close') || $(this).is('.cancel')) {
1222
$(document).trigger('request.contact.close', {
1225
} else if($(this).is('.export')) {
1226
$(document).trigger('request.contact.export', self.metaData());
1227
} else if($(this).is('.delete')) {
1228
$(document).trigger('request.contact.delete', self.metaData());
1232
this.$fullelem.on('keypress', '.value,.parameter', function(event) {
1233
if(event.keyCode === 13 && $(this).is('input')) {
1234
$(this).trigger('change');
1235
// Prevent a second save on blur.
1236
this.previousValue = this.defaultValue || '';
1237
this.defaultValue = this.value;
1239
} else if(event.keyCode === 27) {
1240
$(document).trigger('request.contact.close', {
1246
this.$fullelem.on('change', '.value,.parameter', function(event) {
1247
if($(this).hasClass('value') && this.value === this.defaultValue) {
1250
//console.log('change', this.defaultValue, this.value);
1251
this.defaultValue = this.value;
1252
self.saveProperty({obj:event.target});
1255
var $bdayinput = this.$fullelem.find('[data-element="bday"]').find('input');
1256
$bdayinput.datepicker({
1257
dateFormat : datepickerFormatDate
1259
$bdayinput.attr('placeholder', $.datepicker.formatDate(datepickerFormatDate, new Date()));
1261
this.$fullelem.find('.favorite').on('click', function () {
1262
var state = $(this).hasClass('active');
1267
$(this).switchClass('active', 'inactive');
1269
$(this).switchClass('inactive', 'active');
1271
$(document).trigger('request.contact.setasfavorite', {
1279
this.setEnabled(true);
1280
this.showActions(['cancel']);
1281
return this.$fullelem;
1283
// Loop thru all single occurrence values. If not set hide the
1284
// element, if set disable the add menu entry.
1285
$.each(values, function(name, value) {
1286
if(typeof value === 'undefined') {
1287
return true; //continue
1289
value = value.toString();
1290
if(self.multi_properties.indexOf(value.toUpperCase()) === -1) {
1292
self.$fullelem.find('[data-element="' + name + '"]').hide();
1294
self.$addMenu.find('option[value="' + name.toUpperCase() + '"]').prop('disabled', true);
1298
$.each(this.multi_properties, function(idx, name) {
1299
if(self.data[name]) {
1300
var $list = self.$fullelem.find('ul.' + name.toLowerCase());
1302
for(var p in self.data[name]) {
1303
if(typeof self.data[name][p] === 'object') {
1304
var property = self.data[name][p];
1305
//console.log(name, p, property);
1306
var $property = null;
1311
$property = self.renderStandardProperty(name.toLowerCase(), property);
1312
if(self.data[name].length === 1) {
1313
$property.find('input:checkbox[value="PREF"]').hide();
1317
$property = self.renderAddressProperty(idx, property);
1320
$property = self.renderIMProperty(property);
1321
if(self.data[name].length === 1) {
1322
$property.find('input:checkbox[value="PREF"]').hide();
1329
//console.log('$property', $property);
1331
if(property.label) {
1332
if(!property.parameters['TYPE']) {
1333
property.parameters['TYPE'] = [];
1335
property.parameters['TYPE'].push(property.label);
1336
meta.push(property.label);
1338
for(var param in property.parameters) {
1339
//console.log('param', param);
1340
if(param.toUpperCase() == 'PREF') {
1341
var $cb = $property.find('input[type="checkbox"]');
1342
$cb.attr('checked', 'checked');
1343
meta.push($cb.attr('title'));
1345
else if(param.toUpperCase() == 'TYPE') {
1346
for(var etype in property.parameters[param]) {
1348
var et = property.parameters[param][etype];
1349
if(typeof et !== 'string') {
1352
$property.find('select.type option').each(function() {
1353
if($(this).val().toUpperCase() === et.toUpperCase()) {
1354
$(this).attr('selected', 'selected');
1355
meta.push($(this).text());
1360
$property.find('select.type option:last-child').after('<option value="'+et+'" selected="selected">'+et+'</option>');
1364
else if(param.toUpperCase() == 'X-SERVICE-TYPE') {
1365
//console.log('setting', $property.find('select.impp'), 'to', property.parameters[param].toLowerCase());
1366
$property.find('select.impp').val(property.parameters[param].toLowerCase());
1369
var $meta = $property.find('.meta');
1371
$meta.html(meta.join('/'));
1373
if(self.metadata.owner === OC.currentUser
1374
|| self.metadata.permissions & OC.PERMISSION_UPDATE
1375
|| self.metadata.permissions & OC.PERMISSION_DELETE) {
1376
$property.find('select.type[name="parameters[TYPE][]"]')
1379
classes: ['propertytype', 'float', 'label']
1382
$list.append($property);
1387
if(this.metadata.owner !== OC.currentUser
1388
&& !(this.hasPermission(OC.PERMISSION_UPDATE)
1389
|| this.hasPermission(OC.PERMISSION_DELETE))) {
1390
this.setEnabled(false);
1391
this.showActions(['close', 'export']);
1393
this.setEnabled(true);
1394
this.showActions(['close', 'add', 'export', 'delete']);
1396
return this.$fullelem;
1399
Contact.prototype.isEditable = function() {
1400
return ((this.metadata.owner === OC.currentUser)
1401
|| (this.metadata.permissions & OC.PERMISSION_UPDATE
1402
|| this.metadata.permissions & OC.PERMISSION_DELETE));
1406
* Render a simple property. Used for EMAIL and TEL.
1407
* @return A jquery object to be injected in the DOM
1409
Contact.prototype.renderStandardProperty = function(name, property) {
1410
if(!this.detailTemplates[name]) {
1411
console.error('No template for', name);
1414
var values = property
1415
? { value: property.value, checksum: property.checksum }
1416
: { value: '', checksum: 'new' };
1417
return this.detailTemplates[name].octemplate(values);
1421
* Render an ADR (address) property.
1422
* @return A jquery object to be injected in the DOM
1424
Contact.prototype.renderAddressProperty = function(idx, property) {
1425
if(!this.detailTemplates['adr']) {
1426
console.warn('No template for adr', this.detailTemplates);
1429
if(typeof idx === 'undefined') {
1430
if(this.data && this.data.ADR && this.data.ADR.length > 0) {
1431
idx = this.data.ADR.length - 1;
1436
var values = property ? {
1437
value: property.value.clean('').join(', '),
1438
checksum: property.checksum,
1439
adr0: property.value[0] || '',
1440
adr1: property.value[1] || '',
1441
adr2: property.value[2] || '',
1442
adr3: property.value[3] || '',
1443
adr4: property.value[4] || '',
1444
adr5: property.value[5] || '',
1445
adr6: property.value[6] || '',
1448
: {value:'', checksum:'new', adr0:'', adr1:'', adr2:'', adr3:'', adr4:'', adr5:'', adr6:'', idx: idx};
1449
var $elem = this.detailTemplates['adr'].octemplate(values);
1451
$elem.find('.tooltipped.downwards:not(.onfocus)').tipsy({gravity: 'n'});
1452
$elem.find('.tooltipped.rightwards.onfocus').tipsy({trigger: 'focus', gravity: 'w'});
1453
$elem.find('.display').on('click', function() {
1454
$(this).next('.listactions').hide();
1455
var $editor = $(this).siblings('.adr.editor').first();
1456
var $viewer = $(this);
1457
var bodyListener = function(e) {
1458
if($editor.find($(e.target)).length == 0) {
1459
$editor.toggle('blind');
1460
$viewer.slideDown(400, function() {
1461
var input = $editor.find('input').first();
1462
var val = self.valueFor(input);
1463
var params = self.parametersFor(input, true);
1464
$(this).find('.meta').html(params['TYPE'].join('/'));
1465
$(this).find('.adr').html(self.valueFor($editor.find('input').first()).clean('').join(', '));
1466
$(this).next('.listactions').css('display', 'inline-block');
1467
$('body').unbind('click', bodyListener);
1472
$editor.toggle('blind', function() {
1473
$('body').bind('click', bodyListener);
1476
$elem.find('.value.city')
1478
source: function( request, response ) {
1480
url: "http://ws.geonames.org/searchJSON",
1486
lang: $elem.data('lang'),
1487
name_startsWith: request.term
1489
success: function( data ) {
1490
response( $.map( data.geonames, function( item ) {
1492
label: item.name + (item.adminName1 ? ", " + item.adminName1 : "") + ", " + item.countryName,
1494
country: item.countryName
1501
select: function( event, ui ) {
1502
if(ui.item && $.trim($elem.find('.value.country').val()).length == 0) {
1503
$elem.find('.value.country').val(ui.item.country);
1507
$elem.find('.value.country')
1509
source: function( request, response ) {
1511
url: "http://ws.geonames.org/searchJSON",
1514
/*featureClass: "A",*/
1515
featureCode: "PCLI",
1516
/*countryBias: "true",*/
1520
name_startsWith: request.term
1522
success: function( data ) {
1523
response( $.map( data.geonames, function( item ) {
1538
* Render an IMPP (Instant Messaging) property.
1539
* @return A jquery object to be injected in the DOM
1541
Contact.prototype.renderIMProperty = function(property) {
1542
if(!this.detailTemplates['impp']) {
1543
console.warn('No template for impp', this.detailTemplates);
1546
var values = property ? {
1547
value: property.value,
1548
checksum: property.checksum
1549
} : {value: '', checksum: 'new'};
1550
return this.detailTemplates['impp'].octemplate(values);
1554
* Set a thumbnail for the contact if a PHOTO property exists
1556
Contact.prototype.setThumbnail = function($elem, refresh) {
1557
if(!this.data.thumbnail && !refresh) {
1561
$elem = this.getListItemElement().find('td.name');
1563
if(!$elem.hasClass('thumbnail') && !refresh) {
1566
if(this.data.thumbnail) {
1567
$elem.removeClass('thumbnail');
1568
$elem.css('background-image', 'url(data:image/png;base64,' + this.data.thumbnail + ')');
1570
$elem.addClass('thumbnail');
1571
$elem.removeAttr('style');
1576
* Render the PHOTO property.
1578
Contact.prototype.loadPhoto = function() {
1580
var id = this.id || 'new',
1581
backend = this.metadata.backend,
1582
parent = this.metadata.parent,
1585
var $phototools = this.$fullelem.find('#phototools');
1586
if(!this.$photowrapper) {
1587
this.$photowrapper = this.$fullelem.find('#photowrapper');
1590
var finishLoad = function(image) {
1591
console.log('finishLoad', self.getDisplayName(), image.width, image.height);
1592
$(image).addClass('contactphoto');
1593
self.$photowrapper.removeClass('loading wait');
1594
self.$photowrapper.css({width: image.width + 10, height: image.height + 10});
1595
$(image).insertAfter($phototools).fadeIn();
1598
this.$photowrapper.addClass('loading').addClass('wait');
1599
if(this.getPreferredValue('PHOTO', null) === null) {
1600
$.when(this.storage.getDefaultPhoto())
1601
.then(function(image) {
1602
$('img.contactphoto').detach();
1606
$.when(this.storage.getContactPhoto(backend, parent, id))
1607
.then(function(image) {
1608
$('img.contactphoto').remove();
1612
console.log('Error getting photo, trying default image');
1613
$('img.contactphoto').remove();
1614
$.when(self.storage.getDefaultPhoto())
1615
.then(function(image) {
1616
$('img.contactphoto').detach();
1622
if(this.isEditable()) {
1623
this.$photowrapper.on('mouseenter', function(event) {
1624
if($(event.target).is('.favorite') || !self.data) {
1627
$phototools.slideDown(200);
1628
}).on('mouseleave', function() {
1629
$phototools.slideUp(200);
1631
$phototools.hover( function () {
1632
$(this).removeClass('transparent');
1634
$(this).addClass('transparent');
1636
$phototools.find('li a').tipsy();
1638
$phototools.find('.action').off('click');
1639
$phototools.find('.edit').on('click', function() {
1640
$(document).trigger('request.edit.contactphoto', self.metaData());
1642
$phototools.find('.cloud').on('click', function() {
1643
$(document).trigger('request.select.contactphoto.fromcloud', self.metaData());
1645
$phototools.find('.upload').on('click', function() {
1646
$(document).trigger('request.select.contactphoto.fromlocal', self.metaData());
1648
if(this.getPreferredValue('PHOTO', false)) {
1649
$phototools.find('.delete').show();
1650
$phototools.find('.edit').show();
1652
$phototools.find('.delete').hide();
1653
$phototools.find('.edit').hide();
1655
$(document).bind('status.contact.photoupdated', function(e, data) {
1656
console.log('status.contact.photoupdated', data);
1657
if(!self.data.PHOTO) {
1658
self.data.PHOTO = [];
1660
if(data.thumbnail) {
1661
self.data.thumbnail = data.thumbnail;
1662
self.data.PHOTO[0] = {value:true};
1664
self.data.thumbnail = null;
1665
self.data.PHOTO[0] = {value:false};
1667
self.loadPhoto(true);
1668
self.setThumbnail(null, true);
1674
* Get the jquery element associated with this object
1676
Contact.prototype.getListItemElement = function() {
1677
if(!this.$listelem) {
1678
this.renderListItem();
1680
return this.$listelem;
1684
* Get the preferred value for a property.
1685
* If a preferred value is not found the first one will be returned.
1686
* @param string name The name of the property like EMAIL, TEL or ADR.
1687
* @param def A default value to return if nothing is found.
1689
Contact.prototype.getPreferredValue = function(name, def) {
1690
var pref = def, found = false;
1691
if(this.data && this.data[name]) {
1692
var props = this.data[name];
1693
//console.log('props', props);
1694
$.each(props, function( i, prop ) {
1695
//console.log('prop:', i, prop);
1696
if(i === 0) { // Choose first to start with
1699
for(var param in prop.parameters) {
1700
if(param.toUpperCase() == 'PREF') {
1706
return false; // break out of loop
1710
if(name === 'N' && pref.join('').trim() === '') {
1717
* Returns true/false depending on the contact being in the
1719
* @param String name The group name (not case-sensitive)
1722
Contact.prototype.inGroup = function(name) {
1723
var categories = this.getPreferredValue('CATEGORIES', []);
1726
$.each(categories, function(idx, category) {
1727
if(name.toLowerCase() == $.trim(category).toLowerCase()) {
1737
* Add this contact to a group
1738
* @param String name The group name
1740
Contact.prototype.addToGroup = function(name) {
1741
console.log('addToGroup', name);
1742
if(!this.data.CATEGORIES) {
1743
this.data.CATEGORIES = [{value:[name]}];
1745
if(this.inGroup(name)) {
1748
this.data.CATEGORIES[0].value.push(name);
1749
if(this.$listelem) {
1750
this.$listelem.find('td.categories')
1751
.text(this.getPreferredValue('CATEGORIES', []).clean('').join(' / '));
1757
* Remove this contact from a group
1758
* @param String name The group name
1760
Contact.prototype.removeFromGroup = function(name) {
1762
if(!this.data.CATEGORIES) {
1763
console.warn('removeFromGroup. No groups found');
1767
var categories = [];
1768
$.each(this.data.CATEGORIES[0].value, function(idx, category) {
1769
category = category.trim();
1770
if(name.toLowerCase() === category.toLowerCase()) {
1773
categories.push(category);
1779
this.data.CATEGORIES[0].value = categories;
1780
if(this.$listelem) {
1781
this.$listelem.find('td.categories')
1782
.text(categories.join(' / '));
1787
Contact.prototype.setCurrent = function(on) {
1789
this.$listelem.addClass('active');
1791
this.$listelem.removeClass('active');
1793
$(document).trigger('status.contact.currentlistitem', {
1795
pos: Math.round(this.$listelem.position().top),
1796
height: Math.round(this.$listelem.height())
1800
Contact.prototype.setSelected = function(state) {
1801
//console.log('Selecting', this.getId(), state);
1802
var $elem = this.getListItemElement();
1803
var $input = $elem.find('input:checkbox');
1804
$input.prop('checked', state).trigger('change');
1807
Contact.prototype.next = function() {
1808
// This used to work..?
1809
//var $next = this.$listelem.next('tr:visible');
1810
var $next = this.$listelem.nextAll('tr').filter(':visible').first();
1811
if($next.length > 0) {
1812
this.$listelem.removeClass('active');
1813
$next.addClass('active');
1814
$(document).trigger('status.contact.currentlistitem', {
1815
id: String($next.data('id')),
1816
pos: Math.round($next.position().top),
1817
height: Math.round($next.height())
1822
Contact.prototype.prev = function() {
1823
//var $prev = this.$listelem.prev('tr:visible');
1824
var $prev = this.$listelem.prevAll('tr').filter(':visible').first();
1825
if($prev.length > 0) {
1826
this.$listelem.removeClass('active');
1827
$prev.addClass('active');
1828
$(document).trigger('status.contact.currentlistitem', {
1829
id: String($prev.data('id')),
1830
pos: Math.round($prev.position().top),
1831
height: Math.round($prev.height())
1836
var ContactList = function(
1840
contactlistitemtemplate,
1841
contactdragitemtemplate,
1842
contactfulltemplate,
1843
contactdetailtemplates
1845
//console.log('ContactList', contactlist, contactlistitemtemplate, contactfulltemplate, contactdetailtemplates);
1849
this.addressBooks = addressBooks;
1850
this.deletionQueue = [];
1851
this.storage = storage;
1852
this.$contactList = contactlist;
1853
this.$contactDragItemTemplate = contactdragitemtemplate;
1854
this.$contactListItemTemplate = contactlistitemtemplate;
1855
this.$contactFullTemplate = contactfulltemplate;
1856
this.contactDetailTemplates = contactdetailtemplates;
1857
this.$contactList.scrollTop(0);
1858
//this.getAddressBooks();
1859
$(document).bind('status.contact.added', function(e, data) {
1861
self.contacts[String(data.id)] = data.contact;
1862
//self.insertContact(data.contact.renderListItem(true));
1864
$(document).bind('status.contact.moved', function(e, data) {
1865
var contact = data.contact;
1866
var oldid = contact.getId();
1868
contact.reload(data.data);
1869
self.contacts[contact.getId()] = contact;
1870
$(document).trigger('request.contact.open', {
1873
console.log('status.contact.moved', data);
1875
$(document).bind('request.contact.close', function(e, data) {
1876
self.currentContact = null;
1878
$(document).bind('status.contact.updated', function(e, data) {
1879
if(['FN', 'EMAIL', 'TEL', 'ADR', 'CATEGORIES'].indexOf(data.property) !== -1) {
1880
data.contact.getListItemElement().remove();
1881
self.insertContact(data.contact.renderListItem(true));
1882
} else if(data.property === 'PHOTO') {
1883
$(document).trigger('status.contact.photoupdated', {
1884
id: data.contact.getId()
1888
$(document).bind('status.addressbook.removed', function(e, data) {
1889
var addressBook = data.addressbook;
1890
self.purgeFromAddressbook(addressBook);
1891
$(document).trigger('request.groups.reload');
1892
$(document).trigger('status.contacts.deleted', {
1893
numcontacts: self.length
1896
$(document).bind('status.addressbook.imported', function(e, data) {
1897
console.log('status.addressbook.imported', data);
1898
var addressBook = data.addressbook;
1899
self.purgeFromAddressbook(addressBook);
1900
$.when(self.loadContacts(addressBook.getBackend(), addressBook.getId(), true))
1902
self.setSortOrder();
1903
$(document).trigger('request.groups.reload');
1906
$(document).bind('status.addressbook.activated', function(e, data) {
1907
console.log('status.addressbook.activated', data);
1908
var addressBook = data.addressbook;
1910
self.purgeFromAddressbook(addressBook);
1911
$(document).trigger('status.contacts.deleted', {
1912
numcontacts: self.length
1915
$.when(self.loadContacts(addressBook.getBackend(), addressBook.getId(), true))
1917
self.setSortOrder();
1918
$(document).trigger('request.groups.reload');
1925
* Get the number of contacts in the list
1928
ContactList.prototype.count = function() {
1929
return Object.keys(this.contacts.contacts).length
1933
* Remove contacts from the internal list and the DOM
1935
* @param AddressBook addressBook
1937
ContactList.prototype.purgeFromAddressbook = function(addressBook) {
1939
$.each(this.contacts, function(idx, contact) {
1940
if(contact.getBackend() === addressBook.getBackend()
1941
&& contact.getParent() === addressBook.getId()) {
1942
//console.log('Removing', contact);
1943
delete self.contacts[contact.getId()];
1944
//var c = self.contacts.splice(self.contacts.indexOf(contact.getId()), 1);
1945
//console.log('Removed', c);
1951
$(document).trigger('status.contacts.count', {
1957
* Show/hide contacts belonging to an addressbook.
1958
* @param int aid. Addressbook id.
1959
* @param boolean show. Whether to show or hide.
1960
* @param boolean hideothers. Used when showing shared addressbook as a group.
1962
ContactList.prototype.showFromAddressbook = function(aid, show, hideothers) {
1963
console.log('ContactList.showFromAddressbook', aid, show);
1965
for(var contact in this.contacts) {
1966
if(this.contacts[contact].getParent() === aid) {
1967
this.contacts[contact].getListItemElement().toggle(show);
1968
} else if(hideothers) {
1969
this.contacts[contact].getListItemElement().hide();
1972
this.setSortOrder();
1976
* Show only uncategorized contacts.
1977
* @param int aid. Addressbook id.
1978
* @param boolean show. Whether to show or hide.
1979
* @param boolean hideothers. Used when showing shared addressbook as a group.
1981
ContactList.prototype.showUncategorized = function() {
1982
console.log('ContactList.showUncategorized');
1983
for(var contact in this.contacts) {
1984
if(this.contacts[contact].getPreferredValue('CATEGORIES', []).length === 0) {
1985
this.contacts[contact].getListItemElement().show();
1987
this.contacts[contact].getListItemElement().hide();
1990
this.setSortOrder();
1994
* Show/hide contacts belonging to shared addressbooks.
1995
* @param boolean show. Whether to show or hide.
1997
ContactList.prototype.showSharedAddressbooks = function(show) {
1998
console.log('ContactList.showSharedAddressbooks', show);
1999
for(var contact in this.contacts) {
2000
if(this.contacts[contact].metadata.owner !== OC.currentUser) {
2002
this.contacts[contact].getListItemElement().show();
2004
this.contacts[contact].getListItemElement().hide();
2008
this.setSortOrder();
2012
* Show contacts in list
2013
* @param Array contacts. A list of contact ids.
2015
ContactList.prototype.showContacts = function(contacts) {
2016
console.log('showContacts', contacts);
2018
if(contacts.length === 0) {
2020
$('tr:visible.contact').hide();
2023
if(contacts === 'all') {
2025
var $elems = $('tr.contact:not(:visible)');
2027
$.each($elems, function(idx, elem) {
2029
var id = $(elem).data('id');
2030
self.contacts[id].setThumbnail();
2032
console.warn('Failed getting id from', $elem, e);
2035
this.setSortOrder();
2038
console.time('show');
2039
$('tr.contact').filter(':visible').hide();
2040
$.each(contacts, function(idx, id) {
2041
var contact = self.findById(id);
2042
if(contact === null) {
2043
return true; // continue
2045
contact.getListItemElement().show();
2046
contact.setThumbnail();
2048
console.timeEnd('show');
2050
// Amazingly this is slightly faster
2051
//console.time('show');
2052
for(var id in this.contacts) {
2053
var contact = this.findById(id);
2054
if(contact === null) {
2057
if(contacts.indexOf(String(id)) === -1) {
2058
contact.getListItemElement().hide();
2060
contact.getListItemElement().show();
2061
contact.setThumbnail();
2064
//console.timeEnd('show');*/
2066
this.setSortOrder();
2069
ContactList.prototype.contactPos = function(id) {
2070
var contact = this.findById(id);
2075
var $elem = contact.getListItemElement();
2076
var pos = Math.round($elem.offset().top - (this.$contactList.offset().top + this.$contactList.scrollTop()));
2077
console.log('contactPos', pos);
2081
ContactList.prototype.hideContact = function(id) {
2082
var contact = this.findById(id);
2083
if(contact === null) {
2089
ContactList.prototype.closeContact = function(id) {
2090
var contact = this.findById(id);
2091
if(contact === null) {
2098
* Returns a Contact object by searching for its id
2099
* @param id the id of the node
2100
* @return the Contact object or undefined if not found.
2101
* FIXME: If continious loading is reintroduced this will have
2102
* to load the requested contact if not in list.
2104
ContactList.prototype.findById = function(id) {
2106
console.warn('ContactList.findById: id missing');
2110
if(typeof this.contacts[id] === 'undefined') {
2111
console.warn('Could not find contact with id', id);
2115
return this.contacts[String(id)];
2119
* TODO: Instead of having a timeout the contacts should be moved to a "Trash" backend/address book
2120
* https://github.com/owncloud/contacts/issues/107
2121
* @param object|object[] data An object or array of objects containing contact identification
2123
* contactid: '1234',
2124
* addressbookid: '4321',
2128
ContactList.prototype.delayedDelete = function(data) {
2129
console.log('delayedDelete, data:', typeof data, data);
2131
if(!utils.isArray(data)) {
2132
this.currentContact = null;
2133
//self.$contactList.show();
2134
if(data instanceof Contact) {
2135
this.deletionQueue.push(data);
2137
var contact = this.findById(data.contactId);
2138
if(contact instanceof Contact) {
2139
this.deletionQueue.push(contact);
2142
} else if(utils.isArray(data)) {
2143
$.each(data, function(idx, contact) {
2144
//console.log('delayedDelete, meta:', contact);
2145
if(contact instanceof Contact) {
2146
self.deletionQueue.push(contact);
2149
//$.extend(this.deletionQueue, data);
2151
throw { name: 'WrongParameterType', message: 'ContactList.delayedDelete only accept objects or arrays.'};
2153
//console.log('delayedDelete, deletionQueue', this.deletionQueue);
2154
$.each(this.deletionQueue, function(idx, contact) {
2155
//console.log('delayedDelete', contact);
2156
contact && contact.detach().setChecked(false);
2158
//console.log('deletionQueue', this.deletionQueue);
2159
if(!window.onbeforeunload) {
2160
window.onbeforeunload = function(e) {
2161
e = e || window.event;
2162
var warn = t('contacts', 'Some contacts are marked for deletion, but not deleted yet. Please wait for them to be deleted.');
2164
e.returnValue = String(warn);
2169
if(this.$contactList.find('tr:visible').length === 0) {
2170
$(document).trigger('status.visiblecontacts');
2173
message:t('contacts','Click to undo deletion of {num} contacts', {num: self.deletionQueue.length}),
2175
timeouthandler:function() {
2176
//console.log('timeout');
2177
self.deleteContacts();
2179
clickhandler:function() {
2180
//console.log('clickhandler');
2181
//OC.notify({cancel:true});
2182
OC.notify({cancel:true, message:t('contacts', 'Cancelled deletion of {num} contacts', {num: self.deletionQueue.length})});
2183
$.each(self.deletionQueue, function(idx, contact) {
2184
self.insertContact(contact.getListItemElement());
2186
self.deletionQueue = [];
2187
window.onbeforeunload = null;
2193
* Delete contacts in the queue
2194
* TODO: Batch delete contacts instead of sending multiple requests.
2196
ContactList.prototype.deleteContacts = function() {
2200
console.log('ContactList.deleteContacts, deletionQueue', this.deletionQueue);
2202
if(this.deletionQueue.length === 1) {
2203
contact = this.deletionQueue.shift()
2204
// Let contact remove itself.
2205
var id = contact.getId();
2206
contact.destroy(function(response) {
2207
console.log('deleteContact', response, self.length);
2208
if(!response.error) {
2209
delete self.contacts[id];
2210
$(document).trigger('status.contact.deleted', {
2214
if(self.length === 0) {
2215
$(document).trigger('status.nomorecontacts');
2218
self.insertContact(contact.getListItemElement());
2219
OC.notify({message:response.message});
2224
// Make a map of backends, address books and contacts for easier processing.
2225
while(contact = this.deletionQueue.shift()) {
2226
if(!contactMap[contact.getBackend()]) {
2227
contactMap[contact.getBackend()] = {};
2229
if(!contactMap[contact.getBackend()][contact.getParent()]) {
2230
contactMap[contact.getBackend()][contact.getParent()] = [];
2232
contactMap[contact.getBackend()][contact.getParent()].push(contact.getId());
2234
console.log('map', contactMap);
2236
// Call each backend/addressBook to delete contacts.
2237
$.each(contactMap, function(backend, addressBooks) {
2238
console.log(backend, addressBooks);
2239
$.each(addressBooks, function(addressBook, contacts) {
2240
console.log(addressBook, contacts);
2241
var ab = self.addressBooks.find({backend:backend, id:addressBook});
2242
ab.deleteContacts(contacts, function(response) {
2243
console.log('response', response);
2244
if(!response.error) {
2245
// We get a result set back, so process all of them.
2246
$.each(response.data.result, function(idx, result) {
2247
console.log('deleting', idx, result.id);
2248
if(result.status === 'success') {
2249
delete self.contacts[result.id];
2250
$(document).trigger('status.contact.deleted', {
2254
if(self.length === 0) {
2255
$(document).trigger('status.nomorecontacts');
2258
// Error deleting, so re-insert element.
2259
// TODO: Collect errors and display them when done.
2260
self.insertContact(self.contacts[result.id].getListItemElement());
2269
window.onbeforeunload = null;
2275
* Insert a rendered contact list item into the list
2276
* @param contact jQuery object.
2278
ContactList.prototype.insertContact = function($contact) {
2279
$contact.find('td.name').draggable({
2282
//containment: '#content',
2283
helper: function (e,ui) {
2284
return $(this).clone().appendTo('body').css('zIndex', 5).show();
2289
var name = $contact.find('.nametext').text().toLowerCase();
2291
this.$contactList.find('tr').each(function() {
2292
if ($(this).find('.nametext').text().toLowerCase().localeCompare(name) > 0) {
2293
$(this).before($contact);
2299
this.$contactList.append($contact);
2307
* @param object props
2309
ContactList.prototype.addContact = function(props) {
2310
// Get first address book
2311
var addressBooks = this.addressBooks.selectByPermission(OC.PERMISSION_UPDATE);
2312
var addressBook = addressBooks[0];
2314
parent: addressBook.getId(),
2315
backend: addressBook.getBackend(),
2316
permissions: addressBook.getPermissions(),
2317
owner: addressBook.getOwner()
2319
var contact = new Contact(
2324
this.$contactListItemTemplate,
2325
this.$contactDragItemTemplate,
2326
this.$contactFullTemplate,
2327
this.contactDetailTemplates
2329
if(this.currentContact) {
2330
this.contacts[this.currentContact].close();
2332
return contact.renderContact(props);
2336
* Get contacts selected in list
2338
* @returns array of contact objects.
2340
ContactList.prototype.getSelectedContacts = function() {
2344
$.each(this.$contactList.find('tbody > tr > td > input:checkbox:visible:checked'), function(idx, checkbox) {
2345
var id = String($(checkbox).val());
2346
var contact = self.contacts[id];
2348
contacts.push(contact);
2354
ContactList.prototype.setCurrent = function(id, deselect_other) {
2355
console.log('ContactList.setCurrent', id);
2360
if(deselect_other === true) {
2361
$.each(this.contacts, function(contact) {
2362
self.contacts[contact].setCurrent(false);
2365
this.contacts[String(id)].setCurrent(true);
2369
* (De)-select a contact
2373
* @param bool reverseOthers
2375
ContactList.prototype.setSelected = function(id, state, reverseOthers) {
2376
console.log('ContactList.setSelected', id);
2381
if(reverseOthers === true) {
2382
var $rows = this.$contactList.find('tr:visible.contact');
2383
$.each($rows, function(idx, row) {
2384
self.contacts[$(row).data('id')].setSelected(!state);
2387
this.contacts[String(id)].setSelected(state);
2391
* Select a range of contacts by their id.
2393
* @param string from
2396
ContactList.prototype.selectRange = function(from, to) {
2398
var $rows = this.$contactList.find('tr:visible.contact');
2399
var index1 = $rows.index(this.contacts[String(from)].getListItemElement());
2400
var index2 = $rows.index(this.contacts[String(to)].getListItemElement());
2401
from = Math.min(index1, index2);
2402
to = Math.max(index1, index2)+1;
2403
$rows = $rows.slice(from, to);
2404
$.each($rows, function(idx, row) {
2405
self.contacts[$(row).data('id')].setSelected(true);
2409
ContactList.prototype.setSortOrder = function(order) {
2410
order = order || contacts_sortby;
2411
//console.time('set name');
2412
var $rows = this.$contactList.find('tr:visible.contact');
2414
$.each($rows, function(idx, row) {
2415
self.contacts[$(row).data('id')].setDisplayMethod(order);
2417
//console.timeEnd('set name');
2418
if($rows.length > 1) {
2419
//console.time('sort');
2420
var rows = $rows.get();
2421
if(rows[0].firstElementChild && rows[0].firstElementChild.textContent) {
2422
rows.sort(function(a, b) {
2423
// 10 (TEN!) times faster than using jQuery!
2424
return a.firstElementChild.textContent.trim().toUpperCase()
2425
.localeCompare(b.firstElementChild.textContent.trim().toUpperCase());
2428
// IE8 doesn't support firstElementChild or textContent
2429
rows.sort(function(a, b) {
2430
return $(a).find('.nametext').text().toUpperCase()
2431
.localeCompare($(b).find('td.name').text().toUpperCase());
2434
this.$contactList.prepend(rows);
2435
//console.timeEnd('sort');
2439
ContactList.prototype.insertContacts = function(contacts) {
2440
var self = this, items = [];
2441
$.each(contacts, function(c, contact) {
2442
var id = String(contact.metadata.id);
2449
self.$contactListItemTemplate,
2450
self.$contactDragItemTemplate,
2451
self.$contactFullTemplate,
2452
self.contactDetailTemplates
2455
var $item = self.contacts[id].renderListItem();
2457
console.warn('Contact', contact, 'could not be rendered!');
2458
return true; // continue
2460
items.push($item.get(0));
2462
if(items.length > 0) {
2463
self.$contactList.append(items);
2465
$(document).trigger('status.contacts.count', {
2472
* @param string backend Name of the backend ('local', 'ldap' etc.)
2473
* @param string addressBookId
2475
ContactList.prototype.loadContacts = function(backend, addressBookId, isActive) {
2482
return $.when(self.storage.getAddressBook(backend, addressBookId, false))
2483
.then(function(response) {
2484
console.log('ContactList.loadContacts - fetching', response);
2485
if(!response.error) {
2487
self.insertContacts(response.data.contacts);
2490
console.warn('ContactList.loadContacts - no data!!');
2493
.fail(function(response) {
2494
console.warn('Request Failed:', response.message);
2495
defer.reject({error: true, message: response.message});
2500
OC.Contacts.ContactList = ContactList;
2502
})(window, jQuery, OC);