diff --git a/js/components/contactDetails/contactDetails_controller.js b/js/components/contactDetails/contactDetails_controller.js index 46b4f6e36..cb7fa4032 100644 --- a/js/components/contactDetails/contactDetails_controller.js +++ b/js/components/contactDetails/contactDetails_controller.js @@ -41,11 +41,13 @@ angular.module('contactsApp') }); } ctrl.loading = false; + // Start watching for ctrl.uid when we have addressBooks, as they are needed for fetching + // full details. + $scope.$watch('ctrl.uid', function(newValue) { + ctrl.changeContact(newValue); + }); }); - $scope.$watch('ctrl.uid', function(newValue) { - ctrl.changeContact(newValue); - }); ctrl.changeContact = function(uid) { if (typeof uid === 'undefined') { @@ -53,7 +55,7 @@ angular.module('contactsApp') $('#app-navigation-toggle').removeClass('showdetails'); return; } - ContactService.getById(uid).then(function(contact) { + ContactService.getById(ctrl.addressBooks, uid).then(function(contact) { if (angular.isUndefined(contact)) { ctrl.clearContact(); return; diff --git a/js/components/contactList/contactList_controller.js b/js/components/contactList/contactList_controller.js index a22180d2e..bc9e877c6 100644 --- a/js/components/contactList/contactList_controller.js +++ b/js/components/contactList/contactList_controller.js @@ -1,5 +1,5 @@ angular.module('contactsApp') -.controller('contactlistCtrl', function($scope, $filter, $route, $routeParams, ContactService, SortByService, vCardPropertiesService, SearchService) { +.controller('contactlistCtrl', function($scope, $filter, $route, $routeParams, $timeout, ContactService, SortByService, vCardPropertiesService, SearchService) { var ctrl = this; ctrl.routeParams = $routeParams; @@ -8,6 +8,7 @@ angular.module('contactsApp') ctrl.searchTerm = ''; ctrl.show = true; ctrl.invalid = false; + ctrl.limitTo = 25; ctrl.sortBy = SortByService.getSortBy(); @@ -15,8 +16,16 @@ angular.module('contactsApp') emptySearch : t('contacts', 'No search result for {query}', {query: ctrl.searchTerm}) }; - $scope.getCountString = function(contacts) { - return n('contacts', '%n contact', '%n contacts', contacts.length); + ctrl.resetLimitTo = function () { + ctrl.limitTo = 25; + clearInterval(ctrl.intervalId); + ctrl.intervalId = setInterval( + function () { + if (!ctrl.loading && ctrl.contacts && ctrl.contacts.length > ctrl.limitTo) { + ctrl.limitTo += 25; + $scope.$apply(); + } + }, 300); }; $scope.query = function(contact) { @@ -34,6 +43,7 @@ angular.module('contactsApp') $scope.$apply(); } if (ev.event === 'changeSearch') { + ctrl.resetLimitTo(); ctrl.searchTerm = ev.searchTerm; ctrl.t.emptySearch = t('contacts', 'No search result for {query}', @@ -46,7 +56,7 @@ angular.module('contactsApp') ctrl.loading = true; ContactService.registerObserverCallback(function(ev) { - $scope.$apply(function() { + $timeout(function () { $scope.$apply(function() { if (ev.event === 'delete') { if (ctrl.contactList.length === 1) { $route.updateParams({ @@ -72,7 +82,7 @@ angular.module('contactsApp') }); } ctrl.contacts = ev.contacts; - }); + }); }); }); // Get contacts @@ -86,7 +96,43 @@ angular.module('contactsApp') } }); - // Wait for ctrl.contactList to be updated, load the first contact and kill the watch + var getVisibleNames = function getVisibleNames() { + function isScrolledIntoView(el) { + var elemTop = el.getBoundingClientRect().top; + var elemBottom = el.getBoundingClientRect().bottom; + + var bothAboveViewport = elemBottom < 0; + var bothBelowViewPort = elemTop > window.innerHeight; + var isVisible = !bothAboveViewport && !bothBelowViewPort; + return isVisible; + } + + var elements = Array.prototype.slice.call(document.querySelectorAll('.contact__icon:not(.ng-hide)')); + var names = elements + .filter(isScrolledIntoView) + .map(function (el) { + var siblings = Array.prototype.slice.call(el.parentElement.children); + var nameElement = siblings.find(function (sibling) { + return sibling.getAttribute('class').indexOf('content-list-item-line-one') !== -1; + }); + return nameElement.innerText; + + }); + return names; + }; + + var timeoutId = null; + document.querySelector('.app-content-list').addEventListener('scroll', function () { + clearTimeout(timeoutId); + timeoutId = setTimeout(function () { + var names = getVisibleNames(); + ContactService.getFullContacts(names); + }, 250); + }); + + // Wait for ctrl.contactList to be updated, load the contact requested in the URL if any, and + // load full details for the probably initially visible contacts. + // Then kill the watch. var unbindListWatch = $scope.$watch('ctrl.contactList', function() { if(ctrl.contactList && ctrl.contactList.length > 0) { // Check if a specific uid is requested @@ -102,6 +148,8 @@ angular.module('contactsApp') if(ctrl.loading && $(window).width() > 768) { ctrl.setSelectedId(ctrl.contactList[0].uid()); } + var firstNames = ctrl.contactList.slice(0, 20).map(function (c) { return c.displayName(); }); + ContactService.getFullContacts(firstNames); ctrl.loading = false; unbindListWatch(); } @@ -142,6 +190,7 @@ angular.module('contactsApp') $scope.$watch('ctrl.routeParams.gid', function() { // we might have to wait until ng-repeat filled the contactList ctrl.contactList = []; + ctrl.resetLimitTo(); // not in mobile mode if($(window).width() > 768) { // watch for next contactList update diff --git a/js/components/group/group_directive.js b/js/components/group/group_directive.js index 3aa5a99fc..c1dfdeba5 100644 --- a/js/components/group/group_directive.js +++ b/js/components/group/group_directive.js @@ -6,7 +6,8 @@ angular.module('contactsApp') controller: 'groupCtrl', controllerAs: 'ctrl', bindToController: { - group: '=data' + group: '=groupName', + groupCount: '=groupCount' }, templateUrl: OC.linkTo('contacts', 'templates/group.html') }; diff --git a/js/components/groupList/groupList_controller.js b/js/components/groupList/groupList_controller.js index 92548017e..417f60a71 100644 --- a/js/components/groupList/groupList_controller.js +++ b/js/components/groupList/groupList_controller.js @@ -2,12 +2,10 @@ angular.module('contactsApp') .controller('grouplistCtrl', function($scope, ContactService, SearchService, $routeParams) { var ctrl = this; - var initialGroups = [t('contacts', 'All contacts'), t('contacts', 'Not grouped')]; + ctrl.groups = []; - ctrl.groups = initialGroups; - - ContactService.getGroups().then(function(groups) { - ctrl.groups = _.unique(initialGroups.concat(groups)); + ContactService.getGroupList().then(function(groups) { + ctrl.groups = groups; }); ctrl.getSelected = function() { @@ -15,12 +13,14 @@ angular.module('contactsApp') }; // Update groupList on contact add/delete/update - ContactService.registerObserverCallback(function() { - $scope.$apply(function() { - ContactService.getGroups().then(function(groups) { - ctrl.groups = _.unique(initialGroups.concat(groups)); + ContactService.registerObserverCallback(function(ev) { + if (ev.event !== 'getFullContacts') { + $scope.$apply(function() { + ContactService.getGroupList().then(function(groups) { + ctrl.groups = groups; + }); }); - }); + } }); ctrl.setSelected = function (selectedGroup) { diff --git a/js/components/newContactButton/newContactButton_controller.js b/js/components/newContactButton/newContactButton_controller.js index 9566974c6..c38d8a78c 100644 --- a/js/components/newContactButton/newContactButton_controller.js +++ b/js/components/newContactButton/newContactButton_controller.js @@ -13,9 +13,9 @@ angular.module('contactsApp') contact.addProperty(field, defaultValue); } ); if ([t('contacts', 'All contacts'), t('contacts', 'Not grouped')].indexOf($routeParams.gid) === -1) { - contact.categories($routeParams.gid); + contact.categories([ $routeParams.gid ]); } else { - contact.categories(''); + contact.categories([]); } $('#details-fullName').focus(); }); diff --git a/js/dav/dav.js b/js/dav/dav.js index 0e46f394e..c01369fb8 100644 --- a/js/dav/dav.js +++ b/js/dav/dav.js @@ -938,7 +938,7 @@ exports.createAccount = _co2['default'].wrap(regeneratorRuntime.mark(function ca })); // http redirect. -},{"./calendars":2,"./contacts":5,"./debug":6,"./fuzzy_url_equals":7,"./model":9,"./namespace":10,"./request":12,"co":26,"url":31}],2:[function(require,module,exports){ +},{"./calendars":2,"./contacts":5,"./debug":6,"./fuzzy_url_equals":7,"./model":9,"./namespace":10,"./request":12,"co":32,"url":31}],2:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, '__esModule', { @@ -1329,7 +1329,7 @@ var webdavSync = _co2['default'].wrap(regeneratorRuntime.mark(function callee$0$ } }, callee$0$0, this); })); -},{"./debug":6,"./fuzzy_url_equals":7,"./model":9,"./namespace":10,"./request":12,"./webdav":24,"co":26,"url":31}],3:[function(require,module,exports){ +},{"./debug":6,"./fuzzy_url_equals":7,"./model":9,"./namespace":10,"./request":12,"./webdav":25,"co":32,"url":31}],3:[function(require,module,exports){ /** * @fileoverview Camelcase something. */ @@ -1531,6 +1531,14 @@ var Client = (function () { options.xhr = options.xhr || this.xhr; return contacts.deleteCard(card, options); } + }, { + key: 'getContacts', + value: function getContacts(addressBook, options, hrefs) { + if (options === undefined) options = {}; + + options.xhr = options.xhr || this.xhr; + return contacts.getContacts(addressBook, options, hrefs); + } }, { key: 'syncAddressBook', value: function syncAddressBook(addressBook) { @@ -1743,28 +1751,71 @@ function createCard(addressBook, options) { return webdav.createObject(objectUrl, options.data, options); } +var getFullVcards = _co2['default'].wrap(regeneratorRuntime.mark(function callee$0$0(addressBook, options, hrefs) { + var req, responses; + return regeneratorRuntime.wrap(function callee$0$0$(context$1$0) { + while (1) switch (context$1$0.prev = context$1$0.next) { + case 0: + req = request.addressBookMultiget({ + depth: 1, + props: [{ name: 'getetag', namespace: ns.DAV }, { name: 'address-data', namespace: ns.CARDDAV }], + hrefs: hrefs + }); + context$1$0.next = 3; + return options.xhr.send(req, addressBook.url, { + sandbox: options.sandbox + }); + + case 3: + responses = context$1$0.sent; + return context$1$0.abrupt('return', responses.map(function (res) { + debug('Found vcard with url ' + res.href); + return new _model.VCard({ + data: res, + addressBook: addressBook, + url: _url2['default'].resolve(addressBook.account.rootUrl, res.href), + etag: res.props.getetag, + addressData: res.props.addressData + }); + })); + + case 5: + case 'end': + return context$1$0.stop(); + } + }, callee$0$0, this); +})); + +exports.getFullVcards = getFullVcards; /** * Options: * * (dav.Sandbox) sandbox - optional request sandbox. */ var listVCards = _co2['default'].wrap(regeneratorRuntime.mark(function callee$0$0(addressBook, options) { - var req, responses; + var vCardListFields, req, responses; return regeneratorRuntime.wrap(function callee$0$0$(context$1$0) { while (1) switch (context$1$0.prev = context$1$0.next) { case 0: debug('Doing REPORT on address book ' + addressBook.url + ' which belongs to\n ' + addressBook.account.credentials.username); + vCardListFields = ['EMAIL', 'UID', 'CATEGORIES', 'FN', 'TEL', 'NICKNAME'].map(function (value) { + return { + name: 'prop', + namespace: ns.CARDDAV, + attrs: [{ name: 'name', value: value }] + }; + }); req = request.addressBookQuery({ depth: 1, - props: [{ name: 'getetag', namespace: ns.DAV }, { name: 'address-data', namespace: ns.CARDDAV }] + props: [{ name: 'getetag', namespace: ns.DAV }, { name: 'address-data', namespace: ns.CARDDAV, children: vCardListFields }] }); - context$1$0.next = 4; + context$1$0.next = 5; return options.xhr.send(req, addressBook.url, { sandbox: options.sandbox }); - case 4: + case 5: responses = context$1$0.sent; return context$1$0.abrupt('return', responses.map(function (res) { debug('Found vcard with url ' + res.href); @@ -1777,7 +1828,7 @@ var listVCards = _co2['default'].wrap(regeneratorRuntime.mark(function callee$0$ }); })); - case 6: + case 7: case 'end': return context$1$0.stop(); } @@ -1907,6 +1958,9 @@ var syncCarddavAccount = _co2['default'].wrap(regeneratorRuntime.mark(function c })); exports.syncCarddavAccount = syncCarddavAccount; +var getContacts = getFullVcards; + +exports.getContacts = getContacts; var basicSync = _co2['default'].wrap(regeneratorRuntime.mark(function callee$0$0(addressBook, options) { var sync; return regeneratorRuntime.wrap(function callee$0$0$(context$1$0) { @@ -1979,7 +2033,7 @@ var webdavSync = _co2['default'].wrap(regeneratorRuntime.mark(function callee$0$ } }, callee$0$0, this); })); -},{"./debug":6,"./fuzzy_url_equals":7,"./model":9,"./namespace":10,"./request":12,"./webdav":24,"co":26,"url":31}],6:[function(require,module,exports){ +},{"./debug":6,"./fuzzy_url_equals":7,"./model":9,"./namespace":10,"./request":12,"./webdav":25,"co":32,"url":31}],6:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -2109,7 +2163,7 @@ exports.debug = _debug2['default']; exports.ns = ns; exports.request = request; exports.transport = transport; -},{"../package":35,"./accounts":1,"./calendars":2,"./client":4,"./contacts":5,"./debug":6,"./model":9,"./namespace":10,"./request":12,"./sandbox":13,"./transport":23}],9:[function(require,module,exports){ +},{"../package":36,"./accounts":1,"./calendars":2,"./client":4,"./contacts":5,"./debug":6,"./model":9,"./namespace":10,"./request":12,"./sandbox":13,"./transport":24}],9:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -2493,13 +2547,14 @@ function children(node, localName) { function child(node, localName) { return children(node, localName)[0]; } -},{"./camelize":3,"./debug":6,"xmldom":32}],12:[function(require,module,exports){ +},{"./camelize":3,"./debug":6,"xmldom":33}],12:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); exports.addressBookQuery = addressBookQuery; +exports.addressBookMultiget = addressBookMultiget; exports.basic = basic; exports.calendarQuery = calendarQuery; exports.collectionQuery = collectionQuery; @@ -2532,6 +2587,10 @@ function addressBookQuery(options) { return collectionQuery(template.addressBookQuery({ props: options.props || [] }), { depth: options.depth }); } +function addressBookMultiget(options) { + return collectionQuery(template.addressBookMultiget({ props: options.props || [], hrefs: options.hrefs || [] }), { depth: options.depth }); +} + /** * Options: * @@ -2758,7 +2817,7 @@ function setRequestHeaders(request, options) { request.setRequestHeader('Overwrite', options.overwrite); } } -},{"./parser":11,"./template":17}],13:[function(require,module,exports){ +},{"./parser":11,"./template":18}],13:[function(require,module,exports){ /** * @fileoverview Group requests together and then abort as a group. * @@ -2818,25 +2877,48 @@ function createSandbox() { return new Sandbox(); } },{"./debug":6}],14:[function(require,module,exports){ -'use strict'; +"use strict"; -Object.defineProperty(exports, '__esModule', { +Object.defineProperty(exports, "__esModule", { value: true }); -exports['default'] = addressBookQuery; +exports["default"] = addressBookMultiget; -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } + +var _prop = require('./prop'); + +var _prop2 = _interopRequireDefault(_prop); + +function href(href) { + return "" + href + ""; +} + +function addressBookMultiget(object) { + return "\n \n " + object.props.map(_prop2["default"]).join("") + "\n \n " + object.hrefs.map(href).join("") + "\n "; +} + +module.exports = exports["default"]; +},{"./prop":20}],15:[function(require,module,exports){ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = addressBookQuery; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } var _prop = require('./prop'); var _prop2 = _interopRequireDefault(_prop); function addressBookQuery(object) { - return '\n \n ' + object.props.map(_prop2['default']) + '\n \n \n '; + return "\n \n " + object.props.map(_prop2["default"]).join("") + "\n \n \n "; } -module.exports = exports['default']; -},{"./prop":19}],15:[function(require,module,exports){ +module.exports = exports["default"]; +},{"./prop":20}],16:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, '__esModule', { @@ -2859,7 +2941,7 @@ function calendarQuery(object) { } module.exports = exports['default']; -},{"./filter":16,"./prop":19}],16:[function(require,module,exports){ +},{"./filter":17,"./prop":20}],17:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, '__esModule', { @@ -2886,16 +2968,17 @@ function formatAttrs(attrs) { }).join(' '); } module.exports = exports['default']; -},{}],17:[function(require,module,exports){ +},{}],18:[function(require,module,exports){ 'use strict'; exports.addressBookQuery = require('./address_book_query'); +exports.addressBookMultiget = require('./address_book_multiget'); exports.calendarQuery = require('./calendar_query'); exports.propfind = require('./propfind'); exports.syncCollection = require('./sync_collection'); exports.mkcol = require('./mkcol'); exports.proppatch = require('./proppatch'); -},{"./address_book_query":14,"./calendar_query":15,"./mkcol":18,"./propfind":20,"./proppatch":21,"./sync_collection":22}],18:[function(require,module,exports){ +},{"./address_book_multiget":14,"./address_book_query":15,"./calendar_query":16,"./mkcol":19,"./propfind":21,"./proppatch":22,"./sync_collection":23}],19:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, '__esModule', { @@ -2914,7 +2997,7 @@ function mkcol(object) { } module.exports = exports['default']; -},{"./prop":19}],19:[function(require,module,exports){ +},{"./prop":20}],20:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, '__esModule', { @@ -2965,15 +3048,23 @@ var ns = _interopRequireWildcard(_namespace); */ function prop(item) { + var tagName = xmlnsPrefix(item.namespace) + ':' + item.name; + var attrs = (item.attrs || []).map(makeAttr).join(' '); if (!item.children || !item.children.length) { if (typeof item.value === "undefined") { - return '<' + xmlnsPrefix(item.namespace) + ':' + item.name + ' />'; + return '<' + tagName + ' ' + attrs + '/>'; } - return '<' + xmlnsPrefix(item.namespace) + ':' + item.name + '>' + item.value + ''; + return '<' + tagName + ' ' + attrs + '>' + item.value + ''; } var children = item.children.map(prop); - return '<' + xmlnsPrefix(item.namespace) + ':' + item.name + '>\n ' + children + '\n '; + return '<' + tagName + ' ' + attrs + '>\n ' + children.join('') + '\n '; +} + +function makeAttr(attr) { + if (!attr.name) return ''; + if (!attr.value) return attr.name; + return attr.name + '="' + attr.value + '"'; } function xmlnsPrefix(namespace) { @@ -2993,7 +3084,7 @@ function xmlnsPrefix(namespace) { } } module.exports = exports['default']; -},{"../namespace":10}],20:[function(require,module,exports){ +},{"../namespace":10}],21:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, '__esModule', { @@ -3012,7 +3103,7 @@ function propfind(object) { } module.exports = exports['default']; -},{"./prop":19}],21:[function(require,module,exports){ +},{"./prop":20}],22:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, '__esModule', { @@ -3031,7 +3122,7 @@ function proppatch(object) { } module.exports = exports['default']; -},{"./prop":19}],22:[function(require,module,exports){ +},{"./prop":20}],23:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, '__esModule', { @@ -3050,7 +3141,7 @@ function syncCollection(object) { } module.exports = exports['default']; -},{"./prop":19}],23:[function(require,module,exports){ +},{"./prop":20}],24:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, '__esModule', { @@ -3363,7 +3454,7 @@ var refreshAccessToken = _co2['default'].wrap(regeneratorRuntime.mark(function c } }, callee$0$0, this); })); -},{"./xmlhttprequest":25,"co":26,"querystring":30}],24:[function(require,module,exports){ +},{"./xmlhttprequest":26,"co":32,"querystring":30}],25:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, '__esModule', { @@ -3545,7 +3636,7 @@ var isCollectionDirty = _co2['default'].wrap(regeneratorRuntime.mark(function ca }, callee$0$0, this); })); exports.isCollectionDirty = isCollectionDirty; -},{"./debug":6,"./fuzzy_url_equals":7,"./namespace":10,"./request":12,"co":26}],25:[function(require,module,exports){ +},{"./debug":6,"./fuzzy_url_equals":7,"./namespace":10,"./request":12,"co":32}],26:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, '__esModule', { @@ -3669,358 +3760,119 @@ var XMLHttpRequest = (function () { exports['default'] = XMLHttpRequest; module.exports = exports['default']; -},{"./debug":6}],26:[function(require,module,exports){ - -/** - * slice() reference. - */ - -var slice = Array.prototype.slice; - -/** - * Expose `co`. - */ +},{"./debug":6}],27:[function(require,module,exports){ +(function (global){ +/*! https://mths.be/punycode v1.4.1 by @mathias */ +;(function(root) { -module.exports = co['default'] = co.co = co; + /** Detect free variables */ + var freeExports = typeof exports == 'object' && exports && + !exports.nodeType && exports; + var freeModule = typeof module == 'object' && module && + !module.nodeType && module; + var freeGlobal = typeof global == 'object' && global; + if ( + freeGlobal.global === freeGlobal || + freeGlobal.window === freeGlobal || + freeGlobal.self === freeGlobal + ) { + root = freeGlobal; + } -/** - * Wrap the given generator `fn` into a - * function that returns a promise. - * This is a separate function so that - * every `co()` call doesn't create a new, - * unnecessary closure. - * - * @param {GeneratorFunction} fn - * @return {Function} - * @api public - */ + /** + * The `punycode` object. + * @name punycode + * @type Object + */ + var punycode, -co.wrap = function (fn) { - createPromise.__generatorFunction__ = fn; - return createPromise; - function createPromise() { - return co.call(this, fn.apply(this, arguments)); - } -}; + /** Highest positive signed 32-bit float value */ + maxInt = 2147483647, // aka. 0x7FFFFFFF or 2^31-1 -/** - * Execute the generator function or a generator - * and return a promise. - * - * @param {Function} fn - * @return {Promise} - * @api public - */ + /** Bootstring parameters */ + base = 36, + tMin = 1, + tMax = 26, + skew = 38, + damp = 700, + initialBias = 72, + initialN = 128, // 0x80 + delimiter = '-', // '\x2D' -function co(gen) { - var ctx = this; - var args = slice.call(arguments, 1) + /** Regular expressions */ + regexPunycode = /^xn--/, + regexNonASCII = /[^\x20-\x7E]/, // unprintable ASCII chars + non-ASCII chars + regexSeparators = /[\x2E\u3002\uFF0E\uFF61]/g, // RFC 3490 separators - // we wrap everything in a promise to avoid promise chaining, - // which leads to memory leak errors. - // see https://github.com/tj/co/issues/180 - return new Promise(function(resolve, reject) { - if (typeof gen === 'function') gen = gen.apply(ctx, args); - if (!gen || typeof gen.next !== 'function') return resolve(gen); + /** Error messages */ + errors = { + 'overflow': 'Overflow: input needs wider integers to process', + 'not-basic': 'Illegal input >= 0x80 (not a basic code point)', + 'invalid-input': 'Invalid input' + }, - onFulfilled(); + /** Convenience shortcuts */ + baseMinusTMin = base - tMin, + floor = Math.floor, + stringFromCharCode = String.fromCharCode, - /** - * @param {Mixed} res - * @return {Promise} - * @api private - */ + /** Temporary variable */ + key; - function onFulfilled(res) { - var ret; - try { - ret = gen.next(res); - } catch (e) { - return reject(e); - } - next(ret); - } + /*--------------------------------------------------------------------------*/ - /** - * @param {Error} err - * @return {Promise} - * @api private - */ + /** + * A generic error utility function. + * @private + * @param {String} type The error type. + * @returns {Error} Throws a `RangeError` with the applicable error message. + */ + function error(type) { + throw new RangeError(errors[type]); + } - function onRejected(err) { - var ret; - try { - ret = gen.throw(err); - } catch (e) { - return reject(e); - } - next(ret); - } + /** + * A generic `Array#map` utility function. + * @private + * @param {Array} array The array to iterate over. + * @param {Function} callback The function that gets called for every array + * item. + * @returns {Array} A new array of values returned by the callback function. + */ + function map(array, fn) { + var length = array.length; + var result = []; + while (length--) { + result[length] = fn(array[length]); + } + return result; + } - /** - * Get the next value in the generator, - * return a promise. - * - * @param {Object} ret - * @return {Promise} - * @api private - */ - - function next(ret) { - if (ret.done) return resolve(ret.value); - var value = toPromise.call(ctx, ret.value); - if (value && isPromise(value)) return value.then(onFulfilled, onRejected); - return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, ' - + 'but the following object was passed: "' + String(ret.value) + '"')); - } - }); -} - -/** - * Convert a `yield`ed value into a promise. - * - * @param {Mixed} obj - * @return {Promise} - * @api private - */ - -function toPromise(obj) { - if (!obj) return obj; - if (isPromise(obj)) return obj; - if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj); - if ('function' == typeof obj) return thunkToPromise.call(this, obj); - if (Array.isArray(obj)) return arrayToPromise.call(this, obj); - if (isObject(obj)) return objectToPromise.call(this, obj); - return obj; -} - -/** - * Convert a thunk to a promise. - * - * @param {Function} - * @return {Promise} - * @api private - */ - -function thunkToPromise(fn) { - var ctx = this; - return new Promise(function (resolve, reject) { - fn.call(ctx, function (err, res) { - if (err) return reject(err); - if (arguments.length > 2) res = slice.call(arguments, 1); - resolve(res); - }); - }); -} - -/** - * Convert an array of "yieldables" to a promise. - * Uses `Promise.all()` internally. - * - * @param {Array} obj - * @return {Promise} - * @api private - */ - -function arrayToPromise(obj) { - return Promise.all(obj.map(toPromise, this)); -} - -/** - * Convert an object of "yieldables" to a promise. - * Uses `Promise.all()` internally. - * - * @param {Object} obj - * @return {Promise} - * @api private - */ - -function objectToPromise(obj){ - var results = new obj.constructor(); - var keys = Object.keys(obj); - var promises = []; - for (var i = 0; i < keys.length; i++) { - var key = keys[i]; - var promise = toPromise.call(this, obj[key]); - if (promise && isPromise(promise)) defer(promise, key); - else results[key] = obj[key]; - } - return Promise.all(promises).then(function () { - return results; - }); - - function defer(promise, key) { - // predefine the key in the result - results[key] = undefined; - promises.push(promise.then(function (res) { - results[key] = res; - })); - } -} - -/** - * Check if `obj` is a promise. - * - * @param {Object} obj - * @return {Boolean} - * @api private - */ - -function isPromise(obj) { - return 'function' == typeof obj.then; -} - -/** - * Check if `obj` is a generator. - * - * @param {Mixed} obj - * @return {Boolean} - * @api private - */ - -function isGenerator(obj) { - return 'function' == typeof obj.next && 'function' == typeof obj.throw; -} - -/** - * Check if `obj` is a generator function. - * - * @param {Mixed} obj - * @return {Boolean} - * @api private - */ -function isGeneratorFunction(obj) { - var constructor = obj.constructor; - if (!constructor) return false; - if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true; - return isGenerator(constructor.prototype); -} - -/** - * Check for plain object. - * - * @param {Mixed} val - * @return {Boolean} - * @api private - */ - -function isObject(val) { - return Object == val.constructor; -} - -},{}],27:[function(require,module,exports){ -(function (global){ -/*! https://mths.be/punycode v1.4.1 by @mathias */ -;(function(root) { - - /** Detect free variables */ - var freeExports = typeof exports == 'object' && exports && - !exports.nodeType && exports; - var freeModule = typeof module == 'object' && module && - !module.nodeType && module; - var freeGlobal = typeof global == 'object' && global; - if ( - freeGlobal.global === freeGlobal || - freeGlobal.window === freeGlobal || - freeGlobal.self === freeGlobal - ) { - root = freeGlobal; - } - - /** - * The `punycode` object. - * @name punycode - * @type Object - */ - var punycode, - - /** Highest positive signed 32-bit float value */ - maxInt = 2147483647, // aka. 0x7FFFFFFF or 2^31-1 - - /** Bootstring parameters */ - base = 36, - tMin = 1, - tMax = 26, - skew = 38, - damp = 700, - initialBias = 72, - initialN = 128, // 0x80 - delimiter = '-', // '\x2D' - - /** Regular expressions */ - regexPunycode = /^xn--/, - regexNonASCII = /[^\x20-\x7E]/, // unprintable ASCII chars + non-ASCII chars - regexSeparators = /[\x2E\u3002\uFF0E\uFF61]/g, // RFC 3490 separators - - /** Error messages */ - errors = { - 'overflow': 'Overflow: input needs wider integers to process', - 'not-basic': 'Illegal input >= 0x80 (not a basic code point)', - 'invalid-input': 'Invalid input' - }, - - /** Convenience shortcuts */ - baseMinusTMin = base - tMin, - floor = Math.floor, - stringFromCharCode = String.fromCharCode, - - /** Temporary variable */ - key; - - /*--------------------------------------------------------------------------*/ - - /** - * A generic error utility function. - * @private - * @param {String} type The error type. - * @returns {Error} Throws a `RangeError` with the applicable error message. - */ - function error(type) { - throw new RangeError(errors[type]); - } - - /** - * A generic `Array#map` utility function. - * @private - * @param {Array} array The array to iterate over. - * @param {Function} callback The function that gets called for every array - * item. - * @returns {Array} A new array of values returned by the callback function. - */ - function map(array, fn) { - var length = array.length; - var result = []; - while (length--) { - result[length] = fn(array[length]); - } - return result; - } - - /** - * A simple `Array#map`-like wrapper to work with domain name strings or email - * addresses. - * @private - * @param {String} domain The domain name or email address. - * @param {Function} callback The function that gets called for every - * character. - * @returns {Array} A new string of characters returned by the callback - * function. - */ - function mapDomain(string, fn) { - var parts = string.split('@'); - var result = ''; - if (parts.length > 1) { - // In email addresses, only the domain name should be punycoded. Leave - // the local part (i.e. everything up to `@`) intact. - result = parts[0] + '@'; - string = parts[1]; - } - // Avoid `split(regex)` for IE8 compatibility. See #17. - string = string.replace(regexSeparators, '\x2E'); - var labels = string.split('.'); - var encoded = map(labels, fn).join('.'); - return result + encoded; - } + /** + * A simple `Array#map`-like wrapper to work with domain name strings or email + * addresses. + * @private + * @param {String} domain The domain name or email address. + * @param {Function} callback The function that gets called for every + * character. + * @returns {Array} A new string of characters returned by the callback + * function. + */ + function mapDomain(string, fn) { + var parts = string.split('@'); + var result = ''; + if (parts.length > 1) { + // In email addresses, only the domain name should be punycoded. Leave + // the local part (i.e. everything up to `@`) intact. + result = parts[0] + '@'; + string = parts[1]; + } + // Avoid `split(regex)` for IE8 compatibility. See #17. + string = string.replace(regexSeparators, '\x2E'); + var labels = string.split('.'); + var encoded = map(labels, fn).join('.'); + return result + encoded; + } /** * Creates an array containing the numeric code points of each Unicode @@ -5247,98 +5099,337 @@ Url.prototype.resolveObject = function(relative) { } } - // if the path is allowed to go above the root, restore leading ..s - if (!mustEndAbs && !removeAllDots) { - for (; up--; up) { - srcPath.unshift('..'); + // if the path is allowed to go above the root, restore leading ..s + if (!mustEndAbs && !removeAllDots) { + for (; up--; up) { + srcPath.unshift('..'); + } + } + + if (mustEndAbs && srcPath[0] !== '' && + (!srcPath[0] || srcPath[0].charAt(0) !== '/')) { + srcPath.unshift(''); + } + + if (hasTrailingSlash && (srcPath.join('/').substr(-1) !== '/')) { + srcPath.push(''); + } + + var isAbsolute = srcPath[0] === '' || + (srcPath[0] && srcPath[0].charAt(0) === '/'); + + // put the host back + if (psychotic) { + result.hostname = result.host = isAbsolute ? '' : + srcPath.length ? srcPath.shift() : ''; + //occationaly the auth can get stuck only in host + //this especialy happens in cases like + //url.resolveObject('mailto:local1@domain1', 'local2@domain2') + var authInHost = result.host && result.host.indexOf('@') > 0 ? + result.host.split('@') : false; + if (authInHost) { + result.auth = authInHost.shift(); + result.host = result.hostname = authInHost.shift(); + } + } + + mustEndAbs = mustEndAbs || (result.host && srcPath.length); + + if (mustEndAbs && !isAbsolute) { + srcPath.unshift(''); + } + + if (!srcPath.length) { + result.pathname = null; + result.path = null; + } else { + result.pathname = srcPath.join('/'); + } + + //to support request.http + if (!isNull(result.pathname) || !isNull(result.search)) { + result.path = (result.pathname ? result.pathname : '') + + (result.search ? result.search : ''); + } + result.auth = relative.auth || result.auth; + result.slashes = result.slashes || relative.slashes; + result.href = result.format(); + return result; +}; + +Url.prototype.parseHost = function() { + var host = this.host; + var port = portPattern.exec(host); + if (port) { + port = port[0]; + if (port !== ':') { + this.port = port.substr(1); + } + host = host.substr(0, host.length - port.length); + } + if (host) this.hostname = host; +}; + +function isString(arg) { + return typeof arg === "string"; +} + +function isObject(arg) { + return typeof arg === 'object' && arg !== null; +} + +function isNull(arg) { + return arg === null; +} +function isNullOrUndefined(arg) { + return arg == null; +} + +},{"punycode":27,"querystring":30}],32:[function(require,module,exports){ + +/** + * slice() reference. + */ + +var slice = Array.prototype.slice; + +/** + * Expose `co`. + */ + +module.exports = co['default'] = co.co = co; + +/** + * Wrap the given generator `fn` into a + * function that returns a promise. + * This is a separate function so that + * every `co()` call doesn't create a new, + * unnecessary closure. + * + * @param {GeneratorFunction} fn + * @return {Function} + * @api public + */ + +co.wrap = function (fn) { + createPromise.__generatorFunction__ = fn; + return createPromise; + function createPromise() { + return co.call(this, fn.apply(this, arguments)); + } +}; + +/** + * Execute the generator function or a generator + * and return a promise. + * + * @param {Function} fn + * @return {Promise} + * @api public + */ + +function co(gen) { + var ctx = this; + var args = slice.call(arguments, 1) + + // we wrap everything in a promise to avoid promise chaining, + // which leads to memory leak errors. + // see https://github.com/tj/co/issues/180 + return new Promise(function(resolve, reject) { + if (typeof gen === 'function') gen = gen.apply(ctx, args); + if (!gen || typeof gen.next !== 'function') return resolve(gen); + + onFulfilled(); + + /** + * @param {Mixed} res + * @return {Promise} + * @api private + */ + + function onFulfilled(res) { + var ret; + try { + ret = gen.next(res); + } catch (e) { + return reject(e); + } + next(ret); + } + + /** + * @param {Error} err + * @return {Promise} + * @api private + */ + + function onRejected(err) { + var ret; + try { + ret = gen.throw(err); + } catch (e) { + return reject(e); + } + next(ret); + } + + /** + * Get the next value in the generator, + * return a promise. + * + * @param {Object} ret + * @return {Promise} + * @api private + */ + + function next(ret) { + if (ret.done) return resolve(ret.value); + var value = toPromise.call(ctx, ret.value); + if (value && isPromise(value)) return value.then(onFulfilled, onRejected); + return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, ' + + 'but the following object was passed: "' + String(ret.value) + '"')); } - } + }); +} - if (mustEndAbs && srcPath[0] !== '' && - (!srcPath[0] || srcPath[0].charAt(0) !== '/')) { - srcPath.unshift(''); - } +/** + * Convert a `yield`ed value into a promise. + * + * @param {Mixed} obj + * @return {Promise} + * @api private + */ - if (hasTrailingSlash && (srcPath.join('/').substr(-1) !== '/')) { - srcPath.push(''); - } +function toPromise(obj) { + if (!obj) return obj; + if (isPromise(obj)) return obj; + if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj); + if ('function' == typeof obj) return thunkToPromise.call(this, obj); + if (Array.isArray(obj)) return arrayToPromise.call(this, obj); + if (isObject(obj)) return objectToPromise.call(this, obj); + return obj; +} - var isAbsolute = srcPath[0] === '' || - (srcPath[0] && srcPath[0].charAt(0) === '/'); +/** + * Convert a thunk to a promise. + * + * @param {Function} + * @return {Promise} + * @api private + */ - // put the host back - if (psychotic) { - result.hostname = result.host = isAbsolute ? '' : - srcPath.length ? srcPath.shift() : ''; - //occationaly the auth can get stuck only in host - //this especialy happens in cases like - //url.resolveObject('mailto:local1@domain1', 'local2@domain2') - var authInHost = result.host && result.host.indexOf('@') > 0 ? - result.host.split('@') : false; - if (authInHost) { - result.auth = authInHost.shift(); - result.host = result.hostname = authInHost.shift(); - } - } +function thunkToPromise(fn) { + var ctx = this; + return new Promise(function (resolve, reject) { + fn.call(ctx, function (err, res) { + if (err) return reject(err); + if (arguments.length > 2) res = slice.call(arguments, 1); + resolve(res); + }); + }); +} - mustEndAbs = mustEndAbs || (result.host && srcPath.length); +/** + * Convert an array of "yieldables" to a promise. + * Uses `Promise.all()` internally. + * + * @param {Array} obj + * @return {Promise} + * @api private + */ - if (mustEndAbs && !isAbsolute) { - srcPath.unshift(''); - } +function arrayToPromise(obj) { + return Promise.all(obj.map(toPromise, this)); +} - if (!srcPath.length) { - result.pathname = null; - result.path = null; - } else { - result.pathname = srcPath.join('/'); - } +/** + * Convert an object of "yieldables" to a promise. + * Uses `Promise.all()` internally. + * + * @param {Object} obj + * @return {Promise} + * @api private + */ - //to support request.http - if (!isNull(result.pathname) || !isNull(result.search)) { - result.path = (result.pathname ? result.pathname : '') + - (result.search ? result.search : ''); +function objectToPromise(obj){ + var results = new obj.constructor(); + var keys = Object.keys(obj); + var promises = []; + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + var promise = toPromise.call(this, obj[key]); + if (promise && isPromise(promise)) defer(promise, key); + else results[key] = obj[key]; } - result.auth = relative.auth || result.auth; - result.slashes = result.slashes || relative.slashes; - result.href = result.format(); - return result; -}; + return Promise.all(promises).then(function () { + return results; + }); -Url.prototype.parseHost = function() { - var host = this.host; - var port = portPattern.exec(host); - if (port) { - port = port[0]; - if (port !== ':') { - this.port = port.substr(1); - } - host = host.substr(0, host.length - port.length); + function defer(promise, key) { + // predefine the key in the result + results[key] = undefined; + promises.push(promise.then(function (res) { + results[key] = res; + })); } - if (host) this.hostname = host; -}; +} -function isString(arg) { - return typeof arg === "string"; +/** + * Check if `obj` is a promise. + * + * @param {Object} obj + * @return {Boolean} + * @api private + */ + +function isPromise(obj) { + return 'function' == typeof obj.then; } -function isObject(arg) { - return typeof arg === 'object' && arg !== null; +/** + * Check if `obj` is a generator. + * + * @param {Mixed} obj + * @return {Boolean} + * @api private + */ + +function isGenerator(obj) { + return 'function' == typeof obj.next && 'function' == typeof obj.throw; } -function isNull(arg) { - return arg === null; +/** + * Check if `obj` is a generator function. + * + * @param {Mixed} obj + * @return {Boolean} + * @api private + */ +function isGeneratorFunction(obj) { + var constructor = obj.constructor; + if (!constructor) return false; + if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true; + return isGenerator(constructor.prototype); } -function isNullOrUndefined(arg) { - return arg == null; + +/** + * Check for plain object. + * + * @param {Mixed} val + * @return {Boolean} + * @api private + */ + +function isObject(val) { + return Object == val.constructor; } -},{"punycode":27,"querystring":30}],32:[function(require,module,exports){ +},{}],33:[function(require,module,exports){ function DOMParser(options){ this.options = options ||{locator:{}}; } -DOMParser.prototype.parseFromString = function(source,mimeType){ +DOMParser.prototype.parseFromString = function(source,mimeType){ var options = this.options; var sax = new XMLReader(); var domBuilder = options.domBuilder || new DOMHandler();//contentHandler and LexicalHandler @@ -5361,9 +5452,9 @@ DOMParser.prototype.parseFromString = function(source,mimeType){ if(source){ sax.parse(source,defaultNSMap,entityMap); }else{ - sax.errorHandler.error("invalid document source"); + sax.errorHandler.error("invalid doc source"); } - return domBuilder.document; + return domBuilder.doc; } function buildErrorHandler(errorImpl,domBuilder,locator){ if(!errorImpl){ @@ -5413,13 +5504,13 @@ function position(locator,node){ */ DOMHandler.prototype = { startDocument : function() { - this.document = new DOMImplementation().createDocument(null, null, null); + this.doc = new DOMImplementation().createDocument(null, null, null); if (this.locator) { - this.document.documentURI = this.locator.systemId; + this.doc.documentURI = this.locator.systemId; } }, startElement:function(namespaceURI, localName, qName, attrs) { - var doc = this.document; + var doc = this.doc; var el = doc.createElementNS(namespaceURI, qName||localName); var len = attrs.length; appendElement(this, el); @@ -5431,24 +5522,22 @@ DOMHandler.prototype = { var value = attrs.getValue(i); var qName = attrs.getQName(i); var attr = doc.createAttributeNS(namespaceURI, qName); - if( attr.getOffset){ - position(attr.getOffset(1),attr) - } + this.locator &&position(attrs.getLocator(i),attr); attr.value = attr.nodeValue = value; el.setAttributeNode(attr) } }, endElement:function(namespaceURI, localName, qName) { var current = this.currentElement - var tagName = current.tagName; - this.currentElement = current.parentNode; + var tagName = current.tagName; + this.currentElement = current.parentNode; }, startPrefixMapping:function(prefix, uri) { }, endPrefixMapping:function(prefix) { }, processingInstruction:function(target, data) { - var ins = this.document.createProcessingInstruction(target, data); + var ins = this.doc.createProcessingInstruction(target, data); this.locator && position(this.locator,ins) appendElement(this, ins); }, @@ -5457,13 +5546,17 @@ DOMHandler.prototype = { characters:function(chars, start, length) { chars = _toString.apply(this,arguments) //console.log(chars) - if(this.currentElement && chars){ + if(chars){ if (this.cdata) { - var charNode = this.document.createCDATASection(chars); - this.currentElement.appendChild(charNode); + var charNode = this.doc.createCDATASection(chars); } else { - var charNode = this.document.createTextNode(chars); + var charNode = this.doc.createTextNode(chars); + } + if(this.currentElement){ this.currentElement.appendChild(charNode); + }else if(/^\s*$/.test(chars)){ + this.doc.appendChild(charNode); + //process xml } this.locator && position(this.locator,charNode) } @@ -5471,7 +5564,7 @@ DOMHandler.prototype = { skippedEntity:function(name) { }, endDocument:function() { - this.document.normalize(); + this.doc.normalize(); }, setDocumentLocator:function (locator) { if(this.locator = locator){// && !('lineNumber' in locator)){ @@ -5481,7 +5574,7 @@ DOMHandler.prototype = { //LexicalHandler comment:function(chars, start, length) { chars = _toString.apply(this,arguments) - var comm = this.document.createComment(chars); + var comm = this.doc.createComment(chars); this.locator && position(this.locator,comm) appendElement(this, comm); }, @@ -5495,7 +5588,7 @@ DOMHandler.prototype = { }, startDTD:function(name, publicId, systemId) { - var impl = this.document.implementation; + var impl = this.doc.implementation; if (impl && impl.createDocumentType) { var dt = impl.createDocumentType(name, publicId, systemId); this.locator && position(this.locator,dt) @@ -5571,20 +5664,20 @@ function _toString(chars,start,length){ /* Private static helpers treated below as private instance methods, so don't need to add these to the public API; we might use a Relator to also get rid of non-standard public properties */ function appendElement (hander,node) { if (!hander.currentElement) { - hander.document.appendChild(node); + hander.doc.appendChild(node); } else { hander.currentElement.appendChild(node); } }//appendChild and setAttributeNS are preformance key -if(typeof require == 'function'){ +//if(typeof require == 'function'){ var XMLReader = require('./sax').XMLReader; var DOMImplementation = exports.DOMImplementation = require('./dom').DOMImplementation; exports.XMLSerializer = require('./dom').XMLSerializer ; exports.DOMParser = DOMParser; -} +//} -},{"./dom":33,"./sax":34}],33:[function(require,module,exports){ +},{"./dom":34,"./sax":35}],34:[function(require,module,exports){ /* * DOM Level 2 * Object DOMException @@ -5697,9 +5790,9 @@ NodeList.prototype = { item: function(index) { return this[index] || null; }, - toString:function(){ + toString:function(isHTML,nodeFilter){ for(var buf = [], i = 0;i=0){ var lastIndex = list.length-1 @@ -5772,7 +5866,7 @@ function _removeNamedNode(el,list,attr){ } } }else{ - throw DOMException(NOT_FOUND_ERR,new Error()) + throw DOMException(NOT_FOUND_ERR,new Error(el.tagName+'@'+attr)) } } NamedNodeMap.prototype = { @@ -5782,9 +5876,11 @@ NamedNodeMap.prototype = { // if(key.indexOf(':')>0 || key == 'xmlns'){ // return null; // } + //console.log() var i = this.length; while(i--){ var attr = this[i]; + //console.log(attr.nodeName,key) if(attr.nodeName == key){ return attr; } @@ -5966,7 +6062,7 @@ Node.prototype = { } } } - el = el.nodeType == 2?el.ownerDocument : el.parentNode; + el = el.nodeType == ATTRIBUTE_NODE?el.ownerDocument : el.parentNode; } return null; }, @@ -5981,7 +6077,7 @@ Node.prototype = { return map[prefix] ; } } - el = el.nodeType == 2?el.ownerDocument : el.parentNode; + el = el.nodeType == ATTRIBUTE_NODE?el.ownerDocument : el.parentNode; } return null; }, @@ -6166,7 +6262,7 @@ Document.prototype = { } return newChild; } - if(this.documentElement == null && newChild.nodeType == 1){ + if(this.documentElement == null && newChild.nodeType == ELEMENT_NODE){ this.documentElement = newChild; } @@ -6186,7 +6282,7 @@ Document.prototype = { getElementById : function(id){ var rtv = null; _visitNode(this.documentElement,function(node){ - if(node.nodeType == 1){ + if(node.nodeType == ELEMENT_NODE){ if(node.getAttribute('id') == id){ rtv = node; return true; @@ -6335,6 +6431,7 @@ Element.prototype = { return this.attributes.setNamedItemNS(newAttr); }, removeAttributeNode : function(oldAttr){ + //console.log(this == oldAttr.ownerElement) return this.attributes.removeNamedItem(oldAttr.nodeName); }, //get real attribute name,and remove it by removeAttributeNode @@ -6379,6 +6476,7 @@ Element.prototype = { } }); return ls; + }); } }; @@ -6410,10 +6508,7 @@ CharacterData.prototype = { }, appendChild:function(newChild){ - //if(!(newChild instanceof CharacterData)){ - throw new Error(ExceptionMessage[3]) - //} - return Node.prototype.appendChild.apply(this,arguments) + throw new Error(ExceptionMessage[HIERARCHY_REQUEST_ERR]) }, deleteData: function(offset, count) { this.replaceData(offset,count,""); @@ -6495,39 +6590,132 @@ function ProcessingInstruction() { ProcessingInstruction.prototype.nodeType = PROCESSING_INSTRUCTION_NODE; _extends(ProcessingInstruction,Node); function XMLSerializer(){} -XMLSerializer.prototype.serializeToString = function(node,attributeSorter){ - return node.toString(attributeSorter); +XMLSerializer.prototype.serializeToString = function(node,isHtml,nodeFilter){ + return nodeSerializeToString.call(node,isHtml,nodeFilter); } -Node.prototype.toString =function(attributeSorter){ +Node.prototype.toString = nodeSerializeToString; +function nodeSerializeToString(isHtml,nodeFilter){ var buf = []; - serializeToString(this,buf,attributeSorter); + var refNode = this.nodeType == 9?this.documentElement:this; + var prefix = refNode.prefix; + var uri = refNode.namespaceURI; + + if(uri && prefix == null){ + //console.log(prefix) + var prefix = refNode.lookupPrefix(uri); + if(prefix == null){ + //isHTML = true; + var visibleNamespaces=[ + {namespace:uri,prefix:null} + //{namespace:uri,prefix:''} + ] + } + } + serializeToString(this,buf,isHtml,nodeFilter,visibleNamespaces); + //console.log('###',this.nodeType,uri,prefix,buf.join('')) return buf.join(''); } -function serializeToString(node,buf,attributeSorter,isHTML){ +function needNamespaceDefine(node,isHTML, visibleNamespaces) { + var prefix = node.prefix||''; + var uri = node.namespaceURI; + if (!prefix && !uri){ + return false; + } + if (prefix === "xml" && uri === "http://www.w3.org/XML/1998/namespace" + || uri == 'http://www.w3.org/2000/xmlns/'){ + return false; + } + + var i = visibleNamespaces.length + //console.log('@@@@',node.tagName,prefix,uri,visibleNamespaces) + while (i--) { + var ns = visibleNamespaces[i]; + // get namespace prefix + //console.log(node.nodeType,node.tagName,ns.prefix,prefix) + if (ns.prefix == prefix){ + return ns.namespace != uri; + } + } + //console.log(isHTML,uri,prefix=='') + //if(isHTML && prefix ==null && uri == 'http://www.w3.org/1999/xhtml'){ + // return false; + //} + //node.flag = '11111' + //console.error(3,true,node.flag,node.prefix,node.namespaceURI) + return true; +} +function serializeToString(node,buf,isHTML,nodeFilter,visibleNamespaces){ + if(nodeFilter){ + node = nodeFilter(node); + if(node){ + if(typeof node == 'string'){ + buf.push(node); + return; + } + }else{ + return; + } + //buf.sort.apply(attrs, attributeSorter); + } switch(node.nodeType){ case ELEMENT_NODE: + if (!visibleNamespaces) visibleNamespaces = []; + var startVisibleNamespaces = visibleNamespaces.length; var attrs = node.attributes; var len = attrs.length; var child = node.firstChild; var nodeName = node.tagName; + isHTML = (htmlns === node.namespaceURI) ||isHTML buf.push('<',nodeName); - if(attributeSorter){ - buf.sort.apply(attrs, attributeSorter); + + + + for(var i=0;i'); //if is cdata child node if(isHTML && /^script$/i.test(nodeName)){ - if(child){ - buf.push(child.data); + while(child){ + if(child.data){ + buf.push(child.data); + }else{ + serializeToString(child,buf,isHTML,nodeFilter,visibleNamespaces); + } + child = child.nextSibling; } - }else{ + }else + { while(child){ - serializeToString(child,buf,attributeSorter,isHTML); + serializeToString(child,buf,isHTML,nodeFilter,visibleNamespaces); child = child.nextSibling; } } @@ -6535,12 +6723,14 @@ function serializeToString(node,buf,attributeSorter,isHTML){ }else{ buf.push('/>'); } + // remove added visible namespaces + //visibleNamespaces.length = startVisibleNamespaces; return; case DOCUMENT_NODE: case DOCUMENT_FRAGMENT_NODE: var child = node.firstChild; while(child){ - serializeToString(child,buf,attributeSorter,isHTML); + serializeToString(child,buf,isHTML,nodeFilter,visibleNamespaces); child = child.nextSibling; } return; @@ -6685,8 +6875,8 @@ try{ }, set:function(data){ switch(this.nodeType){ - case 1: - case 11: + case ELEMENT_NODE: + case DOCUMENT_FRAGMENT_NODE: while(this.firstChild){ this.removeChild(this.firstChild); } @@ -6697,7 +6887,7 @@ try{ default: //TODO: this.data = data; - this.value = value; + this.value = data; this.nodeValue = data; } } @@ -6705,8 +6895,8 @@ try{ function getTextContent(node){ switch(node.nodeType){ - case 1: - case 11: + case ELEMENT_NODE: + case DOCUMENT_FRAGMENT_NODE: var buf = []; node = node.firstChild; while(node){ @@ -6728,31 +6918,31 @@ try{ }catch(e){//ie8 } -if(typeof require == 'function'){ +//if(typeof require == 'function'){ exports.DOMImplementation = DOMImplementation; exports.XMLSerializer = XMLSerializer; -} +//} -},{}],34:[function(require,module,exports){ +},{}],35:[function(require,module,exports){ //[4] NameStartChar ::= ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] | [#x370-#x37D] | [#x37F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF] //[4a] NameChar ::= NameStartChar | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | [#x203F-#x2040] //[5] Name ::= NameStartChar (NameChar)* var nameStartChar = /[A-Z_a-z\xC0-\xD6\xD8-\xF6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]///\u10000-\uEFFFF -var nameChar = new RegExp("[\\-\\.0-9"+nameStartChar.source.slice(1,-1)+"\u00B7\u0300-\u036F\\u203F-\u2040]"); +var nameChar = new RegExp("[\\-\\.0-9"+nameStartChar.source.slice(1,-1)+"\\u00B7\\u0300-\\u036F\\u203F-\\u2040]"); var tagNamePattern = new RegExp('^'+nameStartChar.source+nameChar.source+'*(?:\:'+nameStartChar.source+nameChar.source+'*)?$'); //var tagNamePattern = /^[a-zA-Z_][\w\-\.]*(?:\:[a-zA-Z_][\w\-\.]*)?$/ //var handlers = 'resolveEntity,getExternalSubset,characters,endDocument,endElement,endPrefixMapping,ignorableWhitespace,processingInstruction,setDocumentLocator,skippedEntity,startDocument,startElement,startPrefixMapping,notationDecl,unparsedEntityDecl,error,fatalError,warning,attributeDecl,elementDecl,externalEntityDecl,internalEntityDecl,comment,endCDATA,endDTD,endEntity,startCDATA,startDTD,startEntity'.split(',') -//S_TAG, S_ATTR, S_EQ, S_V -//S_ATTR_S, S_E, S_S, S_C +//S_TAG, S_ATTR, S_EQ, S_ATTR_NOQUOT_VALUE +//S_ATTR_SPACE, S_ATTR_END, S_TAG_SPACE, S_TAG_CLOSE var S_TAG = 0;//tag name offerring var S_ATTR = 1;//attr name offerring -var S_ATTR_S=2;//attr name end and space offer +var S_ATTR_SPACE=2;//attr name end and space offer var S_EQ = 3;//=space? -var S_V = 4;//attr value(no quot value only) -var S_E = 5;//attr value end and no space(quot end) -var S_S = 6;//(attr value end || tag end ) && (space offer) -var S_C = 7;//closed el +var S_ATTR_NOQUOT_VALUE = 4;//attr value(no quot value only) +var S_ATTR_END = 5;//attr value end and no space(quot end) +var S_TAG_SPACE = 6;//(attr value end || tag end ) && (space offer) +var S_TAG_CLOSE = 7;//closed el function XMLReader(){ @@ -6769,7 +6959,7 @@ XMLReader.prototype = { } } function parse(source,defaultNSMapCopy,entityMap,domBuilder,errorHandler){ - function fixedFromCharCode(code) { + function fixedFromCharCode(code) { // String.prototype.fromCharCode does not supports // > 2 bytes unicode chars directly if (code > 0xffff) { @@ -6812,7 +7002,7 @@ function parse(source,defaultNSMapCopy,entityMap,domBuilder,errorHandler){ } var lineStart = 0; var lineEnd = 0; - var linePattern = /.+(?:\r\n?|\n)|.*$/g + var linePattern = /.*(?:\r\n?|\n)|.*$/g var locator = domBuilder.locator; var parseStack = [{currentNSMap:defaultNSMapCopy}] @@ -6823,7 +7013,7 @@ function parse(source,defaultNSMapCopy,entityMap,domBuilder,errorHandler){ var tagStart = source.indexOf('<',start); if(tagStart<0){ if(!source.substr(start).match(/^\s*$/)){ - var doc = domBuilder.document; + var doc = domBuilder.doc; var text = doc.createTextNode(source.substr(start)); doc.appendChild(text); domBuilder.currentElement = text; @@ -6838,16 +7028,36 @@ function parse(source,defaultNSMapCopy,entityMap,domBuilder,errorHandler){ var end = source.indexOf('>',tagStart+3); var tagName = source.substring(tagStart+2,end); var config = parseStack.pop(); + if(end<0){ + + tagName = source.substring(tagStart+2).replace(/[\s<].*/,''); + //console.error('#@@@@@@'+tagName) + errorHandler.error("end tag name: "+tagName+' is not complete:'+config.tagName); + end = tagStart+1+tagName.length; + }else if(tagName.match(/\sstart){ start = end; @@ -6917,7 +7136,7 @@ function copyLocator(f,t){ * @see #appendElement(source,elStartEnd,el,selfClosed,entityReplacer,domBuilder,parseStack); * @return end of the elementStartPart(end of elementEndPart for selfClosed el) */ -function parseElementStartPart(source,start,el,entityReplacer,errorHandler){ +function parseElementStartPart(source,start,el,currentNSMap,entityReplacer,errorHandler){ var attrName; var value; var p = ++start; @@ -6929,7 +7148,7 @@ function parseElementStartPart(source,start,el,entityReplacer,errorHandler){ if(s === S_ATTR){//attrName attrName = source.slice(start,p); s = S_EQ; - }else if(s === S_ATTR_S){ + }else if(s === S_ATTR_SPACE){ s = S_EQ; }else{ //fatalError: equal must after attrName or space after attrName @@ -6938,25 +7157,30 @@ function parseElementStartPart(source,start,el,entityReplacer,errorHandler){ break; case '\'': case '"': - if(s === S_EQ){//equal + if(s === S_EQ || s === S_ATTR //|| s == S_ATTR_SPACE + ){//equal + if(s === S_ATTR){ + errorHandler.warning('attribute value must after "="') + attrName = source.slice(start,p) + } start = p+1; p = source.indexOf(c,start) if(p>0){ value = source.slice(start,p).replace(/&#?\w+;/g,entityReplacer); el.add(attrName,value,start-1); - s = S_E; + s = S_ATTR_END; }else{ //fatalError: no end quot match throw new Error('attribute value no end \''+c+'\' match'); } - }else if(s == S_V){ + }else if(s == S_ATTR_NOQUOT_VALUE){ value = source.slice(start,p).replace(/&#?\w+;/g,entityReplacer); //console.log(attrName,value,start,p) el.add(attrName,value,start); //console.dir(el) errorHandler.warning('attribute "'+attrName+'" missed start quot('+c+')!!'); start = p+1; - s = S_E + s = S_ATTR_END }else{ //fatalError: no equal before throw new Error('attribute value must after "="'); @@ -6966,14 +7190,14 @@ function parseElementStartPart(source,start,el,entityReplacer,errorHandler){ switch(s){ case S_TAG: el.setTagName(source.slice(start,p)); - case S_E: - case S_S: - case S_C: - s = S_C; + case S_ATTR_END: + case S_TAG_SPACE: + case S_TAG_CLOSE: + s =S_TAG_CLOSE; el.closed = true; - case S_V: + case S_ATTR_NOQUOT_VALUE: case S_ATTR: - case S_ATTR_S: + case S_ATTR_SPACE: break; //case S_EQ: default: @@ -6983,30 +7207,36 @@ function parseElementStartPart(source,start,el,entityReplacer,errorHandler){ case ''://end document //throw new Error('unexpected end of input') errorHandler.error('unexpected end of input'); + if(s == S_TAG){ + el.setTagName(source.slice(start,p)); + } + return p; case '>': switch(s){ case S_TAG: el.setTagName(source.slice(start,p)); - case S_E: - case S_S: - case S_C: + case S_ATTR_END: + case S_TAG_SPACE: + case S_TAG_CLOSE: break;//normal - case S_V://Compatible state + case S_ATTR_NOQUOT_VALUE://Compatible state case S_ATTR: value = source.slice(start,p); if(value.slice(-1) === '/'){ el.closed = true; value = value.slice(0,-1) } - case S_ATTR_S: - if(s === S_ATTR_S){ + case S_ATTR_SPACE: + if(s === S_ATTR_SPACE){ value = attrName; } - if(s == S_V){ + if(s == S_ATTR_NOQUOT_VALUE){ errorHandler.warning('attribute "'+value+'" missed quot(")!!'); el.add(attrName,value.replace(/&#?\w+;/g,entityReplacer),start) }else{ - errorHandler.warning('attribute "'+value+'" missed value!! "'+value+'" instead!!') + if(currentNSMap[''] !== 'http://www.w3.org/1999/xhtml' || !value.match(/^(?:disabled|checked|selected)$/i)){ + errorHandler.warning('attribute "'+value+'" missed value!! "'+value+'" instead!!') + } el.add(value,value,start) } break; @@ -7023,64 +7253,68 @@ function parseElementStartPart(source,start,el,entityReplacer,errorHandler){ switch(s){ case S_TAG: el.setTagName(source.slice(start,p));//tagName - s = S_S; + s = S_TAG_SPACE; break; case S_ATTR: attrName = source.slice(start,p) - s = S_ATTR_S; + s = S_ATTR_SPACE; break; - case S_V: + case S_ATTR_NOQUOT_VALUE: var value = source.slice(start,p).replace(/&#?\w+;/g,entityReplacer); errorHandler.warning('attribute "'+value+'" missed quot(")!!'); el.add(attrName,value,start) - case S_E: - s = S_S; + case S_ATTR_END: + s = S_TAG_SPACE; break; - //case S_S: + //case S_TAG_SPACE: //case S_EQ: - //case S_ATTR_S: + //case S_ATTR_SPACE: // void();break; - //case S_C: + //case S_TAG_CLOSE: //ignore warning } }else{//not space -//S_TAG, S_ATTR, S_EQ, S_V -//S_ATTR_S, S_E, S_S, S_C +//S_TAG, S_ATTR, S_EQ, S_ATTR_NOQUOT_VALUE +//S_ATTR_SPACE, S_ATTR_END, S_TAG_SPACE, S_TAG_CLOSE switch(s){ //case S_TAG:void();break; //case S_ATTR:void();break; - //case S_V:void();break; - case S_ATTR_S: - errorHandler.warning('attribute "'+attrName+'" missed value!! "'+attrName+'" instead!!') + //case S_ATTR_NOQUOT_VALUE:void();break; + case S_ATTR_SPACE: + var tagName = el.tagName; + if(currentNSMap[''] !== 'http://www.w3.org/1999/xhtml' || !attrName.match(/^(?:disabled|checked|selected)$/i)){ + errorHandler.warning('attribute "'+attrName+'" missed value!! "'+attrName+'" instead2!!') + } el.add(attrName,attrName,start); start = p; s = S_ATTR; break; - case S_E: + case S_ATTR_END: errorHandler.warning('attribute space is required"'+attrName+'"!!') - case S_S: + case S_TAG_SPACE: s = S_ATTR; start = p; break; case S_EQ: - s = S_V; + s = S_ATTR_NOQUOT_VALUE; start = p; break; - case S_C: + case S_TAG_CLOSE: throw new Error("elements closed character '/' and '>' must be connected to"); } } - } + }//end outer switch + //console.log('p++',p) p++; } } /** - * @return end of the elementStartPart(end of elementEndPart for selfClosed el) + * @return true if has new namespace define */ -function appendElement(el,domBuilder,parseStack){ +function appendElement(el,domBuilder,currentNSMap){ var tagName = el.tagName; var localNSMap = null; - var currentNSMap = parseStack[parseStack.length-1].currentNSMap; + //var currentNSMap = parseStack[parseStack.length-1].currentNSMap; var i = el.length; while(i--){ var a = el[i]; @@ -7119,7 +7353,7 @@ function appendElement(el,domBuilder,parseStack){ if(prefix === 'xml'){ a.uri = 'http://www.w3.org/XML/1998/namespace'; }if(prefix !== 'xmlns'){ - a.uri = currentNSMap[prefix] + a.uri = currentNSMap[prefix || ''] //{console.log('###'+a.qName,domBuilder.locator.systemId+'',currentNSMap,a.uri)} } @@ -7148,7 +7382,8 @@ function appendElement(el,domBuilder,parseStack){ }else{ el.currentNSMap = currentNSMap; el.localNSMap = localNSMap; - parseStack.push(el); + //parseStack.push(el); + return true; } } function parseHtmlSpecialContent(source,elStartEnd,tagName,entityReplacer,domBuilder){ @@ -7178,7 +7413,11 @@ function fixSelfClosed(source,elStartEnd,tagName,closeMap){ var pos = closeMap[tagName]; if(pos == null){ //console.log(tagName) - pos = closeMap[tagName] = source.lastIndexOf('') + pos = source.lastIndexOf('') + if(pos { + debug(`Found vcard with url ${res.href}`); + return new VCard({ + data: res, + addressBook: addressBook, + url: url.resolve(addressBook.account.rootUrl, res.href), + etag: res.props.getetag, + addressData: res.props.addressData + }); + }); +}); + /** * Options: * @@ -152,11 +178,19 @@ export let listVCards = co.wrap(function *(addressBook, options) { debug(`Doing REPORT on address book ${addressBook.url} which belongs to ${addressBook.account.credentials.username}`); + var vCardListFields = [ 'EMAIL', 'UID', 'CATEGORIES', 'FN', 'TEL', 'NICKNAME' ] + .map(function (value) { + return { + name: 'prop', + namespace: ns.CARDDAV, + attrs: [ { name: 'name', value: value } ] + }; + }); var req = request.addressBookQuery({ depth: 1, props: [ { name: 'getetag', namespace: ns.DAV }, - { name: 'address-data', namespace: ns.CARDDAV } + { name: 'address-data', namespace: ns.CARDDAV, children: vCardListFields } ] }); @@ -268,6 +302,8 @@ export let syncCarddavAccount = co.wrap(function *(account, options={}) { return account; }); +export let getContacts = getFullVcards; + let basicSync = co.wrap(function *(addressBook, options) { let sync = webdav.isCollectionDirty(addressBook, options) if (!sync) { diff --git a/js/dav/lib/request.js b/js/dav/lib/request.js index 97de3c3e1..10d79af52 100644 --- a/js/dav/lib/request.js +++ b/js/dav/lib/request.js @@ -14,6 +14,13 @@ export function addressBookQuery(options) { ); } +export function addressBookMultiget(options) { + return collectionQuery( + template.addressBookMultiget({ props: options.props || [], hrefs: options.hrefs || [] }), + { depth: options.depth } + ); +} + /** * Options: * diff --git a/js/dav/lib/template/address_book_multiget.js b/js/dav/lib/template/address_book_multiget.js new file mode 100644 index 000000000..d13e03d5a --- /dev/null +++ b/js/dav/lib/template/address_book_multiget.js @@ -0,0 +1,15 @@ +import prop from './prop'; + +function href(href) { + return `${href}`; +} + +export default function addressBookMultiget(object) { + return ` + + ${object.props.map(prop).join("")} + + ${object.hrefs.map(href).join("")} + `; +} diff --git a/js/dav/lib/template/address_book_query.js b/js/dav/lib/template/address_book_query.js index 3b6e57969..a03bef2da 100644 --- a/js/dav/lib/template/address_book_query.js +++ b/js/dav/lib/template/address_book_query.js @@ -4,7 +4,7 @@ export default function addressBookQuery(object) { return ` - ${object.props.map(prop)} + ${object.props.map(prop).join("")} diff --git a/js/dav/lib/template/href.js b/js/dav/lib/template/href.js new file mode 100644 index 000000000..e69de29bb diff --git a/js/dav/lib/template/index.js b/js/dav/lib/template/index.js index 005a0f823..3258fb1d5 100644 --- a/js/dav/lib/template/index.js +++ b/js/dav/lib/template/index.js @@ -1,4 +1,5 @@ exports.addressBookQuery = require('./address_book_query'); +exports.addressBookMultiget = require('./address_book_multiget'); exports.calendarQuery = require('./calendar_query'); exports.propfind = require('./propfind'); exports.syncCollection = require('./sync_collection'); diff --git a/js/dav/lib/template/prop.js b/js/dav/lib/template/prop.js index def9e9bc8..54c6a5fd5 100644 --- a/js/dav/lib/template/prop.js +++ b/js/dav/lib/template/prop.js @@ -36,17 +36,25 @@ import * as ns from '../namespace'; * } */ export default function prop(item) { + var tagName = `${xmlnsPrefix(item.namespace)}:${item.name}`; + var attrs = (item.attrs || []).map(makeAttr).join(' '); if (!item.children || !item.children.length) { if (typeof item.value === "undefined") { - return `<${xmlnsPrefix(item.namespace)}:${item.name} />`; + return `<${tagName} ${attrs}/>`; } - return `<${xmlnsPrefix(item.namespace)}:${item.name}>${item.value}`; + return `<${tagName} ${attrs}>${item.value}`; } let children = item.children.map(prop); - return `<${xmlnsPrefix(item.namespace)}:${item.name}> - ${children} - `; + return `<${tagName} ${attrs}> + ${children.join('')} + `; +} + +function makeAttr(attr) { + if (!attr.name) return ''; + if (!attr.value) return attr.name; + return `${attr.name}="${attr.value}"`; } function xmlnsPrefix(namespace) { diff --git a/js/filters/counterFormatter_filter.js b/js/filters/counterFormatter_filter.js new file mode 100644 index 000000000..a73402653 --- /dev/null +++ b/js/filters/counterFormatter_filter.js @@ -0,0 +1,12 @@ +// from https://docs.nextcloud.com/server/11/developer_manual/app/css.html#menus +angular.module('contactsApp') +.filter('counterFormatter', function () { + 'use strict'; + return function (count) { + if (count > 999) { + return '999+'; + } + return count; + }; +}); + diff --git a/js/models/contact_model.js b/js/models/contact_model.js index 53d0ab841..5d96414d0 100644 --- a/js/models/contact_model.js +++ b/js/models/contact_model.js @@ -417,6 +417,7 @@ angular.module('contactsApp') var property = this.getProperty('categories'); if(!property) { + // categories should always have the same type (an array) this.categories([]); } else { if (angular.isString(property.value)) { diff --git a/js/services/contact_service.js b/js/services/contact_service.js index 5cd7c6621..98ec7c32c 100644 --- a/js/services/contact_service.js +++ b/js/services/contact_service.js @@ -4,6 +4,7 @@ angular.module('contactsApp') var cacheFilled = false; var contacts = CacheFactory('contacts'); + var urlsByDisplayname = CacheFactory('urlsByDisplayname'); var observerCallbacks = []; @@ -24,6 +25,32 @@ angular.module('contactsApp') }); }; + this.getFullContacts = function getFullContacts(names) { + AddressBookService.getAll().then(function (enabledAddressBooks) { + var promises = []; + enabledAddressBooks.forEach(function (addressBook) { + var urlLists = names.map(function (name) { return urlsByDisplayname.get(name); }); + var urls = [].concat.apply([], urlLists); + var promise = DavClient.getContacts(addressBook, {}, urls) + .then( + function (vcards) { + return vcards.map(function (vcard) { + return new Contact(addressBook, vcard); + }); + }) + .then(function (contacts_) { + contacts_.map(function (contact) { + contacts.put(contact.uid(), contact); + }); + }); + promises.push(promise); + }); + $q.all(promises).then(function () { + notifyObservers('getFullContacts', ''); + }); + }); + }; + this.fillCache = function() { if (_.isUndefined(loadPromise)) { loadPromise = AddressBookService.getAll().then(function (enabledAddressBooks) { @@ -35,8 +62,10 @@ angular.module('contactsApp') if (addressBook.objects[i].addressData) { var contact = new Contact(addressBook, addressBook.objects[i]); contacts.put(contact.uid(), contact); + var oldList = urlsByDisplayname.get(contact.displayName()) || []; + urlsByDisplayname.put(contact.displayName(), oldList.concat(contact.data.url)); } else { - // custom console + // eslint-disable-next-line no-console console.log('Invalid contact received: ' + addressBook.objects[i].url); } } @@ -61,6 +90,39 @@ angular.module('contactsApp') } }; + // get list of groups and the count of contacts in said groups + this.getGroupList = function () { + return this.getAll().then(function(contacts) { + // the translated names for all and not-grouped are used in filtering, they must be exactly like this + var allContacts = [t('contacts', 'All contacts'), contacts.length]; + var notGrouped = + [t('contacts', 'Not grouped'), + contacts.filter( + function (contact) { + return contact.categories().length === 0; + }).length + ]; + + // allow groups with names such as toString + var otherGroups = Object.create(null); + + // collect categories and their associated counts + contacts.forEach(function (contact) { + contact.categories().forEach(function (category) { + otherGroups[category] = otherGroups[category] ? otherGroups[category] + 1 : 1; + }); + }); + + return [allContacts, notGrouped] + .concat(_.keys(otherGroups).map( + function (key) { + return [key, otherGroups[key]]; + })); + + + }); + }; + this.getGroups = function () { return this.getAll().then(function(contacts) { return _.uniq(contacts.map(function (element) { @@ -71,14 +133,29 @@ angular.module('contactsApp') }); }; - this.getById = function(uid) { - if(cacheFilled === false) { - return this.fillCache().then(function() { - return contacts.get(uid); + this.getById = function(addressBooks, uid) { + return (function () { + if(cacheFilled === false) { + return this.fillCache().then(function() { + return contacts.get(uid); + }); + } else { + return $q.when(contacts.get(uid)); + } + }).call(this) + .then(function (contact) { + var addressBook = _.find(addressBooks, function(book) { + return book.displayName === contact.addressBookId; + }); + return addressBook + ? DavClient.getContacts(addressBook, {}, [ contact.data.url ]).then( + function (vcards) { return new Contact(addressBook, vcards[0]); } + ).then(function (contact) { + contacts.put(contact.uid(), contact); + notifyObservers('getFullContacts', contact.uid()); + return contact; + }) : contact; }); - } else { - return $q.when(contacts.get(uid)); - } }; this.create = function(newContact, addressBook, uid) { diff --git a/js/tests/filters/counterFormatter_filter.js b/js/tests/filters/counterFormatter_filter.js new file mode 100644 index 000000000..f9cc490fe --- /dev/null +++ b/js/tests/filters/counterFormatter_filter.js @@ -0,0 +1,21 @@ +// from https://github.com/owncloud/contacts/blob/31e403af1db859f85e84c2337134e20e5719c825/js/tests/filters/counterFormatter_filter.js by @skjnldsv +describe('counterFormatter filter', function() { + + var $filter; + + beforeEach(module('contactsApp')); + + beforeEach(inject(function(_$filter_){ + $filter = _$filter_; + })); + + it('should return the same number or 999+ if greater than 999', function() { + var counterFormatter = $filter('counterFormatter'); + expect(counterFormatter(Number.NaN)).to.be.Nan; + expect(counterFormatter(15)).to.equal(15); + expect(counterFormatter(0)).to.equal(0); + expect(counterFormatter(-5)).to.equal(-5); + expect(counterFormatter(999)).to.equal(999); + expect(counterFormatter(1000)).to.equal('999+'); + }); +}); diff --git a/templates/contactList.html b/templates/contactList.html index 47d300673..fa9969a07 100644 --- a/templates/contactList.html +++ b/templates/contactList.html @@ -1,6 +1,6 @@
@@ -11,7 +11,4 @@

{{ctrl.t.emptySearch}}

-
- {{ getCountString(filtered) }} -
diff --git a/templates/group.html b/templates/group.html index 694a29fa0..320259c2a 100644 --- a/templates/group.html +++ b/templates/group.html @@ -1 +1,7 @@ -{{ctrl.group}} +{{ ctrl.group }} +
+
    +
  • {{ctrl.groupCount | counterFormatter}}
  • +
+
+
diff --git a/templates/groupList.html b/templates/groupList.html index 840a18226..9c999d769 100644 --- a/templates/groupList.html +++ b/templates/groupList.html @@ -1 +1 @@ -
  • +